Skip to content

Conversation

@softmarshmallow
Copy link
Member

@softmarshmallow softmarshmallow commented Oct 28, 2025

  • ruler on by default, add context menu to ruler
  • fix forms edit page

Summary by CodeRabbit

Release Notes

  • New Features

    • Added context menu for rulers with option to toggle visibility
    • Enhanced corner radius control with individual per-corner adjustment toggle
    • Improved form input grouping and organization components
    • Added keyboard shortcut display elements
  • UI Improvements

    • Refined button styling and added new icon size variants
    • Improved layout spacing and visual refinements
  • Bug Fixes

    • Improved paste behavior with smarter parent selection logic
    • Fixed ruler initialization state
  • Documentation

    • Added comprehensive AI Agent workflow guide

@codesandbox
Copy link

codesandbox bot commented Oct 28, 2025

Review or Edit in CodeSandbox

Open the branch in Web EditorVS CodeInsiders

Open Preview

@vercel
Copy link

vercel bot commented Oct 28, 2025

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

Project Deployment Preview Comments Updated (UTC)
docs Ready Ready Preview Comment Oct 28, 2025 10:36am
grida Canceled Canceled Oct 28, 2025 10:36am
5 Skipped Deployments
Project Deployment Preview Comments Updated (UTC)
code Ignored Ignored Oct 28, 2025 10:36am
legacy Ignored Ignored Oct 28, 2025 10:36am
backgrounds Skipped Skipped Oct 28, 2025 10:36am
blog Skipped Skipped Oct 28, 2025 10:36am
viewer Skipped Skipped Oct 28, 2025 10:36am

@coderabbitai
Copy link

coderabbitai bot commented Oct 28, 2025

Walkthrough

This PR refactors the Spinner component from a custom implementation to a lucide-react-based one, reorganizing it from components/spinner to components/ui/spinner. It updates 40+ files to use the new import path, adds several new UI components (InputGroup, ButtonGroup, Kbd), enhances canvas paste logic with null-safe handling, improves corner radius and ruler controls, and bumps Radix UI dependencies.

Changes

Cohort / File(s) Summary
Documentation
apps/docs/AGENTS.md, docs/wg/feat-layout/index.md
Added AI Agent workflow documentation; updated reference formatting and minor wording adjustments
Spinner Component Migration
editor/components/spinner/index.ts, editor/components/spinner/spinner.tsx
Removed old custom Spinner re-export and component implementation
Spinner UI Component (New)
editor/components/ui/spinner.tsx
Created new Spinner component using lucide-react Loader2Icon with accessibility attributes
New UI Components
editor/components/ui/button-group.tsx, editor/components/ui/input-group.tsx, editor/components/ui/kbd.tsx
Added ButtonGroup (with variants and subcomponents), InputGroup (with Addon, Button, Text, Input, Textarea variants), and Kbd/KbdGroup components
Existing UI Component Updates
editor/components/ui/button.tsx, editor/components/ui/input.tsx, editor/components/ui/separator.tsx
Removed shadow-xs from buttons; added icon-sm/icon-lg sizes; removed flex utility from input; normalized separator data-slot and formatting
AI Component Adjustments
editor/app/(dev)/canvas/tools/ai/_components/model-selector.tsx, editor/app/(dev)/canvas/tools/ai/page.tsx
Changed button width from w-full to w-auto; updated Spinner import path
Spinner Import Path Updates (40+ files)
editor/app/(dev)/ui/components/spinner/page.tsx, editor/app/(library)/library/_components/gallery.tsx, editor/app/(site)/organizations/new/page.tsx, editor/app/(site)/sign-in/email/_page.tsx, editor/app/(tenant)/~/[tenant]/(p)/p/login/login.tsx, editor/app/(workbench)/[org]/[proj]/(console)/(campaign)/campaigns/[campaign]/_components/logs-table.tsx, editor/app/(workbench)/[org]/[proj]/(console)/(campaign)/campaigns/[campaign]/_components/settings.tsx, editor/app/(workbench)/[org]/[proj]/(console)/(campaign)/campaigns/[campaign]/design/page.tsx, editor/app/(workbench)/[org]/[proj]/(console)/(resources)/campaigns/page.tsx, editor/app/(workbench)/[org]/[proj]/(console)/(resources)/customers/[uid]/page.tsx, editor/app/(workbench)/[org]/[proj]/(console)/(resources)/www/page.tsx, editor/app/(workbench)/[org]/[proj]/[id]/canvas/page.tsx, editor/app/(workbench)/[org]/[proj]/[id]/connect/database/supabase/page.tsx, editor/app/(workbench)/[org]/[proj]/[id]/data/(data)/page.tsx, editor/app/(workbench)/[org]/[proj]/[id]/data/(data)/table/[tablename]/page.tsx, editor/app/(workbench)/[org]/[proj]/[id]/form/edit/page.tsx, editor/app/(workbench)/[org]/[proj]/[id]/form/start/page.tsx, editor/app/(workbench)/[org]/[proj]/[id]/objects/[[...path]]/page.tsx, editor/components/dialogs/delete-confirmation-dialog.tsx, editor/components/formfield/file-upload-field/file-upload-field.tsx, editor/components/mediaviewer/index.tsx, editor/components/pdf-viewer/pdf-viewer.tsx, editor/grida-forms-hosted/e/formview.tsx, editor/scaffolds/ai/form-field-schema-assistant.tsx, editor/scaffolds/grid-editor/components/count.tsx, editor/scaffolds/grid-editor/components/refresh.tsx, editor/scaffolds/grid/cells/file-cell.tsx, editor/scaffolds/grid/cells/json-cell.tsx, editor/scaffolds/grid/widgets/fk-referenced-row-lookup-popover.tsx, editor/scaffolds/mediapicker/index.tsx, editor/scaffolds/panels/extensions/field-x-sb-storage-settings.tsx, editor/scaffolds/platform/customer/customer-edit-dialog.tsx, editor/scaffolds/platform/www/www-layout-provider.tsx, editor/scaffolds/settings/closing-preference.tsx, editor/scaffolds/settings/customize/custom-ending-page-preferences.tsx, editor/scaffolds/settings/customize/custom-ending-redirect-preferences.tsx, editor/scaffolds/settings/data-dynamic-field-preferences.tsx, editor/scaffolds/settings/form-method-preference.tsx, editor/scaffolds/settings/response-preferences.tsx, editor/scaffolds/settings/scheduling-preference/index.tsx, editor/scaffolds/workbench/saving-indicator.tsx, editor/scaffolds/workspace/create-new-document-button/create-document-button.tsx, editor/scaffolds/workspace/new-project-dialog.tsx, editor/scaffolds/workspace/workspace.tsx, editor/theme/templates/west-referral/referrer/share.tsx
Updated Spinner import from @/components/spinner to @/components/ui/spinner
Canvas & Viewport Updates
editor/grida-canvas-react/viewport/surface.tsx, editor/grida-canvas/editor.i.ts
Added RulerContextMenu wrapper for context-based ruler hiding; changed default ruler state to "on"
Document Reducer & Transform Logic
editor/grida-canvas/reducers/document.reducer.ts, editor/grida-canvas/reducers/methods/transform.ts, editor/grida-canvas/reducers/surface.reducer.ts
Refactored paste logic with target parent calculation and hit-test fallback; improved null-safe parent comparison; robustified layout snapshot with nullable bounding rect handling
Side Control UI Refactoring
editor/scaffolds/sidecontrol/controls/corner-radius.tsx
Replaced Popover-based corner radius UI with state-driven inline toggle between uniform and individual corner inputs
Side Control Provider Reorganization
editor/scaffolds/sidecontrol/index.tsx, editor/scaffolds/sidecontrol/sidecontrol-doctype-canvas.tsx
Removed FontFamilyListProvider from main index; moved it into sidecontrol-doctype-canvas
Side Control Global & Selection
editor/scaffolds/sidecontrol/sidecontrol-global.tsx, editor/scaffolds/sidecontrol/sidecontrol-node-selection.tsx
Reduced imports; disabled FormStartPageControl rendering; added optional disabled prop to Align component tied to root selection state
Sidebar & Package Updates
editor/scaffolds/sidebar/sidebar.tsx, editor/package.json
Added CaretLeftIcon to Back to Dashboard menu item; bumped @radix-ui/react-separator, @radix-ui/react-slot, react-resizable-panels versions

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant Surface as Viewport Surface
    participant RCM as RulerContextMenu
    participant Ruler as AxisRuler
    participant Editor as Editor
    participant Toast as Toast

    User->>Surface: Right-click on ruler
    Surface->>RCM: Trigger context menu
    RCM->>Ruler: Render ruler with menu wrapper
    User->>RCM: Click "Hide ruler"
    RCM->>Editor: Call editor.surface.surfaceConfigureRuler("off")
    Editor->>Editor: Update ruler state
    RCM->>Toast: Show "Ruler off" notification
    Toast-->>User: Display success message
Loading
sequenceDiagram
    participant App as App
    participant Paste as Paste Handler
    participant Target as Target Selection
    participant HitTest as Hit Test
    participant Transform as Transform

    App->>Paste: User pastes nodes
    Paste->>Target: Calculate target parents from selection
    alt Selection exists
        Target->>Target: Use container parent or parent container
        Paste->>Transform: Transform with selected parent
    else No selection
        Paste->>HitTest: Perform nested insertion hit-test
        HitTest->>HitTest: Exclude copied nodes, filter containers
        alt Parent found via hit-test
            Paste->>Transform: Adjust positions relative to hit-tested parent
            Transform->>App: Apply layout adjustments
        else No parent found
            Paste->>App: Paste at scene root
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Areas requiring extra attention:

  • Document Reducer Paste Logic (editor/grida-canvas/reducers/document.reducer.ts): Substantial refactor of paste target selection with new hit-test fallback and position adjustment logic; requires verification that sibling pasting and hit-test scenarios work correctly across all paste workflows.
  • Corner Radius Control UI Rewrite (editor/scaffolds/sidecontrol/controls/corner-radius.tsx): High-density component rewrite replacing Popover with inline state-driven UI; needs validation of toggle state synchronization, individual corner input sync, and uniform-to-individual mode transitions.
  • Canvas Reducer & Transform Null-Safety Changes (editor/grida-canvas/reducers/methods/transform.ts, editor/grida-canvas/reducers/surface.reducer.ts): Logic changes for nullable parent references and bounding rect handling; verify that normalization of null parent to scene_id works correctly and that layout snapshots handle missing rects gracefully.
  • Spinner Migration (40+ files): While repetitive, verify that the new UI spinner (@/components/ui/spinner) with lucide-react Loader2Icon renders and animates consistently across all consuming pages and that the old spinner module is fully removed/unused.
  • FontFamilyListProvider Relocation (editor/scaffolds/sidecontrol/): Provider moved from main index to doctype-canvas; ensure font list is accessible to all child components that previously relied on it and that the hook usage (useCurrentEditor, useEditorState) correctly derives the font list.
  • Align Component disabled Prop (editor/scaffolds/sidecontrol/sidecontrol-node-selection.tsx): New disabled logic tied to root node selection; verify that alignment controls disable correctly for root nodes and that the disabled state propagates properly to the underlying _AlignControl.

Possibly related PRs

Poem

🐰 A spinner reborn in Lucide's glow,
UI components dance in a winding row,
Paste logic strengthened with safer ways,
Rulers with context menus blaze,
Canvas control refined, layer by layer—
The editor sharpens its painter!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title Check ❓ Inconclusive The title "Daily RC" is a generic, non-descriptive label that does not convey meaningful information about the changeset. While the PR description mentions that the changes include ruler configuration updates and forms fixes, the title itself fails to reflect either of these points or any other specific aspect of the substantial changes present. The title reads as an internal naming convention (potentially "daily release candidate") rather than a summary of the actual modifications, leaving reviewers without clear guidance about the primary intent or content of the changes from the title alone. Consider revising the title to be more descriptive and specific. Examples might include "Migrate Spinner component to UI directory and enable ruler by default" or "Relocate Spinner component and add ruler context menu" to better reflect the substantive changes in the changeset. This would help reviewers quickly understand the primary modifications without needing to review the full changeset or PR description.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ 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.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

import { Spinner } from "../../../components/spinner";

P0 Badge Update Tiptap image view to new spinner module

The relative import "../../../components/spinner" points to the deleted editor/components/spinner folder, so this module can no longer be resolved and the editor fails to build. Switch the import to the new location (e.g. @/components/ui/spinner) or restore the previous barrel export.


import * as React from "react";
import { Spinner } from "../../../components/spinner";
import { cn } from "@/components/lib/utils";

P0 Badge Update Tiptap image overlay to new spinner module

