[pro-web] feat: add text and oval manual tools to media tab canvas#617
[pro-web] feat: add text and oval manual tools to media tab canvas#617derianrddev wants to merge 17 commits intodevelopfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
Reviewer's GuideImplements a new responsive canvas framing system for the media tab, adds manual drawing tools (text boxes now, ovals stubbed) with layer state management, and wires these tools into the AI actions dropdown and prompt form while simplifying media image-generation flows and layout. Sequence diagram for toggling Tool Mode and adding a text boxsequenceDiagram
actor User
participant AIActionsDropdown
participant PromptForm
participant WorkspaceMedia as WorkspaceMediaProvider
participant MediaCanvas
participant CanvasFrame
participant MediaToolbar
participant CanvasLayers as CanvasLayersContainer
participant TextBoxElement
User->>AIActionsDropdown: click media-tool-mode action
AIActionsDropdown->>PromptForm: onAction(media-tool-mode)
PromptForm->>WorkspaceMedia: toggleToolMode()
WorkspaceMedia-->>WorkspaceMedia: isToolMode = !isToolMode
WorkspaceMedia-->>PromptForm: return
PromptForm-->>AIActionsDropdown: action handled
Note over MediaCanvas,WorkspaceMedia: React re-renders media tab with Tool Mode enabled
MediaCanvas->>CanvasFrame: render with CanvasRenderSizeProvider
CanvasFrame->>MediaToolbar: render
MediaToolbar->>WorkspaceMedia: read isToolMode
WorkspaceMedia-->>MediaToolbar: isToolMode = true
MediaToolbar-->>User: show text and ovals buttons
User->>MediaToolbar: click Text tool
MediaToolbar->>WorkspaceMedia: addTextBox()
WorkspaceMedia-->>WorkspaceMedia: create TextLayer and set activeLayerId
Note over CanvasLayers,WorkspaceMedia: layers state updated
MediaCanvas->>CanvasFrame: re-render with updated layers
CanvasFrame->>CanvasLayers: render CanvasLayersContainer
CanvasLayers->>WorkspaceMedia: read layers and activeLayerId
WorkspaceMedia-->>CanvasLayers: TextLayer[] and activeLayerId
CanvasLayers->>TextBoxElement: render TextBoxElement for each TextLayer
TextBoxElement->>WorkspaceMedia: onSelect(layer.id) when clicked
WorkspaceMedia-->>WorkspaceMedia: setActiveLayerId(layer.id)
Class diagram for media canvas tools, layers, and sizingclassDiagram
class WorkspaceMediaProvider {
+boolean isToolMode
+TextLayer[] layers
+ActiveLayerId activeLayerId
+Template selectedTemplate
+GeneratedImage generatedImage
+void toggleToolMode()
+void addTextBox()
+void addOval()
+void removeTextLayer(id)
+void updateTextLayer(id, updates)
+void setActiveLayerId(id)
+Promise void handlePromptSubmit(prompt)
+Promise void generateImageForSize(size)
}
class NormalizedRect {
+number x
+number y
+number width
}
class TextStyle {
+string fontFamily
+string fontWeight
+number fontSize
+string color
+string textAlign
}
class BaseLayer {
+string id
+LayerType type
+NormalizedRect rect
+number zIndex
}
class TextLayer {
+string id
+string type
+NormalizedRect rect
+number zIndex
+string content
+TextStyle style
}
class CanvasLayer {
}
class CanvasRenderSize {
+number width
+number height
}
class CanvasRenderSizeProvider {
+CanvasRenderSize renderSize
+ReactNode children
}
class useCanvasRenderSize {
+CanvasRenderSize useCanvasRenderSize()
}
class CanvasFrame {
+Size imageSize
+ReactNode children
+string className
}
class MediaCanvas {
+void MediaCanvas()
}
class CanvasLayersContainer {
+void CanvasLayersContainer()
}
class TextBoxElement {
+TextLayer layer
+boolean isActive
+function onSelect(id)
}
class MediaToolbar {
+boolean isToolMode
+void MediaToolbar()
}
class AIActionsDropdown {
+string activeAction
+function onAction(actionId, payload)
}
class PromptForm {
+function handleAIAction(actionId, payload)
}
class CanvasUtils {
+number toPixels(normalized, axisDimension)
+number toNormalized(pixels, axisDimension)
+number clamp(value, min, max)
}
WorkspaceMediaProvider --> TextLayer
WorkspaceMediaProvider --> CanvasLayer
WorkspaceMediaProvider --> CanvasRenderSizeProvider
WorkspaceMediaProvider --> MediaToolbar
WorkspaceMediaProvider --> CanvasLayersContainer
WorkspaceMediaProvider --> PromptForm
WorkspaceMediaProvider --> AIActionsDropdown
TextLayer --> NormalizedRect
TextLayer --> TextStyle
CanvasLayer <|-- TextLayer
BaseLayer <|-- TextLayer
CanvasFrame --> CanvasRenderSizeProvider
CanvasFrame --> CanvasLayersContainer
CanvasFrame --> MediaToolbar
MediaCanvas --> CanvasFrame
CanvasLayersContainer --> TextLayer
CanvasLayersContainer --> TextBoxElement
TextBoxElement --> useCanvasRenderSize
TextBoxElement --> CanvasUtils
MediaToolbar --> WorkspaceMediaProvider
CanvasLayersContainer --> WorkspaceMediaProvider
MediaCanvas --> WorkspaceMediaProvider
AIActionsDropdown --> WorkspaceMediaProvider
PromptForm --> WorkspaceMediaProvider
CanvasUtils <.. TextBoxElement
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
- Add isToolMode boolean state with toggleToolMode action - Add addTextBox and addOval stub actions for canvas drawing - Import useSonner for better UX error notifications - Replace console.warn with customSonner toasts in handlePromptSubmit and generateImageForSize - Remove selectedSize requirement from handlePromptSubmit guard condition - Expose isToolMode, toggleToolMode, addTextBox, addOval through context
- New MediaToolbar component rendered top-right of the canvas - Shows Text and Ovals tool buttons when Tool Mode is active - Renders nothing when isToolMode is false (no layout impact) - Export MediaToolbar from canvas barrel index
…izing - Replace hardcoded aspect-ratio Tailwind classes with dynamic sizing - Add useImageNaturalSize hook to read intrinsic image dimensions - Add fitContain() to compute 'object-contain' render size for the frame - Add CanvasFrame component using ResizeObserver to measure available space - Integrate MediaToolbar above the canvas card - Simplify render logic: showEmpty / showTemplate / showGenerated flags - Remove selectedSize dependency from canvas layout - Improve loading/error overlay styles and copy
…ediaWorkspace - Swap flex overflow-y-auto div for ScrollArea (project standard) - Simplify wrapper class to min-w-0 / min-h-0 to let ScrollArea control overflow - Remove redundant gap/flex-col classes from inner padding div
…n/tool-mode - Replace erase/replace/add-text media actions with edit/expand/redesign/tool-mode - Add isToolMode from useWorkspaceMedia to reflect tool-mode toggle state in dropdown - media-tool-mode is a pure toggle that bypasses activeAction tracking - Separate document vs media action handling in handleActionClick - Allow media AI actions to fire without requiring activeProject/activeDocument - Add selectedTemplate and toggleToolMode to handleAIAction dependencies - Wire media-edit / media-expand / media-redesign / media-tool-mode cases in PromptForm - Show coming-soon sonner for media-expand and media-redesign - Gate media-edit on existing generatedImage or selectedTemplate
- Add canvas.types.ts with NormalizedRect, TextStyle, TextLayer, CanvasLayer, ActiveLayerId - Coordinates stored as 0-100 (% of canvas dimensions) for stable responsive positioning - Add canvas-utils.ts with toPixels(), toNormalized(), and clamp() helpers
…sitioning - New useCanvasRenderSize hook exposes the measured canvas card dimensions - CanvasRenderSizeProvider wraps the canvas subtree to propagate render size - Scoped context limits resize-driven re-renders to canvas consumers only
- Add layers (TextLayer[]) and activeLayerId state - Implement addTextBox to create a centered default text layer with nanoid id - Implement removeTextLayer and updateTextLayer for layer CRUD - Implement setActiveLayerId to track selection - Expose all layer state and actions through WorkspaceMediaContext - Replace addOval stub log with phase-comment placeholder
- Add TextBoxElement: renders a single text layer using normalized coords converted to pixels via useCanvasRenderSize + toPixels() - Font size stored as % of canvas height for proportional resize scaling - Active layer gets a blue outline; hover shows a subtle blue preview - Hidden until canvas is measured to prevent mis-positioned flash - Add CanvasLayersContainer: maps layers from context onto the canvas with pointer-events: none on wrapper; each layer opts in independently - Export both components from the canvas barrel index
…MediaCanvas - Wrap CanvasFrame card children in CanvasRenderSizeProvider to propagate pixel size - Mount CanvasLayersContainer inside the provider so text layers render over the base image - Add onClick on the Card to deselect the active layer (setActiveLayerId(null)) - Import useWorkspaceMedia in CanvasFrame for layer deselect action - Minor comment and whitespace cleanup
…selection UX - Add SelectionOverlay: transparent hit area at z-index 5 that clears the active layer when the canvas background is clicked; only mounts when a layer is selected to avoid unnecessary DOM nodes - Add FloatingTextToolbar: context toolbar that tracks the active text layer, positioned above the layer (flips below when near the top edge) and clamped horizontally to stay within canvas bounds; shows layer type label and a deselect (X) button - Mount both inside CanvasRenderSizeProvider in CanvasFrame with documented z-index stack: base image (0) → overlay (5) → layers (10) → toolbar (20) - Export SelectionOverlay and FloatingTextToolbar from the canvas barrel index
…d state - TextBoxElement: double-click enters contentEditable editing mode; blur commits draft to context; Escape exits editing and keeps layer selected - Draft content is local during editing to avoid a context write per keystroke; committed only on blur when content has changed - A keyed div forces a fresh DOM mount on edit start to avoid React/contentEditable conflicts - Add editingLayerId ActiveLayerId state and setEditingLayerId action to WorkspaceMediaContext (separate from activeLayerId) - Wrap setActiveLayerId to atomically clear editingLayerId when deselecting - FloatingTextToolbar: show blue 'Editing' label with Pencil icon when the active layer is in edit mode, 'Text' label otherwise
- Pointer events (down/move/up/cancel) handle dragging when a layer is selected - Drag state lives in a ref to avoid re-renders during pointermove at 60fps; position is written directly to the DOM during the gesture and committed to context (updateTextLayer) once on pointer up - DRAG_THRESHOLD (4px) prevents micro-movements from stealing click/double-click - hasDraggedRef suppresses the synthetic click fired after a drag ends - cursor changes: pointer (idle) → grab (selected) → grabbing (dragging) - setPointerCapture keeps the element receiving events if the pointer strays outside
…ontrols - Add font family picker (system: Arial, Times New Roman; Google: Inter, Poppins, Montserrat, DM Sans, Oswald, Bebas Neue, Anton, Playfair Display, Lora, Roboto Slab) with live preview in the trigger and dropdown items - Add bold toggle, font size stepper (+/-2px with direct input), color picker, and left/center/right alignment toggles - Font size stored as % of canvas height; converted to/from px at render time via toPixels/toNormalized so it scales correctly on resize - Google fonts applied via next/font class (inline fontFamily skipped to avoid overriding the class); system fonts fall back to inline style - Add AlignmentOption interface to canvas.types.ts - Apply Google font class to TextBoxElement button so displayed text matches the selected font while in idle/selected state
…xt insertion - Delete/Backspace removes the active text layer when focus is not in an input or contentEditable (CanvasLayersContainer useEffect) - 'T' key adds a new text box when a canvas is loaded and not editing (CanvasLayersContainer useEffect) - Delete/Backspace on the active TextBoxElement button also removes the layer (mirrors the global shortcut for pointer-focused interactions)
Adds 6 resize handles (right, left, top-left, top-right, bottom-left, bottom-right) to the active text layer. Interaction follows the same pattern as drag: direct DOM writes during pointermove, single updateTextLayer call on pointerup, and a cancel handler that reverts to start values without touching context. - canvas.types.ts: ResizeHandleId union + ResizeHandleConfig interface - TextBoxElement: resizeRef tracks gesture state (handle id, start pointer/layer coords, start height for proportional scaling) - Horizontal handles (right/left) adjust width only; corner handles also scale fontSize proportionally to the new height - DRAG_THRESHOLD guard updated to skip drag start while resize is active - overflow:visible added to sharedStyle so handles render outside bounds - ResizeHandleDot sub-component renders each 8px white/blue-border dot
1a9482e to
2f725ed
Compare
Adds the ability to bake all text layers into the base image and revert.
canvas-flatten.ts (new):
- Off-screen Canvas 2D renderer at natural image resolution
- wrapText: matches UI CSS (white-space:pre-wrap + word-break:break-word)
- loadFont: awaits FontFaceSet.load() before measuring/drawing text
- flattenLayersToBase64: loads base image, draws sorted layers, exports PNG
canvas.types.ts:
- FlattenHistory discriminated union (kind:'template'|'generated') for undo
use-workspace-media:
- flattenHistory / isFlatteningImage state
- flattenImage(): probes natural size, calls flattenLayersToBase64, saves
undo snapshot, pushes flattened GeneratedImage, clears layer stack
- revertFlatten(): restores pre-flatten image and layers from snapshot
- Fix handleMediaPromptSubmit guard (!generatedImage && !selectedTemplate)
- Exposes flattenImage / revertFlatten / flattenHistory / isFlatteningImage
MediaToolbar:
- Apply (Check) button shown when layers exist and no flattenHistory
- Revert (RotateCcw) button shown when flattenHistory exists
MediaCanvas:
- Pass showModelLabel={false} to ImageDisplay
🖼️ Media (screenshots/videos)
https://www.loom.com/share/eb0bb899e5354e2f91a3d848a5b648e9
Summary by Sourcery
Introduce a responsive media canvas with overlay layers and a toolbar, and wire it into media workspace tooling and AI actions.
New Features:
Enhancements: