Skip to content

[pro-web] feat: add text and oval manual tools to media tab canvas#617

Draft
derianrddev wants to merge 17 commits intodevelopfrom
feat/tool-mode-toggle
Draft

[pro-web] feat: add text and oval manual tools to media tab canvas#617
derianrddev wants to merge 17 commits intodevelopfrom
feat/tool-mode-toggle

Conversation

@derianrddev
Copy link
Contributor

@derianrddev derianrddev commented Feb 20, 2026

🖼️ 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:

  • Add a media canvas frame with responsive sizing based on the underlying image and viewport.
  • Introduce tool mode for the media tab, including a toolbar with text and oval tools and support for text layers on the canvas.
  • Provide canvas layer types and utilities for normalized coordinate handling, plus a context for sharing canvas render size.

Enhancements:

  • Unify media image display/layout logic into a reusable CanvasFrame with overlays for loading and error states.
  • Allow media AI actions to operate without document selection and add new image-focused actions (edit, expand, redesign, tool mode).
  • Improve media workspace scrolling and layout with a ScrollArea wrapping the media canvas.
  • Use toast notifications instead of console warnings when media actions are invoked without a base image.

@derianrddev derianrddev self-assigned this Feb 20, 2026
@vercel
Copy link

vercel bot commented Feb 20, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
masterbots-pro Ready Ready Preview, Comment Feb 25, 2026 7:32pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
masterbots Skipped Skipped Feb 25, 2026 7:32pm

Request Review

@sourcery-ai
Copy link

sourcery-ai bot commented Feb 20, 2026

Reviewer's Guide

Implements 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 box

sequenceDiagram
  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)
Loading

Class diagram for media canvas tools, layers, and sizing

classDiagram
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
Loading

File-Level Changes

Change Details Files
Refactor media canvas to use a responsive CanvasFrame with toolbar and overlay layers, and support both templates and generated images with loading/error overlays.
  • Introduce useImageNaturalSize and fitContain helpers to compute a stable contain-fit render size for the canvas based on the visible image.
  • Add CanvasFrame component that measures available viewport space with ResizeObserver, reserves space for a top toolbar, and wraps the canvas Card with CanvasRenderSizeProvider and CanvasLayersContainer.
  • Rework MediaCanvas logic to derive showEmpty/showTemplate/showGenerated states, compute a sizingSrc based on either generated image or template, and render template/generated previews inside CanvasFrame with updated loading and error overlays.
apps/pro-web/components/routes/workspace/media-tab/ui/canvas/media-canvas.tsx
Add AI actions for media tool mode and manual edit/expand/redesign operations, with tab-aware validation and state handling.
  • Inject useWorkspaceMedia into AIActionsDropdown to read isToolMode and adjust menu item highlighting.
  • Split action handling so document actions still require an active document section while media actions do not, and treat media-tool-mode as a pure toggle that bypasses activeAction tracking.
  • Replace previous erase/replace/add-text media actions with media-edit, media-expand, media-redesign, and media-tool-mode definitions and icons, and update click wiring accordingly.
apps/pro-web/components/routes/chat/prompt-form/ai-actions-dropdown.tsx
Extend workspace media context with tool mode and canvas layer state/actions to support manual text and shape tools, and improve image-generation preconditions and feedback.
  • Augment MediaWorkspaceState with isToolMode, layers, and activeLayerId, and MediaWorkspaceActions with toggleToolMode, addTextBox, addOval, removeTextLayer, updateTextLayer, and setActiveLayerId.
  • Implement local state and callbacks in WorkspaceMediaProvider for toggling tool mode, creating default text layers with normalized coordinates and styles, managing active layer selection, and updating/removing layers.
  • Replace console warnings in handlePromptSubmit and generateImageForSize with user-facing Sonner toasts, and slightly adjust generation preconditions to require an existing template/image where appropriate.
apps/pro-web/lib/hooks/use-workspace-media.tsx
Wire media AI actions into the prompt form, including media edit/tool-mode behavior and relaxed project/document requirements.
  • Pull selectedTemplate and toggleToolMode from useWorkspaceMedia in PromptForm to support media actions.
  • Update handleAIAction to allow media actions without active project/document, interpret new media-edit/media-expand/media-redesign/media-tool-mode IDs, and show Sonner toasts for missing base image/template or not-yet-implemented actions.
  • Ensure graceful handling of optional payload/error objects and update useCallback dependencies to include new media dependencies.
apps/pro-web/components/routes/chat/prompt-form/index.tsx
Adjust media workspace layout to be scrollable while preserving a fixed-height, responsive canvas area.
  • Wrap the main media content area in a ScrollArea with full-height/width and move padding into an inner container.
  • Simplify the flex layout so the media canvas area can grow within a scrollable column instead of using nested flex-1 containers with overflow-y-auto.
apps/pro-web/components/routes/workspace/media-tab/media-workspace.tsx
Add reusable canvas UI building blocks: toolbar, layer container, text element, render-size context, and numeric utilities.
  • Create MediaToolbar with text and oval tools that conditionally render when Tool Mode is active and dispatch addTextBox/addOval via workspace media context.
  • Implement CanvasLayersContainer to render overlay layers on top of the canvas using TextBoxElement for text layers and an absolute, pointer-events-none wrapper.
  • Add TextBoxElement component that converts normalized rect/font-size values to pixels using CanvasRenderSize, renders positioned text buttons with active outlines, and forwards selection via onSelect.
  • Introduce CanvasRenderSize context and hook to share the canvas Card’s measured render size with descendants, and add canvas-utils (toPixels, toNormalized, clamp) plus canvas.types defining normalized rects, text styles, layers, and ActiveLayerId.
  • Export the new canvas utilities (MediaToolbar, CanvasLayersContainer, TextBoxElement) from the canvas index barrel.
apps/pro-web/components/routes/workspace/media-tab/ui/canvas/media-toolbar.tsx
apps/pro-web/components/routes/workspace/media-tab/ui/canvas/canvas-layers-container.tsx
apps/pro-web/components/routes/workspace/media-tab/ui/canvas/text-box-element.tsx
apps/pro-web/lib/hooks/use-canvas-render-size.tsx
apps/pro-web/lib/canvas-utils.ts
apps/pro-web/components/routes/workspace/media-tab/ui/canvas/index.ts
apps/pro-web/types/canvas.types.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 20, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/tool-mode-toggle

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

- 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
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant