Skip to content

Conversation

@softmarshmallow
Copy link
Member

@softmarshmallow softmarshmallow commented Dec 29, 2025

Main Menu

Grida Canvas - Main Menu Grida Canvas - Keyboard shortcuts settings dialog

File

Path Name Description Shortcut Ready
File > Open .grida Open .grida Opens a Grida document file
File > Save as .grida Save as .grida Exports current document as a Grida file
File > Import Image Import Image Imports image files (PNG, JPEG, WebP, SVG) into the canvas
File > Import Figma Import Figma Imports designs from Figma via URL or .fig file

Edit

Path Name Description Shortcut Ready
Edit > Undo Undo Undoes the last action ⌘Z
Edit > Redo Redo Redoes the last undone action ⌘⇧Z
Edit > Cut Cut Cuts the selected items to clipboard ⌘X
Edit > Copy Copy Copies the selected items to clipboard ⌘C
Edit > Paste Paste Pastes items from clipboard ⌘V
Edit > Copy as PNG Copy as PNG Copies the selected items as PNG image (canvas backend only) ⇧⌘C
Edit > Copy as SVG Copy as SVG Copies the selected items as SVG (canvas backend only)
Edit > Pick color Pick color Opens color picker to pick color from screen I
Edit > Duplicate Duplicate Duplicates the selected items ⌘D
Edit > Delete Delete Deletes the selected items

Text

Path Name Description Shortcut Ready
Text > Bold Bold Toggles bold text style ⌘B
Text > Italic Italic Toggles italic text style ⌘I
Text > Underline Underline Toggles underline text decoration ⌘U
Text > Strikethrough Strikethrough Toggles strikethrough text decoration ⇧⌘X
Text > Create link Create link Creates a hyperlink from selected text ⇧⌘U
Text > Bulleted list Bulleted list Converts text to a bulleted list ⇧⌘8
Text > Numbered list Numbered list Converts text to a numbered list ⇧⌘7
Text > Alignment > Text align left Text align left Aligns text to the left ⌥⌘L
Text > Alignment > Text align center Text align center Centers text horizontally ⌥⌘T
Text > Alignment > Text align right Text align right Aligns text to the right ⌥⌘R
Text > Alignment > Text align justified Text align justified Justifies text horizontally ⌥⌘J
Text > Alignment > Text align top Text align top Aligns text to the top vertically
Text > Alignment > Text align middle Text align middle Centers text vertically
Text > Alignment > Text align bottom Text align bottom Aligns text to the bottom vertically
Text > Adjust > Increase indentation Increase indentation Increases text indentation Tab
Text > Adjust > Decrease indentation Decrease indentation Decreases text indentation ⇧ + Tab
Text > Adjust > Increase font size Increase font size Increases font size by 1px ⇧⌘>
Text > Adjust > Decrease font size Decrease font size Decreases font size by 1px ⇧⌘<
Text > Adjust > Increase font weight Increase font weight Increases font weight ⌥⌘>
Text > Adjust > Decrease font weight Decrease font weight Decreases font weight ⌥⌘<
Text > Adjust > Increase line height Increase line height Increases line height ⌥⇧>
Text > Adjust > Decrease line height Decrease line height Decreases line height ⌥⇧<
Text > Adjust > Increase letter spacing Increase letter spacing Increases letter spacing ⌥>
Text > Adjust > Decrease letter spacing Decrease letter spacing Decreases letter spacing ⌥<
Text > Case > Original case Original case Resets text to original case (removes transform)
Text > Case > Uppercase Uppercase Transforms text to uppercase
Text > Case > Lowercase Lowercase Transforms text to lowercase

Object

Path Name Description Shortcut Ready
Object > Container selection Container selection Wraps selection in a container ⌥⌘G
Object > Group selection Group selection Groups selected items ⌘G
Object > Ungroup selection Ungroup selection Ungroups selected group ⇧⌘G
Object > Wrap in new section Wrap in new section Wraps selection in a new section -
Object > Convert to section Convert to section Converts selection to a section
Object > Convert to container Convert to container Converts selection to a container
Object > Use as mask Use as mask Uses selection as a clipping mask
Object > Restore default thumbnail Restore default thumbnail Restores the default thumbnail for selection
Object > Add layout Add layout Applies layout to selection ⇧A
Object > Create arc Create arc Creates an arc shape from current ellipse
Object > More layout options > Suggest layout Suggest layout Suggests layout configuration ⇧⌃A
Object > More layout options > Remove all layout Remove all layout Removes layout and bakes pos from recursively
Object > More layout options > Lock aspect ratio Lock aspect ratio Locks aspect ratio for selection
Object > More layout options > Unlock aspect ratio Unlock aspect ratio Unlocks aspect ratio for selection
Object > More layout options > Resize to fit Resize to fit Resizes containers to fit its contents ⌥⇧⌘R
Object > More layout options > Set width to hug contents Set width to hug contents Sets width to hug contents
Object > More layout options > Set height to hug contents Set height to hug contents Sets height to hug contents
Object > More layout options > Set width to fill container Set width to fill container Sets width to fill container
Object > More layout options > Set height to fill container Set height to fill container Sets height to fill container
Object > Create component Create component Creates a component from selection ⌥⌘K
Object > Reset instance Reset instance Resets component instance to main component
Object > Detach instance Detach instance Detaches component instance ⌥⌘B
Object > Main component > Go to main component Go to main component Navigates to the main component ⌃⌥⌘K
Object > Main component > Push changes to main component Push changes to main component Pushes overrides to main component
Object > Main component > Restore main component Restore main component Restores main component state
Object > Bring to front Bring to front Brings selection to the front ]
Object > Bring forward Bring forward Brings selection forward one layer ⌘]
Object > Send backward Send backward Sends selection backward one layer ⌘[
Object > Send to back Send to back Sends selection to the back [
Object > Flip horizontal Flip horizontal Flips selection horizontally ⇧H
Object > Flip vertical Flip vertical Flips selection vertically ⇧V
Object > Rotate 180° Rotate 180° Rotates selection by 180 degrees
Object > Rotate 90° left Rotate 90° left Rotates selection 90 degrees counterclockwise
Object > Rotate 90° right Rotate 90° right Rotates selection 90 degrees clockwise
Object > Flatten Flatten Converts selection to vector paths ⌘E
Object > Outline stroke Outline stroke Converts stroke to filled shape ⌥⌘O
Object > Boolean groups > Union Union Combines shapes using union operation
Object > Boolean groups > Subtract Subtract Subtracts shapes using subtract operation
Object > Boolean groups > Intersect Intersect Creates intersection of shapes
Object > Boolean groups > Exclude Exclude Creates exclusion of shapes
Object > Rasterize selection Rasterize selection Converts selection to raster image
Object > Show/Hide selection Show/Hide selection Toggles visibility of selection ⇧⌘H
Object > Lock/Unlock selection Lock/Unlock selection Toggles lock state of selection ⇧⌘L
Object > Hide other layers Hide other layers Hides all layers except selection
Object > Collapse layers Collapse layers Collapses layer hierarchy ⌥L
Object > Remove fill Remove fill Removes fill from selection ⌥/
Object > Remove stroke Remove stroke Removes stroke from selection (sets width to 0) ⇧/
Object > Swap fill and stroke Swap fill and stroke Swaps fill and stroke paints ⇧X

Arrange

Path Name Description Shortcut Ready
Arrange > Round to pixel Round to pixel
Arrange > Align left Align left Aligns selection to the left ⌥A
Arrange > Align horizontal centers Align horizontal centers Aligns selection horizontally centered ⌥H
Arrange > Align right Align right Aligns selection to the right ⌥D
Arrange > Align top Align top Aligns selection to the top ⌥W
Arrange > Align vertical centers Align vertical centers Aligns selection vertically centered ⌥V
Arrange > Align bottom Align bottom Aligns selection to the bottom ⌥S
Arrange > Tidy up Tidy up ⇧⌥T
Arrange > Pack horizontal Pack horizontal
Arrange > Pack vertical Pack vertical
Arrange > Distribute horizontal spacing Distribute horizontal spacing Distributes selection evenly horizontally ⌥⌃V
Arrange > Distribute vertical spacing Distribute vertical spacing Distributes selection evenly vertically ⌥⌃H
Arrange > Distribute left Distribute left
Arrange > Distribute horizontal centers Distribute horizontal centers
Arrange > Distribute right Distribute right
Arrange > Distribute top Distribute top
Arrange > Distribute vertical centers Distribute vertical centers
Arrange > Distribute bottom Distribute bottom

View

Path Name Description Shortcut Ready
View > Zoom in Zoom in Zooms in on the canvas ⌘+
View > Zoom out Zoom out Zooms out on the canvas ⌘-
View > Zoom to 100% Zoom to 100% Resets zoom to 100% ⇧0
View > Zoom to fit Zoom to fit Zooms to fit all content on canvas ⇧1
View > Zoom to selection Zoom to selection Zooms to fit selected items ⇧2
View > Pixel grid Pixel grid Toggles pixel grid overlay ⇧'
View > Ruler Ruler Toggles ruler display ⇧R
View > Show/Hide UI Show/Hide UI Toggles UI visibility (when available) ⌘\
View > Minimize UI Minimize UI Minimizes UI elements (when available) ⇧⌘\

Settings

Path Name Description Shortcut Ready
Settings > General General Opens general settings dialog
Settings > Keyboard shortcuts Keyboard shortcuts Opens keyboard shortcuts settings dialog

Summary by CodeRabbit

Release Notes

  • New Features

    • Dynamic keyboard shortcuts throughout menus and toolbars for better platform consistency
    • Enhanced text formatting options: alignment, bold/italic/underline, font size/weight, line height, letter spacing
    • Layer navigation shortcuts (Move forward/backward)
    • Stroke/fill management: swap, remove fill, remove stroke actions
    • Color picker with EyeDropper API integration
    • Keyboard shortcuts settings panel with search and platform selection
  • Documentation

    • Updated keyboard shortcuts documentation with clearer descriptions
    • Added main menu structure documentation
  • Refactor

    • Redesigned Settings dialog with sidebar navigation
    • Modularized menu system for better organization
    • UI component system improvements

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link

vercel bot commented Dec 29, 2025

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

Project Deployment Review Updated (UTC)
docs Ready Ready Preview, Comment Dec 30, 2025 8:23am
grida Ready Ready Preview, Comment Dec 30, 2025 8:23am
5 Skipped Deployments
Project Deployment Review Updated (UTC)
code Ignored Ignored Dec 30, 2025 8:23am
legacy Ignored Ignored Dec 30, 2025 8:23am
backgrounds Skipped Skipped Dec 30, 2025 8:23am
blog Skipped Skipped Dec 30, 2025 8:23am
viewer Skipped Skipped Dec 30, 2025 8:23am

@coderabbitai
Copy link

coderabbitai bot commented Dec 29, 2025

Walkthrough

Introduces a comprehensive keyboard shortcut system with platform-aware keybinding infrastructure, extends the editor API with text formatting and fill/stroke operations, adds UI components for keyboard shortcut discovery and settings, and updates menus and toolbars to dynamically render shortcuts based on action definitions.

Changes

Cohort / File(s) Summary
Shortcuts Documentation
docs/editor/shortcuts/index.md
Comprehensive rework of keyboard shortcuts table with expanded text formatting section, new layer navigation actions (Move forward/backward), improved eye dropper mapping, and updated Object Properties entries with descriptive action names.
Keybinding Infrastructure
editor/grida-canvas/keybinding.ts, editor/grida-canvas/keycode.ts
New complete keybinding system introducing Modifier flags (M), Chunk/Sequence/Keybindings types, platform-aware resolution utilities (resolveMods, resolveChunk, resolveSequence), OS detection, UI label mapping, and cross-platform keybinding-to-KeyCode conversion.
UI Field Component System
editor/components/ui/field.tsx, editor/components/ui/label.tsx, editor/components/ui/separator.tsx
New slot-based Field composition module with FieldLabel, FieldDescription, FieldError, FieldGroup, FieldSeparator, etc. Label and Separator components migrated to forwardRef with variant-based styling via cva.
Keyboard Shortcut Actions Registry
editor/grida-canvas-hosted/playground/uxhost-actions.ts, editor/grida-canvas-hosted/playground/uxhost-shortcut-renderer.tsx
New action registry defining UXHostAction interface with name, description, command, and keybindings; static actions map with predefined actions; helper functions keyboardShortcutText, keyboardShortcutTextAll, keyboardShortcutTextFormatted for rendering shortcuts.
Settings & Shortcuts UI
editor/grida-canvas-hosted/playground/uxhost-settings-keyboardshortcuts.tsx, editor/grida-canvas-hosted/playground/uxhost-settings.tsx, editor/grida-canvas-hosted/playground/uxhost-menu.md
New KeyboardShortcuts component with search, platform selector, and rendered shortcut rows; Settings dialog refactored to sidebar-based navigation with General and Keyboard Shortcuts panels; menu documentation added.
Menu & Toolbar Dynamic Shortcuts
editor/grida-canvas-hosted/playground/uxhost-menu.tsx, editor/grida-canvas-hosted/playground/uxhost-toolbar.tsx, editor/grida-canvas-hosted/playground/playground.tsx, editor/grida-canvas-react-starter-kit/starterkit-toolbar/index.tsx, editor/grida-canvas-react-starter-kit/starterkit-toolbar/path-toolbar.tsx
Extensive rewrite of menu/toolbar components replacing hardcoded shortcut strings with dynamic rendering via keyboardShortcutText; expanded menus with new text formatting, alignment, and arrange operations; toolbar tooltips now reference action-based shortcuts.
Context Menu & View Updates
editor/grida-canvas-react/use-context-menu-actions.ts, editor/grida-canvas-react/viewport/hotkeys.tsx, editor/grida-canvas-react/viewport/surface.tsx
Replaced static shortcut strings with keyboardShortcutText calls; removed keybindings_sheet constant; refactored hotkeys to use OS-aware bindings and surface methods for text formatting; added new color picker and stroke/fill swap shortcuts.
Removed Deprecated Symbol Mapping
editor/grida-canvas-react/devtools/keysymbols.ts
Removed keysymbols export; symbol mapping now provided by keybinding.ts infrastructure.
Core Editor API Extensions
editor/grida-canvas/editor.i.ts, editor/grida-canvas/editor.ts
Added swapFillAndStroke, addNodeStroke with ensureStrokeWidth parameter; new a11y methods for text alignment, font size/weight/line-height/letter-spacing, fill/stroke clearing, and aspect ratio locking; added promptColorPicker and surfacePickColor with EyeDropper support.
Provider & Scaffold Updates
editor/grida-canvas-react/provider.tsx, editor/scaffolds/sidecontrol/chunks/section-strokes.tsx, editor/scaffolds/sidecontrol/controls/ext-align.tsx, editor/scaffolds/sidecontrol/controls/ext-zoom.tsx, editor/scaffolds/sidecontrol/sidecontrol-node-selection.tsx
Updated addStroke signature to include ensureStrokeWidth parameter; ZoomControl and zoom-related shortcuts use keyboardShortcutText; removed unused Button import; added TODO for future refactor.
Dependency Update
editor/package.json
Bumped @radix-ui/react-label from ^2.1.6 to ^2.1.7.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Settings as Settings Dialog
    participant KS as KeyboardShortcuts Component
    participant Actions as Actions Registry
    participant Renderer as Shortcut Renderer
    participant UI as Kbd UI Components

    User->>Settings: Open Settings
    Settings->>KS: Render KeyboardShortcuts
    
    User->>KS: Enter search query
    KS->>Actions: Filter actions by search
    Actions-->>KS: Filtered actions
    
    User->>KS: Select platform (macOS/Windows/Linux)
    KS->>Renderer: Resolve keybindings for each action
    
    loop For each filtered action
        Renderer->>Actions: Get action keybindings
        Actions-->>Renderer: Keybindings (platform-specific)
        Renderer->>Renderer: Resolve platform-specific binding
        Renderer->>UI: Render Kbd components with keys
        UI-->>KS: Display formatted shortcuts
    end
    
    KS-->>User: Show filtered, platform-specific shortcuts
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • #462: Renames fill/stroke-related fields (fills→fill_paints, strokes→stroke_paints) that directly overlap with the editor API extensions in this PR.
  • #482: Modifies uxhost-menu.tsx with surface API changes; this PR performs a comprehensive menu restructuring in the same file.
  • #468: Updates playground keyboard hints and Shift+I "Open Library" tooltip; this PR expands the same shortcut handling infrastructure.

Suggested labels

ux, keyboard-shortcuts, editor-api, ui-components

Poem

🐰 Keys aligned in platforms three,
From Mac to Win, they dance with glee!
Shortcuts sparkle, shortcuts shine,
Dynamic bindings, oh so fine!
Now every action finds its key,
A keyboard symphony! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.98% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main changes: comprehensive updates to keyboard shortcuts, actions system, and menu UI restructuring across multiple files.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch canary

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.

@vercel vercel bot temporarily deployed to Preview – backgrounds December 30, 2025 08:19 Inactive
@vercel vercel bot temporarily deployed to Preview – viewer December 30, 2025 08:19 Inactive
@vercel vercel bot temporarily deployed to Preview – blog December 30, 2025 08:19 Inactive
@softmarshmallow softmarshmallow marked this pull request as ready for review December 30, 2025 08:20
@softmarshmallow softmarshmallow changed the title Daily RC Daily RC - Main Menu & Actions Dec 30, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
editor/grida-canvas-hosted/playground/playground.tsx (1)

380-397: Fix mismatch between “Open Library” tooltip and actual hotkey

useHotkeys still binds the Library window to "shift+i", but the tooltip now renders Ctrl/Cmd + I via uikbdk(M.CtrlCmd) and uikbdk(KeyCode.KeyI). That will confuse users and is hard to discover.

Either change the hotkey to match the new chord (e.g. meta+i, ctrl+i using the keybinding system) or revert the tooltip to show Shift+I via the same helper.

Also applies to: 651-653

editor/grida-canvas-react/viewport/hotkeys.tsx (1)

241-249: Avoid overlapping ctrl+c bindings between color picker and copy on macOS

Color picker now uses:

useHotkeys(
  keyboardOS === "mac" ? "i, ctrl+c" : "i",
  () => editor.surface.surfacePickColor(),
  ...
);

while copy still binds "copy, meta+c, ctrl+c".

On macOS, Ctrl+C isn’t the typical copy chord, but registering both handlers on the same combination means pressing Ctrl+C will trigger both copy and color picking in the canvas, which is surprising and hard to reason about.

Consider either:

  • Dropping ctrl+c from one of the handlers (keep color picker on i, and copy on copy, meta+c), or
  • Splitting the bindings so ctrl+c is only associated with one semantic action.

This keeps the mental model simple and avoids accidental multi‑action triggers.

Also applies to: 542-546

🧹 Nitpick comments (17)
editor/grida-canvas-react-starter-kit/starterkit-toolbar/index.tsx (1)

43-43: Dynamic shortcut wiring for toolbar tools looks consistent

Using keyboardShortcutText(...) for pencil/path/brush/eraser, cursor tools, container/text, and basic shapes is consistent with the rest of the UX host shortcut system, and the tooltip / menu rendering already avoids displaying empty strings when no binding exists. I don’t see functional issues here.

If you want to DRY things up a bit, you could centralize the action IDs in a small mapping (e.g. toolActionIdByToolbarValue) so both hotkeys and UI hints share a single source, but that’s purely optional.

Also applies to: 116-123, 149-151, 164-165, 172-173, 181-184

editor/grida-canvas-react/use-context-menu-actions.ts (1)

8-8: Context menu shortcuts correctly hooked into keyboardShortcutText

The new keyboardShortcutText(...) usage for PNG copy, z-order, group/ungroup, auto layout, flatten, active/locked toggles, zoom-to-fit, and delete is consistent with the UX host action namespace and remains safely optional (empty string → no label).

You also still compute targetSingleOrSelection above but re-inline the same ternary in flatten. If you want to simplify, you could call editor.commands.flatten(targetSingleOrSelection) and drop the duplication.

Also applies to: 108-205

editor/grida-canvas/editor.i.ts (2)

3328-3347: Stroke paint helpers and swapFillAndStroke API are well‑structured

The new swapFillAndStroke(target, ensureStroke?) command plus the extended changeNodePropertyStrokes(..., ensureStrokeWidth?) and addNodeStroke(..., at?, ensureStrokeWidth?) overloads give you a single, well‑documented path for stroke updates and stroke‑visibility guarantees.

The signatures are backward compatible (new args are optional and appended), and the docs make clear that only paint arrays are swapped while other stroke props are preserved, which matches how the hotkeys use these operations.

Implementation-wise, just ensure all internal callers of changeNodePropertyStrokes are updated to pass the ensureStrokeWidth flag where behavior should change; the type-level surface looks correct.

Also applies to: 3489-3541, 3585-3639


3819-4044: New a11y text, paint, and aspect‑ratio APIs line up with viewport hotkeys

The additions to IEditorA11yActions:

  • a11yTextAlign / a11yTextVerticalAlign
  • a11yChangeTextFontSize/LineHeight/LetterSpacing/FontWeight
  • a11yClearFill, a11yClearStroke, a11ySwapFillAndStroke
  • a11yLockAspectRatio, a11yUnlockAspectRatio

are all consistent with how useEditorHotKeys invokes them and with the underlying lower‑level command set (changeTextNode*, changeNodePropertyStrokes, lockAspectRatio/unlockAspectRatio). The JSDoc examples also match the actual keybindings you wired up (e.g. ⌘+⇧+>/< for font size).

From a public API standpoint this is a clean, semantic layer over the existing commands. Just be sure the implementations enforce the “text nodes only” and aspect‑ratio preconditions described in the comments.

editor/grida-canvas-react/viewport/hotkeys.tsx (1)

367-455: Text alignment and rich text formatting hotkeys are wired correctly

The meta/ctrl+alt alignment shortcuts and the period/comma‑based text formatting hotkeys correctly route through the new a11yTextAlign, a11yChangeTextFontSize/LineHeight/LetterSpacing/FontWeight surface methods and use shared textFormattingOptions with splitKey: "|", which works correctly in react-hotkeys-hook v4.6.1 to avoid conflicts with the default comma separator.

To tidy things up, remove the redundant hotkey patterns where both sides of the pipe are identical: at lines 433, 438, 447, and 452, simplify "alt+shift+period | alt+shift+period" to "alt+shift+period", "alt+shift+comma | alt+shift+comma" to "alt+shift+comma", "alt+period | alt+period" to "alt+period", and "alt+comma | alt+comma" to "alt+comma".

editor/scaffolds/sidecontrol/sidecontrol-node-selection.tsx (1)

822-825: Documented stroke-width tech debt is clear and accurate

The TODO clearly describes the current ad‑hoc stroke_width handling and the plan to migrate to the centralized ensureStrokeWidth path in addNodeStroke/changeNodePropertyStrokes. This is a good marker for a future cleanup when you align mixed-selection strokes with SectionStrokes.

editor/grida-canvas-hosted/playground/uxhost-menu.md (1)

5-58: Keep shortcut docs in sync with dynamic keybindings; minor wording nit

This markdown gives a solid overview of menus/shortcuts, but since the runtime now resolves shortcuts dynamically, these static glyphs can drift over time (especially on non‑macOS platforms). Consider either:

  • Generating this table from the central keybinding registry, or
  • Adding a note that the listed shortcuts reflect the default macOS layout and may vary by platform/config.

Also, in the “Copy as PNG” row you can drop “image” (“Copies the selected items as PNG”) to avoid the “PNG image” tautology.

Also applies to: 61-148, 151-154

editor/grida-canvas-react/provider.tsx (1)

146-151: Extended addStroke API cleanly exposes ensureStrokeWidth option

Updating addStroke to accept an optional ensureStrokeWidth?: boolean and forwarding it to addNodeStroke is a straightforward way to surface the new behavior without breaking existing call sites. Callers that don’t care about auto‑width can continue using the 1–2 argument form.

editor/grida-canvas-hosted/playground/uxhost-settings-keyboardshortcuts.tsx (1)

42-55: Platform fallback order could be more intuitive.

The fallback chain (maclinuxwindows) may produce unexpected results when viewing Windows shortcuts but no Windows-specific binding exists. Consider using the selected platform first, then falling back to a more common default:

     const platformBinding =
       keybindings[selectedPlatform] ||
-      keybindings.mac ||
-      keybindings.linux ||
-      keybindings.windows;
+      keybindings.windows ||
+      keybindings.mac ||
+      keybindings.linux;

Alternatively, document why mac is preferred as the primary fallback.

editor/grida-canvas-hosted/playground/uxhost-shortcut-renderer.tsx (1)

86-111: Consider extracting shared sequence-to-string logic.

keyboardShortcutTextFormatted duplicates the mapping logic from sequenceToString (lines 33-46). Consider reusing sequenceToString or extracting a shared helper:

 export function keyboardShortcutTextFormatted(
   actionId: UXHostActionId,
   platform?: "mac" | "windows" | "linux",
   separator: string = " "
 ): string {
   const sequences = getResolvedSequences(actionId, platform);
   if (sequences.length === 0) {
     return "";
   }

   const targetPlatform = platform || getKeyboardOS();
-  const sequence = sequences[0];
-
-  return sequence
-    .map((chunk) => {
-      const parts: string[] = [];
-      chunk.mods.forEach((mod) => {
-        parts.push(keycodeToPlatformUILabel(mod, targetPlatform));
-      });
-      chunk.keys.forEach((key) => {
-        parts.push(keycodeToPlatformUILabel(key, targetPlatform));
-      });
-      return parts.join("");
-    })
-    .join(separator);
+  return sequenceToString(sequences[0], targetPlatform).replace(/ /g, separator);
 }

Note: This assumes the default separator in sequenceToString is a space.

editor/grida-canvas-hosted/playground/uxhost-menu.tsx (1)

619-634: Consider moving applyToTextNodes helper to shared scope.

The applyToTextNodes helper is useful but defined inside TextMenuContent. If similar iteration patterns are needed elsewhere (e.g., ObjectMenuContent for multi-selection operations), consider extracting it to a shared utility or the component's parent scope.

editor/grida-canvas-hosted/playground/uxhost-actions.ts (1)

26-35: Minor: Unused import.

The Keybinding type is imported but doesn't appear to be used in this file. The file uses Keybindings (plural) for the action keybindings field.

 import {
-  type Keybinding,
   type Keybindings,
   kb,
   c,
   seq,
   M,
   platformKb,
 } from "@/grida-canvas/keybinding";
editor/components/ui/field.tsx (1)

10-244: Field composition API and slot structure look solid

The overall Field/FieldSet/FieldGroup/FieldLabel/... design is cohesive, variant handling via cva is clean, and FieldError’s memoized rendering correctly handles single vs multiple error messages. From a correctness and accessibility standpoint this is good to ship.

If you later need ref access for these wrappers (e.g., for programmatic focus), consider wrapping the main primitives (Field, FieldSet, FieldGroup, FieldContent, etc.) with forwardRef, but that can be deferred.

editor/grida-canvas/keycode.ts (1)

1-10: Avoid local tweaks that drift from upstream VS Code source

This file is explicitly marked as a direct copy of VS Code’s keyCodes.ts and meant to be kept in sync. The Biome warning about toString (on ScanCodeUtils.toString / KeyCodeUtils.toString) is coming from lint rules, not a runtime issue.

Rather than renaming these APIs (which would diverge from upstream and complicate future syncs), consider disabling noShadowRestrictedNames for this file or for these specific exports in your lint configuration.

Also applies to: 474-478, 778-822

editor/grida-canvas/editor.ts (3)

1752-1782: Stroke helpers are functionally correct; be aware of undo granularity and non‑stroke nodes

The new stroke helpers (changeNodePropertyStrokes, addNodeStroke, swapFillAndStroke) behave correctly:

  • ensureStrokeWidth only applies when needed and uses the existing changeNodePropertyStrokeWidth path.
  • swapFillAndStroke correctly pulls paints via editor.resolvePaints and routes stroke updates back through changeNodePropertyStrokes so width is normalized when requested.

Two small considerations:

  1. Undo granularity:
    changeNodePropertyStrokes dispatches once per node to update stroke_paints, then may dispatch again per node to update stroke_width. That means a single user action (e.g. “swap fill & stroke with ensure stroke”) can produce multiple history entries. If you want a single undo step for this UX operation, you might later introduce a dedicated reducer action that applies both changes in one dispatch.

  2. Nodes without stroke_width:
    The guard

    const currentStrokeWidth =
      "stroke_width" in node ? node.stroke_width : undefined;

    avoids reading a missing property, and the set change type means resolveNumberChangeValue won’t touch the node in that case, so this is safe even for nodes that conceptually don’t support strokes.

Given the current goals, this is fine to ship, but keep the undo behavior in mind if users expect a single-step history operation for swap/ensure‑stroke flows.

Also applies to: 1838-1863, 1784-1808


3692-3728: Font‑weight navigation helper is well‑structured; watch repeated family parsing

Editor.getFontWeightsForFontFamily and EditorSurface.a11yChangeTextFontWeight nicely encapsulate “next/previous weight within a family” behavior:

  • Weights are derived from font.styles filtered by italic state and deduplicated, then sorted.
  • The a11y helper wraps around when there’s no heavier/lighter weight, and falls back to changeTextNodeFontWeight if selectFontStyle can’t find a matching face.

The only minor concern is potential repeated work when many nodes share the same family: each call to a11yChangeTextFontWeight awaits getFontWeightsForFontFamily, which in turn calls getFontFamilyDetailsSync. If _fontManager.parseFontFamily isn’t already caching by family, you may want to add an internal cache in the font manager so repeated weight queries are effectively free.

Also applies to: 5223-5288


5301-5317: Color picker flow is UX‑friendly; ensure it only runs on the client

The promptColorPicker / surfacePickColor duo is well‑designed:

  • promptColorPicker isolates the EyeDropper API and returns sRGBHex | undefined while gracefully handling cancellation.
  • surfacePickColor applies a solid fill to the current selection when present, or copies the hex to clipboard and shows a toast when there’s no selection, with error handling around both EyeDropper and clipboard failures.

Two small environment notes:

  • These methods assume a browser context (window.EyeDropper, navigator.clipboard, toast); ensure they’re only called from client‑side code paths (which is already implied by their UX nature).
  • If you ever need to support non‑Chrome browsers that don’t expose EyeDropper, you may want to downgrade the thrown error in promptColorPicker to a more generic feature‑detection path (e.g., return undefined and let the caller show a UI hint), but the current toast‑based error reporting is acceptable for now.

Also applies to: 5380-5445

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2267c1e and 52d8128.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (28)
  • docs/editor/shortcuts/index.md
  • editor/components/ui/field.tsx
  • editor/components/ui/label.tsx
  • editor/components/ui/separator.tsx
  • editor/grida-canvas-hosted/playground/playground.tsx
  • editor/grida-canvas-hosted/playground/uxhost-actions.ts
  • editor/grida-canvas-hosted/playground/uxhost-menu.md
  • editor/grida-canvas-hosted/playground/uxhost-menu.tsx
  • editor/grida-canvas-hosted/playground/uxhost-settings-keyboardshortcuts.tsx
  • editor/grida-canvas-hosted/playground/uxhost-settings.tsx
  • editor/grida-canvas-hosted/playground/uxhost-shortcut-renderer.tsx
  • editor/grida-canvas-hosted/playground/uxhost-toolbar.tsx
  • editor/grida-canvas-react-starter-kit/starterkit-toolbar/index.tsx
  • editor/grida-canvas-react-starter-kit/starterkit-toolbar/path-toolbar.tsx
  • editor/grida-canvas-react/devtools/keysymbols.ts
  • editor/grida-canvas-react/provider.tsx
  • editor/grida-canvas-react/use-context-menu-actions.ts
  • editor/grida-canvas-react/viewport/hotkeys.tsx
  • editor/grida-canvas-react/viewport/surface.tsx
  • editor/grida-canvas/editor.i.ts
  • editor/grida-canvas/editor.ts
  • editor/grida-canvas/keybinding.ts
  • editor/grida-canvas/keycode.ts
  • editor/package.json
  • editor/scaffolds/sidecontrol/chunks/section-strokes.tsx
  • editor/scaffolds/sidecontrol/controls/ext-align.tsx
  • editor/scaffolds/sidecontrol/controls/ext-zoom.tsx
  • editor/scaffolds/sidecontrol/sidecontrol-node-selection.tsx
💤 Files with no reviewable changes (2)
  • editor/grida-canvas-react/devtools/keysymbols.ts
  • editor/scaffolds/sidecontrol/controls/ext-align.tsx
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{ts,tsx}: Use TypeScript 5 as the main language for most apps
Use Lucide or Radix Icons for icons

Files:

  • editor/grida-canvas-react/viewport/surface.tsx
  • editor/scaffolds/sidecontrol/controls/ext-zoom.tsx
  • editor/grida-canvas-react-starter-kit/starterkit-toolbar/index.tsx
  • editor/grida-canvas-hosted/playground/uxhost-settings-keyboardshortcuts.tsx
  • editor/scaffolds/sidecontrol/sidecontrol-node-selection.tsx
  • editor/grida-canvas-hosted/playground/uxhost-shortcut-renderer.tsx
  • editor/grida-canvas-react/use-context-menu-actions.ts
  • editor/components/ui/field.tsx
  • editor/grida-canvas-react/provider.tsx
  • editor/grida-canvas-hosted/playground/playground.tsx
  • editor/grida-canvas-react-starter-kit/starterkit-toolbar/path-toolbar.tsx
  • editor/grida-canvas-hosted/playground/uxhost-menu.tsx
  • editor/components/ui/separator.tsx
  • editor/grida-canvas-hosted/playground/uxhost-toolbar.tsx
  • editor/grida-canvas/keybinding.ts
  • editor/grida-canvas-react/viewport/hotkeys.tsx
  • editor/grida-canvas-hosted/playground/uxhost-settings.tsx
  • editor/components/ui/label.tsx
  • editor/grida-canvas-hosted/playground/uxhost-actions.ts
  • editor/grida-canvas/keycode.ts
  • editor/scaffolds/sidecontrol/chunks/section-strokes.tsx
  • editor/grida-canvas/editor.i.ts
  • editor/grida-canvas/editor.ts
**/*.{jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use React.js 19 for web applications

Files:

  • editor/grida-canvas-react/viewport/surface.tsx
  • editor/scaffolds/sidecontrol/controls/ext-zoom.tsx
  • editor/grida-canvas-react-starter-kit/starterkit-toolbar/index.tsx
  • editor/grida-canvas-hosted/playground/uxhost-settings-keyboardshortcuts.tsx
  • editor/scaffolds/sidecontrol/sidecontrol-node-selection.tsx
  • editor/grida-canvas-hosted/playground/uxhost-shortcut-renderer.tsx
  • editor/components/ui/field.tsx
  • editor/grida-canvas-react/provider.tsx
  • editor/grida-canvas-hosted/playground/playground.tsx
  • editor/grida-canvas-react-starter-kit/starterkit-toolbar/path-toolbar.tsx
  • editor/grida-canvas-hosted/playground/uxhost-menu.tsx
  • editor/components/ui/separator.tsx
  • editor/grida-canvas-hosted/playground/uxhost-toolbar.tsx
  • editor/grida-canvas-react/viewport/hotkeys.tsx
  • editor/grida-canvas-hosted/playground/uxhost-settings.tsx
  • editor/components/ui/label.tsx
  • editor/scaffolds/sidecontrol/chunks/section-strokes.tsx
{editor/**/*.{ts,tsx},packages/grida-canvas-*/**/*.{ts,tsx}}

📄 CodeRabbit inference engine (AGENTS.md)

Use DOM (plain DOM as canvas) for website builder canvas, bound with React

Files:

  • editor/grida-canvas-react/viewport/surface.tsx
  • editor/scaffolds/sidecontrol/controls/ext-zoom.tsx
  • editor/grida-canvas-react-starter-kit/starterkit-toolbar/index.tsx
  • editor/grida-canvas-hosted/playground/uxhost-settings-keyboardshortcuts.tsx
  • editor/scaffolds/sidecontrol/sidecontrol-node-selection.tsx
  • editor/grida-canvas-hosted/playground/uxhost-shortcut-renderer.tsx
  • editor/grida-canvas-react/use-context-menu-actions.ts
  • editor/components/ui/field.tsx
  • editor/grida-canvas-react/provider.tsx
  • editor/grida-canvas-hosted/playground/playground.tsx
  • editor/grida-canvas-react-starter-kit/starterkit-toolbar/path-toolbar.tsx
  • editor/grida-canvas-hosted/playground/uxhost-menu.tsx
  • editor/components/ui/separator.tsx
  • editor/grida-canvas-hosted/playground/uxhost-toolbar.tsx
  • editor/grida-canvas/keybinding.ts
  • editor/grida-canvas-react/viewport/hotkeys.tsx
  • editor/grida-canvas-hosted/playground/uxhost-settings.tsx
  • editor/components/ui/label.tsx
  • editor/grida-canvas-hosted/playground/uxhost-actions.ts
  • editor/grida-canvas/keycode.ts
  • editor/scaffolds/sidecontrol/chunks/section-strokes.tsx
  • editor/grida-canvas/editor.i.ts
  • editor/grida-canvas/editor.ts
editor/grida-*/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use /editor/grida-* directories to isolate domain-specific modules; promote well-defined modules to /packages

Files:

  • editor/grida-canvas-react/viewport/surface.tsx
  • editor/grida-canvas-react-starter-kit/starterkit-toolbar/index.tsx
  • editor/grida-canvas-hosted/playground/uxhost-settings-keyboardshortcuts.tsx
  • editor/grida-canvas-hosted/playground/uxhost-shortcut-renderer.tsx
  • editor/grida-canvas-react/use-context-menu-actions.ts
  • editor/grida-canvas-react/provider.tsx
  • editor/grida-canvas-hosted/playground/playground.tsx
  • editor/grida-canvas-react-starter-kit/starterkit-toolbar/path-toolbar.tsx
  • editor/grida-canvas-hosted/playground/uxhost-menu.tsx
  • editor/grida-canvas-hosted/playground/uxhost-toolbar.tsx
  • editor/grida-canvas/keybinding.ts
  • editor/grida-canvas-react/viewport/hotkeys.tsx
  • editor/grida-canvas-hosted/playground/uxhost-settings.tsx
  • editor/grida-canvas-hosted/playground/uxhost-actions.ts
  • editor/grida-canvas/keycode.ts
  • editor/grida-canvas/editor.i.ts
  • editor/grida-canvas/editor.ts
editor/scaffolds/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use /editor/scaffolds for feature-specific larger components, pages, and editors

Files:

  • editor/scaffolds/sidecontrol/controls/ext-zoom.tsx
  • editor/scaffolds/sidecontrol/sidecontrol-node-selection.tsx
  • editor/scaffolds/sidecontrol/chunks/section-strokes.tsx
**/components/ui/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use Shadcn UI for UI component library

Files:

  • editor/components/ui/field.tsx
  • editor/components/ui/separator.tsx
  • editor/components/ui/label.tsx
editor/components/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use /editor/components for generally reusable components

Files:

  • editor/components/ui/field.tsx
  • editor/components/ui/separator.tsx
  • editor/components/ui/label.tsx
editor/components/ui/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use /editor/components/ui for shadcn UI components

Files:

  • editor/components/ui/field.tsx
  • editor/components/ui/separator.tsx
  • editor/components/ui/label.tsx
docs/**/*.md

📄 CodeRabbit inference engine (AGENTS.md)

Documentation source of truth is in the ./docs directory; deployment is handled by apps/docs

Files:

  • docs/editor/shortcuts/index.md
🧠 Learnings (16)
📚 Learning: 2025-12-14T23:30:42.112Z
Learnt from: CR
Repo: gridaco/grida PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T23:30:42.112Z
Learning: Applies to packages/grida-canvas-*/**/*.{ts,tsx,js,jsx} : Packages under /packages/grida-canvas-* power the canvas; some are published to npm, refer to individual package README

Applied to files:

  • editor/grida-canvas-react/viewport/surface.tsx
  • editor/grida-canvas-hosted/playground/playground.tsx
📚 Learning: 2025-12-14T23:30:42.112Z
Learnt from: CR
Repo: gridaco/grida PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T23:30:42.112Z
Learning: Applies to **/components/ui/**/*.{ts,tsx} : Use Shadcn UI for UI component library

Applied to files:

  • editor/package.json
  • editor/components/ui/field.tsx
  • editor/grida-canvas-hosted/playground/playground.tsx
  • editor/components/ui/separator.tsx
  • editor/grida-canvas-hosted/playground/uxhost-settings.tsx
📚 Learning: 2025-12-14T23:30:42.112Z
Learnt from: CR
Repo: gridaco/grida PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T23:30:42.112Z
Learning: Applies to **/*.{ts,tsx} : Use Lucide or Radix Icons for icons

Applied to files:

  • editor/package.json
  • editor/grida-canvas-hosted/playground/uxhost-menu.tsx
📚 Learning: 2025-12-14T23:30:42.112Z
Learnt from: CR
Repo: gridaco/grida PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T23:30:42.112Z
Learning: Applies to **/*.{jsx,tsx} : Use React.js 19 for web applications

Applied to files:

  • editor/package.json
📚 Learning: 2025-12-14T23:30:42.112Z
Learnt from: CR
Repo: gridaco/grida PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T23:30:42.112Z
Learning: Applies to {editor/**/*.{ts,tsx},packages/grida-canvas-*/**/*.{ts,tsx}} : Use DOM (plain DOM as canvas) for website builder canvas, bound with React

Applied to files:

  • editor/scaffolds/sidecontrol/controls/ext-zoom.tsx
  • editor/grida-canvas-hosted/playground/playground.tsx
  • editor/grida-canvas-hosted/playground/uxhost-toolbar.tsx
📚 Learning: 2025-12-01T00:22:56.899Z
Learnt from: CR
Repo: gridaco/grida PR: 0
File: editor/app/(tools)/tools/halftone/AGENTS.md:0-0
Timestamp: 2025-12-01T00:22:56.899Z
Learning: Applies to editor/app/(tools)/tools/halftone/app/(tools)/tools/halftone/_page.tsx : Use React hooks for state management (imageSrc, shape, grid, maxRadius, gamma, jitter, opacity, color, customShapeImage, imageDataRef, sizeRef)

Applied to files:

  • editor/scaffolds/sidecontrol/controls/ext-zoom.tsx
  • editor/grida-canvas-react-starter-kit/starterkit-toolbar/index.tsx
  • editor/grida-canvas-hosted/playground/uxhost-toolbar.tsx
  • editor/grida-canvas-hosted/playground/uxhost-settings.tsx
📚 Learning: 2025-12-01T00:22:56.899Z
Learnt from: CR
Repo: gridaco/grida PR: 0
File: editor/app/(tools)/tools/halftone/AGENTS.md:0-0
Timestamp: 2025-12-01T00:22:56.899Z
Learning: Applies to editor/app/(tools)/tools/halftone/app/(tools)/tools/halftone/_page.tsx : When adding new shape types, update the Shape type union, add cases in drawShape() function, add cases in shapeToSVG() function, and add SelectItem in UI

Applied to files:

  • editor/grida-canvas-react-starter-kit/starterkit-toolbar/index.tsx
  • editor/scaffolds/sidecontrol/sidecontrol-node-selection.tsx
  • editor/grida-canvas-react/provider.tsx
  • editor/grida-canvas-hosted/playground/uxhost-toolbar.tsx
  • editor/scaffolds/sidecontrol/chunks/section-strokes.tsx
  • editor/grida-canvas/editor.i.ts
  • editor/grida-canvas/editor.ts
📚 Learning: 2025-12-14T23:30:42.112Z
Learnt from: CR
Repo: gridaco/grida PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T23:30:42.112Z
Learning: Applies to editor/components/ui/**/*.{ts,tsx} : Use /editor/components/ui for shadcn UI components

Applied to files:

  • editor/grida-canvas-hosted/playground/uxhost-settings-keyboardshortcuts.tsx
  • editor/components/ui/field.tsx
  • editor/grida-canvas-hosted/playground/playground.tsx
  • editor/grida-canvas-hosted/playground/uxhost-menu.tsx
  • editor/grida-canvas-hosted/playground/uxhost-settings.tsx
📚 Learning: 2025-12-01T00:22:56.899Z
Learnt from: CR
Repo: gridaco/grida PR: 0
File: editor/app/(tools)/tools/halftone/AGENTS.md:0-0
Timestamp: 2025-12-01T00:22:56.899Z
Learning: Applies to editor/app/(tools)/tools/halftone/app/(tools)/tools/halftone/_page.tsx : When adding new parameters to the halftone tool, add state with useState, include in useEffect dependency array, pass to renderHalftone() function, use in rendering logic, and add UI control

Applied to files:

  • editor/scaffolds/sidecontrol/sidecontrol-node-selection.tsx
  • editor/scaffolds/sidecontrol/chunks/section-strokes.tsx
📚 Learning: 2025-12-14T23:30:42.112Z
Learnt from: CR
Repo: gridaco/grida PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T23:30:42.112Z
Learning: Applies to editor/scaffolds/**/*.{ts,tsx} : Use /editor/scaffolds for feature-specific larger components, pages, and editors

Applied to files:

  • editor/scaffolds/sidecontrol/sidecontrol-node-selection.tsx
📚 Learning: 2025-12-14T23:30:42.112Z
Learnt from: CR
Repo: gridaco/grida PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T23:30:42.112Z
Learning: Applies to editor/components/**/*.{ts,tsx} : Use /editor/components for generally reusable components

Applied to files:

  • editor/components/ui/field.tsx
📚 Learning: 2025-12-14T23:30:42.112Z
Learnt from: CR
Repo: gridaco/grida PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T23:30:42.112Z
Learning: Applies to editor/lib/**/*.{ts,tsx} : Use /editor/lib for core, strictly designed modules with non-opinionated, reusable, and stable implementations

Applied to files:

  • editor/components/ui/field.tsx
  • editor/grida-canvas/keybinding.ts
📚 Learning: 2025-12-14T23:30:42.112Z
Learnt from: CR
Repo: gridaco/grida PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-14T23:30:42.112Z
Learning: Applies to editor/grida-*/**/*.{ts,tsx} : Use /editor/grida-* directories to isolate domain-specific modules; promote well-defined modules to <root>/packages

Applied to files:

  • editor/components/ui/field.tsx
  • editor/grida-canvas/keybinding.ts
  • editor/grida-canvas/keycode.ts
📚 Learning: 2025-12-01T00:22:56.899Z
Learnt from: CR
Repo: gridaco/grida PR: 0
File: editor/app/(tools)/tools/halftone/AGENTS.md:0-0
Timestamp: 2025-12-01T00:22:56.899Z
Learning: Applies to editor/app/(tools)/tools/halftone/app/(tools)/tools/halftone/_page.tsx : Use Canvas 2D API with path commands for rendering geometric shapes (circle, square, triangle, etc.)

Applied to files:

  • editor/grida-canvas-react/provider.tsx
  • editor/grida-canvas-react-starter-kit/starterkit-toolbar/path-toolbar.tsx
  • editor/grida-canvas-react/viewport/hotkeys.tsx
  • editor/scaffolds/sidecontrol/chunks/section-strokes.tsx
  • editor/grida-canvas/editor.i.ts
  • editor/grida-canvas/editor.ts
📚 Learning: 2025-12-01T00:22:19.083Z
Learnt from: CR
Repo: gridaco/grida PR: 0
File: crates/grida-canvas-wasm/AGENTS.md:0-0
Timestamp: 2025-12-01T00:22:19.083Z
Learning: Applies to crates/grida-canvas-wasm/**/main.rs : Update `grida-canvas-wasm.d.ts` TypeScript definitions file when new APIs are introduced via `main.rs`

Applied to files:

  • editor/grida-canvas/keybinding.ts
  • editor/grida-canvas/keycode.ts
📚 Learning: 2025-12-01T00:22:56.899Z
Learnt from: CR
Repo: gridaco/grida PR: 0
File: editor/app/(tools)/tools/halftone/AGENTS.md:0-0
Timestamp: 2025-12-01T00:22:56.899Z
Learning: Applies to editor/app/(tools)/tools/halftone/app/(tools)/tools/halftone/_page.tsx : For SVG export, convert circles to <circle> elements, rectangles to <rect> elements, and polygons to <polygon> elements with calculated points

Applied to files:

  • editor/grida-canvas/editor.ts
🧬 Code graph analysis (13)
editor/grida-canvas-react/viewport/surface.tsx (1)
editor/grida-canvas-hosted/playground/uxhost-shortcut-renderer.tsx (1)
  • keyboardShortcutText (55-66)
editor/scaffolds/sidecontrol/controls/ext-zoom.tsx (2)
editor/components/ui/dropdown-menu.tsx (2)
  • DropdownMenuShortcut (253-253)
  • DropdownMenuCheckboxItem (249-249)
editor/grida-canvas-hosted/playground/uxhost-shortcut-renderer.tsx (1)
  • keyboardShortcutText (55-66)
editor/grida-canvas-react-starter-kit/starterkit-toolbar/index.tsx (2)
editor/grida-canvas-hosted/playground/uxhost-shortcut-renderer.tsx (1)
  • keyboardShortcutText (55-66)
editor/grida-canvas-react-starter-kit/starterkit-toolbar/utils.ts (1)
  • ToolbarToolType (3-22)
editor/grida-canvas-hosted/playground/uxhost-settings-keyboardshortcuts.tsx (5)
editor/grida-canvas/keybinding.ts (1)
  • Keybindings (55-61)
editor/components/ui/kbd.tsx (2)
  • KbdGroup (28-28)
  • Kbd (28-28)
editor/grida-canvas-hosted/playground/uxhost-actions.ts (1)
  • actions (50-899)
editor/components/ui/input-group.tsx (3)
  • InputGroup (164-164)
  • InputGroupAddon (165-165)
  • InputGroupInput (168-168)
editor/components/ui/label.tsx (1)
  • Label (26-26)
editor/grida-canvas-react/use-context-menu-actions.ts (1)
editor/grida-canvas-hosted/playground/uxhost-shortcut-renderer.tsx (1)
  • keyboardShortcutText (55-66)
editor/components/ui/field.tsx (2)
editor/components/ui/label.tsx (1)
  • Label (26-26)
editor/components/ui/separator.tsx (1)
  • Separator (31-31)
editor/grida-canvas-react-starter-kit/starterkit-toolbar/path-toolbar.tsx (2)
editor/grida-canvas-hosted/playground/uxhost-shortcut-renderer.tsx (1)
  • keyboardShortcutText (55-66)
editor/grida-canvas/keybinding.ts (1)
  • uikbdk (382-404)
editor/grida-canvas-hosted/playground/uxhost-toolbar.tsx (3)
editor/grida-canvas-hosted/playground/uxhost-shortcut-renderer.tsx (1)
  • keyboardShortcutText (55-66)
editor/grida-canvas-react-starter-kit/starterkit-toolbar/index.tsx (1)
  • ToolGroupItem (45-83)
editor/grida-canvas-react-starter-kit/starterkit-toolbar/utils.ts (1)
  • ToolbarToolType (3-22)
editor/grida-canvas-react/viewport/hotkeys.tsx (2)
editor/grida-canvas/keybinding.ts (1)
  • getKeyboardOS (202-207)
editor/grida-canvas-hosted/playground/uxhost-shortcut-renderer.tsx (1)
  • keyboardShortcutText (55-66)
editor/grida-canvas-hosted/playground/uxhost-settings.tsx (4)
editor/components/ui/dialog.tsx (4)
  • Dialog (133-133)
  • DialogContent (135-135)
  • DialogHeader (138-138)
  • DialogTitle (141-141)
editor/components/sidebar/index.tsx (1)
  • SidebarMenuItem (179-275)
editor/components/ui/field.tsx (5)
  • FieldGroup (238-238)
  • Field (234-234)
  • FieldLabel (235-235)
  • FieldDescription (236-236)
  • FieldSeparator (240-240)
editor/grida-canvas-hosted/playground/uxhost-settings-keyboardshortcuts.tsx (1)
  • KeyboardShortcuts (115-190)
editor/grida-canvas-hosted/playground/uxhost-actions.ts (1)
editor/grida-canvas/keybinding.ts (5)
  • Keybindings (55-61)
  • kb (99-101)
  • platformKb (106-112)
  • seq (89-91)
  • c (81-83)
editor/grida-canvas/editor.i.ts (5)
packages/grida-canvas-schema/grida.ts (1)
  • NodeID (1182-1182)
editor/grida-canvas/utils/supports.ts (2)
  • strokes (322-329)
  • stroke (314-321)
packages/grida-canvas-cg/lib.ts (3)
  • Paint (699-705)
  • TextAlign (263-263)
  • TextAlignVertical (273-273)
editor/grida-canvas/utils/properties.ts (1)
  • stroke (87-87)
packages/grida-canvas-io-svg/lib.ts (1)
  • stroke (147-178)
editor/grida-canvas/editor.ts (3)
editor/grida-canvas/utils/supports.ts (2)
  • strokes (322-329)
  • stroke (314-321)
packages/grida-canvas-cg/lib.ts (2)
  • Paint (699-705)
  • SolidPaint (737-742)
editor/grida-canvas/editor.i.ts (2)
  • FontStyleInstance (656-687)
  • NodeID (136-136)
🪛 Biome (2.1.2)
editor/grida-canvas/keycode.ts

[error] 779-779: Do not shadow the global "toString" property.

Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.

(lint/suspicious/noShadowRestrictedNames)

🪛 LanguageTool
editor/grida-canvas-hosted/playground/uxhost-menu.md

[style] ~21-~21: This phrase is redundant (‘G’ stands for ‘graphic’). Use simply “PNG”.
Context: ...y as PNG | Copies the selected items as PNG image (canvas backend only) | ⇧⌘C | ✅ ...

(ACRONYM_TAUTOLOGY)

🔇 Additional comments (23)
editor/grida-canvas-hosted/playground/playground.tsx (1)

662-670: Confirm non‑modal dropdown behavior is desired

Setting <DropdownMenu modal={false}> changes focus trapping and allows interaction with other parts of the UI while the menu is open. That’s often desired for app menus but does alter Radix’s default behavior; worth double‑checking that it matches your UX expectations for this header menu.

editor/grida-canvas-react/viewport/hotkeys.tsx (2)

467-505: Fill/stroke manipulation and stacking hotkeys align with new surface APIs

  • alt+slash, shift+slash, and shift+x correctly call a11yClearFill, a11yClearStroke, and a11ySwapFillAndStroke("selection") and guard with preventDefault and form/contentEditable disables, so they won’t interfere with typing or browser shortcuts.
  • The new meta+]/[, ctrl+]/[ bindings for "forward"/"backward" stack moves nicely complement the existing ]/[ front/back behavior and route through editor.surface.order, matching the documentation in editor.api.order.

I don’t see functional issues here; the mappings are coherent with the rest of the ordering and paint‑editing semantics.

Also applies to: 797-811


848-857: Toast message now correctly reflects configured group‑with‑container shortcut

Using keyboardShortcutText("workbench.surface.object.group-with-container") in the error toast keeps the “use … for grouping” hint in sync with the actual keymap, including OS‑specific symbols. This avoids drift when shortcuts change elsewhere.

editor/grida-canvas-react/viewport/surface.tsx (1)

76-76: Dynamic shortcut rendering for ruler context menu looks correct

Importing keyboardShortcutText and using the "workbench.surface.view.hide-show-ruler" action in ContextMenuShortcut keeps the context menu in sync with platform-aware keybindings. No issues spotted here.

Also applies to: 1985-1988

editor/scaffolds/sidecontrol/controls/ext-zoom.tsx (1)

20-20: Zoom dropdown now correctly uses centralized, platform-aware shortcuts

Wiring all relevant zoom/pixel grid/ruler items through keyboardShortcutText(...) aligns this menu with the centralized keybinding system and avoids hardcoded glyphs. The action IDs look consistent with the rest of the editor.

Also applies to: 56-58, 66-68, 76-78, 86-88, 103-105, 123-127, 137-139

editor/package.json (1)

75-75: Radix Label minor version bump – verify against upstream notes

The @radix-ui/react-label dependency is bumped to ^2.1.7. This looks fine, but please double-check the Radix changelog to ensure there are no breaking changes affecting your Label wrapper and related form components.

editor/grida-canvas-hosted/playground/uxhost-toolbar.tsx (1)

38-38: Toolbar now correctly delegates all tool shortcuts to the central resolver

Using keyboardShortcutText across the cursor/shape/draw tool groups and the bitmap “Paint Bucket” tooltip removes hardcoded key labels and keeps the toolbar aligned with the keybinding registry. The action IDs look consistent and existing selection behavior remains unchanged.

Also applies to: 69-92, 101-107, 111-114, 121-147, 162-187, 228-247

editor/grida-canvas-react-starter-kit/starterkit-toolbar/path-toolbar.tsx (1)

12-14: Path toolbar tooltips now correctly use centralized shortcut helpers

Switching these tooltips to keyboardShortcutText (and uikbdk(M.CtrlCmd) for the bend modifier) makes the path tools’ shortcuts platform-aware and consistent with the rest of the editor. The IDs look coherent and no functional regressions are apparent.

Also applies to: 46-48, 61-64, 77-77, 90-93, 110-113

editor/scaffolds/sidecontrol/chunks/section-strokes.tsx (1)

147-153: LGTM! Clean refactor of stroke addition logic.

The change correctly delegates stroke width normalization to addStroke via the ensureStrokeWidth parameter, removing the need for a separate stroke width mutation. The dependency array is properly updated to exclude stroke_width since it's no longer used in the callback body.

editor/grida-canvas-hosted/playground/uxhost-settings-keyboardshortcuts.tsx (1)

115-133: LGTM! Well-structured search and filtering logic.

The component correctly initializes platform from getKeyboardOS(), memoizes filtered actions, and handles the empty search case efficiently. The search covers name, description, and command fields appropriately.

docs/editor/shortcuts/index.md (2)

91-92: LGTM! New layer navigation shortcuts documented.

Move forward (⌘ + ]) and Move backward (⌘ + [) are correctly documented with platform-appropriate mappings, matching the definitions in uxhost-actions.ts.


118-135: LGTM! Comprehensive text formatting shortcuts.

The expanded text formatting section covers all major typography controls with correct platform-specific mappings. The shortcuts align with the action definitions in uxhost-actions.ts.

editor/grida-canvas-hosted/playground/uxhost-settings.tsx (3)

79-105: LGTM! Well-structured sidebar navigation.

The sidebar layout with SidebarProvider properly scopes the sidebar state. Menu items are cleanly defined as a data structure, and the active tab state management is straightforward.


107-145: LGTM! Theme selector implementation.

The theme selector correctly integrates with next-themes and provides light/dark/system options with appropriate icons. The Field-based layout is clean and accessible.


154-160: No action required—editor.debug assignment is properly reactive.

The editor.debug property uses a reactive getter/setter pattern (editor/grida-canvas/editor.ts lines 2672–2681). The setter calls this.doc.reduce() to update state, which triggers subscriptions that React syncs via useSyncExternalStore in the useEditor hook. The direct assignment editor.debug = v correctly invokes the setter and maintains sync between the Switch component and the editor state.

Likely an incorrect or invalid review comment.

editor/components/ui/separator.tsx (1)

8-31: LGTM! Clean forwardRef refactor.

The Separator component now correctly forwards refs to the underlying Radix primitive. The simplified className logic improves readability while maintaining the same visual behavior. This follows shadcn UI conventions.

editor/grida-canvas-hosted/playground/uxhost-menu.tsx (2)

207-254: LGTM! Well-organized menu structure.

The refactor cleanly separates menu content into dedicated sub-components (FileMenuContent, EditMenuContent, ViewMenuContent, etc.), improving maintainability and readability. The dynamic keyboardShortcutText integration ensures shortcuts stay in sync with the action registry.


1012-1125: LGTM! Comprehensive arrange menu with proper alignment controls.

The ArrangeMenuContent component provides a complete set of alignment and distribution options with correct editor action bindings. Selection state is properly checked before enabling operations.

editor/grida-canvas-hosted/playground/uxhost-actions.ts (3)

1-45: LGTM! Excellent documentation and type-safe design.

The fileoverview clearly explains the current state and future vision for this action registry. The UXHostAction interface is well-defined, and the deprecated marker on command appropriately signals that the command execution system isn't implemented yet.


714-723: LGTM! Good use of platform-specific keybindings.

The eye dropper action correctly uses platformKb to define different keybindings for macOS (I or Ctrl+C) vs Windows/Linux (just I). This is a clean pattern for platform-specific shortcuts.


901-912: LGTM! Type-safe action retrieval.

The UXHostActionId type derived from keyof typeof actions ensures compile-time safety when referencing actions. The getAction helper provides a clean API for runtime retrieval.

editor/components/ui/label.tsx (1)

9-24: Label refactor to forwardRef + CVA is clean

The new Label implementation correctly wraps LabelPrimitive.Root with forwardRef, centralizes styling via labelVariants, and keeps the displayName intact. No issues from a typing or runtime perspective.

editor/grida-canvas/editor.ts (1)

3613-3618: Expose swapFillAndStroke consistently via Editor and a11y surface

The new Editor.swapFillAndStroke and EditorSurface.a11ySwapFillAndStroke wrappers correctly delegate down to EditorDocumentStore.swapFillAndStroke and respect the ensureStroke flag. This gives both programmatic and a11y layers a consistent API for fill/stroke swapping without duplicating paint resolution logic.

Also applies to: 5319-5325

Comment on lines +178 to +184
filteredActions.map((action) => (
<KeyboardShortcutRow
key={action.command}
action={action}
selectedPlatform={selectedPlatform}
/>
))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Consider using action ID instead of command as key.

Using action.command as the React key may cause issues since multiple actions have command: "todo!()" for unimplemented features (visible in uxhost-actions.ts). This would result in duplicate keys.

Consider using the action's unique identifier or index instead:

           filteredActions.map((action) => (
             <KeyboardShortcutRow
-              key={action.command}
+              key={action.name}
               action={action}
               selectedPlatform={selectedPlatform}
             />

Or better, iterate with action ID from the original actions object.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
filteredActions.map((action) => (
<KeyboardShortcutRow
key={action.command}
action={action}
selectedPlatform={selectedPlatform}
/>
))
filteredActions.map((action) => (
<KeyboardShortcutRow
key={action.name}
action={action}
selectedPlatform={selectedPlatform}
/>
))
🤖 Prompt for AI Agents
In editor/grida-canvas-hosted/playground/uxhost-settings-keyboardshortcuts.tsx
around lines 178 to 184, using action.command as the React key can produce
duplicates because many unimplemented actions share command "todo!()"; replace
it with a stable unique identifier (preferably action.id from the original
actions collection) so each KeyboardShortcutRow gets a unique, stable key—if
action.id is not present, fall back to a composed unique string (e.g., index or
a combination of action properties) but prefer the original action ID for
stability.

Comment on lines +49 to +66
/**
* Get the first keybinding as a string for an action
* Returns empty string if not found
*
* @throws {Error} If actionId is not a valid action ID
*/
export function keyboardShortcutText(
actionId: UXHostActionId,
platform?: "mac" | "windows" | "linux"
): string {
const sequences = getResolvedSequences(actionId, platform);
if (sequences.length === 0) {
return "";
}

const targetPlatform = platform || getKeyboardOS();
return sequenceToString(sequences[0], targetPlatform);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Incorrect JSDoc: function doesn't throw.

The JSDoc states @throws {Error} If actionId is not a valid action ID, but the function actually returns an empty string when the action is not found (via getResolvedSequences returning []). Either update the JSDoc or add the throw behavior.

 /**
  * Get the first keybinding as a string for an action
  * Returns empty string if not found
- *
- * @throws {Error} If actionId is not a valid action ID
  */
 export function keyboardShortcutText(
🤖 Prompt for AI Agents
In editor/grida-canvas-hosted/playground/uxhost-shortcut-renderer.tsx around
lines 49 to 66, the JSDoc incorrectly states the function throws an Error for
invalid actionId while the implementation returns an empty string when no
sequences are found; update the JSDoc to remove the @throws tag and document
that the function returns an empty string if the actionId has no keybinding, or
alternatively change the implementation to throw an Error when sequences.length
=== 0 (choose one approach and apply consistently across similar functions), and
ensure the JSDoc matches the chosen behavior.

Comment on lines +191 to +207
function isApplePlatform(): boolean {
const platform = typeof navigator === "object" ? navigator.platform : "";
return /Mac|iPod|iPhone|iPad/.test(platform);
}

/**
* Detect the current keyboard OS
* this is not reliable for general platform/os detection, but good enough for keyboard OS detection
*
* mostly to determine cmdctrl key (and for cases that keybindings are fundamentally different, e.g. ctrl+c being color picker on mac, but copy on windows/linux)
*/
export function getKeyboardOS(): "mac" | "windows" | "linux" {
if (isApplePlatform()) return "mac";
const platform = navigator.platform.toLowerCase();
if (platform.includes("win")) return "windows";
return "linux";
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make getKeyboardOS safe in non‑browser / SSR environments

getKeyboardOS() unconditionally accesses navigator.platform, which will throw (ReferenceError: navigator is not defined) if this code is ever executed during SSR or in a non‑DOM environment. Since keybindingsToKeyCodes() defaults to calling getKeyboardOS() when platform is omitted, any server‑side usage will break.

Consider short‑circuiting for non‑browser contexts:

export function getKeyboardOS(): "mac" | "windows" | "linux" {
  if (typeof navigator === "undefined" || !navigator.platform) {
    // Reasonable default for headless/SSR; pick whichever fits your UX best
    return "linux";
  }
  if (isApplePlatform()) return "mac";
  const platform = navigator.platform.toLowerCase();
  if (platform.includes("win")) return "windows";
  return "linux";
}

and keep keybindingsToKeyCodes callers passing an explicit platform where possible.

Also applies to: 216-267

Comment on lines +382 to +404
export function uikbdk(
key: M | KeyCode,
platform?: "mac" | "windows" | "linux"
): string {
const targetPlatform = platform || getKeyboardOS();

// Check if it's a modifier bitmask (M enum values are small bitmasks: 1, 2, 4, 8, 16)
// KeyCodes are enum values that start from a much higher number
// We check if it's a valid modifier by trying to resolve it
// If it resolves to modifier keys, it's a modifier; otherwise treat as KeyCode
const resolvedMods = resolveMods(key, targetPlatform);
if (
resolvedMods.length > 0 &&
key & (M.Ctrl | M.Shift | M.Alt | M.Meta | M.CtrlCmd)
) {
// It's a modifier bitmask - return the first resolved key's label
// In practice, most single modifiers resolve to a single key
return keycodeToPlatformUILabel(resolvedMods[0], targetPlatform);
}

// It's a KeyCode
return keycodeToPlatformUILabel(key as KeyCode, targetPlatform);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

uikbdk misclassifies normal KeyCode values as modifiers

uikbdk currently determines whether key is a modifier mask or a KeyCode by:

const resolvedMods = resolveMods(key, targetPlatform);
if (
  resolvedMods.length > 0 &&
  key & (M.Ctrl | M.Shift | M.Alt | M.Meta | M.CtrlCmd)
) {
  // treat as modifier
}

This assumes KeyCode values are “much higher numbers” than the small bitmasks in M, but your KeyCode enum starts at 0 and increments (e.g. Backspace = 1, Tab = 2, etc.). As a result, many normal keys (letters, digits, etc.) will share low bits with M.*, resolveMods will return non‑empty, and uikbdk(KeyCode.KeyI) will incorrectly be treated as a modifier and rendered as something like /Ctrl instead of "I".

You should detect modifier masks explicitly rather than by bit overlap. For example:

export function uikbdk(
  key: M | KeyCode,
  platform?: "mac" | "windows" | "linux"
): string {
  const targetPlatform = platform || getKeyboardOS();

  const isSingleModifier =
    key === M.Ctrl ||
    key === M.Shift ||
    key === M.Alt ||
    key === M.Meta ||
    key === M.CtrlCmd;

  if (isSingleModifier) {
    const resolvedMods = resolveMods(key, targetPlatform);
    if (resolvedMods.length > 0) {
      return keycodeToPlatformUILabel(resolvedMods[0], targetPlatform);
    }
  }

  // Treat everything else as a KeyCode
  return keycodeToPlatformUILabel(key as KeyCode, targetPlatform);
}

This keeps uikbdk(M.CtrlCmd) working while ensuring uikbdk(KeyCode.KeyI) returns the correct per‑platform label.

🤖 Prompt for AI Agents
In editor/grida-canvas/keybinding.ts around lines 382 to 404, uikbdk
misclassifies normal KeyCode values as modifier masks by checking bit overlap;
change the logic to explicitly detect single modifier constants (M.Ctrl,
M.Shift, M.Alt, M.Meta, M.CtrlCmd) and only call resolveMods/return a modifier
label for those exact matches, otherwise always treat the input as a KeyCode and
return keycodeToPlatformUILabel(key as KeyCode, targetPlatform).

@softmarshmallow softmarshmallow merged commit c52f3b7 into main Dec 30, 2025
10 checks passed
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