This component still imports Spinner from the removed editor/components/spinner directory, so bundling or typechecking will fail with “Cannot find module '../../../components/spinner'”. Import the spinner from @/components/ui/spinner instead.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +24 to +29
function ButtonGroup({
className,
orientation,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
return (

Choose a reason for hiding this comment

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

P0 Badge Add React import for button group component

This file uses React.ComponentProps to type the component props, but React is never imported. With jsx: "react-jsx" the runtime import is optional, yet the React namespace is still required for type references and tsc will fail with “Cannot find namespace 'React'” when building. Add import * as React from "react"; (or import just the needed types) before using React.ComponentProps.

Useful? React with 👍 / 👎.

Comment on lines +5 to +6
function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
return (

Choose a reason for hiding this comment

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

P0 Badge Add React import for Spinner component

The new spinner component also types its props via React.ComponentProps<"svg"> without importing React, which causes the same Cannot find namespace 'React' TypeScript error as soon as this module is compiled. Import React (or just type ComponentProps) so the file type-checks.

Useful? React with 👍 / 👎.

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: 5

Caution

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

⚠️ Outside diff range comments (4)
editor/app/(site)/sign-in/email/_page.tsx (1)

86-92: Block open redirects from query params

next and redirect_uri are used directly. This allows open-redirect to external origins. Constrain to same-origin relative paths.

Apply:

@@
-    if (next) {
-      router.replace(next);
-    } else if (redirect_uri) {
-      router.replace(redirect_uri);
-    } else {
-      router.replace("/dashboard");
-    }
+    const safeRedirect = (target?: string) => {
+      if (!target) return "/dashboard";
+      try {
+        const url = new URL(target, window.location.origin);
+        if (url.origin !== window.location.origin) return "/dashboard";
+        return url.pathname + url.search + url.hash;
+      } catch {
+        return target.startsWith("/") ? target : "/dashboard";
+      }
+    };
+    if (next) {
+      router.replace(safeRedirect(next));
+    } else if (redirect_uri) {
+      router.replace(safeRedirect(redirect_uri));
+    } else {
+      router.replace("/dashboard");
+    }
editor/scaffolds/settings/response-preferences.tsx (3)

100-113: Coerce number input to a number in Controller

Without coercion, RHF will store a string; n comparisons and API payload may be wrong.

               <Controller
                 name="max"
                 control={control}
                 render={({ field }) => (
                   <Input
                     type="number"
+                    inputMode="numeric"
                     min={1}
                     value={field.value}
-                    onChange={field.onChange}
+                    step={1}
+                    onChange={(e) => field.onChange(e.target.valueAsNumber)}
                   />
                 )}
               />

268-279: Apply the same numeric coercion in “total responses” section

Mirror the fix to keep types consistent and avoid string payloads.

                 <Controller
                   name="max"
                   control={control}
                   render={({ field }) => (
                     <Input
                       type="number"
                       placeholder="Leave empty for unlimited responses"
                       min={1}
-                      value={field.value}
-                      onChange={field.onChange}
+                      inputMode="numeric"
+                      step={1}
+                      value={field.value}
+                      onChange={(e) => field.onChange(e.target.valueAsNumber)}
                     />
                   )}
                 />

163-176: Fix minor text typos

Small copy fixes for polish.

-            Lean more
+            Learn more
@@
-            Fingerprint generation for some platform/environment may confict
+            Fingerprint generation for some platforms/environments may conflict
@@
-            <strong>Vunarable platforms:</strong>
+            <strong>Vulnerable platforms:</strong>

Also applies to: 168-170, 172-176

🧹 Nitpick comments (20)
editor/scaffolds/sidebar/sidebar.tsx (1)

61-64: Good UI enhancement with the back navigation icon.

The CaretLeftIcon provides a clear visual cue for the back action. For consistency with the SlashIcon usage on line 68 (which has explicit width/height props), consider adding size props to the CaretLeftIcon.

-                  <CaretLeftIcon />
+                  <CaretLeftIcon width={15} height={15} />
editor/scaffolds/sidecontrol/sidecontrol-node-selection.tsx (1)

1361-1361: Good use of the disabled prop for root nodes.

Disabling alignment controls when is_root is true makes sense—alignment operations require sibling context, which root nodes lack by definition. The logic correctly prevents meaningless operations.

Optional enhancement: Consider adding a tooltip to the Align component that explains why alignment is disabled when viewing a root node. This would improve discoverability, though it's not critical.

Example enhancement to the Align component:

function Align({ disabled }: { disabled?: boolean }) {
  const editor = useCurrentEditor();
  const { selection } = useSelectionState();
  const has_selection = selection.length >= 1;

  return (
    <Tooltip>
      <TooltipTrigger asChild>
        <_AlignControl
          disabled={!has_selection || disabled}
          onAlign={(alignment) => {
            editor.commands.align("selection", alignment);
          }}
          onDistributeEvenly={(axis) => {
            editor.commands.distributeEvenly("selection", axis);
          }}
          className="justify-between"
        />
      </TooltipTrigger>
      {disabled && (
        <TooltipContent>
          Alignment unavailable for root nodes
        </TooltipContent>
      )}
    </Tooltip>
  );
}
editor/grida-canvas/reducers/surface.reducer.ts (1)

43-53: Consider null-safe handling for child node bounding rects.

For consistency with the null-safety improvements made to parent bounding rects (lines 36-40), consider handling the case where abs_rect might be undefined. While child nodes in a layout snapshot should typically have bounding rects, defensive coding would prevent potential runtime errors.

Example:

 const objects: editor.gesture.LayoutSnapshot["objects"] = items.map(
   (node_id) => {
-    const abs_rect = context.geometry.getNodeAbsoluteBoundingRect(node_id)!;
+    const abs_rect = context.geometry.getNodeAbsoluteBoundingRect(node_id);
+    if (!abs_rect) {
+      throw new Error(`Node ${node_id} has no bounding rect`);
+    }
     const rel_rect = cmath.rect.translate(abs_rect, reldelta);

     return {
       ...rel_rect,
       id: node_id,
     };
   }
 );
editor/grida-canvas/reducers/document.reducer.ts (2)

688-701: Null-guard hit‑tested parent rect to avoid rare runtime crash

Geometry may return null for degenerate/invisible nodes. Guard instead of asserting.

-            if (parent_was_hit_tested && parent) {
-              const parent_rect =
-                context.geometry.getNodeAbsoluteBoundingRect(parent)!;
-              sub.scene.children_refs.forEach((node_id) => {
-                const node = sub.nodes[node_id];
-                if ("position" in node && node.position === "absolute") {
-                  node.left = (node.left ?? 0) - parent_rect.x;
-                  node.top = (node.top ?? 0) - parent_rect.y;
-                }
-              });
-            }
+            if (parent_was_hit_tested && parent) {
+              const parent_rect =
+                context.geometry.getNodeAbsoluteBoundingRect(parent);
+              if (parent_rect) {
+                sub.scene.children_refs.forEach((node_id) => {
+                  const node = sub.nodes[node_id];
+                  if ("position" in node && node.position === "absolute") {
+                    node.left = (node.left ?? 0) - parent_rect.x;
+                    node.top = (node.top ?? 0) - parent_rect.y;
+                  }
+                });
+              }
+            }

603-608: UX follow‑up: remember last hit‑tested parent for subsequent pastes

Current limitation makes only the first paste position-adjusted when using hit‑test. Consider storing the last hit‑tested parent in transient editor state and using it as the implicit target on immediate subsequent pastes (until selection changes), to keep paste behavior consistent.

Is duplicating across multiple selected parents an intended behavior change vs. previous “first valid target” logic?

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

1644-1654: Consider adding bidirectional ruler toggle in context menu.

Currently, the context menu only provides a "Hide ruler" option. For improved UX, consider adding conditional logic to show either "Hide ruler" or "Show ruler" based on the current ruler state. This would allow users to toggle the ruler directly from the context menu without needing to access other menus.

Example implementation:

+  const ruler = useEditorState(editor, (state) => state.ruler);
+
   return (
     <ContextMenu>
       <ContextMenuTrigger asChild>{children}</ContextMenuTrigger>
       <ContextMenuContent className="w-48">
         <ContextMenuItem
           className="text-xs"
           onSelect={() => {
-            editor.surface.surfaceConfigureRuler("off");
-            toast.success("Ruler off");
+            const newState = ruler === "on" ? "off" : "on";
+            editor.surface.surfaceConfigureRuler(newState);
+            toast.success(`Ruler ${newState}`);
           }}
         >
-          Hide ruler
+          {ruler === "on" ? "Hide ruler" : "Show ruler"}
           <ContextMenuShortcut>⇧R</ContextMenuShortcut>
         </ContextMenuItem>
       </ContextMenuContent>
     </ContextMenu>
   );
editor/components/ui/separator.tsx (1)

1-6: Code formatting improvements.

Added semicolons for consistency across the file. This improves code style uniformity.

Also applies to: 25-28

editor/app/(workbench)/[org]/[proj]/[id]/canvas/page.tsx (1)

30-32: Add minimal a11y context for loading state.

A lone spinner can be silent for screen readers. Optional: wrap with a status container and a visually hidden label.

-  if (!document) {
-    return <Spinner />;
-  }
+  if (!document) {
+    return (
+      <div role="status" aria-live="polite" className="flex items-center gap-2">
+        <Spinner aria-label="Loading canvas" />
+        <span className="sr-only">Loading canvas…</span>
+      </div>
+    );
+  }
editor/scaffolds/grid-editor/components/refresh.tsx (1)

16-26: Improve loading announcement on the button (optional).

Expose busy state and announce label changes for AT users.

-    <Button
-      disabled={refreshing}
-      onClick={onRefreshClick}
-      variant="outline"
-      size="sm"
-    >
-      <span className="me-2">
+    <Button
+      disabled={refreshing}
+      onClick={onRefreshClick}
+      variant="outline"
+      size="sm"
+      aria-busy={!!refreshing}
+    >
+      <span className="me-2" aria-live="polite">
         {refreshing ? <Spinner /> : <ReloadIcon className="w-3.5 h-3.5" />}
       </span>
       {refreshing ? "Loading..." : "Refresh"}
     </Button>
editor/scaffolds/workspace/create-new-document-button/create-document-button.tsx (1)

341-346: Optional: expose busy state and add SR text on create actions.

Keeps the UI identical while improving assistive tech feedback.

-          <Button
-            disabled={save_disabled}
-            onClick={onSaveClick}
-            className="min-w-20"
-          >
-            {busy ? <Spinner /> : <>Create</>}
+          <Button
+            disabled={save_disabled}
+            onClick={onSaveClick}
+            className="min-w-20"
+            aria-busy={busy}
+          >
+            {busy ? (
+              <>
+                <Spinner aria-label="Creating database" />
+                <span className="sr-only">Creating database…</span>
+              </>
+            ) : (
+              <>Create</>
+            )}
           </Button>
-          <Button disabled={busy} onClick={onSaveClick} className="min-w-20">
-            {busy ? <Spinner /> : <>Create</>}
+          <Button disabled={busy} onClick={onSaveClick} className="min-w-20" aria-busy={busy}>
+            {busy ? (
+              <>
+                <Spinner aria-label="Creating bucket" />
+                <span className="sr-only">Creating bucket…</span>
+              </>
+            ) : (
+              <>Create</>
+            )}
           </Button>

Also applies to: 445-446

editor/app/(site)/sign-in/email/_page.tsx (1)

41-67: Harden loading/error handling with try/finally and clear previous errors

Ensure loading is reset on throw and clear stale error before verification.

@@
   const handleEmail = async (e: React.FormEvent) => {
     e.preventDefault();
@@
-    setIsLoading(true);
-
-    // Simulate API call
-    const { data, error } = await supabase.auth.signInWithOtp({
-      email: email,
-      options: {
-        shouldCreateUser: false,
-      },
-    });
-    setIsLoading(false);
-
-    if (error) {
-      console.log("error", error);
-      toast.error(error.message);
-      return;
-    }
-
-    setStep("otp");
+    setIsLoading(true);
+    try {
+      const { data, error } = await supabase.auth.signInWithOtp({
+        email,
+        options: { shouldCreateUser: false },
+      });
+      if (error) {
+        toast.error(error.message);
+        return;
+      }
+      setStep("otp");
+    } finally {
+      setIsLoading(false);
+    }
   };
@@
-  const handleOtp = async (otp: string) => {
-    setIsLoading(true);
-    const {
-      data: { session },
-      error,
-    } = await supabase.auth.verifyOtp({
-      email,
-      token: otp,
-      type: "email",
-    });
-    setIsLoading(false);
-
-    if (error) {
-      setError(error.message);
-      return;
-    }
-
-    if (next) {
-      router.replace(next);
-    } else if (redirect_uri) {
-      router.replace(redirect_uri);
-    } else {
-      router.replace("/dashboard");
-    }
-  };
+  const handleOtp = async (otp: string) => {
+    setError("");
+    setIsLoading(true);
+    try {
+      const {
+        data: { session },
+        error,
+      } = await supabase.auth.verifyOtp({
+        email,
+        token: otp,
+        type: "email",
+      });
+      if (error) {
+        setError(error.message);
+        return;
+      }
+      // redirect handled below with safeRedirect in the other diff
+      if (next) {
+        router.replace(next);
+      } else if (redirect_uri) {
+        router.replace(redirect_uri);
+      } else {
+        router.replace("/dashboard");
+      }
+    } finally {
+      setIsLoading(false);
+    }
+  };

Also applies to: 69-93

editor/components/ui/spinner.tsx (2)

5-14: Type props from Loader2Icon and add aria-live

Use the icon’s props for better TS coverage and announce politely to AT.

-function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
+function Spinner({
+  className,
+  ...props
+}: React.ComponentProps<typeof Loader2Icon>) {
   return (
     <Loader2Icon
       role="status"
-      aria-label="Loading"
+      aria-label="Loading"
+      aria-live="polite"
       className={cn("size-4 animate-spin", className)}
       {...props}
     />
   )
 }

3-3: Optional: unify utils import path

Some files import "@/components/lib/utils", others "@/components/lib/utils/index". Consider standardizing to one.

editor/components/ui/input-group.tsx (2)

71-76: Make Addon focus textareas too and ignore link clicks

Current query focuses only inputs and only ignores buttons.

-      onClick={(e) => {
-        if ((e.target as HTMLElement).closest("button")) {
-          return;
-        }
-        e.currentTarget.parentElement?.querySelector("input")?.focus();
-      }}
+      onClick={(e) => {
+        const target = e.target as HTMLElement;
+        if (target.closest("button, a")) return;
+        const control = e.currentTarget.parentElement?.querySelector(
+          "input, textarea"
+        ) as HTMLElement | null;
+        control?.focus();
+      }}

60-80: Optional: keyboard accessibility for Addon

If Addon is intended to be interactive, support keyboard focus with tabIndex and key handlers; otherwise consider role="presentation".

 function InputGroupAddon({
@@
-    <div
+    <div
+      tabIndex={0}
@@
-      {...props}
+      onKeyDown={(e) => {
+        if (e.key === "Enter" || e.key === " ") {
+          (e.currentTarget.parentElement?.querySelector(
+            "input, textarea"
+          ) as HTMLElement | null)?.focus();
+          e.preventDefault();
+        }
+      }}
+      {...props}
     />
editor/scaffolds/settings/response-preferences.tsx (1)

191-197: Exported name typo: MaxRespoonses requires multi-file refactor

Function has a spelling mistake and is imported and used in editor/app/(workbench)/[org]/[proj]/[id]/form/page.tsx (lines 14, 48). Rename will require updating both files.

-export function MaxRespoonses() {
+export function MaxResponses() {

Update import and usage in editor/app/(workbench)/[org]/[proj]/[id]/form/page.tsx:

  • Line 14: change import from MaxRespoonses to MaxResponses
  • Line 48: change usage from <MaxRespoonses /> to <MaxResponses />
editor/scaffolds/sidecontrol/controls/corner-radius.tsx (4)

1-1: Import useEffect for state sync (optional: type-only React import).

You’ll need useEffect for the sync fix below. Also consider type-only React imports to avoid a runtime default import when only using types.

-import React, { useMemo, useState } from "react";
+import React, { useMemo, useState, useEffect } from "react";

65-71: Default and sync the “individual” view to non-uniform values.

If value is already non-uniform, the UI still opens in “all” mode; also it doesn’t react when value becomes non-uniform later. Initialize and sync to reduce confusion.

-  const [showIndividual, setShowIndividual] = useState(false);
+  const [showIndividual, setShowIndividual] = useState(
+    () => (value ? !isUniform(value) : false)
+  );
@@
   const mode = useMemo(() => {
     if (!value) return "all";
     return isUniform(value) ? "all" : "each";
   }, [value]);
+
+  useEffect(() => {
+    if (value && !isUniform(value)) setShowIndividual(true);
+  }, [value]);

97-104: Avoid coercing NaN/falsy values; use nullish coalescing.

|| 0 treats NaN as falsy and overwrites it to 0. ?? 0 preserves valid falsy numbers semantics and only falls back on undefined.

-    newCorners[index] = newValue || 0;
+    newCorners[index] = newValue ?? 0;

229-238: Use type-only PropsWithChildren to avoid runtime React import.

This keeps runtime imports lean and clarifies intent.

-const Label = ({ children }: React.PropsWithChildren) => {
+import type { PropsWithChildren } from "react";
+const Label = ({ children }: PropsWithChildren) => {
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ba1ad04 and 4c1a144.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (70)
  • apps/docs/AGENTS.md (1 hunks)
  • docs/wg/feat-layout/index.md (1 hunks)
  • editor/app/(dev)/canvas/tools/ai/_components/model-selector.tsx (1 hunks)
  • editor/app/(dev)/canvas/tools/ai/page.tsx (1 hunks)
  • editor/app/(dev)/ui/components/spinner/page.tsx (1 hunks)
  • editor/app/(library)/library/_components/gallery.tsx (1 hunks)
  • editor/app/(site)/organizations/new/page.tsx (1 hunks)
  • editor/app/(site)/sign-in/email/_page.tsx (1 hunks)
  • editor/app/(tenant)/~/[tenant]/(p)/p/login/login.tsx (1 hunks)
  • editor/app/(workbench)/[org]/[proj]/(console)/(campaign)/campaigns/[campaign]/_components/logs-table.tsx (1 hunks)
  • editor/app/(workbench)/[org]/[proj]/(console)/(campaign)/campaigns/[campaign]/_components/settings.tsx (1 hunks)
  • editor/app/(workbench)/[org]/[proj]/(console)/(campaign)/campaigns/[campaign]/design/page.tsx (1 hunks)
  • editor/app/(workbench)/[org]/[proj]/(console)/(resources)/campaigns/page.tsx (1 hunks)
  • editor/app/(workbench)/[org]/[proj]/(console)/(resources)/customers/[uid]/page.tsx (1 hunks)
  • editor/app/(workbench)/[org]/[proj]/(console)/(resources)/www/page.tsx (1 hunks)
  • editor/app/(workbench)/[org]/[proj]/[id]/canvas/page.tsx (1 hunks)
  • editor/app/(workbench)/[org]/[proj]/[id]/connect/database/supabase/page.tsx (1 hunks)
  • editor/app/(workbench)/[org]/[proj]/[id]/data/(data)/page.tsx (1 hunks)
  • editor/app/(workbench)/[org]/[proj]/[id]/data/(data)/table/[tablename]/page.tsx (1 hunks)
  • editor/app/(workbench)/[org]/[proj]/[id]/form/edit/page.tsx (1 hunks)
  • editor/app/(workbench)/[org]/[proj]/[id]/form/start/page.tsx (1 hunks)
  • editor/app/(workbench)/[org]/[proj]/[id]/objects/[[...path]]/page.tsx (1 hunks)
  • editor/components/dialogs/delete-confirmation-dialog.tsx (1 hunks)
  • editor/components/formfield/file-upload-field/file-upload-field.tsx (1 hunks)
  • editor/components/mediaviewer/index.tsx (1 hunks)
  • editor/components/pdf-viewer/pdf-viewer.tsx (1 hunks)
  • editor/components/spinner/index.ts (0 hunks)
  • editor/components/spinner/spinner.tsx (0 hunks)
  • editor/components/ui/button-group.tsx (1 hunks)
  • editor/components/ui/button.tsx (2 hunks)
  • editor/components/ui/input-group.tsx (1 hunks)
  • editor/components/ui/input.tsx (1 hunks)
  • editor/components/ui/kbd.tsx (1 hunks)
  • editor/components/ui/separator.tsx (3 hunks)
  • editor/components/ui/spinner.tsx (1 hunks)
  • editor/grida-canvas-react/viewport/surface.tsx (3 hunks)
  • editor/grida-canvas/editor.i.ts (1 hunks)
  • editor/grida-canvas/reducers/document.reducer.ts (2 hunks)
  • editor/grida-canvas/reducers/methods/transform.ts (1 hunks)
  • editor/grida-canvas/reducers/surface.reducer.ts (2 hunks)
  • editor/grida-forms-hosted/e/formview.tsx (1 hunks)
  • editor/package.json (2 hunks)
  • editor/scaffolds/ai/form-field-schema-assistant.tsx (1 hunks)
  • editor/scaffolds/grid-editor/components/count.tsx (1 hunks)
  • editor/scaffolds/grid-editor/components/refresh.tsx (1 hunks)
  • editor/scaffolds/grid/cells/file-cell.tsx (1 hunks)
  • editor/scaffolds/grid/cells/json-cell.tsx (1 hunks)
  • editor/scaffolds/grid/widgets/fk-referenced-row-lookup-popover.tsx (1 hunks)
  • editor/scaffolds/mediapicker/index.tsx (1 hunks)
  • editor/scaffolds/panels/extensions/field-x-sb-storage-settings.tsx (1 hunks)
  • editor/scaffolds/platform/customer/customer-edit-dialog.tsx (1 hunks)
  • editor/scaffolds/platform/www/www-layout-provider.tsx (1 hunks)
  • editor/scaffolds/settings/closing-preference.tsx (1 hunks)
  • editor/scaffolds/settings/customize/custom-ending-page-preferences.tsx (1 hunks)
  • editor/scaffolds/settings/customize/custom-ending-redirect-preferences.tsx (1 hunks)
  • editor/scaffolds/settings/data-dynamic-field-preferences.tsx (1 hunks)
  • editor/scaffolds/settings/form-method-preference.tsx (1 hunks)
  • editor/scaffolds/settings/response-preferences.tsx (1 hunks)
  • editor/scaffolds/settings/scheduling-preference/index.tsx (1 hunks)
  • editor/scaffolds/sidebar/sidebar.tsx (2 hunks)
  • editor/scaffolds/sidecontrol/controls/corner-radius.tsx (4 hunks)
  • editor/scaffolds/sidecontrol/index.tsx (1 hunks)
  • editor/scaffolds/sidecontrol/sidecontrol-doctype-canvas.tsx (1 hunks)
  • editor/scaffolds/sidecontrol/sidecontrol-global.tsx (5 hunks)
  • editor/scaffolds/sidecontrol/sidecontrol-node-selection.tsx (2 hunks)
  • editor/scaffolds/workbench/saving-indicator.tsx (1 hunks)
  • editor/scaffolds/workspace/create-new-document-button/create-document-button.tsx (1 hunks)
  • editor/scaffolds/workspace/new-project-dialog.tsx (1 hunks)
  • editor/scaffolds/workspace/workspace.tsx (1 hunks)
  • editor/theme/templates/west-referral/referrer/share.tsx (1 hunks)
💤 Files with no reviewable changes (2)
  • editor/components/spinner/spinner.tsx
  • editor/components/spinner/index.ts
🧰 Additional context used
📓 Path-based instructions (10)
editor/scaffolds/**

📄 CodeRabbit inference engine (AGENTS.md)

Place feature-specific larger components/pages/editors under editor/scaffolds

Files:

  • editor/scaffolds/grid-editor/components/count.tsx
  • editor/scaffolds/panels/extensions/field-x-sb-storage-settings.tsx
  • editor/scaffolds/settings/form-method-preference.tsx
  • editor/scaffolds/grid-editor/components/refresh.tsx
  • editor/scaffolds/workbench/saving-indicator.tsx
  • editor/scaffolds/settings/customize/custom-ending-redirect-preferences.tsx
  • editor/scaffolds/settings/scheduling-preference/index.tsx
  • editor/scaffolds/sidecontrol/index.tsx
  • editor/scaffolds/workspace/create-new-document-button/create-document-button.tsx
  • editor/scaffolds/settings/closing-preference.tsx
  • editor/scaffolds/sidecontrol/sidecontrol-doctype-canvas.tsx
  • editor/scaffolds/platform/www/www-layout-provider.tsx
  • editor/scaffolds/grid/cells/file-cell.tsx
  • editor/scaffolds/platform/customer/customer-edit-dialog.tsx
  • editor/scaffolds/settings/data-dynamic-field-preferences.tsx
  • editor/scaffolds/settings/customize/custom-ending-page-preferences.tsx
  • editor/scaffolds/sidecontrol/sidecontrol-node-selection.tsx
  • editor/scaffolds/sidebar/sidebar.tsx
  • editor/scaffolds/mediapicker/index.tsx
  • editor/scaffolds/ai/form-field-schema-assistant.tsx
  • editor/scaffolds/grid/widgets/fk-referenced-row-lookup-popover.tsx
  • editor/scaffolds/grid/cells/json-cell.tsx
  • editor/scaffolds/settings/response-preferences.tsx
  • editor/scaffolds/workspace/workspace.tsx
  • editor/scaffolds/sidecontrol/controls/corner-radius.tsx
  • editor/scaffolds/workspace/new-project-dialog.tsx
  • editor/scaffolds/sidecontrol/sidecontrol-global.tsx
editor/app/(site)/**

📄 CodeRabbit inference engine (AGENTS.md)

Place non-SEO site pages under editor/app/(site)

Files:

  • editor/app/(site)/organizations/new/page.tsx
  • editor/app/(site)/sign-in/email/_page.tsx
editor/components/**

📄 CodeRabbit inference engine (AGENTS.md)

Put generally reusable components under editor/components

Files:

  • editor/components/mediaviewer/index.tsx
  • editor/components/ui/spinner.tsx
  • editor/components/dialogs/delete-confirmation-dialog.tsx
  • editor/components/ui/button-group.tsx
  • editor/components/ui/kbd.tsx
  • editor/components/pdf-viewer/pdf-viewer.tsx
  • editor/components/formfield/file-upload-field/file-upload-field.tsx
  • editor/components/ui/input-group.tsx
  • editor/components/ui/separator.tsx
  • editor/components/ui/input.tsx
  • editor/components/ui/button.tsx
editor/grida-*/**

📄 CodeRabbit inference engine (AGENTS.md)

Use editor/grida-* directories to isolate domain-specific modules pending promotion to /packages

Files:

  • editor/grida-forms-hosted/e/formview.tsx
  • editor/grida-canvas/editor.i.ts
  • editor/grida-canvas-react/viewport/surface.tsx
  • editor/grida-canvas/reducers/methods/transform.ts
  • editor/grida-canvas/reducers/document.reducer.ts
  • editor/grida-canvas/reducers/surface.reducer.ts
editor/app/(workbench)/**

📄 CodeRabbit inference engine (AGENTS.md)

Keep the main editor page under editor/app/(workbench)

Files:

  • editor/app/(workbench)/[org]/[proj]/[id]/data/(data)/page.tsx
  • editor/app/(workbench)/[org]/[proj]/[id]/form/start/page.tsx
  • editor/app/(workbench)/[org]/[proj]/[id]/form/edit/page.tsx
  • editor/app/(workbench)/[org]/[proj]/[id]/data/(data)/table/[tablename]/page.tsx
  • editor/app/(workbench)/[org]/[proj]/[id]/objects/[[...path]]/page.tsx
  • editor/app/(workbench)/[org]/[proj]/[id]/canvas/page.tsx
  • editor/app/(workbench)/[org]/[proj]/(console)/(resources)/customers/[uid]/page.tsx
  • editor/app/(workbench)/[org]/[proj]/(console)/(resources)/www/page.tsx
  • editor/app/(workbench)/[org]/[proj]/(console)/(campaign)/campaigns/[campaign]/_components/settings.tsx
  • editor/app/(workbench)/[org]/[proj]/[id]/connect/database/supabase/page.tsx
  • editor/app/(workbench)/[org]/[proj]/(console)/(campaign)/campaigns/[campaign]/_components/logs-table.tsx
  • editor/app/(workbench)/[org]/[proj]/(console)/(resources)/campaigns/page.tsx
  • editor/app/(workbench)/[org]/[proj]/(console)/(campaign)/campaigns/[campaign]/design/page.tsx
editor/components/ui/**

📄 CodeRabbit inference engine (AGENTS.md)

Keep shadcn UI components under editor/components/ui

Files:

  • editor/components/ui/spinner.tsx
  • editor/components/ui/button-group.tsx
  • editor/components/ui/kbd.tsx
  • editor/components/ui/input-group.tsx
  • editor/components/ui/separator.tsx
  • editor/components/ui/input.tsx
  • editor/components/ui/button.tsx
docs/**

📄 CodeRabbit inference engine (AGENTS.md)

Author and maintain all documentation under the root docs directory; it is the source of truth

Files:

  • docs/wg/feat-layout/index.md
editor/app/(library)/**

📄 CodeRabbit inference engine (AGENTS.md)

Place Grida Library (open assets) pages under editor/app/(library)

Files:

  • editor/app/(library)/library/_components/gallery.tsx
apps/docs/**

📄 CodeRabbit inference engine (AGENTS.md)

Do not author documentation directly in apps/docs; treat it as the deployed Docusaurus site that syncs from /docs

Files:

  • apps/docs/AGENTS.md
editor/app/(tenant)/**

📄 CodeRabbit inference engine (AGENTS.md)

Place tenant-site rendered pages under editor/app/(tenant)

Files:

  • editor/app/(tenant)/~/[tenant]/(p)/p/login/login.tsx
🧠 Learnings (1)
📚 Learning: 2025-10-14T08:23:46.382Z
Learnt from: CR
PR: gridaco/grida#0
File: AGENTS.md:0-0
Timestamp: 2025-10-14T08:23:46.382Z
Learning: Applies to apps/docs/** : Do not author documentation directly in apps/docs; treat it as the deployed Docusaurus site that syncs from /docs

Applied to files:

  • apps/docs/AGENTS.md
🧬 Code graph analysis (13)
editor/components/ui/spinner.tsx (1)
editor/components/lib/utils/index.ts (1)
  • cn (4-6)
editor/scaffolds/sidecontrol/index.tsx (2)
editor/scaffolds/sidecontrol/controls/src.tsx (1)
  • SrcUploaderProvider (31-45)
editor/scaffolds/sidecontrol/sidecontrol-doctype-canvas.tsx (1)
  • SideControlDoctypeCanvas (13-37)
editor/scaffolds/sidecontrol/sidecontrol-doctype-canvas.tsx (3)
editor/grida-canvas/query/index.ts (1)
  • fonts (606-614)
editor/scaffolds/sidecontrol/controls/font-family.tsx (1)
  • FontFamilyListProvider (39-48)
editor/scaffolds/sidecontrol/sidecontrol-document-properties.tsx (1)
  • DocumentProperties (52-111)
editor/components/ui/button-group.tsx (1)
editor/components/lib/utils/index.ts (1)
  • cn (4-6)
editor/components/ui/kbd.tsx (1)
editor/components/lib/utils/index.ts (1)
  • cn (4-6)
editor/scaffolds/sidecontrol/sidecontrol-node-selection.tsx (2)
editor/grida-canvas-react/use-editor.tsx (1)
  • useCurrentEditor (98-106)
editor/grida-canvas-react/provider.tsx (1)
  • useSelectionState (292-298)
editor/grida-canvas-react/viewport/surface.tsx (2)
packages/grida-canvas-ruler/react.tsx (1)
  • AxisRuler (25-62)
editor/grida-canvas-react/use-editor.tsx (1)
  • useCurrentEditor (98-106)
editor/components/ui/input-group.tsx (1)
editor/components/lib/utils/index.ts (1)
  • cn (4-6)
editor/grida-canvas/reducers/document.reducer.ts (1)
editor/grida-canvas/utils/insertion.ts (1)
  • hitTestNestedInsertionTarget (76-97)
editor/components/ui/input.tsx (1)
editor/components/lib/utils/index.ts (1)
  • cn (4-6)
editor/scaffolds/sidecontrol/controls/corner-radius.tsx (3)
packages/grida-canvas-cg/lib.ts (1)
  • CornerRadius4 (1214-1214)
editor/scaffolds/sidecontrol/ui/number.tsx (1)
  • InputPropertyNumber (123-238)
editor/components/lib/utils/index.ts (1)
  • cn (4-6)
editor/grida-canvas/reducers/surface.reducer.ts (1)
crates/grida-canvas/src/window/state.rs (1)
  • context (65-67)
editor/scaffolds/sidecontrol/sidecontrol-global.tsx (2)
editor/grida-canvas/editor.ts (3)
  • state (323-325)
  • state (2255-2257)
  • state (3267-3269)
editor/grida-canvas-react/index.ts (1)
  • useEditorState (1-1)
🪛 LanguageTool
apps/docs/AGENTS.md

[uncategorized] ~39-~39: Did you mean the formatting language “Markdown” (= proper noun)?
Context: ...se plain URLs: https://example.com or markdown links: [text](url) - MDX reserves `<>...

(MARKDOWN_NNP)

🪛 markdownlint-cli2 (0.18.1)
docs/wg/feat-layout/index.md

180-180: Bare URL used

(MD034, no-bare-urls)


182-182: Bare URL used

(MD034, no-bare-urls)


184-184: Bare URL used

(MD034, no-bare-urls)


186-186: Bare URL used

(MD034, no-bare-urls)


193-193: Bare URL used

(MD034, no-bare-urls)


196-196: Bare URL used

(MD034, no-bare-urls)


199-199: Bare URL used

(MD034, no-bare-urls)


202-202: Bare URL used

(MD034, no-bare-urls)

🔇 Additional comments (76)
editor/scaffolds/sidebar/sidebar.tsx (1)

18-18: LGTM!

The import addition is clean and follows the existing pattern in the file.

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

137-154: LGTM! Clean implementation of the disabled prop.

The optional disabled parameter correctly extends the Align component's functionality. The combined logic !has_selection || disabled ensures the control is disabled when there's no selection OR when explicitly requested, which is the correct approach.

editor/grida-canvas/reducers/surface.reducer.ts (2)

36-40: LGTM! Null-safe handling for root nodes.

The conditional check for parent_rect before accessing its properties prevents potential runtime errors when the parent is a scene node. The logic correctly defaults reldelta to [0, 0] when no bounding rect exists, and the comment clearly explains the edge case.


705-705: LGTM! Call site correctly aligned with null-safe signature.

The call site now properly passes parent_id as a potentially nullable value, matching the updated createLayoutSnapshot signature. This ensures consistency with the null-safety improvements.

editor/grida-canvas/reducers/methods/transform.ts (1)

280-285: LGTM! Correct parent comparison normalization.

The normalization logic correctly treats null parent references as equivalent to scene_id, preventing false positives where a node staying in the scene (either as null or scene_id parent) would be incorrectly identified as having changed parents. The implementation is consistent with the pattern used at line 304 (new_parent_id ?? draft.scene_id!), and the non-null assertion is safe given the assertion at line 120.

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

1512-1512: LGTM! Ruler now enabled by default.

This change aligns with the PR objectives and works well with the new context menu feature added in surface.tsx that allows users to hide the ruler.

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

51-57: LGTM! Context menu and toast imports added correctly.

The imports for the ContextMenu components and toast notification are properly structured and support the new ruler context menu feature.

Also applies to: 61-61


1563-1622: LGTM! Context menu integration preserves existing functionality.

The RulerContextMenu wrappers are correctly integrated around both the X-axis and Y-axis rulers. The original gesture handling (bindX/bindY) and AxisRuler rendering are preserved, ensuring backward compatibility while adding the new context menu feature.


1634-1658: Keyboard shortcut is implemented and functional.

The ⇧R (Shift+R) shortcut for toggling the ruler is properly implemented in editor/grida-canvas-react/viewport/hotkeys.tsx (line 746-748) and registered in the keybindings sheet (line 201-203). The shortcut calls editor.surface.surfaceToggleRuler() and displays a success toast, confirming it's fully functional. The context menu correctly displays this shortcut hint.

editor/scaffolds/platform/customer/customer-edit-dialog.tsx (1)

16-16: LGTM! Spinner import path updated.

The import path change aligns with the project-wide refactor to consolidate UI components under @/components/ui. As per coding guidelines.

editor/app/(workbench)/[org]/[proj]/[id]/form/edit/page.tsx (1)

8-8: LGTM! Spinner import path updated.

The import path change aligns with the project-wide refactor to consolidate UI components under @/components/ui. As per coding guidelines.

editor/app/(workbench)/[org]/[proj]/[id]/objects/[[...path]]/page.tsx (1)

68-68: LGTM! Spinner import path updated.

The import path change aligns with the project-wide refactor to consolidate UI components under @/components/ui. As per coding guidelines.

editor/app/(dev)/canvas/tools/ai/_components/model-selector.tsx (1)

43-43: Width change from full to auto.

The button width changed from w-full to w-auto, which will make it size according to content rather than taking full container width. Please verify that this doesn't cause layout issues in different viewport sizes.

You can verify the visual impact by testing the model selector in the canvas tools UI at different screen sizes.

editor/scaffolds/panels/extensions/field-x-sb-storage-settings.tsx (1)

23-23: LGTM! Spinner import path updated.

The import path change aligns with the project-wide refactor to consolidate UI components under @/components/ui. As per coding guidelines.

editor/scaffolds/platform/www/www-layout-provider.tsx (1)

5-5: LGTM! Spinner import path updated.

The import path change aligns with the project-wide refactor to consolidate UI components under @/components/ui. As per coding guidelines.

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

16-16: The review comment appears to be incorrect. After thorough verification:

  • No separator-root found: A comprehensive search of the entire codebase returned zero matches for "separator-root". This value does not exist and therefore cannot have been the previous value.

  • No CSS selectors targeting data-slot: All CSS files in the codebase were searched; none contain [data-slot] attribute selectors that could be affected by this change.

  • Wrapper components are unaffected: Components like button-group.tsx and sidebar.tsx that use the Separator component override the base data-slot attribute with their own context-specific values ("button-group-separator" and "sidebar-separator" respectively), so they won't be impacted.

  • No test dependencies found: No tests reference the data-slot attribute or target the old value.

The change to data-slot="separator" is localized and does not break existing CSS selectors, tests, or downstream code that depends on separator-root.

Likely an incorrect or invalid review comment.

editor/package.json (1)

79-81: Dependency updates verified as backward compatible—no breaking changes detected.

Web search confirmed that all three dependencies are safe:

  • @radix-ui/react-separator v1.1.7 has no breaking changes listed
  • @radix-ui/react-slot v1.2.3 has no documented breaking changes
  • react-resizable-panels v3.0.6 contains only a bug fix with no breaking changes

The updates are indeed minor/patch versions and safe to merge.

editor/scaffolds/sidecontrol/sidecontrol-global.tsx (5)

3-3: LGTM: Removed unused import.

The useEffect import has been removed as it's no longer used in this file.


74-74: LGTM: Simplified imports.

Removed useDocumentState as it's no longer needed, keeping only the required hooks.


90-90: LGTM: Simplified to state-only usage.

The component now only reads state without dispatching actions, which aligns with the refactoring pattern in this PR.


154-192: Renamed to indicate disabled state.

The function has been prefixed with underscore to indicate it's not actively used. This is a good convention for disabled code.


112-113: Form Start Page control is intentionally disabled in the global side control panel, but the feature remains fully functional elsewhere.

The _FormStartPageControl component exists (defined at line 154) and the entire form start page infrastructure remains active throughout the codebase—including a dedicated editor page at editor/app/(workbench)/[org]/[proj]/[id]/form/start/page.tsx, state management, sync mechanisms, and template dialogs. However, the component has a FIXME comment ("FIXME: 250303 UNKNOWN" at line 157) suggesting known issues.

The disabling appears intentional, likely because start page editing functionality is provided through the dedicated form start page rather than the global side control panel. No explicit documentation or issue reference explains the specific reason.

docs/wg/feat-layout/index.md (2)

180-186: LGTM: Fixed MDX-incompatible URL syntax.

The angle-bracketed URLs have been correctly changed to plain URLs, which resolves MDX parsing issues as noted in the AGENTS.md documentation. Angle brackets are reserved for JSX/HTML tags in MDX.


192-203: LGTM: Improved formatting consistency.

The wording adjustments (apostrophes, capitalization, and single-line descriptions) improve readability and consistency across the references section.

editor/scaffolds/sidecontrol/sidecontrol-doctype-canvas.tsx (3)

7-11: LGTM: Added necessary imports for font provider.

The imports support the new FontFamilyListProvider integration, enabling font list context for child components.


14-18: LGTM: Retrieves font list from canvas editor state.

The font list is properly retrieved from state.webfontlist.items using the canvas editor state hook.


22-34: LGTM: Improved provider scope.

Moving FontFamilyListProvider to this doctype-specific component (from the parent index.tsx) narrows its scope and provides better separation of concerns. The fonts are now only provided where they're actually needed.

editor/components/ui/button.tsx (2)

12-18: LGTM: Simplified button shadows.

Removed shadow-xs from default, destructive, and secondary variants for a flatter design. The outline variant retains its shadow, providing visual distinction.


28-29: LGTM: Added granular icon button sizes.

The new icon-sm (size-8) and icon-lg (size-10) variants provide more flexibility for icon button sizing alongside the existing icon (size-9) variant.

editor/scaffolds/sidecontrol/index.tsx (1)

23-27: LGTM: Removed font provider from parent scope.

The FontFamilyListProvider has been moved from this parent component to SideControlDoctypeCanvas where it's actually needed. This improves separation of concerns by scoping the provider only to the canvas doctype.

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

3-16: LGTM: Well-implemented Kbd component.

The component follows the established data-slot pattern and provides appropriate styling for keyboard input display.

editor/components/ui/button-group.tsx (4)

7-22: LGTM: Well-structured variant system.

The buttonGroupVariants uses CVA effectively to manage horizontal and vertical orientations with appropriate Tailwind classes for border radius and border collapsing.


24-38: LGTM: Accessible button group component.

The component properly uses role="group" for accessibility and data-orientation for styling hooks. The implementation follows established patterns in the UI component library.


40-58: LGTM: Flexible text component.

The asChild pattern provides flexibility for composition while maintaining consistent styling.


60-76: LGTM: Properly integrated separator.

The separator component correctly extends the existing Separator component and adapts its orientation based on the group's orientation.

editor/scaffolds/grid/widgets/fk-referenced-row-lookup-popover.tsx (1)

13-13: LGTM! Clean import path update.

The Spinner import path has been correctly updated to the new UI module location with no functional changes.

editor/app/(workbench)/[org]/[proj]/(console)/(campaign)/campaigns/[campaign]/_components/settings.tsx (1)

54-54: LGTM! Import path updated correctly.

The Spinner import has been successfully migrated to the UI module with no behavioral changes.

editor/scaffolds/workspace/workspace.tsx (1)

15-15: LGTM! Import path correctly updated.

The Spinner import has been properly migrated to the new UI module location.

editor/scaffolds/settings/data-dynamic-field-preferences.tsx (1)

23-23: LGTM! Import path migration successful.

The Spinner import has been correctly updated to the UI module with no functional impact.

editor/scaffolds/settings/form-method-preference.tsx (1)

22-22: LGTM! Import path correctly updated.

The Spinner import has been properly migrated to the new UI module location with no behavioral changes.

editor/app/(workbench)/[org]/[proj]/[id]/connect/database/supabase/page.tsx (1)

60-60: LGTM! Import path migration successful.

The Spinner import has been correctly updated to the UI module. All usages throughout the file remain consistent.

editor/app/(workbench)/[org]/[proj]/(console)/(resources)/campaigns/page.tsx (1)

6-6: LGTM! Import path correctly updated.

The Spinner import has been successfully migrated to the new UI module location.

editor/theme/templates/west-referral/referrer/share.tsx (1)

3-3: LGTM! Import path migration complete.

The Spinner import has been correctly updated to the UI module, completing the consistent refactor across all reviewed files.

editor/app/(dev)/canvas/tools/ai/page.tsx (1)

22-22: LGTM - Clean import path refactor.

The Spinner import has been successfully updated to the centralized UI components path. The usage throughout the file remains unchanged and consistent.

editor/scaffolds/workspace/new-project-dialog.tsx (1)

3-3: LGTM - Consistent with UI component reorganization.

The import path update aligns with the project-wide refactor to centralize UI components under the ui/ directory.

editor/scaffolds/settings/customize/custom-ending-page-preferences.tsx (1)

51-51: LGTM - Import path successfully updated.

The Spinner component import has been updated consistently with the broader UI reorganization effort.

editor/scaffolds/settings/scheduling-preference/index.tsx (1)

24-24: LGTM - Clean refactor.

The import update maintains consistency with the centralized UI component structure.

editor/scaffolds/settings/customize/custom-ending-redirect-preferences.tsx (1)

17-17: LGTM - Consistent import update.

The Spinner import path has been updated correctly with no impact on functionality.

editor/components/dialogs/delete-confirmation-dialog.tsx (1)

14-14: LGTM - Import path modernized.

The import has been updated from a relative path to the centralized UI components location, improving maintainability.

editor/app/(site)/organizations/new/page.tsx (1)

17-17: LGTM - Import successfully refactored.

The Spinner import aligns with the centralized UI component structure.

editor/app/(library)/library/_components/gallery.tsx (1)

23-23: LGTM - Consistent refactor complete.

The import path update completes the migration to the centralized UI Spinner component with no behavioral changes.

editor/app/(tenant)/~/[tenant]/(p)/p/login/login.tsx (1)

21-21: LGTM! Clean import path update.

The Spinner import has been correctly updated to use the new UI components path. This aligns with the repository-wide refactor to organize UI components under @/components/ui/.

editor/scaffolds/grid/cells/json-cell.tsx (1)

11-11: LGTM! Import path correctly updated.

The Spinner import path has been updated to match the new UI components structure. No functional changes.

editor/components/mediaviewer/index.tsx (1)

25-25: LGTM! Import path refactored correctly.

The Spinner import has been updated to the new UI components location, consistent with the codebase-wide reorganization.

editor/scaffolds/grid/cells/file-cell.tsx (1)

51-51: LGTM! Spinner import updated correctly.

The import path has been successfully updated to align with the new UI components organization. All Spinner usages throughout the file remain unchanged.

editor/app/(workbench)/[org]/[proj]/(console)/(resources)/customers/[uid]/page.tsx (1)

23-23: LGTM! Import path updated correctly.

The Spinner import has been updated to use the new UI components path. This change is consistent with the repository-wide refactoring effort.

editor/components/pdf-viewer/pdf-viewer.tsx (1)

8-8: LGTM! Import refactored successfully.

The Spinner import path has been updated to the UI components directory, maintaining consistency with the broader refactor.

editor/app/(workbench)/[org]/[proj]/(console)/(resources)/www/page.tsx (1)

17-17: LGTM! Import path correctly updated.

The Spinner import has been successfully migrated to the new UI components location. No functional changes.

editor/app/(workbench)/[org]/[proj]/[id]/data/(data)/page.tsx (1)

4-4: LGTM! Import path updated correctly.

The Spinner import has been successfully refactored to use the new UI components structure, completing the consistent migration across the codebase.

editor/components/formfield/file-upload-field/file-upload-field.tsx (1)

12-12: LGTM! Clean import path refactoring.

The Spinner import path has been correctly updated to the centralized UI module location, consistent with the broader codebase reorganization.

editor/scaffolds/grid-editor/components/count.tsx (1)

1-1: LGTM! Clean import path refactoring.

The Spinner import path has been correctly updated to the centralized UI module location.

editor/app/(workbench)/[org]/[proj]/(console)/(campaign)/campaigns/[campaign]/design/page.tsx (1)

11-11: LGTM! Clean import path refactoring.

The Spinner import path has been correctly updated to the centralized UI module location.

editor/scaffolds/settings/closing-preference.tsx (1)

16-16: LGTM! Clean import path refactoring.

The Spinner import path has been correctly updated to the centralized UI module location.

editor/app/(workbench)/[org]/[proj]/(console)/(campaign)/campaigns/[campaign]/_components/logs-table.tsx (1)

13-13: LGTM! Clean import path refactoring.

The Spinner import path has been correctly updated to the centralized UI module location.

editor/grida-forms-hosted/e/formview.tsx (1)

52-52: LGTM! Clean import path refactoring.

The Spinner import path has been correctly updated to the centralized UI module location.

editor/app/(workbench)/[org]/[proj]/[id]/form/start/page.tsx (1)

51-51: LGTM! Clean import path refactoring.

The Spinner import path has been correctly updated to the centralized UI module location.

editor/scaffolds/mediapicker/index.tsx (1)

10-10: LGTM! Clean import path refactoring.

The Spinner import path has been correctly updated to the centralized UI module location.

editor/app/(workbench)/[org]/[proj]/[id]/data/(data)/table/[tablename]/page.tsx (1)

24-24: LGTM on the Spinner import path change.

Consistent with the UI components reorg. No runtime impact.

editor/app/(workbench)/[org]/[proj]/[id]/canvas/page.tsx (1)

3-3: Import path update looks correct.

Matches the new ui module location.

editor/scaffolds/grid-editor/components/refresh.tsx (1)

4-4: LGTM on the Spinner import switch.

No behavior change; consistent with the refactor.

editor/app/(dev)/ui/components/spinner/page.tsx (1)

4-4: Import path update approved.

Dev docs page now points at the consolidated UI Spinner.

editor/scaffolds/workspace/create-new-document-button/create-document-button.tsx (1)

34-34: Spinner import path change looks good.

Consistent with the project-wide migration.

editor/scaffolds/ai/form-field-schema-assistant.tsx (1)

9-9: Approved: Spinner import updated to UI module.

No behavioral change; matches the refactor pattern across the app.

editor/scaffolds/workbench/saving-indicator.tsx (1)

4-4: Spinner import path migration verified and correct.

✓ No legacy imports from @/components/spinner remain in the codebase
✓ Spinner component properly exported as named export from editor/components/ui/spinner.tsx
✓ Import in saving-indicator.tsx and 43 other files consistently use the new path
✓ All checks passed

editor/app/(site)/sign-in/email/_page.tsx (1)

25-25: Import path update looks good

Switching to "@/components/ui/spinner" aligns with the UI placement guideline.

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

11-15: Input components don't require flex display for layout contexts

The review comment misunderstands how CSS flexbox affects native form inputs. Native <input> elements are inline-replaced elements that work correctly whether their parent uses flex layout or not. Adding display: flex to an input doesn't improve flex growth behavior.

Current code already lacks the flex class. If inputs previously had it, removing it was correct—flex display provides no layout benefit to native form elements and adds unnecessary CSS.

Likely an incorrect or invalid review comment.

Comment on lines +1 to +40
# Grida Docs - AI Agent Guide

## Important: Document Source Location

**DO NOT edit files under `apps/docs/docs/` directory directly!**

The documentation content is **NOT** maintained in `apps/docs/docs/`. Instead:

- **Source of Truth**: `/docs/` (root directory)
- **Build Destination**: `/apps/docs/docs/` (auto-generated, overwritten on build)

### How it works

The build process (`pnpm content:setup` or `pnpm build`) runs scripts that:

1. Copy all documentation from `/docs/``/apps/docs/docs/`
2. Process translations from `translations/` subdirectories
3. Build the Docusaurus site

See:

- `scripts/postinstall.js` - Entry point
- `scripts/setup-docs.js` - Setup orchestrator
- `scripts/docs-site-gen/copy-docs.js` - Copies from root `/docs` to `apps/docs/docs/`
- `scripts/docs-site-gen/copy-translations.js` - Handles translations

### When fixing documentation issues

1. **Always edit files in `/docs/`** (root directory)
2. Never edit files in `apps/docs/docs/` (they will be overwritten)
3. Run `pnpm build` to test your changes
4. The build will copy your changes from `/docs/` to `apps/docs/docs/`

### MDX/Markdown Syntax Issues

When fixing MDX compilation errors:

- Angle-bracketed URLs like `<https://example.com>` break MDX parsing
- Use plain URLs: `https://example.com` or markdown links: `[text](url)`
- MDX reserves `<>` for JSX/HTML tags only
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

CRITICAL: Documentation added in wrong directory.

This file is being created in apps/docs/AGENTS.md, which contradicts its own guidance and the project's documentation workflow. According to the retrieved learnings and the content of this very file, all documentation must be authored in /docs/ (root directory), not in apps/docs/.

The build process copies from /docs/ to /apps/docs/docs/, so any file added directly to apps/docs/ will not follow the established pattern and may be overwritten or lost.

Move this file to /docs/AGENTS.md instead:

-apps/docs/AGENTS.md
+docs/AGENTS.md

Based on learnings

📝 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
# Grida Docs - AI Agent Guide
## Important: Document Source Location
**DO NOT edit files under `apps/docs/docs/` directory directly!**
The documentation content is **NOT** maintained in `apps/docs/docs/`. Instead:
- **Source of Truth**: `/docs/` (root directory)
- **Build Destination**: `/apps/docs/docs/` (auto-generated, overwritten on build)
### How it works
The build process (`pnpm content:setup` or `pnpm build`) runs scripts that:
1. Copy all documentation from `/docs/``/apps/docs/docs/`
2. Process translations from `translations/` subdirectories
3. Build the Docusaurus site
See:
- `scripts/postinstall.js` - Entry point
- `scripts/setup-docs.js` - Setup orchestrator
- `scripts/docs-site-gen/copy-docs.js` - Copies from root `/docs` to `apps/docs/docs/`
- `scripts/docs-site-gen/copy-translations.js` - Handles translations
### When fixing documentation issues
1. **Always edit files in `/docs/`** (root directory)
2. Never edit files in `apps/docs/docs/` (they will be overwritten)
3. Run `pnpm build` to test your changes
4. The build will copy your changes from `/docs/` to `apps/docs/docs/`
### MDX/Markdown Syntax Issues
When fixing MDX compilation errors:
- Angle-bracketed URLs like `<https://example.com>` break MDX parsing
- Use plain URLs: `https://example.com` or markdown links: `[text](url)`
- MDX reserves `<>` for JSX/HTML tags only
# Grida Docs - AI Agent Guide
## Important: Document Source Location
**DO NOT edit files under `apps/docs/docs/` directory directly!**
The documentation content is **NOT** maintained in `apps/docs/docs/`. Instead:
- **Source of Truth**: `/docs/` (root directory)
- **Build Destination**: `/apps/docs/docs/` (auto-generated, overwritten on build)
### How it works
The build process (`pnpm content:setup` or `pnpm build`) runs scripts that:
1. Copy all documentation from `/docs/``/apps/docs/docs/`
2. Process translations from `translations/` subdirectories
3. Build the Docusaurus site
See:
- `scripts/postinstall.js` - Entry point
- `scripts/setup-docs.js` - Setup orchestrator
- `scripts/docs-site-gen/copy-docs.js` - Copies from root `/docs` to `apps/docs/docs/`
- `scripts/docs-site-gen/copy-translations.js` - Handles translations
### When fixing documentation issues
1. **Always edit files in `/docs/`** (root directory)
2. Never edit files in `apps/docs/docs/` (they will be overwritten)
3. Run `pnpm build` to test your changes
4. The build will copy your changes from `/docs/` to `apps/docs/docs/`
### MDX/Markdown Syntax Issues
When fixing MDX compilation errors:
- Angle-bracketed URLs like `<https://example.com>` break MDX parsing
- Use plain URLs: `https://example.com` or Markdown links: `[text](url)`
- MDX reserves `<>` for JSX/HTML tags only
🧰 Tools
🪛 LanguageTool

[uncategorized] ~39-~39: Did you mean the formatting language “Markdown” (= proper noun)?
Context: ...se plain URLs: https://example.com or markdown links: [text](url) - MDX reserves `<>...

(MARKDOWN_NNP)

🤖 Prompt for AI Agents
In apps/docs/AGENTS.md around lines 1 to 40: this documentation file was created
in the generated site directory (apps/docs/) instead of the canonical source
location; move the file to the repository root docs/ directory (i.e.,
/docs/AGENTS.md), remove the duplicate from apps/docs/, and ensure any edits are
made in /docs/ so the build process can copy it into apps/docs/docs/ during pnpm
content:setup or pnpm build.

Comment on lines +18 to +26
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<kbd
data-slot="kbd-group"
className={cn("inline-flex items-center gap-1", className)}
{...props}
/>
)
}
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

Fix semantic HTML mismatch.

Line 20 uses <kbd> element but the type definition on line 18 specifies React.ComponentProps<"div">. A group container should use a <div> element, not <kbd>, as <kbd> represents keyboard input, not a container.

Apply this diff to fix the semantic HTML issue:

-function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
+function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
   return (
-    <kbd
+    <div
       data-slot="kbd-group"
       className={cn("inline-flex items-center gap-1", className)}
       {...props}
-    />
+    />
   )
 }
📝 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
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<kbd
data-slot="kbd-group"
className={cn("inline-flex items-center gap-1", className)}
{...props}
/>
)
}
function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="kbd-group"
className={cn("inline-flex items-center gap-1", className)}
{...props}
/>
)
}
🤖 Prompt for AI Agents
In editor/components/ui/kbd.tsx around lines 18 to 26, the component is typed as
React.ComponentProps<"div"> but renders a <kbd>, causing a semantic HTML
mismatch; change the rendered element to a <div> so the output matches the
declared prop types and intent as a group container, update the data-slot
attribute if needed (e.g., keep data-slot="kbd-group"), and ensure className and
{...props} are passed to the div.

Comment on lines +597 to 639
// Find target parents for each selected node:
// - If selected node is a container -> paste as child (target parent = node itself)
// - If selected node is not a container -> paste as sibling (target parent = node's parent)
// - Target parent must be a container or null (scene)
// - Target parent must not be one of the copied nodes
//
// KNOWN LIMITATION:
// When no selection exists, hit test is used to find parent (see below).
// However, after first paste, newly pasted nodes become selected, so subsequent
// pastes use this selection-based logic instead of hit test. This means position
// adjustment for hit-tested parents only applies to the first paste in a sequence.
const target_parents = Array.from(
new Set(
selection
.map((node_id) => {
const node = dq.__getNodeById(draft, node_id);

// If node is a container, use it as target parent (paste as child)
if (node.type === "container") {
return node_id;
}

// Otherwise, use its parent as target parent (paste as sibling)
const parent_id = dq.getParentId(draft.document_ctx, node_id);

// Parent can be null (scene) or a container
if (!parent_id) return null;

const parent = dq.__getNodeById(draft, parent_id);
// Only return valid container parents
return parent?.type === "container" ? parent_id : null;
})
.filter((target_id) => {
// Ensure target parent is not one of the originals
if (target_id && ids.includes(target_id)) return false;
return true;
})
)
);

const targets: Array<string | null> =
valid_target_selection.length > 0 ? valid_target_selection : [null];
target_parents.length > 0 ? target_parents : [null];

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

Avoid duplicate paste when mixed parents; also use Set for O(1) copied-id checks

If selection yields both explicit container parents and null (scene), current targets includes null, causing an extra hit‑tested paste in addition to container pastes. Also, ids.includes runs in O(n) in two hot paths. Fix by preferring non‑null targets when present and using a Set.

Apply this diff:

@@
-        const target_parents = Array.from(
-          new Set(
-            selection
-              .map((node_id) => {
-                const node = dq.__getNodeById(draft, node_id);
-
-                // If node is a container, use it as target parent (paste as child)
-                if (node.type === "container") {
-                  return node_id;
-                }
-
-                // Otherwise, use its parent as target parent (paste as sibling)
-                const parent_id = dq.getParentId(draft.document_ctx, node_id);
-
-                // Parent can be null (scene) or a container
-                if (!parent_id) return null;
-
-                const parent = dq.__getNodeById(draft, parent_id);
-                // Only return valid container parents
-                return parent?.type === "container" ? parent_id : null;
-              })
-              .filter((target_id) => {
-                // Ensure target parent is not one of the originals
-                if (target_id && ids.includes(target_id)) return false;
-                return true;
-              })
-          )
-        );
-
-        const targets: Array<string | null> =
-          target_parents.length > 0 ? target_parents : [null];
+        // O(1) lookup for originals
+        const copiedSet = new Set(ids);
+        const target_parents = Array.from(
+          new Set(
+            selection
+              .map((node_id) => {
+                const node = dq.__getNodeById(draft, node_id);
+                if (node.type === "container") return node_id; // paste as child
+                const parent_id = dq.getParentId(draft.document_ctx, node_id); // paste as sibling
+                if (!parent_id) return null; // scene/root
+                const parent = dq.__getNodeById(draft, parent_id);
+                return parent?.type === "container" ? parent_id : null;
+              })
+              .filter((target_id) => (target_id ? !copiedSet.has(target_id) : true))
+          )
+        );
+        // Prefer explicit parents; only fall back to root/hit-test when none exist
+        const nonNullTargets = target_parents.filter(
+          (t): t is string => t !== null
+        );
+        const targets: Array<string | null> =
+          nonNullTargets.length > 0 ? nonNullTargets : [null];
@@
-                (id) => {
-                  // Exclude originals from hit test
-                  if (ids.includes(id)) return false;
-                  return dq.__getNodeById(draft, id).type === "container";
-                },
+                (id) => {
+                  // Exclude originals from hit test
+                  if (copiedSet.has(id)) return false;
+                  return dq.__getNodeById(draft, id).type === "container";
+                },

Also applies to: 678-683

Comment on lines +129 to +143
<Toggle
size="sm"
variant="outline"
disabled={disabled}
pressed={showIndividual}
onPressedChange={setShowIndividual}
className="bg-transparent border-none shadow-none size-6 min-w-6 px-0 data-[state=on]:*:[svg]:text-workbench-accent-sky"
aria-label="Toggle individual corner radius controls"
>
<CornersIcon
className="size-3.5 text-muted-foreground"
aria-hidden="true"
/>
</Toggle>
</div>
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

Fix Tailwind descendant selector for pressed icon color.

data-[state=on]:*:[svg]:… won’t match. Use the arbitrary variant for descendants.

-            className="bg-transparent border-none shadow-none size-6 min-w-6 px-0 data-[state=on]:*:[svg]:text-workbench-accent-sky"
+            className="bg-transparent border-none shadow-none size-6 min-w-6 px-0 data-[state=on]:[&_svg]:text-workbench-accent-sky"
📝 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
<Toggle
size="sm"
variant="outline"
disabled={disabled}
pressed={showIndividual}
onPressedChange={setShowIndividual}
className="bg-transparent border-none shadow-none size-6 min-w-6 px-0 data-[state=on]:*:[svg]:text-workbench-accent-sky"
aria-label="Toggle individual corner radius controls"
>
<CornersIcon
className="size-3.5 text-muted-foreground"
aria-hidden="true"
/>
</Toggle>
</div>
<Toggle
size="sm"
variant="outline"
disabled={disabled}
pressed={showIndividual}
onPressedChange={setShowIndividual}
className="bg-transparent border-none shadow-none size-6 min-w-6 px-0 data-[state=on]:[&_svg]:text-workbench-accent-sky"
aria-label="Toggle individual corner radius controls"
>
<CornersIcon
className="size-3.5 text-muted-foreground"
aria-hidden="true"
/>
</Toggle>
</div>
🤖 Prompt for AI Agents
In editor/scaffolds/sidecontrol/controls/corner-radius.tsx around lines 129 to
143, the Tailwind descendant selector
`data-[state=on]:*:[svg]:text-workbench-accent-sky` is invalid; replace it with
an arbitrary variant that targets descendant svgs when the Toggle is pressed,
e.g. use `data-[state=on]:[&_*_svg]:text-workbench-accent-sky` (update the
className string accordingly) so the pressed state correctly applies the color
to the icon.

Comment on lines 146 to 224
{/* Second Row: Individual Corner Radius Controls */}
{showIndividual && (
<div className="flex items-center">
{/* Top Left */}
<div className="flex flex-col items-center flex-1">
<InputPropertyNumber
mode="fixed"
type="number"
value={cornerValues[0]}
placeholder="0"
min={0}
step={1}
className="w-full h-7 rounded-none rounded-l-md border-r-0"
onValueCommit={(v) => handleIndividualChange(0, v)}
aria-label="Corner radius top left"
/>
<Label>
<CornerTopLeftIcon className="size-3" />
</Label>
</div>
{/* Separator */}
<hr className="w-px h-10" />
{/* Top Right */}
<div className="flex flex-col items-center flex-1">
<InputPropertyNumber
mode="fixed"
type="number"
value={cornerValues[1]}
placeholder="0"
min={0}
step={1}
className="w-full h-7 rounded-none border-x-0"
onValueCommit={(v) => handleIndividualChange(1, v)}
aria-label="Corner radius top right"
/>
<Label>
<CornerTopRightIcon className="size-3" />
</Label>
</div>
{/* Separator */}
<hr className="w-px h-10" />
{/* Bottom Right */}
<div className="flex flex-col items-center flex-1">
<InputPropertyNumber
mode="fixed"
type="number"
value={cornerValues[2]}
placeholder="0"
min={0}
step={1}
className="w-full h-7 rounded-none border-x-0"
onValueCommit={(v) => handleIndividualChange(2, v)}
aria-label="Corner radius bottom right"
/>
<Label>
<CornerBottomRightIcon className="size-3" />
</Label>
</div>
{/* Separator */}
<hr className="w-px h-10" />
{/* Bottom Left */}
<div className="flex flex-col items-center flex-1">
<InputPropertyNumber
mode="fixed"
type="number"
value={cornerValues[3]}
placeholder="0"
min={0}
step={1}
className="w-full h-7 rounded-none rounded-r-md border-l-0"
onValueCommit={(v) => handleIndividualChange(3, v)}
aria-label="Corner radius bottom left"
/>
<Label>
<CornerBottomLeftIcon className="size-3" />
</Label>
</div>
</div>
)}
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

Disabled state isn’t applied to per-corner inputs.

When disabled is true, the four inputs remain interactive. Forward the prop to each input.

             <InputPropertyNumber
               mode="fixed"
               type="number"
+              disabled={disabled}
               value={cornerValues[0]}
@@
             <InputPropertyNumber
               mode="fixed"
               type="number"
+              disabled={disabled}
               value={cornerValues[1]}
@@
             <InputPropertyNumber
               mode="fixed"
               type="number"
+              disabled={disabled}
               value={cornerValues[2]}
@@
             <InputPropertyNumber
               mode="fixed"
               type="number"
+              disabled={disabled}
               value={cornerValues[3]}
📝 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
{/* Second Row: Individual Corner Radius Controls */}
{showIndividual && (
<div className="flex items-center">
{/* Top Left */}
<div className="flex flex-col items-center flex-1">
<InputPropertyNumber
mode="fixed"
type="number"
value={cornerValues[0]}
placeholder="0"
min={0}
step={1}
className="w-full h-7 rounded-none rounded-l-md border-r-0"
onValueCommit={(v) => handleIndividualChange(0, v)}
aria-label="Corner radius top left"
/>
<Label>
<CornerTopLeftIcon className="size-3" />
</Label>
</div>
{/* Separator */}
<hr className="w-px h-10" />
{/* Top Right */}
<div className="flex flex-col items-center flex-1">
<InputPropertyNumber
mode="fixed"
type="number"
value={cornerValues[1]}
placeholder="0"
min={0}
step={1}
className="w-full h-7 rounded-none border-x-0"
onValueCommit={(v) => handleIndividualChange(1, v)}
aria-label="Corner radius top right"
/>
<Label>
<CornerTopRightIcon className="size-3" />
</Label>
</div>
{/* Separator */}
<hr className="w-px h-10" />
{/* Bottom Right */}
<div className="flex flex-col items-center flex-1">
<InputPropertyNumber
mode="fixed"
type="number"
value={cornerValues[2]}
placeholder="0"
min={0}
step={1}
className="w-full h-7 rounded-none border-x-0"
onValueCommit={(v) => handleIndividualChange(2, v)}
aria-label="Corner radius bottom right"
/>
<Label>
<CornerBottomRightIcon className="size-3" />
</Label>
</div>
{/* Separator */}
<hr className="w-px h-10" />
{/* Bottom Left */}
<div className="flex flex-col items-center flex-1">
<InputPropertyNumber
mode="fixed"
type="number"
value={cornerValues[3]}
placeholder="0"
min={0}
step={1}
className="w-full h-7 rounded-none rounded-r-md border-l-0"
onValueCommit={(v) => handleIndividualChange(3, v)}
aria-label="Corner radius bottom left"
/>
<Label>
<CornerBottomLeftIcon className="size-3" />
</Label>
</div>
</div>
)}
{/* Second Row: Individual Corner Radius Controls */}
{showIndividual && (
<div className="flex items-center">
{/* Top Left */}
<div className="flex flex-col items-center flex-1">
<InputPropertyNumber
mode="fixed"
type="number"
disabled={disabled}
value={cornerValues[0]}
placeholder="0"
min={0}
step={1}
className="w-full h-7 rounded-none rounded-l-md border-r-0"
onValueCommit={(v) => handleIndividualChange(0, v)}
aria-label="Corner radius top left"
/>
<Label>
<CornerTopLeftIcon className="size-3" />
</Label>
</div>
{/* Separator */}
<hr className="w-px h-10" />
{/* Top Right */}
<div className="flex flex-col items-center flex-1">
<InputPropertyNumber
mode="fixed"
type="number"
disabled={disabled}
value={cornerValues[1]}
placeholder="0"
min={0}
step={1}
className="w-full h-7 rounded-none border-x-0"
onValueCommit={(v) => handleIndividualChange(1, v)}
aria-label="Corner radius top right"
/>
<Label>
<CornerTopRightIcon className="size-3" />
</Label>
</div>
{/* Separator */}
<hr className="w-px h-10" />
{/* Bottom Right */}
<div className="flex flex-col items-center flex-1">
<InputPropertyNumber
mode="fixed"
type="number"
disabled={disabled}
value={cornerValues[2]}
placeholder="0"
min={0}
step={1}
className="w-full h-7 rounded-none border-x-0"
onValueCommit={(v) => handleIndividualChange(2, v)}
aria-label="Corner radius bottom right"
/>
<Label>
<CornerBottomRightIcon className="size-3" />
</Label>
</div>
{/* Separator */}
<hr className="w-px h-10" />
{/* Bottom Left */}
<div className="flex flex-col items-center flex-1">
<InputPropertyNumber
mode="fixed"
type="number"
disabled={disabled}
value={cornerValues[3]}
placeholder="0"
min={0}
step={1}
className="w-full h-7 rounded-none rounded-r-md border-l-0"
onValueCommit={(v) => handleIndividualChange(3, v)}
aria-label="Corner radius bottom left"
/>
<Label>
<CornerBottomLeftIcon className="size-3" />
</Label>
</div>
</div>
)}
🤖 Prompt for AI Agents
In editor/scaffolds/sidecontrol/controls/corner-radius.tsx around lines 146–224,
the per-corner InputPropertyNumber controls do not receive the component's
disabled prop so they remain interactive when disabled is true; update each of
the four InputPropertyNumber usages to forward the disabled prop (e.g.,
disabled={disabled}) so they become non-interactive when disabled, and ensure
any visual disabled styles rely on that prop (no other logic changes required).

@vercel vercel bot temporarily deployed to Preview – backgrounds October 28, 2025 10:20 Inactive
@softmarshmallow softmarshmallow merged commit 66b58d1 into main Oct 28, 2025
3 of 5 checks passed
@vercel vercel bot temporarily deployed to Preview – viewer October 28, 2025 10:20 Inactive
@vercel vercel bot temporarily deployed to Preview – blog October 28, 2025 10:20 Inactive
@vercel vercel bot temporarily deployed to Preview – grida October 28, 2025 10:36 Inactive
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