Skip to content

feat(layout): Add grid node type for scrollable dashboard layouts - #575

Merged
ilyabo merged 19 commits into
mainfrom
codex/add-grid-layout-node-type-with-draggable-panels
May 4, 2026
Merged

ilyabo merged 19 commits into
mainfrom
codex/add-grid-layout-node-type-with-draggable-panels

Conversation

@ilyabo

@ilyabo ilyabo commented Apr 29, 2026 •

Copy link
Copy Markdown
Collaborator

layout example:

image

--
CLI app:

image

Motivation

  • Provide an alternative grid layout node for dashboard-style, scrollable grid pages (Notion / BI dashboards) so dashboards can be created as either dock or grid layouts without runtime conversion.
  • Use react-grid-layout as the implementation to enable draggable and resizable panels in a scrollable container while keeping the layout API terminology consistent with existing nodes.
  • Constrain drag/resize ergonomics so dragging starts only from a header handle and resizing is limited to right (e) and bottom (s) edges to match the requested UX.

Description

  • Add a new grid node schema and types in @sqlrooms/layout-config including LayoutGridNode, LayoutGridItem, cols, layouts, breakpoints, compactType, margin, rowHeight, resizeHandles, and an isLayoutGridNode type guard.
  • Implement a GridLayout renderer (packages/layout/src/node-renderers/grid-node-renderer/GridLayout.tsx) that uses react-grid-layout (responsive + width provider) and reads per-node configuration to render scrollable, draggable, resizable dashboard panels; CSS imports for react-grid-layout / react-resizable are included.
  • Wire the new renderer into the recursive renderer switch (renderLayoutNode) and extend LayoutNodeContext, PanelContainerType, and public exports so grid nodes are first-class in the layout package.
  • Add data-layout-drag-handle="true" to the panel drag handle component so react-grid-layout can restrict dragging to header handles, and default resize handles are limited to ['e','s'].
  • Add react-grid-layout, react-resizable, and @types/react-grid-layout to packages/layout/package.json and update the package README to document grid support.
  • Update examples/layout to include a new growth-grid dashboard (helper createDashboardGridNode) that demonstrates the grid node beside the existing dock dashboard.

Testing

  • Ran pnpm --filter @sqlrooms/layout build in the workspace and the layout package build completed after adding typings for react-grid-layout. (success)
  • Ran unit tests with pnpm --filter @sqlrooms/layout test and all layout test suites passed (4 test suites, 37 tests). (success)
  • Attempted to build the layout example with pnpm --filter layout-example build, which fails in this environment because broader workspace build artifacts / type availability for @sqlrooms/room-shell are not present; this failure is environment-specific and unrelated to the layout package changes. (environment-dependent failure)

Codex Task

Summary by CodeRabbit

  • New Features
    • Dashboards now support two layout types: "Scrollable grid" and "Docked panels" with creation UI choices; some automatic flows default to "grid".
    • Grid dashboards auto-scroll new items into view; header drag handle gives clearer grab/grabbing cursor feedback.
  • Bug Fixes
    • Prevented chart addition when a panel identifier is missing.
  • Documentation
    • README/help text updated to explain layout types, defaults, and creation behavior.
  • Tests
    • Added coverage for grid layout behavior and migration scenarios.

@coderabbitai

coderabbitai Bot commented Apr 29, 2026 •

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 70d5c805-a335-4dcb-98cd-446864f788aa

📥 Commits

Reviewing files that changed from the base of the PR and between c28f5da and 9d6a578.

📒 Files selected for processing (1)
  • examples/layout/src/store.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/layout/src/store.tsx

📝 Walkthrough

Walkthrough

Adds first-class scrollable grid dashboards: new LayoutGridNode types and renderer (react-grid-layout), grid-aware layout traversal/context, Mosaic dashboards now persist a layoutType (dock|grid) with grid placement logic, CLI/tools/store thread layoutType through dashboard creation, plus tests, docs, and dependency updates.

Changes

Grid dashboard end-to-end

Layer / File(s) Summary
Data Shape / Types
packages/layout-config/src/LayoutConfig.ts, packages/layout-config/src/index.ts, packages/layout/src/layout-base-types.ts, packages/layout/src/types.ts, packages/mosaic/src/dashboard/MosaicDashboardSlice.ts
Adds LayoutGridItem/LayoutGridNode schemas and isLayoutGridNode; expands PanelContainerType to include dock/grid; introduces MosaicDashboardLayoutType and persists layoutType on Mosaic dashboard entries.
Core Slice / State
packages/mosaic/src/dashboard/MosaicDashboardSlice.ts, apps/sqlrooms-cli-ui/src/store.ts, apps/sqlrooms-cli-ui/src/store-types.ts
Dashboard APIs (createDashboard, ensureDashboard, etc.) accept optional layoutType; dashboards initialize layout as a grid root when layoutType === 'grid'; adds grid placement/append/remove/normalize helpers and grid-id helper.
Layout Engine / Tree
packages/layout/src/layout-tree.ts, packages/layout/src/resolvePanelIdentity.ts, packages/layout/src/layout-slice-types.ts, packages/layout-config/src/LayoutConfig.ts
Traversal, lookup, identity resolution, and mutation now recurse into grid.children; LayoutNode union updated to include LayoutGridNode.
Renderer Implementation
packages/layout/src/node-renderers/grid-node-renderer/GridLayout.tsx, packages/layout/src/node-renderers/grid-node-renderer/useScrollNewGridChildIntoView.ts, packages/layout/src/node-renderers/leaf-node-renderer/*, packages/layout/src/node-renderers/renderLayoutNode.tsx, packages/layout/src/LayoutRenderer.tsx
Adds GridLayout renderer using react-grid-layout, layout normalization preserving constraints, scroll-into-view hook for new children, forwards parent container context to leaves, integrates grid rendering into node dispatch.
CLI / Commands / UI wiring
apps/sqlrooms-cli-ui/src/createDashboardCommands.ts, apps/sqlrooms-cli-ui/src/createDashboardAiTools.ts, apps/sqlrooms-cli-ui/src/createDashboardChartTemplateTool.ts, apps/sqlrooms-cli-ui/src/artifactTypes.tsx, apps/sqlrooms-cli-ui/src/workspace/ArtifactsContainerPanel.tsx, apps/sqlrooms-cli-ui/src/store.ts
Extends dashboard creation schemas/commands/tools to accept optional layoutType (`'dock'
Examples, Tests & Config
examples/layout/src/store.tsx, examples/layout/src/components/DashboardTabs.tsx, packages/layout-config/__tests__/LayoutConfig.test.ts, packages/layout/src/__tests__/*, packages/mosaic/__tests__/*, packages/mosaic/jest.config.js, packages/preset-jest/node.js, knip.json
Examples updated for grid dashboards; adds tests for grid node parsing, traversal, Mosaic grid behavior; Jest stylesheet mocks and moduleNameMapper added; knip.json ignore updated.
Docs & Dependencies
apps/sqlrooms-cli-ui/README.md, packages/layout/README.md, packages/mosaic/README.md, packages/layout/package.json
Documents layoutType behavior in READMEs; adds react-grid-layout/react-resizable deps and @types/react-grid-layout dev dependency for the grid renderer.

Sequence Diagram

sequenceDiagram
    participant User
    participant CLI as CLI UI
    participant Store as Room Store
    participant Mosaic as Mosaic Slice
    participant Layout as Layout Renderer
    participant Grid as react-grid-layout

    User->>CLI: Create dashboard (title, layoutType='grid')
    CLI->>Store: createDashboardArtifact(title, 'grid')
    Store->>Mosaic: createDashboard(title, layoutType='grid')
    Mosaic->>Mosaic: Initialize LayoutGridNode root & layouts
    Mosaic-->>Store: dashboardId
    Store-->>CLI: dashboardId

    CLI->>Layout: Render dashboard artifact
    Layout->>Mosaic: Request dashboard layout
    Mosaic-->>Layout: LayoutGridNode root
    Layout->>Grid: Render responsive grid items
    Grid-->>User: Interactive grid (drag/resize)

    User->>Grid: Drag/resize item
    Grid->>Layout: onLayoutChange(normalized)
    Layout->>Mosaic: Persist updated grid layouts
    Mosaic->>Store: Update dashboard layout state
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • bdjulbic
  • mihajlo-stojkovic

Poem

🐰 I hopped through nodes both grid and dock,
Panels landed where the pixels clock,
From CLI seed to rendered block,
New grids scroll in a joyful flock,
I twitch my nose — deployment rock! 🎨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.84% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely summarizes the main change: adding a grid node type to enable scrollable dashboard layouts as a new feature in the layout system.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/add-grid-layout-node-type-with-draggable-panels

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

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

@ilyabo
ilyabo marked this pull request as ready for review April 30, 2026 15:05

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (1)
packages/mosaic/src/dashboard/MosaicDashboardSlice.ts (1)

403-425: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Normalize invalid grid layouts before repairing panel membership.

For layoutType === 'grid', this path only appends missing panels. If setLayout() receives a non-grid layout that already contains some dashboard panels, the first missing panel rebuilds it as a fresh grid with only that missing panel, so the panels already present in the incoming layout are lost; and when no panels are missing, the dashboard can stay in a non-grid shape.

Suggested fix
 function ensureLayoutContainsDashboardPanels(
   layout: LayoutNode | null,
   dashboardId: string,
   panelIds: string[],
   layoutType: MosaicDashboardLayoutType = 'dock',
 ): LayoutNode | null {
-  let nextLayout = layout;
+  let nextLayout =
+    layoutType === 'grid' && !isLayoutGridNode(layout)
+      ? createDashboardGridLayout(dashboardId)
+      : layout;
   const existing = collectPanelIds(nextLayout);

   for (const panelId of panelIds) {
     const layoutPanelId = getMosaicDashboardPanelId(dashboardId, panelId);
     if (!existing.has(layoutPanelId)) {

Also applies to: 796-810

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/mosaic/src/dashboard/MosaicDashboardSlice.ts` around lines 403 -
425, ensureLayoutContainsDashboardPanels currently only appends missing panels
and when layoutType === 'grid' it may rebuild a fresh grid containing only the
first missing panel (losing dashboard panels that were present in a non-grid
incoming layout) or leave non-grid shapes unchanged; fix by normalizing any
incoming layout to a proper grid before adding panels: when layoutType ===
'grid' gather existing dashboard panel nodes (using collectPanelIds and
getMosaicDashboardPanelId to identify them), transform the current layout into a
grid container preserving those existing panel nodes, then append any missing
panels via appendPanelToGridLayout; keep appendPanelToLayout for non-grid paths
and reuse createDashboardPanelNode for new nodes so no panels are lost.
🧹 Nitpick comments (2)
apps/sqlrooms-cli-ui/src/createDashboardCommands.ts (1)

22-27: ⚡ Quick win

Make layoutType default to 'grid' in command schema for backward compatibility.

Line 24 currently makes layoutType mandatory, which can break existing dashboard.create-artifact callers that only send a title or empty input.

Proposed diff
-const DashboardCreateArtifactCommandInput = z.object({
+const DashboardCreateArtifactCommandInput = z
+  .object({
   title: z.string().optional().describe('Optional artifact title.'),
   layoutType: z
     .enum(['dock', 'grid'])
+    .default('grid')
     .describe('Dashboard layout node type to use at creation time.'),
-});
+  })
+  .default({});

Also applies to: 130-133

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/sqlrooms-cli-ui/src/createDashboardCommands.ts` around lines 22 - 27,
The DashboardCreateArtifactCommandInput schema currently makes layoutType
required; change the z.enum(['dock','grid']) call in
DashboardCreateArtifactCommandInput to include .optional().default('grid')
(e.g., z.enum([...]).optional().default('grid').describe(...)) so callers that
omit layoutType get 'grid' by default; apply the same change to the other
layoutType schema occurrence in the file (the second layoutType definition
around lines 130-133) to ensure backward compatibility across both command
schemas.
packages/layout/src/node-renderers/leaf-node-renderer/LeafLayoutHeader.tsx (1)

12-14: ⚡ Quick win

Move ancestor derivation out of the Zustand selector.

The selector currently performs a derived lookup (findAncestorOfType). Select stable raw state first, then derive via useMemo in-component to follow the selector stability rule.

Suggested refactor
-import {FC, PropsWithChildren} from 'react';
+import {FC, PropsWithChildren, useMemo} from 'react';
@@
-  const gridAncestor = useStoreWithLayout((state) =>
-    state.layout.findAncestorOfType(nodeId, 'grid'),
-  );
+  const layout = useStoreWithLayout((state) => state.layout);
+  const gridAncestor = useMemo(
+    () => layout.findAncestorOfType(nodeId, 'grid'),
+    [layout, nodeId],
+  );

As per coding guidelines: "When using Zustand/room-store selectors in React, do not create derived arrays/objects/functions inside the selector; instead select stable raw state first, then derive with React.useMemo in the component".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/layout/src/node-renderers/leaf-node-renderer/LeafLayoutHeader.tsx`
around lines 12 - 14, The selector is performing derived work by calling
state.layout.findAncestorOfType inside useStoreWithLayout; instead, select the
stable raw piece (e.g., select state.layout or a getter like layout) with
useStoreWithLayout and then compute gridAncestor inside the LeafLayoutHeader
component using React.useMemo calling layout.findAncestorOfType(nodeId, 'grid');
update references to gridAncestor to use the memoized value and keep nodeId and
useStoreWithLayout as the lookup anchors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/sqlrooms-cli-ui/src/createDashboardAiTools.ts`:
- Around line 14-17: The schema field layoutType is currently required and
causes minimal create_dashboard_artifact calls (e.g., {} or { title }) to fail;
update the Zod definition for layoutType to be optional with a sensible default
(e.g., .optional().default('dock')) so validation succeeds when callers omit
it—modify the layoutType declaration in the dashboard schema (the symbol
layoutType) and ensure create_dashboard_artifact consumers rely on that default
rather than passing a value.

In `@examples/layout/src/store.tsx`:
- Around line 86-103: createGridLayoutItem always returns x: 0 causing new
charts to stack in the left column; change it to compute x from existing layout
so items fill the next grid slot (matching createDashboardGridNode's two-column
behavior). Inside createGridLayoutItem, compute colsPerRow = Math.max(1,
Math.floor(cols / w)), derive an index from the current layout (e.g.,
layout.length or count of items at that y), and set x = (index % colsPerRow) * w
so new items alternate into the next column(s) instead of always x: 0; keep y,
w, h logic the same and reference the layout array, cols, w, and
createGridLayoutItem.

In `@packages/layout/src/node-renderers/grid-node-renderer/GridLayout.tsx`:
- Around line 123-126: createDefaultItem and the layouts fallback use a fixed
12-column and increment y by 1 which causes overlap because each item has h: 2
and ignores node.cols; update createDefaultItem to read the effective column
count (use node.cols || 12) and compute x = (index * w) % cols and y =
Math.floor((index * w) / cols) * h so rows advance by item height; likewise,
when generating the fallback layouts (the code that maps breakpoints into
LayoutGridItem arrays), pass the resolved cols for that breakpoint into the
layout generator so each breakpoint uses its correct column count instead of
hardcoded 12. Ensure you reference and update createDefaultItem and the
layouts-generation block that produces the LayoutGridItem arrays to use the same
cols logic and formula.

---

Outside diff comments:
In `@packages/mosaic/src/dashboard/MosaicDashboardSlice.ts`:
- Around line 403-425: ensureLayoutContainsDashboardPanels currently only
appends missing panels and when layoutType === 'grid' it may rebuild a fresh
grid containing only the first missing panel (losing dashboard panels that were
present in a non-grid incoming layout) or leave non-grid shapes unchanged; fix
by normalizing any incoming layout to a proper grid before adding panels: when
layoutType === 'grid' gather existing dashboard panel nodes (using
collectPanelIds and getMosaicDashboardPanelId to identify them), transform the
current layout into a grid container preserving those existing panel nodes, then
append any missing panels via appendPanelToGridLayout; keep appendPanelToLayout
for non-grid paths and reuse createDashboardPanelNode for new nodes so no panels
are lost.

---

Nitpick comments:
In `@apps/sqlrooms-cli-ui/src/createDashboardCommands.ts`:
- Around line 22-27: The DashboardCreateArtifactCommandInput schema currently
makes layoutType required; change the z.enum(['dock','grid']) call in
DashboardCreateArtifactCommandInput to include .optional().default('grid')
(e.g., z.enum([...]).optional().default('grid').describe(...)) so callers that
omit layoutType get 'grid' by default; apply the same change to the other
layoutType schema occurrence in the file (the second layoutType definition
around lines 130-133) to ensure backward compatibility across both command
schemas.

In `@packages/layout/src/node-renderers/leaf-node-renderer/LeafLayoutHeader.tsx`:
- Around line 12-14: The selector is performing derived work by calling
state.layout.findAncestorOfType inside useStoreWithLayout; instead, select the
stable raw piece (e.g., select state.layout or a getter like layout) with
useStoreWithLayout and then compute gridAncestor inside the LeafLayoutHeader
component using React.useMemo calling layout.findAncestorOfType(nodeId, 'grid');
update references to gridAncestor to use the memoized value and keep nodeId and
useStoreWithLayout as the lookup anchors.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6447aefd-aea9-4f7a-8ad3-ea3595497cdc

📥 Commits

Reviewing files that changed from the base of the PR and between ae2c193 and a8cb308.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (38)
  • apps/sqlrooms-cli-ui/README.md
  • apps/sqlrooms-cli-ui/src/artifactTypes.tsx
  • apps/sqlrooms-cli-ui/src/createDashboardAiTools.ts
  • apps/sqlrooms-cli-ui/src/createDashboardChartTemplateTool.ts
  • apps/sqlrooms-cli-ui/src/createDashboardCommands.ts
  • apps/sqlrooms-cli-ui/src/store-types.ts
  • apps/sqlrooms-cli-ui/src/store.ts
  • apps/sqlrooms-cli-ui/src/workspace/ArtifactsContainerPanel.tsx
  • examples/layout/src/components/DashboardTabs.tsx
  • examples/layout/src/store.tsx
  • packages/layout-config/__tests__/LayoutConfig.test.ts
  • packages/layout-config/src/LayoutConfig.ts
  • packages/layout-config/src/index.ts
  • packages/layout-config/src/tabs-helpers.ts
  • packages/layout/README.md
  • packages/layout/package.json
  • packages/layout/src/LayoutNodeContext.tsx
  • packages/layout/src/__tests__/layout-tree.test.ts
  • packages/layout/src/__tests__/resolvePanelIdentity.test.ts
  • packages/layout/src/index.ts
  • packages/layout/src/layout-base-types.ts
  • packages/layout/src/layout-slice-types.ts
  • packages/layout/src/layout-tree.ts
  • packages/layout/src/node-renderers/grid-node-renderer/GridLayout.tsx
  • packages/layout/src/node-renderers/leaf-node-renderer/LeafLayout.tsx
  • packages/layout/src/node-renderers/leaf-node-renderer/LeafLayoutDragHandle.tsx
  • packages/layout/src/node-renderers/leaf-node-renderer/LeafLayoutHeader.tsx
  • packages/layout/src/node-renderers/renderLayoutNode.tsx
  • packages/layout/src/resolvePanelIdentity.ts
  • packages/layout/src/types.ts
  • packages/mosaic/README.md
  • packages/mosaic/__tests__/MosaicDashboardSlice.test.ts
  • packages/mosaic/__tests__/mocks/style.js
  • packages/mosaic/jest.config.js
  • packages/mosaic/src/dashboard/MosaicDashboardPanels.tsx
  • packages/mosaic/src/dashboard/MosaicDashboardSlice.ts
  • packages/mosaic/src/index.ts
  • packages/room-shell/src/panels/RoomPanelHeader.tsx
💤 Files with no reviewable changes (1)
  • apps/sqlrooms-cli-ui/src/artifactTypes.tsx

Comment thread apps/sqlrooms-cli-ui/src/createDashboardAiTools.ts
Comment thread packages/layout/src/node-renderers/grid-node-renderer/GridLayout.tsx Outdated
ilyabo added 8 commits May 4, 2026 11:45
…id-layout-node-type-with-draggable-panels

Signed-off-by: Ilya Boyandin <[email protected]>
Signed-off-by: Ilya Boyandin <[email protected]>
Signed-off-by: Ilya Boyandin <[email protected]>
Signed-off-by: Ilya Boyandin <[email protected]>
Signed-off-by: Ilya Boyandin <[email protected]>
Signed-off-by: Ilya Boyandin <[email protected]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
packages/layout/src/node-renderers/grid-node-renderer/useScrollNewGridChildIntoView.ts (1)

6-13: 💤 Low value

getGridItemElement allocates a full array to find a single element by known attribute value

querySelectorAll + Array.from + .find() collects every [data-layout-grid-item-id] descendant into memory before scanning. A direct attribute-value CSS selector reaches the target element in a single DOM pass with no intermediate allocations.

♻️ Proposed refactor
 function getGridItemElement(
   scrollContainer: HTMLElement,
   itemId: string,
 ): HTMLElement | undefined {
-  return Array.from(
-    scrollContainer.querySelectorAll<HTMLElement>('[data-layout-grid-item-id]'),
-  ).find((element) => element.dataset.layoutGridItemId === itemId);
+  return (
+    scrollContainer.querySelector<HTMLElement>(
+      `[data-layout-grid-item-id="${CSS.escape(itemId)}"]`,
+    ) ?? undefined
+  );
 }

Note: CSS.escape() is available in all modern browsers and guards against IDs containing characters that are special in CSS selectors.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/layout/src/node-renderers/grid-node-renderer/useScrollNewGridChildIntoView.ts`
around lines 6 - 13, getGridItemElement currently builds an array of all
[data-layout-grid-item-id] elements then .finds the match, which unnecessarily
allocates memory; change it to query for the specific attribute value using a
single DOM query (use scrollContainer.querySelector with an attribute-value
selector) and protect the itemId by escaping it with CSS.escape() so you perform
one pass and return the matching HTMLElement or undefined without creating an
intermediate array.
packages/layout-config/src/LayoutConfig.ts (1)

164-166: 💤 Low value

.strict() is legacy in Zod 4 — consider z.strictObject() for new schemas.

.strict() is still available for backwards compatibility and will not be removed, but the preferred Zod 4 approach for new schemas is the top-level z.strictObject(). The pattern is consistent with the existing LayoutSplitNode/LayoutTabsNode schemas, so this is only worth addressing if the codebase ever migrates those existing nodes too.

♻️ Proposed refactor
-export const LayoutGridNode = BaseLayoutGridNode.extend({
-  children: z.lazy(() => z.array(LayoutNode)),
-}).strict();
+export const LayoutGridNode = z.strictObject({
+  ...BaseLayoutGridNode.shape,
+  children: z.lazy(() => z.array(LayoutNode)),
+});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/layout-config/src/LayoutConfig.ts` around lines 164 - 166, Replace
the legacy .strict() call on the LayoutGridNode schema by using the Zod
top-level z.strictObject pattern to match the project's existing schemas (e.g.,
LayoutSplitNode/LayoutTabsNode); specifically update the LayoutGridNode
definition that currently extends BaseLayoutGridNode and lazily includes
LayoutNode children to be created with z.strictObject so the schema is created
via z.strictObject({...}) rather than calling .strict() on the result.
examples/layout/src/store.tsx (1)

71-73: Remove getLayoutNodeKey helper and use getLayoutNodeId from @sqlrooms/layout.

This local function duplicates the canonical getLayoutNodeId utility already exported by the @sqlrooms/layout package (which the file already imports from). Replace the local definition with the standard helper to reduce duplication and improve consistency with the rest of the codebase.

♻️ Proposed refactor

Add getLayoutNodeId to the import:

 import {
   findNodeById,
   isLayoutDockNode,
   LayoutGridNode,
   isLayoutGridNode,
   isLayoutSplitNode,
   isLayoutNodeKey,
   LayoutDockNode,
+  getLayoutNodeId,
 } from '@sqlrooms/layout';

Remove the local helper (lines 71-73):

-function getLayoutNodeKey(node: LayoutNode): string {
-  return isLayoutNodeKey(node) ? node : node.id;
-}
-
 function getGridColsForBreakpoint(

Replace the call in createDashboardGridNode:

-        i: getLayoutNodeKey(child),
+        i: getLayoutNodeId(child),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/layout/src/store.tsx` around lines 71 - 73, The file defines a
redundant local helper getLayoutNodeKey that duplicates the canonical
getLayoutNodeId from `@sqlrooms/layout`; remove the local getLayoutNodeKey, add
getLayoutNodeId to the existing import list, and update calls (e.g., inside
createDashboardGridNode) to use getLayoutNodeId instead of getLayoutNodeKey so
the file uses the shared utility.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/sqlrooms-cli-ui/src/createDashboardAiTools.ts`:
- Line 56: Update the instruction string in DASHBOARD_AI_INSTRUCTIONS to reflect
the actual default layout type: change the text that says omitted values default
to "dock" to state they default to "grid" so the LLM will assume the correct
default when calling create_dashboard_artifact; this should match the Zod schema
default ('grid') and the store function createDashboardArtifact which sets
layoutType = 'grid'.

In
`@packages/layout/src/node-renderers/grid-node-renderer/useScrollNewGridChildIntoView.ts`:
- Around line 3-4: The module-level Maps previousGridChildIdsByNodeId and
pendingGridScrollItemByNodeId are never pruned on unmount; update the
useScrollNewGridChildIntoView hook to remove the current node's entries from
both Maps when the component unmounts: inside the effect that currently cancels
the animation frame (the cleanup associated with the hook), also call
previousGridChildIdsByNodeId.delete(nodeId) and
pendingGridScrollItemByNodeId.delete(nodeId) (using the same nodeId captured by
the hook) so entries are removed alongside cancelAnimationFrame, ensuring no
memory leak or test cross-talk.
- Around line 30-42: The two back-to-back calls in useScrollNewGridChildIntoView
(scrollContainer.scrollTo(...) and addedElement.scrollIntoView(...)) conflict
and cancel the precise scroll; remove the addedElement.scrollIntoView(...) call
and keep the computed scrollContainer.scrollTo(...) call so the exact offset is
applied (ensure you only call scrollTo with behavior:'smooth' and then return
true).

---

Nitpick comments:
In `@examples/layout/src/store.tsx`:
- Around line 71-73: The file defines a redundant local helper getLayoutNodeKey
that duplicates the canonical getLayoutNodeId from `@sqlrooms/layout`; remove the
local getLayoutNodeKey, add getLayoutNodeId to the existing import list, and
update calls (e.g., inside createDashboardGridNode) to use getLayoutNodeId
instead of getLayoutNodeKey so the file uses the shared utility.

In `@packages/layout-config/src/LayoutConfig.ts`:
- Around line 164-166: Replace the legacy .strict() call on the LayoutGridNode
schema by using the Zod top-level z.strictObject pattern to match the project's
existing schemas (e.g., LayoutSplitNode/LayoutTabsNode); specifically update the
LayoutGridNode definition that currently extends BaseLayoutGridNode and lazily
includes LayoutNode children to be created with z.strictObject so the schema is
created via z.strictObject({...}) rather than calling .strict() on the result.

In
`@packages/layout/src/node-renderers/grid-node-renderer/useScrollNewGridChildIntoView.ts`:
- Around line 6-13: getGridItemElement currently builds an array of all
[data-layout-grid-item-id] elements then .finds the match, which unnecessarily
allocates memory; change it to query for the specific attribute value using a
single DOM query (use scrollContainer.querySelector with an attribute-value
selector) and protect the itemId by escaping it with CSS.escape() so you perform
one pass and return the matching HTMLElement or undefined without creating an
intermediate array.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3241b222-6c26-4582-b923-69040af80107

📥 Commits

Reviewing files that changed from the base of the PR and between a8cb308 and 15b3771.

📒 Files selected for processing (13)
  • apps/sqlrooms-cli-ui/src/createDashboardAiTools.ts
  • apps/sqlrooms-cli-ui/src/createDashboardCommands.ts
  • examples/layout/src/store.tsx
  • knip.json
  • packages/layout-config/__tests__/LayoutConfig.test.ts
  • packages/layout-config/src/LayoutConfig.ts
  • packages/layout/src/LayoutRenderer.tsx
  • packages/layout/src/node-renderers/grid-node-renderer/GridLayout.tsx
  • packages/layout/src/node-renderers/grid-node-renderer/useScrollNewGridChildIntoView.ts
  • packages/layout/src/node-renderers/leaf-node-renderer/LeafLayoutHeader.tsx
  • packages/mosaic/__tests__/MosaicDashboardSlice.test.ts
  • packages/mosaic/src/dashboard/MosaicDashboardSlice.ts
  • packages/preset-jest/node.js
✅ Files skipped from review due to trivial changes (5)
  • knip.json
  • packages/layout-config/tests/LayoutConfig.test.ts
  • packages/mosaic/tests/MosaicDashboardSlice.test.ts
  • packages/layout/src/node-renderers/grid-node-renderer/GridLayout.tsx
  • packages/mosaic/src/dashboard/MosaicDashboardSlice.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/sqlrooms-cli-ui/src/createDashboardCommands.ts

Comment thread apps/sqlrooms-cli-ui/src/createDashboardAiTools.ts Outdated
ilyabo added 3 commits May 4, 2026 14:34
Signed-off-by: Ilya Boyandin <[email protected]>
Signed-off-by: Ilya Boyandin <[email protected]>
…of github.com:sqlrooms/sqlrooms into codex/add-grid-layout-node-type-with-draggable-panels

Signed-off-by: Ilya Boyandin <[email protected]>
@ilyabo ilyabo changed the title feat(layout): add grid node type for scrollable dashboard layouts feat(layout): Add grid node type for scrollable dashboard layouts May 4, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/layout/src/store.tsx`:
- Around line 51-69: getDashboardTitle currently returns generic "Grid layout"
or "Dock layout" for any dashboard with layoutType 'grid' or 'dock', which makes
user-created dashboards indistinguishable; update getDashboardTitle to return
the friendly names only for the seeded/known dashboardIds (e.g., 'growth-grid'
-> 'Grid layout', 'overview' -> 'Dock layout') and otherwise prefer an explicit
meta.title or fall back to the dashboardId (meta.title ?? `Dashboard
${meta.dashboardId}`) for user-created dashboards; locate and update
getDashboardTitle and any duplicate implementation (the other instance around
the later block) to check meta.dashboardId against the seeded IDs rather than
switching solely on meta.layoutType, and ensure addDashboard-created items keep
unique titles.
- Around line 83-125: createGridLayoutItem currently returns layout items
missing the required resizeHandles field; update the function to read
resizeHandles from the LayoutGridNode (e.g., const { resizeHandles } = node or
node.resizeHandles ?? default) and include that resizeHandles property on both
the early return item and the fallback return item (the objects with i, x, y, w,
h). Also ensure any seeded layouts.lg entries elsewhere in the file are updated
to include the same resizeHandles value extracted from the node so all
generated/seeded LayoutGridItem objects conform to the LayoutGridItem schema.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 229e8208-fc9b-4e18-92dc-15ebc6033943

📥 Commits

Reviewing files that changed from the base of the PR and between 15b3771 and c28f5da.

📒 Files selected for processing (5)
  • apps/sqlrooms-cli-ui/src/createDashboardAiTools.ts
  • examples/layout/src/store.tsx
  • packages/layout-config/src/LayoutConfig.ts
  • packages/layout/package.json
  • packages/layout/src/node-renderers/grid-node-renderer/useScrollNewGridChildIntoView.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/layout/package.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • apps/sqlrooms-cli-ui/src/createDashboardAiTools.ts
  • packages/layout-config/src/LayoutConfig.ts
  • packages/layout/src/node-renderers/grid-node-renderer/useScrollNewGridChildIntoView.ts

Comment thread examples/layout/src/store.tsx
Comment on lines +83 to +125
function createGridLayoutItem(
node: LayoutGridNode,
breakpoint: string,
chartId: string,
) {
const layout = node.layouts?.[breakpoint] ?? [];
const cols = Math.max(1, getGridColsForBreakpoint(node, breakpoint));
const w = Math.min(6, cols);
const h = 2;
const bottom = layout.reduce(
(maxY, item) => Math.max(maxY, item.y + item.h),
0,
);

for (let y = 0; y <= bottom; y += 1) {
for (let x = 0; x <= cols - w; x += 1) {
const overlaps = layout.some(
(item) =>
x < item.x + item.w &&
x + w > item.x &&
y < item.y + item.h &&
y + h > item.y,
);
if (!overlaps) {
return {
i: chartId,
x,
y,
w,
h,
};
}
}
}

return {
i: chartId,
x: 0,
y: bottom,
w,
h,
};
}

@coderabbitai coderabbitai Bot May 4, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Generated grid items in examples/layout/src/store.tsx ==\n'
sed -n '83,168p' examples/layout/src/store.tsx

printf '\n== LayoutGridItem contract in packages/layout-config/src/LayoutConfig.ts ==\n'
sed -n '126,171p' packages/layout-config/src/LayoutConfig.ts

Repository: sqlrooms/sqlrooms

Length of output: 3493


🏁 Script executed:

rg "LayoutGridResizeHandles" packages/layout-config/src/ -A 3

Repository: sqlrooms/sqlrooms

Length of output: 896


🏁 Script executed:

# Check if node.resizeHandles is always populated in createGridLayoutItem calls
rg "createGridLayoutItem" examples/layout/src/ -B 3 -A 3

Repository: sqlrooms/sqlrooms

Length of output: 878


Add resizeHandles to every generated grid item.

Generated grid items in createGridLayoutItem() and seeded layouts.lg entries omit the resizeHandles field required by the LayoutGridItem schema in packages/layout-config/src/LayoutConfig.ts. Extract the field from the node and propagate it to all returned items.

Proposed fix
 function createGridLayoutItem(
   node: LayoutGridNode,
   breakpoint: string,
   chartId: string,
 ) {
+  const resizeHandles = node.resizeHandles;
   const layout = node.layouts?.[breakpoint] ?? [];
   const cols = Math.max(1, getGridColsForBreakpoint(node, breakpoint));
   const w = Math.min(6, cols);
   const h = 2;
   const bottom = layout.reduce(
@@
       if (!overlaps) {
         return {
           i: chartId,
           x,
           y,
           w,
           h,
+          resizeHandles,
         };
       }
     }
   }
 
   return {
     i: chartId,
     x: 0,
     y: bottom,
     w,
     h,
+    resizeHandles,
   };
 }
 
 function createDashboardGridNode(
   dashboardId: string,
   children: LayoutNode[],
 ): LayoutGridNode {
+  const resizeHandles = ['e', 's', 'se'] as const;
   return {
     type: 'grid',
     id: dashboardId,
     panel: {key: 'dashboard', meta: {dashboardId, layoutType: 'grid'}},
     cols: 12,
     rowHeight: 220,
     margin: [12, 12],
     compactType: 'vertical',
-    resizeHandles: ['e', 's', 'se'],
+    resizeHandles,
     children,
     layouts: {
       lg: children.map((child, index) => ({
         i: getLayoutNodeId(child),
         x: (index * 6) % 12,
         y: Math.floor(index / 2) * 2,
         w: 6,
         h: 2,
+        resizeHandles,
       })),
     },
   };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/layout/src/store.tsx` around lines 83 - 125, createGridLayoutItem
currently returns layout items missing the required resizeHandles field; update
the function to read resizeHandles from the LayoutGridNode (e.g., const {
resizeHandles } = node or node.resizeHandles ?? default) and include that
resizeHandles property on both the early return item and the fallback return
item (the objects with i, x, y, w, h). Also ensure any seeded layouts.lg entries
elsewhere in the file are updated to include the same resizeHandles value
extracted from the node so all generated/seeded LayoutGridItem objects conform
to the LayoutGridItem schema.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

resizeHandles is intentionally optional on LayoutGridItem: LayoutConfig.ts defines LayoutGridResizeHandles as optional, and item-level resizeHandles is only for overrides. The grid-level setting is the source of truth: GridLayout.tsx resolves node.resizeHandles with defaults, then passes it to ResponsiveGridLayout at GridLayout.tsx.

Also, we explicitly normalize saved layouts to avoid storing redundant per-item handles: GridLayout.tsx only preserves item.resizeHandles when it differs from the grid-level handles. So adding resizeHandles to every generated item in store.tsx would be a step backward and would bloat persisted layout state.

The feedback is based on the premise that LayoutGridItem.resizeHandles is required, but in the current schema it isn’t.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@ilyabo, you're absolutely right — I apologize for the incorrect review. The design is clear and intentional:

  1. LayoutGridItem.resizeHandles is optional and serves only as a per-item override.
  2. The grid-level node.resizeHandles is the authoritative source, resolved in GridLayout.tsx and passed directly to ResponsiveGridLayout.
  3. The normalization logic deliberately strips redundant per-item handles to keep persisted state lean.

My suggestion was based on a false premise. The current implementation is correct as-is.


✏️ Learnings added
Learnt from: ilyabo
Repo: sqlrooms/sqlrooms PR: 575
File: examples/layout/src/store.tsx:83-125
Timestamp: 2026-05-04T14:34:29.038Z
Learning: In `packages/layout-config/src/LayoutConfig.ts`, `LayoutGridItem.resizeHandles` is intentionally optional. It is only used for per-item overrides. The grid-level `LayoutGridNode.resizeHandles` is the source of truth and is resolved/passed to `ResponsiveGridLayout` in `packages/layout/src/node-renderers/grid-node-renderer/GridLayout.tsx`. The normalization logic in `GridLayout.tsx` explicitly strips `item.resizeHandles` from saved layout state when it matches the grid-level setting, to avoid bloat. Do not flag missing `resizeHandles` on generated `LayoutGridItem` objects in `examples/layout/src/store.tsx`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Signed-off-by: Ilya Boyandin <[email protected]>
@ilyabo
ilyabo merged commit 0166d1e into main May 4, 2026
4 checks passed
@ilyabo
ilyabo deleted the codex/add-grid-layout-node-type-with-draggable-panels branch May 4, 2026 19:20
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