Skip to content

feat: dnd-kit docking layout - #552

Merged
dmitriy-kostianetskiy merged 111 commits into
mainfrom
feat/docking-layout
Apr 29, 2026
Merged

dmitriy-kostianetskiy merged 111 commits into
mainfrom
feat/docking-layout

Conversation

@dmitriy-kostianetskiy

@dmitriy-kostianetskiy dmitriy-kostianetskiy commented Apr 20, 2026 •

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

Release Notes

  • New Features

    • Redesigned layout system with improved panel management, including split layouts, tabbed panels, and dock-based rearrangement capabilities.
    • Added assistant panel integration for in-app AI chat and configuration.
    • Added new layout example demonstrating advanced panel composition and docking workflows.
  • Bug Fixes

    • Improved panel visibility and rendering in complex layout scenarios.
  • Documentation

    • Updated layout configuration guides to reflect new split/tabs architecture.
    • Added upgrade guide for layout configuration migration.

ilyabo added 30 commits March 24, 2026 16:37
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]>
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]>
…com:sqlrooms/sqlrooms into react-mosaic-update
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]>
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]>
Signed-off-by: Ilya Boyandin <[email protected]>
Signed-off-by: Ilya Boyandin <[email protected]>

Copilot AI 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.

Pull request overview

Copilot reviewed 203 out of 207 changed files in this pull request and generated 8 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/sql-editor/src/SqlEditor.tsx
Comment thread packages/webcontainer/src/components/WebContainerWorkbench.tsx
Comment thread examples/notebook/src/room.tsx
Comment thread examples/multi-room/src/RoomPage.tsx
Comment thread packages/layout-config/src/common.ts
Comment thread packages/layout-config/src/migrate.ts
Comment thread packages/layout/src/types.ts
Comment thread packages/room-shell/src/panels/RoomPanelHeader.tsx

@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: 11

Caution

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

⚠️ Outside diff range comments (2)
packages/ui/src/components/tab-strip.tsx (1)

566-579: ⚠️ Potential issue | 🟡 Minor

Search dropdown ignores the new strip-level closeable prop.

renderOpenTabActions still renders the close X action for open tabs based solely on preventCloseLastTab. When a consumer sets closeable={false} on the TabStrip (as the layout tabs renderer does for docked panels), inline tab close buttons are hidden, but the search dropdown still exposes a close control and calls handleClose, letting users close tabs that the strip declares non-closeable.

Consider gating this action on closeable as well (destructure it from useTabStripContext() in TabStripSearchDropdown and skip rendering the close action when it is false).

🔧 Suggested fix
   const {
     openTabItems,
     search,
     setSearch,
     closedTabs,
     filteredTabs,
     closedTabIds,
     openTabs,
     preventCloseLastTab,
+    closeable,
     onOpenTabsChange,
     onSelect,
     renderSearchItemActions,
     getLastOpenedAt,
     handleClose,
   } = useTabStripContext();
@@
   const renderOpenTabActions = (tab: TabDescriptor) => {
-    const disableClose = preventCloseLastTab && (openTabsList.length ?? 0) <= 1;
+    if (!closeable) {
+      return renderSearchItemActions?.(tab) ?? null;
+    }
+    const disableClose = preventCloseLastTab && (openTabsList.length ?? 0) <= 1;
     return (
       <>
         {renderSearchItemActions?.(tab)}
         <TabStripSearchItemAction
           icon={<XIcon className="h-3 w-3" />}
           aria-label={`Close ${tab.name}`}
           onClick={disableClose ? undefined : () => handleClose(tab.id)}
           className={cn(disableClose && 'pointer-events-none opacity-40')}
         />
       </>
     );
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/components/tab-strip.tsx` around lines 566 - 579, The search
dropdown still shows the inline close action regardless of the strip-level
closeable prop; update TabStripSearchDropdown to read closeable from
useTabStripContext() and modify renderOpenTabActions to check both closeable and
preventCloseLastTab before rendering TabStripSearchItemAction (and before
calling handleClose). Specifically, destructure closeable from
useTabStripContext(), compute disableClose = !closeable || (preventCloseLastTab
&& (openTabsList.length ?? 0) <= 1), and skip rendering the
TabStripSearchItemAction when closeable is false so the dropdown respects the
strip-level closeable setting.
examples/deckgl-mosaic/src/store.ts (1)

2-3: ⚠️ Potential issue | 🟡 Minor

Fix import path to use package public entry point

MosaicSliceState is exported from the @sqlrooms/mosaic package root and should be imported from there instead of the deep dist subpath. Change line 3 from:

import {MosaicSliceState} from '@sqlrooms/mosaic/dist/MosaicSlice';

to:

import {MosaicSliceState} from '@sqlrooms/mosaic';

Importing from private dist paths is fragile and will break if the package's internal build structure changes.

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

In `@examples/deckgl-mosaic/src/store.ts` around lines 2 - 3, Update the import to
use the package public entry point: replace the deep import of MosaicSliceState
from '@sqlrooms/mosaic/dist/MosaicSlice' with an import from '@sqlrooms/mosaic'
so that both MosaicSliceState and createMosaicSlice are imported from the
package root (ensure the import statement imports { MosaicSliceState } from
'@sqlrooms/mosaic' alongside createMosaicSlice).
♻️ Duplicate comments (1)
apps/sqlrooms-cli-ui/src/createDashboardCommands.ts (1)

109-118: ⚠️ Potential issue | 🟠 Major

createDashboardCreateSheetCommand.execute still doesn't select the new sheet.

The description says "Create a new dashboard sheet and select it", but execute only calls createDashboardSheet(title) and never sets the newly created sheet as current. The generic createSheetCommand helper above (line 80) correctly calls getState().cells.setCurrentSheet(sheetId) — mirror that here so the dashboard command matches its contract and is consistent with the other create commands.

🔧 Suggested fix
     execute: ({getState}, input) => {
       const {title} = (input as CreateSheetCommandInput | undefined) ?? {};
       const sheetId = getState().dashboard.createDashboardSheet(title);
+      getState().cells.setCurrentSheet(sheetId);
       return {
         success: true,
         commandId: 'dashboard.create-sheet',
         message: `Created dashboard sheet "${sheetId}".`,
         data: {sheetId},
       };
     },
🤖 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 109 - 118,
The execute implementation in createDashboardCommands (the execute function that
calls getState().dashboard.createDashboardSheet(title)) creates a sheet but
doesn't select it; update this execute to call
getState().cells.setCurrentSheet(sheetId) after obtaining sheetId (mirror the
behavior from the generic createSheetCommand helper) so the new dashboard sheet
is set as the current sheet before returning the success response.
🧹 Nitpick comments (17)
examples/mosaic/src/components/DataSourcesPanel.tsx (1)

59-61: Remove commented-out AddDataModal block.

Dead/commented code adds noise; drop it or convert to a TODO with a tracked issue if the modal is still planned.

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

In `@examples/mosaic/src/components/DataSourcesPanel.tsx` around lines 59 - 61,
Remove the commented-out AddDataModal block to eliminate dead code: delete the
commented JSX lines referencing AddDataModal, isOpen, and handleModalClose in
DataSourcesPanel.tsx; if the modal is still intended to be implemented later,
replace the commented block with a concise TODO comment referencing a tracking
issue (e.g., "TODO: implement AddDataModal - see ISSUE-###") so intent is
preserved without leftover commented JSX.
examples/canvas/src/components/DataSourcesPanel.tsx (1)

7-9: Align with codebase convention by using type-only import for FC.

The repository consistently uses import type for type-only imports throughout the codebase. Change to import type {FC} to maintain consistency.

♻️ Proposed fix
-import {FC} from 'react';
+import type {FC} from 'react';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/canvas/src/components/DataSourcesPanel.tsx` around lines 7 - 9, The
import of React's FC should be a type-only import to match repo conventions;
change the top import to use a type-only import (e.g., import type { FC } from
'react') and ensure the component declaration DataSourcesPanel: FC remains
unchanged so only type information is imported.
packages/ui/src/components/tab-strip.tsx (1)

136-141: TAB_STRIP_BUTTON_CLASSNAMES mixes sizing/padding classes into surfaces that also pass size="icon".

TAB_STRIP_BUTTON_CLASSNAMES bakes in px-6 py-1 and min-h-7, and is applied to:

  • the TabsTrigger (a text tab — expected),
  • the search dropdown trigger which is a Button size="icon" (line 586),
  • TabStripButton which is also Button size="icon" (line 765).

For the two icon buttons, the shared classes override size="icon"'s own padding/size via tailwind-merge, which works but couples all three surfaces to a single shape. If the text-tab padding (px-6) is ever tuned, the icon buttons silently change too.

Consider splitting this into two groups — a focus/hover ring set shared by all, and a padding/sizing set applied only to the tab trigger — so the icon-button surfaces stop paying for text-tab padding. Not blocking.

Also applies to: 194-203, 581-592, 762-775

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

In `@packages/ui/src/components/tab-strip.tsx` around lines 136 - 141,
TAB_STRIP_BUTTON_CLASSNAMES currently mixes focus/hover ring classes with
sizing/padding (px-6 py-1, min-h-7) which overrides Button size="icon"; split
this constant into two: e.g. TAB_STRIP_SURFACE_CLASSNAMES (shared
focus/hover/ring/overflow classes) and TAB_STRIP_TEXT_PADDING_CLASSNAMES
(padding/min-height for text tabs), then update uses so TabsTrigger and the text
tab composition include both arrays but the search dropdown trigger and
TabStripButton (Button size="icon") only receive TAB_STRIP_SURFACE_CLASSNAMES so
their intrinsic size/padding from size="icon" is preserved.
examples/get-started/src/components/MainView.tsx (1)

10-12: Use import type for FC since it's only used as a type.

FC is only used as a TypeScript type annotation and is never referenced at runtime. Using import type clarifies intent and prevents accidental runtime references.

Proposed fix
-import {FC} from 'react';
+import type {FC} from 'react';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/get-started/src/components/MainView.tsx` around lines 10 - 12,
Change the runtime import of FC to a type-only import: replace the current
import of FC with a type import (use "import type { FC } from 'react';") so FC
is erased at runtime; keep the component declaration export const MainView: FC =
() => { ... } unchanged. This ensures FC is only used as a TypeScript type and
prevents accidental runtime references.
examples/cosmos-embedding/src/components/DataSourcesPanel.tsx (1)

83-85: Remove the commented-out AddDataModal block.

Stale debug/dead code; either restore it as a tracked TODO with context or drop it to keep the example clean.

🧹 Proposed cleanup
-      {/* {AddDataModal ? (
-        <AddDataModal isOpen={isOpen} onClose={handleModalClose} />
-      ) : null} */}
     </RoomPanel>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/cosmos-embedding/src/components/DataSourcesPanel.tsx` around lines
83 - 85, Remove the stale commented-out JSX block referencing AddDataModal (the
fragment containing "<AddDataModal isOpen={isOpen} onClose={handleModalClose}
/>") from DataSourcesPanel; either delete the commented lines entirely or, if
you want to preserve intent, replace them with a short tracked TODO comment
explaining why AddDataModal was removed and what props (isOpen,
handleModalClose) it used to require so future restoration is clear.
examples/minimal/src/room.tsx (1)

9-9: Use import type for the React FC type.

FC is used only in a type annotation. Using import type improves code clarity by explicitly marking it as a type-only dependency.

Proposed diff
-import {FC} from 'react';
+import type {FC} from 'react';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/minimal/src/room.tsx` at line 9, The import in room.tsx currently
brings in FC as a regular value import; change it to a type-only import to
reflect that FC is used only in type annotations by replacing the import of FC
with an `import type` declaration (e.g., import type { FC } from 'react') so the
symbol FC is treated as a type-only dependency and bundlers/typecheckers can
optimize accordingly; update the import statement where FC is referenced.
examples/deckgl-mosaic/src/store.ts (1)

27-28: Minor: as const on z.enum argument is redundant.

When passing an array literal directly to z.enum(...), the literal union is inferred without as const. The assertion can be dropped. Additionally, declaring the runtime const and the inferred type with the same identifier (RoomPanelTypes) is confusing at call sites — consider using RoomPanelType for the type instead.

Proposed change
-export const RoomPanelTypes = z.enum(['left', 'data', 'main'] as const);
-export type RoomPanelTypes = z.infer<typeof RoomPanelTypes>;
+export const RoomPanelTypes = z.enum(['left', 'data', 'main']);
+export type RoomPanelType = z.infer<typeof RoomPanelTypes>;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/deckgl-mosaic/src/store.ts` around lines 27 - 28, The z.enum call
uses an unnecessary "as const" and the runtime and type share the same name
which is confusing; update the declaration by removing the "as const" from the
z.enum argument (keep export const RoomPanelTypes = z.enum(['left', 'data',
'main'])) and rename the inferred type to a distinct singular name (export type
RoomPanelType = z.infer<typeof RoomPanelTypes>); also search for any usages of
the old type name RoomPanelTypes in the code and replace them with
RoomPanelType.
examples/query-motherduck/src/components/MainView.tsx (1)

22-22: Move shared token constants out of the root App module.

Line 22 makes this child component depend on ../App just to read MD_TOKEN_KEY. A neutral constants module avoids root-component coupling and circular-import surprises as the Room/MainView tree evolves.

♻️ Proposed refactor
+// examples/query-motherduck/src/constants.ts
+export const MD_TOKEN_KEY = 'motherduck-token';
 // examples/query-motherduck/src/App.tsx
 import {ThemeProvider} from '@sqlrooms/ui';
 import {FC, useEffect, useState} from 'react';
+import {MD_TOKEN_KEY} from './constants';
 import {MdConnectPane} from './components/MdConnectPane';
 import {Room} from './Room';
 
-export const MD_TOKEN_KEY = 'motherduck-token';
-
 export const App: FC = () => {
 // examples/query-motherduck/src/components/MainView.tsx
-import {MD_TOKEN_KEY} from '../App';
+import {MD_TOKEN_KEY} from '../constants';
 import {useRoomStore} from '../store';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/query-motherduck/src/components/MainView.tsx` at line 22,
MainView.tsx currently imports MD_TOKEN_KEY from ../App which couples the child
component to the root; create a neutral constants module (e.g., export
MD_TOKEN_KEY from a new module like tokenConstants or constants) and move the
MD_TOKEN_KEY definition there, then update MainView.tsx and App (and any other
consumers) to import MD_TOKEN_KEY from the new module (ensure the symbol name
MD_TOKEN_KEY is exported as-is to avoid breaking usages).
examples/query-motherduck/src/store.ts (1)

19-19: 'left' enum value is a container id, not a panel type.

RoomPanelTypes is used both as the layout node id for the tabs container (Line 51) and as keys into layout.panels (Lines 70, 73). The 'left' value is only used for the container and has no corresponding entry in panels, which conflates two different namespaces in one enum. Consider either adding a separate type for container ids, or documenting that 'left' is intentionally a container slot without a panels entry, to avoid confusion for downstream consumers who iterate RoomPanelTypes.options.

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

In `@examples/query-motherduck/src/store.ts` at line 19, RoomPanelTypes currently
mixes a container id ('left') with actual panel keys causing confusion when
iterating RoomPanelTypes.options and indexing layout.panels; split the
namespaces by creating a new enum (e.g., ContainerIds = z.enum(['left'] as
const)) and change RoomPanelTypes to only include real panel keys (e.g.,
z.enum(['data','main'] as const)), then update usages that refer to the tabs
container id to use ContainerIds (or add a clarifying comment if you
intentionally keep 'left' in the enum) and ensure code that indexes
layout.panels uses RoomPanelTypes only.
examples/query/src/components/DataPanel.tsx (1)

7-9: Use import type for type-only imports.

FC is only used in a type position here. While the repository does not currently enforce this via stricter TypeScript configs, importing type-only symbols with import type is a best practice that improves clarity and aligns with the pattern used in other packages in this repository.

♻️ Proposed fix
-import {FC} from 'react';
+import type {FC} from 'react';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/query/src/components/DataPanel.tsx` around lines 7 - 9, The import
of FC from 'react' in the DataPanel component is type-only; change the import to
a type import by replacing "import {FC} from 'react'" with a type-only import
(e.g., "import type { FC } from 'react'") so the DataPanel: FC declaration uses
a type import rather than a runtime import.
examples/pivot/src/App.tsx (1)

4-6: Make the React FC import type-only.

FC is only used as a type annotation; prefer import type to avoid issues with stricter TypeScript module emit settings.

♻️ Proposed fix
-import {FC} from 'react';
+import type {FC} from 'react';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/pivot/src/App.tsx` around lines 4 - 6, The import of the React type
FC in App.tsx is not type-only; update the import statement that brings in FC so
it is a type-only import (used by the App: FC annotation) to avoid runtime
import emission under stricter TS settings—replace the regular import of FC with
a type-only import and keep the App component signature as App: FC.
examples/pivot/src/components/DataPanel.tsx (1)

7-9: Use import type for the FC import.

FC is only used as a type annotation here. While this codebase doesn't have verbatimModuleSyntax enabled, using import type is a best practice to ensure the import is properly tree-shaken and avoid any unnecessary runtime overhead.

♻️ Proposed fix
-import {FC} from 'react';
+import type {FC} from 'react';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/pivot/src/components/DataPanel.tsx` around lines 7 - 9, The import
of FC is only used as a type; change the import statement to a type-only import
so it won't be kept at runtime. Update the top-level import from "import {FC}
from 'react';" to use "import type" for the FC symbol, leaving the DataPanel: FC
usage unchanged; ensure the symbol FC is imported via a type-only import to
allow proper tree-shaking.
packages/layout/src/docking/dock-layout.ts (1)

117-128: Gate or remove docking debug logs.

These logs run from core docking/size paths and can spam production consoles during panel rearrangement. Prefer a debug logger or remove them before release.

Also applies to: 149-164, 342-348, 434-447, 472-476, 559-574

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

In `@packages/layout/src/docking/dock-layout.ts` around lines 117 - 128, The
console.log in dock-layout.ts (inside equalizeChildren) and the other debug
prints at the indicated ranges should be gated or removed so production consoles
aren't spammed; replace direct console.log calls with a debug-level logger or
guard them behind a runtime/dev flag (e.g., only run when DEBUG_DOCKING or
NODE_ENV !== 'production') and keep the same structured payload (references:
equalizeChildren, getLayoutNodeId, getSizeProps) so behavior is unchanged when
enabled; do this for all instances (lines ~117-128, 149-164, 342-348, 434-447,
472-476, 559-574). Ensure no console.* calls remain unguarded in
DockLayout-related functions.
examples/layout/src/components/ResultsPanel.tsx (1)

17-32: Minor: column header/data field naming mismatch.

The header is labelled created_at (Line 17) but the row objects use date (Lines 22-26, rendered via row.date on Line 32). Since the SchemaPanel advertises a created_at: TIMESTAMP column, renaming the field to created_at would make the demo data self-consistent.

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

In `@examples/layout/src/components/ResultsPanel.tsx` around lines 17 - 32, The
table header uses created_at but the mock rows use date; update the mock data
objects and render access in ResultsPanel (the array of rows and the JSX that
reads row.date) to use created_at instead of date so the header, rendered cell
(row.created_at) and the advertised SchemaPanel column (created_at: TIMESTAMP)
are consistent.
packages/layout/src/docking/DockDragOverlay.tsx (1)

15-30: Consider clamping badge position to viewport.

Near the right/bottom edges the badge can extend past the viewport (adding BADGE_OFFSET unconditionally). Consider flipping to the opposite side or clamping against window.innerWidth/innerHeight minus the badge size when it would overflow. Low priority since it’s cosmetic.

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

In `@packages/layout/src/docking/DockDragOverlay.tsx` around lines 15 - 30, The
badge currently places itself at cursorPosition + BADGE_OFFSET in
DockDragOverlay (used when activePanelTitle and cursorPosition exist) which can
overflow the viewport; update the positioning logic used for the createPortal
inline style to clamp or flip coordinates against
window.innerWidth/window.innerHeight so the badge stays fully visible: measure
or approximate the badge size (via a ref or known constants) and if
cursorPosition.x + BADGE_OFFSET + badgeWidth > window.innerWidth then position
it to the left of the cursor (cursorPosition.x - BADGE_OFFSET - badgeWidth) and
similarly for vertical overflow use cursorPosition.y - BADGE_OFFSET -
badgeHeight, otherwise use the original offset; ensure BADGE_OFFSET,
cursorPosition, and createPortal usage in DockDragOverlay are updated to apply
these computed left/top values.
packages/layout/src/docking/DockingProvider.tsx (1)

121-144: Cursor tracking breaks under non-pointer sensors.

handleDragMove reads event.activatorEvent.clientX/Y to seed the cursor position. Today you only register a PointerSensor, so this is safe, but any future addition of KeyboardSensor (or similar) will make hasPointerCoordinates return false and the custom drag overlay will stop following the caret. Consider driving the overlay from translatedRect.left/top + window.scrollX/Y (which is available for any sensor) so this remains sensor-agnostic. Non-blocking today.

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

In `@packages/layout/src/docking/DockingProvider.tsx` around lines 121 - 144, In
handleDragMove, avoid relying on event.activatorEvent.clientX/Y (used only by
pointer sensors); instead compute the overlay cursor from the translatedRect
coordinates plus page scroll so it works with non-pointer sensors—use
translatedRect.left/top combined with window.scrollX/window.scrollY (or similar)
to setCursorPosition; keep the existing hasPointerCoordinates branch only for
pointer-specific offsets and fallback to using translatedRect + scroll when
activatorEvent lacks pointer coordinates, updating the logic in handleDragMove
(and leaving updatePreview/setCursorPosition calls intact).
examples/layout/src/store.tsx (1)

289-348: Minor: redundant first lookup and lossy equal-size distribution.

Two observations on addChartToDashboard:

  1. Redundant findNodeById — lines 294–304 look up and validate the dock on the original config, then lines 308–316 repeat the same lookup/validation on the cloned newConfig. Since you only mutate the clone, the first pass is redundant and can be dropped.
  2. Math.floor(100 / totalChildren) loses percent (line 328) — e.g., 3 children → 33 each → 99% total; 6 → 96%. Depending on how the split renderer normalizes sizes this may cause a small residual gap or truncation. Consider distributing the remainder to the first child, or simply omitting defaultSize and letting the panel library normalize.
🛠️ Suggested simplification
-        const dockResult = findNodeById(config, dashboardId);
-        if (!dockResult || !isLayoutDockNode(dockResult.node)) {
-          return;
-        }
-
-        const dockNode = dockResult.node;
-
-        // Find the root split of the dock node
-        if (!isLayoutSplitNode(dockNode.root)) {
-          return;
-        }
-
-        // Clone the config and add the new chart panel to the dock's root split
-        const newConfig = JSON.parse(JSON.stringify(config)) as LayoutConfig;
+        // Clone up front and operate on the clone only.
+        const newConfig =
+          typeof structuredClone === 'function'
+            ? (structuredClone(config) as LayoutConfig)
+            : (JSON.parse(JSON.stringify(config)) as LayoutConfig);
         const newDockResult = findNodeById(newConfig, dashboardId);
         if (!newDockResult || !isLayoutDockNode(newDockResult.node)) {
           return;
         }
🤖 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 289 - 348, In
addChartToDashboard: remove the initial lookup/validation against the original
config (the block using findNodeById(config) and isLayoutDockNode on the
original dock) and only perform the lookup/validation after cloning (use
newConfig and newDockResult/newDockNode), and change the equal-size assignment
so it doesn’t use Math.floor(100 / totalChildren) which drops the remainder —
either omit setting defaultSize so the panel library normalizes sizes, or
compute base = Math.floor(100 / totalChildren) and add the leftover (100 - base
* totalChildren) to the first child in newDockNode.root.children; update
references to totalChildren, equalSize (or base/leftover) accordingly before
calling setConfig(newConfig).
🤖 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/ai/src/components/DataSourcesPanel.tsx`:
- Around line 7-9: The import of FC should be type-only; change the import to a
type-only import (use "import type { FC } from 'react'") and keep the component
signature export const DataSourcesPanel: FC = () => { ... } unchanged so the
runtime bundle doesn't include a value import for FC; update the import
statement that currently imports FC to use "import type" referencing the FC type
and DataSourcesPanel component.

In `@examples/deckgl-discuss/src/store.ts`:
- Line 26: The code uses the string-indexed enum access
RoomPanelTypes.enum['main'] which hardcodes the literal; replace those
occurrences with the imported MAIN_VIEW constant (i.e., use MAIN_VIEW wherever
RoomPanelTypes.enum['main'] is referenced) so the value stays in sync with the
source of truth; update all references (e.g., the two spots currently accessing
RoomPanelTypes.enum['main']) to use MAIN_VIEW and run type checks to ensure
compatibility with RoomPanelTypes usage.

In `@examples/notebook/src/room.tsx`:
- Around line 14-15: Two identical instances of RoomShell.LoadingProgress are
rendered causing duplicate UI and potential duplicated effects/subscriptions;
remove the duplicate so only a single RoomShell.LoadingProgress remains in the
render. Locate the repeated components (both references to
RoomShell.LoadingProgress) and delete one of them, keeping a single instance to
preserve expected behavior and avoid duplicate side-effects.

In `@examples/query-motherduck/src/store.ts`:
- Around line 49-61: The left tabs container (id RoomPanelTypes.enum['left']) is
initialized with collapsed: true, collapsedSize: 0 and hideTabStrip: true which
hides the Data panel on first load; either ensure there is a UI trigger that
toggles this container (e.g., implement or wire a SidebarButton that flips the
collapsed state for RoomPanelTypes.enum['left']) or change the initial layout to
make the panel discoverable by setting collapsed: false or collapsedSize to a
non-zero value (and/or show the tab strip) in the layout config where children
includes RoomPanelTypes.enum['data'] so users can see and expand the Data panel
on first load.

In `@examples/query/src/App.tsx`:
- Around line 4-6: The import of FC should be type-only since it's only used for
the App type annotation; change the import to use a type-only import (import
type { FC } from 'react') and keep the App: FC annotation as-is so the runtime
bundle no longer includes the FC symbol.

In `@packages/layout-config/README.md`:
- Around line 12-15: The README's public schema section omits the dock node and
its type guards; update the exports/listing to include LayoutDockNode,
isLayoutDockNode and the 'dock' node type so the documentation matches the new
layout model and public API. Specifically add mentions of LayoutDockNode and
isLayoutDockNode alongside LayoutPanelNode/LayoutSplitNode/LayoutTabsNode, and
include 'dock' in the node type list and example snippets (e.g., where
createDefaultLayout(), LayoutConfig, and node-type predicates are documented) so
consumers can find and use the dock node.

In `@packages/layout/README.md`:
- Around line 92-108: The Button handlers in PanelButtons are calling undefined
functions (setActiveTab, addTab, setCollapsed); update each onClick to call the
actions selected from useRoomStore: call setActivePanel for the "Show Data
Panel" button, call addPanelToArea for the "Add Schema Tab" button, and call
setAreaCollapsed for the "Collapse Sidebar" button so the component uses the
declared layout actions (PanelButtons, useRoomStore, layout.setActivePanel,
layout.addPanelToArea, layout.setAreaCollapsed).

In `@packages/layout/src/docking/dock-layout.ts`:
- Around line 449-457: The current logic picks sizedChildren by either
equalizeChildren(children) or splitTargetShare(children, targetIndexAfterInsert,
insertIndex, targetSize), but when moving an existing child (source was removed)
siblings retain old defaultSize and splitTargetShare can produce totals <100%;
normalize sizes after removal. Update the block that computes sizedChildren (the
use of equalizeChildren and splitTargetShare) so that when performing a move
(i.e., sourceRemoved is true or when targetSize is provided and the children
list was mutated by removal) you first normalize remaining children sizes (call
equalizeChildren or a normalizeDefaultSizes helper) before calling
splitTargetShare, ensuring the final sizedChildren totals sum to 100% and
preventing the undersized result shown; reference splitTargetShare,
equalizeChildren, sizedChildren, targetIndexAfterInsert, insertIndex, and
targetSize to locate and change the logic.

In `@packages/layout/src/docking/docking-helpers.ts`:
- Around line 52-65: buildPreview currently generates wrap/insert previews even
when movePanel would reject the drop; update buildPreview to mirror movePanel's
eligibility checks by early-returning null when the target is missing, not
inside a dock (i.e., findNodeById(rootLayout, targetId) yields no valid parent
that is a layout split node), or belongs to a different dock than the source.
Locate buildPreview in docking-helpers.ts and use the same guards as movePanel:
check found from findNodeById, ensure parent exists and
isLayoutSplitNode(parent), and verify the dock/container identity via
getContainerElement(sameAxisParent?.id) (or compare ancestor roots) before
proceeding to compute containerRect so invalid drops produce no preview.

In `@packages/layout/src/layout-tree.ts`:
- Around line 163-175: visitLayoutLeafNodes currently short-circuits based on
truthiness so valid falsy visitor returns (false, 0, '') are treated as
no-result; update the two short-circuit checks inside the isLayoutSplitNode and
isLayoutTabsNode branches (the lines that read "if (result) return result;") to
explicitly check for undefined (e.g., "if (result !== undefined) return
result;") so any non-undefined T value is preserved and returned.

In
`@packages/layout/src/node-renderers/leaf-node-renderer/LeafLayoutDragHandle.tsx`:
- Around line 40-57: The overlay is only revealed via hover
(group-hover:opacity-100) so keyboard users can't see focusable items; update
the container CSS/classes to also reveal on focus by adding focus-within (and
optionally focus-visible on children) so when the inner div with {...attributes}
{...listeners} or the close button (onClick={handleRemove}) receives focus the
parent overlay becomes visible; target the same element that currently has
"opacity-0 group-hover:opacity-100" (the div wrapping GripHorizontalIcon and the
button) and add classes for focus-within:opacity-100 and/or
focus-visible:opacity-100 so the drag grip and XIcon become visible for keyboard
navigation.

---

Outside diff comments:
In `@examples/deckgl-mosaic/src/store.ts`:
- Around line 2-3: Update the import to use the package public entry point:
replace the deep import of MosaicSliceState from
'@sqlrooms/mosaic/dist/MosaicSlice' with an import from '@sqlrooms/mosaic' so
that both MosaicSliceState and createMosaicSlice are imported from the package
root (ensure the import statement imports { MosaicSliceState } from
'@sqlrooms/mosaic' alongside createMosaicSlice).

In `@packages/ui/src/components/tab-strip.tsx`:
- Around line 566-579: The search dropdown still shows the inline close action
regardless of the strip-level closeable prop; update TabStripSearchDropdown to
read closeable from useTabStripContext() and modify renderOpenTabActions to
check both closeable and preventCloseLastTab before rendering
TabStripSearchItemAction (and before calling handleClose). Specifically,
destructure closeable from useTabStripContext(), compute disableClose =
!closeable || (preventCloseLastTab && (openTabsList.length ?? 0) <= 1), and skip
rendering the TabStripSearchItemAction when closeable is false so the dropdown
respects the strip-level closeable setting.

---

Duplicate comments:
In `@apps/sqlrooms-cli-ui/src/createDashboardCommands.ts`:
- Around line 109-118: The execute implementation in createDashboardCommands
(the execute function that calls
getState().dashboard.createDashboardSheet(title)) creates a sheet but doesn't
select it; update this execute to call getState().cells.setCurrentSheet(sheetId)
after obtaining sheetId (mirror the behavior from the generic createSheetCommand
helper) so the new dashboard sheet is set as the current sheet before returning
the success response.

---

Nitpick comments:
In `@examples/canvas/src/components/DataSourcesPanel.tsx`:
- Around line 7-9: The import of React's FC should be a type-only import to
match repo conventions; change the top import to use a type-only import (e.g.,
import type { FC } from 'react') and ensure the component declaration
DataSourcesPanel: FC remains unchanged so only type information is imported.

In `@examples/cosmos-embedding/src/components/DataSourcesPanel.tsx`:
- Around line 83-85: Remove the stale commented-out JSX block referencing
AddDataModal (the fragment containing "<AddDataModal isOpen={isOpen}
onClose={handleModalClose} />") from DataSourcesPanel; either delete the
commented lines entirely or, if you want to preserve intent, replace them with a
short tracked TODO comment explaining why AddDataModal was removed and what
props (isOpen, handleModalClose) it used to require so future restoration is
clear.

In `@examples/deckgl-mosaic/src/store.ts`:
- Around line 27-28: The z.enum call uses an unnecessary "as const" and the
runtime and type share the same name which is confusing; update the declaration
by removing the "as const" from the z.enum argument (keep export const
RoomPanelTypes = z.enum(['left', 'data', 'main'])) and rename the inferred type
to a distinct singular name (export type RoomPanelType = z.infer<typeof
RoomPanelTypes>); also search for any usages of the old type name RoomPanelTypes
in the code and replace them with RoomPanelType.

In `@examples/get-started/src/components/MainView.tsx`:
- Around line 10-12: Change the runtime import of FC to a type-only import:
replace the current import of FC with a type import (use "import type { FC }
from 'react';") so FC is erased at runtime; keep the component declaration
export const MainView: FC = () => { ... } unchanged. This ensures FC is only
used as a TypeScript type and prevents accidental runtime references.

In `@examples/layout/src/components/ResultsPanel.tsx`:
- Around line 17-32: The table header uses created_at but the mock rows use
date; update the mock data objects and render access in ResultsPanel (the array
of rows and the JSX that reads row.date) to use created_at instead of date so
the header, rendered cell (row.created_at) and the advertised SchemaPanel column
(created_at: TIMESTAMP) are consistent.

In `@examples/layout/src/store.tsx`:
- Around line 289-348: In addChartToDashboard: remove the initial
lookup/validation against the original config (the block using
findNodeById(config) and isLayoutDockNode on the original dock) and only perform
the lookup/validation after cloning (use newConfig and
newDockResult/newDockNode), and change the equal-size assignment so it doesn’t
use Math.floor(100 / totalChildren) which drops the remainder — either omit
setting defaultSize so the panel library normalizes sizes, or compute base =
Math.floor(100 / totalChildren) and add the leftover (100 - base *
totalChildren) to the first child in newDockNode.root.children; update
references to totalChildren, equalSize (or base/leftover) accordingly before
calling setConfig(newConfig).

In `@examples/minimal/src/room.tsx`:
- Line 9: The import in room.tsx currently brings in FC as a regular value
import; change it to a type-only import to reflect that FC is used only in type
annotations by replacing the import of FC with an `import type` declaration
(e.g., import type { FC } from 'react') so the symbol FC is treated as a
type-only dependency and bundlers/typecheckers can optimize accordingly; update
the import statement where FC is referenced.

In `@examples/mosaic/src/components/DataSourcesPanel.tsx`:
- Around line 59-61: Remove the commented-out AddDataModal block to eliminate
dead code: delete the commented JSX lines referencing AddDataModal, isOpen, and
handleModalClose in DataSourcesPanel.tsx; if the modal is still intended to be
implemented later, replace the commented block with a concise TODO comment
referencing a tracking issue (e.g., "TODO: implement AddDataModal - see
ISSUE-###") so intent is preserved without leftover commented JSX.

In `@examples/pivot/src/App.tsx`:
- Around line 4-6: The import of the React type FC in App.tsx is not type-only;
update the import statement that brings in FC so it is a type-only import (used
by the App: FC annotation) to avoid runtime import emission under stricter TS
settings—replace the regular import of FC with a type-only import and keep the
App component signature as App: FC.

In `@examples/pivot/src/components/DataPanel.tsx`:
- Around line 7-9: The import of FC is only used as a type; change the import
statement to a type-only import so it won't be kept at runtime. Update the
top-level import from "import {FC} from 'react';" to use "import type" for the
FC symbol, leaving the DataPanel: FC usage unchanged; ensure the symbol FC is
imported via a type-only import to allow proper tree-shaking.

In `@examples/query-motherduck/src/components/MainView.tsx`:
- Line 22: MainView.tsx currently imports MD_TOKEN_KEY from ../App which couples
the child component to the root; create a neutral constants module (e.g., export
MD_TOKEN_KEY from a new module like tokenConstants or constants) and move the
MD_TOKEN_KEY definition there, then update MainView.tsx and App (and any other
consumers) to import MD_TOKEN_KEY from the new module (ensure the symbol name
MD_TOKEN_KEY is exported as-is to avoid breaking usages).

In `@examples/query-motherduck/src/store.ts`:
- Line 19: RoomPanelTypes currently mixes a container id ('left') with actual
panel keys causing confusion when iterating RoomPanelTypes.options and indexing
layout.panels; split the namespaces by creating a new enum (e.g., ContainerIds =
z.enum(['left'] as const)) and change RoomPanelTypes to only include real panel
keys (e.g., z.enum(['data','main'] as const)), then update usages that refer to
the tabs container id to use ContainerIds (or add a clarifying comment if you
intentionally keep 'left' in the enum) and ensure code that indexes
layout.panels uses RoomPanelTypes only.

In `@examples/query/src/components/DataPanel.tsx`:
- Around line 7-9: The import of FC from 'react' in the DataPanel component is
type-only; change the import to a type import by replacing "import {FC} from
'react'" with a type-only import (e.g., "import type { FC } from 'react'") so
the DataPanel: FC declaration uses a type import rather than a runtime import.

In `@packages/layout/src/docking/dock-layout.ts`:
- Around line 117-128: The console.log in dock-layout.ts (inside
equalizeChildren) and the other debug prints at the indicated ranges should be
gated or removed so production consoles aren't spammed; replace direct
console.log calls with a debug-level logger or guard them behind a runtime/dev
flag (e.g., only run when DEBUG_DOCKING or NODE_ENV !== 'production') and keep
the same structured payload (references: equalizeChildren, getLayoutNodeId,
getSizeProps) so behavior is unchanged when enabled; do this for all instances
(lines ~117-128, 149-164, 342-348, 434-447, 472-476, 559-574). Ensure no
console.* calls remain unguarded in DockLayout-related functions.

In `@packages/layout/src/docking/DockDragOverlay.tsx`:
- Around line 15-30: The badge currently places itself at cursorPosition +
BADGE_OFFSET in DockDragOverlay (used when activePanelTitle and cursorPosition
exist) which can overflow the viewport; update the positioning logic used for
the createPortal inline style to clamp or flip coordinates against
window.innerWidth/window.innerHeight so the badge stays fully visible: measure
or approximate the badge size (via a ref or known constants) and if
cursorPosition.x + BADGE_OFFSET + badgeWidth > window.innerWidth then position
it to the left of the cursor (cursorPosition.x - BADGE_OFFSET - badgeWidth) and
similarly for vertical overflow use cursorPosition.y - BADGE_OFFSET -
badgeHeight, otherwise use the original offset; ensure BADGE_OFFSET,
cursorPosition, and createPortal usage in DockDragOverlay are updated to apply
these computed left/top values.

In `@packages/layout/src/docking/DockingProvider.tsx`:
- Around line 121-144: In handleDragMove, avoid relying on
event.activatorEvent.clientX/Y (used only by pointer sensors); instead compute
the overlay cursor from the translatedRect coordinates plus page scroll so it
works with non-pointer sensors—use translatedRect.left/top combined with
window.scrollX/window.scrollY (or similar) to setCursorPosition; keep the
existing hasPointerCoordinates branch only for pointer-specific offsets and
fallback to using translatedRect + scroll when activatorEvent lacks pointer
coordinates, updating the logic in handleDragMove (and leaving
updatePreview/setCursorPosition calls intact).

In `@packages/ui/src/components/tab-strip.tsx`:
- Around line 136-141: TAB_STRIP_BUTTON_CLASSNAMES currently mixes focus/hover
ring classes with sizing/padding (px-6 py-1, min-h-7) which overrides Button
size="icon"; split this constant into two: e.g. TAB_STRIP_SURFACE_CLASSNAMES
(shared focus/hover/ring/overflow classes) and TAB_STRIP_TEXT_PADDING_CLASSNAMES
(padding/min-height for text tabs), then update uses so TabsTrigger and the text
tab composition include both arrays but the search dropdown trigger and
TabStripButton (Button size="icon") only receive TAB_STRIP_SURFACE_CLASSNAMES so
their intrinsic size/padding from size="icon" is preserved.
🪄 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: 9f1b0ea4-436f-4a66-9ae1-bec7a3b461f5

📥 Commits

Reviewing files that changed from the base of the PR and between f4d5985 and 3c9b044.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (116)
  • apps/sqlrooms-cli-ui/src/artifactTypes.tsx
  • apps/sqlrooms-cli-ui/src/createDashboardAiTools.ts
  • apps/sqlrooms-cli-ui/src/createDashboardCommands.ts
  • apps/sqlrooms-cli-ui/src/layout.tsx
  • apps/sqlrooms-cli-ui/src/workspace/ArtifactsContainerPanel.tsx
  • docs/upgrade-guide.md
  • examples/ai-rag/src/components/DataSourcesPanel.tsx
  • examples/ai-rag/src/components/MainView.tsx
  • examples/ai-rag/src/room.tsx
  • examples/ai-rag/src/store.ts
  • examples/ai/src/components/DataSourcesPanel.tsx
  • examples/ai/src/components/MainView.tsx
  • examples/ai/src/room.tsx
  • examples/ai/src/store.ts
  • examples/canvas-sync/src/components/DataSourcesPanel.tsx
  • examples/canvas-sync/src/components/InputApiKey.tsx
  • examples/canvas-sync/src/room.tsx
  • examples/canvas-sync/src/store.ts
  • examples/canvas/src/components/DataSourcesPanel.tsx
  • examples/canvas/src/components/InputApiKey.tsx
  • examples/canvas/src/room.tsx
  • examples/canvas/src/store.ts
  • examples/cosmos-embedding/src/components/DataSourcesPanel.tsx
  • examples/cosmos-embedding/src/room.tsx
  • examples/cosmos-embedding/src/store.ts
  • examples/cosmos/src/components/DataSourcesPanel.tsx
  • examples/cosmos/src/room.tsx
  • examples/cosmos/src/store.ts
  • examples/deckgl-discuss/src/room.tsx
  • examples/deckgl-discuss/src/store.ts
  • examples/deckgl-mosaic/src/room.tsx
  • examples/deckgl-mosaic/src/store.ts
  • examples/deckgl/package.json
  • examples/deckgl/src/components/DataPanel.tsx
  • examples/deckgl/src/room.tsx
  • examples/deckgl/src/store.ts
  • examples/get-started/src/components/MainView.tsx
  • examples/get-started/src/room.tsx
  • examples/kepler/src/components/DataPanel.tsx
  • examples/kepler/src/room.tsx
  • examples/kepler/src/store.ts
  • examples/layout/package.json
  • examples/layout/src/App.tsx
  • examples/layout/src/components/BottomTabs.tsx
  • examples/layout/src/components/CollapseBottomButton.tsx
  • examples/layout/src/components/ConsolePanel.tsx
  • examples/layout/src/components/DashboardTabs.tsx
  • examples/layout/src/components/DataSourcesPanel.tsx
  • examples/layout/src/components/DynamicChartPanel.tsx
  • examples/layout/src/components/MainPanel.tsx
  • examples/layout/src/components/ResultsPanel.tsx
  • examples/layout/src/components/SchemaPanel.tsx
  • examples/layout/src/panel-types.ts
  • examples/layout/src/store.tsx
  • examples/minimal/src/room.tsx
  • examples/mosaic/src/components/DataSourcesPanel.tsx
  • examples/mosaic/src/components/MainView.tsx
  • examples/mosaic/src/components/RoomDetailsPanel.tsx
  • examples/mosaic/src/room.tsx
  • examples/mosaic/src/store.ts
  • examples/multi-room/src/RoomPage.tsx
  • examples/nextjs/src/components/app-shell-client.tsx
  • examples/nextjs/src/components/main-view.tsx
  • examples/nextjs/src/components/room.tsx
  • examples/notebook/src/components/DataSourcesPanel.tsx
  • examples/notebook/src/components/NotebookPanel.tsx
  • examples/notebook/src/room.tsx
  • examples/notebook/src/store.ts
  • examples/pivot/src/App.tsx
  • examples/pivot/src/components/DataPanel.tsx
  • examples/pivot/src/components/MainView.tsx
  • examples/pivot/src/store.ts
  • examples/query-motherduck/src/App.tsx
  • examples/query-motherduck/src/Room.tsx
  • examples/query-motherduck/src/components/DataPanel.tsx
  • examples/query-motherduck/src/components/MainView.tsx
  • examples/query-motherduck/src/components/MdConnectPane.tsx
  • examples/query-motherduck/src/store.ts
  • examples/query-pwa/src/App.tsx
  • examples/query-pwa/src/components/DataPanel.tsx
  • examples/query-pwa/src/components/MainView.tsx
  • examples/query-pwa/src/store.ts
  • examples/query-websocket/src/App.tsx
  • examples/query-websocket/src/components/DataSourcesPanel.tsx
  • examples/query-websocket/src/components/MainView.tsx
  • examples/query-websocket/src/store.ts
  • examples/query/src/App.tsx
  • examples/query/src/components/DataPanel.tsx
  • examples/query/src/components/MainView.tsx
  • examples/query/src/store.ts
  • packages/layout-config/README.md
  • packages/layout/README.md
  • packages/layout/package.json
  • packages/layout/src/LayoutRenderer.tsx
  • packages/layout/src/LayoutSlice.ts
  • packages/layout/src/docking/DockDragOverlay.tsx
  • packages/layout/src/docking/DockPreviewOverlay.tsx
  • packages/layout/src/docking/DockingContext.tsx
  • packages/layout/src/docking/DockingProvider.tsx
  • packages/layout/src/docking/dock-layout.ts
  • packages/layout/src/docking/docking-helpers.ts
  • packages/layout/src/docking/docking-types.ts
  • packages/layout/src/index.ts
  • packages/layout/src/layout-tree.ts
  • packages/layout/src/node-renderers/RenderNodeContext.tsx
  • packages/layout/src/node-renderers/dock-node-renderer/DockLayout.tsx
  • packages/layout/src/node-renderers/leaf-node-renderer/LeafLayout.tsx
  • packages/layout/src/node-renderers/leaf-node-renderer/LeafLayoutContent.tsx
  • packages/layout/src/node-renderers/leaf-node-renderer/LeafLayoutDragHandle.tsx
  • packages/layout/src/node-renderers/renderLayoutNode.tsx
  • packages/layout/src/node-renderers/split-node-renderer/SplitLayoutPanelContent.tsx
  • packages/layout/src/node-renderers/tabs-node-renderer/TabsLayoutTabContent.tsx
  • packages/layout/src/node-renderers/utils.ts
  • packages/layout/src/types.ts
  • packages/room-shell/src/index.ts
  • packages/ui/src/components/tab-strip.tsx
✅ Files skipped from review due to trivial changes (17)
  • examples/canvas/src/components/InputApiKey.tsx
  • examples/deckgl/package.json
  • examples/canvas-sync/src/components/InputApiKey.tsx
  • examples/notebook/src/components/NotebookPanel.tsx
  • examples/kepler/src/components/DataPanel.tsx
  • examples/ai/src/components/MainView.tsx
  • examples/pivot/src/components/MainView.tsx
  • examples/deckgl/src/components/DataPanel.tsx
  • examples/layout/src/components/DataSourcesPanel.tsx
  • examples/layout/src/components/ConsolePanel.tsx
  • examples/nextjs/src/components/app-shell-client.tsx
  • examples/layout/src/App.tsx
  • examples/mosaic/src/components/MainView.tsx
  • examples/layout/package.json
  • apps/sqlrooms-cli-ui/src/artifactTypes.tsx
  • packages/layout/src/docking/docking-types.ts
  • packages/layout/src/types.ts
🚧 Files skipped from review as they are similar to previous changes (22)
  • packages/layout/src/node-renderers/tabs-node-renderer/TabsLayoutTabContent.tsx
  • examples/mosaic/src/room.tsx
  • packages/layout/package.json
  • packages/layout/src/node-renderers/split-node-renderer/SplitLayoutPanelContent.tsx
  • apps/sqlrooms-cli-ui/src/layout.tsx
  • examples/kepler/src/store.ts
  • examples/deckgl/src/store.ts
  • examples/query/src/store.ts
  • examples/ai/src/store.ts
  • packages/layout/src/node-renderers/dock-node-renderer/DockLayout.tsx
  • apps/sqlrooms-cli-ui/src/workspace/ArtifactsContainerPanel.tsx
  • packages/layout/src/LayoutRenderer.tsx
  • examples/notebook/src/store.ts
  • examples/query-pwa/src/store.ts
  • examples/ai-rag/src/store.ts
  • packages/layout/src/node-renderers/utils.ts
  • examples/canvas/src/store.ts
  • examples/cosmos/src/store.ts
  • examples/query-websocket/src/store.ts
  • docs/upgrade-guide.md
  • packages/layout/src/docking/DockingContext.tsx
  • examples/mosaic/src/store.ts

Comment thread examples/ai/src/components/DataSourcesPanel.tsx
Comment thread examples/deckgl-discuss/src/store.ts
Comment thread examples/notebook/src/room.tsx
Comment thread examples/query-motherduck/src/store.ts
Comment thread examples/query/src/App.tsx
Comment thread packages/layout/README.md
Comment thread packages/layout/src/docking/dock-layout.ts
Comment thread packages/layout/src/docking/docking-helpers.ts
Comment thread packages/layout/src/layout-tree.ts
Comment thread packages/layout/src/node-renderers/leaf-node-renderer/LeafLayoutDragHandle.tsx Outdated

@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

🧹 Nitpick comments (3)
examples/layout/src/components/DynamicChartHeader.tsx (1)

22-45: Nit: unnecessary Fragment wrapper.

The <>...</> wraps a single LeafLayout.Header child and can be removed.

♻️ Proposed cleanup
   return (
-    <>
-      <LeafLayout.Header>
+    <LeafLayout.Header>
         <div className="text-muted-foreground flex h-8 w-full border-b">
           ...
         </div>
-      </LeafLayout.Header>
-    </>
+    </LeafLayout.Header>
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/layout/src/components/DynamicChartHeader.tsx` around lines 22 - 45,
Remove the unnecessary React fragment wrapping the single LeafLayout.Header
element in DynamicChartHeader: delete the surrounding <> and </> so the
component returns the LeafLayout.Header directly (locate the return that
currently renders <> <LeafLayout.Header> ... </LeafLayout.Header> </> and
replace it with just <LeafLayout.Header> ... </LeafLayout.Header>). Ensure no
other JSX siblings are left at the same return level and keep existing props and
children (GripVerticalIcon, title, Button and handleRemove) unchanged.
packages/layout/src/node-renderers/leaf-node-renderer/useIsDockable.ts (1)

1-13: Nit: filename doesn't match the exported hook.

The file is useIsDockable.ts but the only export is useIsDockablePanel. Renaming the file to useIsDockablePanel.ts would keep import paths and symbol names aligned and make the module easier to discover.

🤖 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/useIsDockable.ts`
around lines 1 - 13, The exported hook is named useIsDockablePanel but the file
is useIsDockable.ts; rename the module file to useIsDockablePanel.ts so the
filename matches the exported symbol (useIsDockablePanel) and imports are
discoverable; update any imports that reference useIsDockable.ts to the new
filename if your tooling doesn't auto-update them.
packages/layout/src/node-renderers/leaf-node-renderer/LeafLayoutPanel.tsx (1)

49-58: Redundant opacity-100 class and dead branch when not dockable.

  • opacity-100 is the default; the second object key only exists to cancel the first, so you can drop it.
  • When isDockablePanel is false, isDragging can never be true (the draggable is disabled), so that condition is effectively isDragging alone. Simplifying makes intent clearer.
♻️ Suggested simplification
         className={cn(
           'group relative flex h-full w-full flex-col overflow-hidden',
-          {
-            'opacity-30': isDockablePanel && isDragging,
-            'opacity-100': isDockablePanel && !isDragging,
-          },
+          isDragging && 'opacity-30',
         )}
🤖 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/LeafLayoutPanel.tsx`
around lines 49 - 58, The className object in LeafLayoutPanel's JSX (the cn call
using setNodeRef) contains a redundant 'opacity-100' branch and a dead branch
when !isDockablePanel; remove the 'opacity-100' entry and simplify the opacity
logic to only conditionally apply 'opacity-30' (e.g., 'opacity-30':
isDockablePanel && isDragging) so when not dockable no opacity classes are
toggled; update the className object in the component where setNodeRef, cn,
isDockablePanel, and isDragging are used.
🤖 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/components/DynamicChart.tsx`:
- Around line 53-64: The chart markup in DynamicChart is currently unlabeled and
should be made accessible: update the top-level chart container (the inner div
with class "flex flex-1 items-end...") to either include role="img" and a
descriptive aria-label (or aria-labelledby pointing to a visible/visually-hidden
title) when the visualization conveys information, or mark it decorative by
adding aria-hidden="true" (or role="presentation") if it does not convey
meaningful data; ensure whichever approach you choose uses the container div as
the target so screen readers properly include or ignore the chart.

In
`@packages/layout/src/node-renderers/leaf-node-renderer/LeafLayoutDragHandle.tsx`:
- Around line 14-18: The drag-handle div in the LeafLayoutDragHandle component
currently relies on children for an accessible name but may be announced as an
unlabeled button by assistive tech; update the component signature
(LeafLayoutDragHandle) to accept an optional ariaLabel and ariaDescribedBy (or
aria-describedby) prop and spread them onto the root div along with attributes
and listeners so the role="button" element has an explicit accessible
name/description, or alternatively validate/document that callers must pass
labeled content if you prefer not to add props. Ensure you reference the props
in the component and include them in the JSX output for the div that currently
returns <div className={className} {...attributes} {...listeners}>.

---

Nitpick comments:
In `@examples/layout/src/components/DynamicChartHeader.tsx`:
- Around line 22-45: Remove the unnecessary React fragment wrapping the single
LeafLayout.Header element in DynamicChartHeader: delete the surrounding <> and
</> so the component returns the LeafLayout.Header directly (locate the return
that currently renders <> <LeafLayout.Header> ... </LeafLayout.Header> </> and
replace it with just <LeafLayout.Header> ... </LeafLayout.Header>). Ensure no
other JSX siblings are left at the same return level and keep existing props and
children (GripVerticalIcon, title, Button and handleRemove) unchanged.

In `@packages/layout/src/node-renderers/leaf-node-renderer/LeafLayoutPanel.tsx`:
- Around line 49-58: The className object in LeafLayoutPanel's JSX (the cn call
using setNodeRef) contains a redundant 'opacity-100' branch and a dead branch
when !isDockablePanel; remove the 'opacity-100' entry and simplify the opacity
logic to only conditionally apply 'opacity-30' (e.g., 'opacity-30':
isDockablePanel && isDragging) so when not dockable no opacity classes are
toggled; update the className object in the component where setNodeRef, cn,
isDockablePanel, and isDragging are used.

In `@packages/layout/src/node-renderers/leaf-node-renderer/useIsDockable.ts`:
- Around line 1-13: The exported hook is named useIsDockablePanel but the file
is useIsDockable.ts; rename the module file to useIsDockablePanel.ts so the
filename matches the exported symbol (useIsDockablePanel) and imports are
discoverable; update any imports that reference useIsDockable.ts to the new
filename if your tooling doesn't auto-update them.
🪄 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: 68effce0-2fac-4083-b29b-1ea628ccb648

📥 Commits

Reviewing files that changed from the base of the PR and between 3c9b044 and 8e57089.

📒 Files selected for processing (10)
  • examples/layout/src/components/DynamicChart.tsx
  • examples/layout/src/components/DynamicChartHeader.tsx
  • examples/layout/src/components/DynamicChartPanel.tsx
  • packages/layout/src/index.ts
  • 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/leaf-node-renderer/LeafLayoutPanel.tsx
  • packages/layout/src/node-renderers/leaf-node-renderer/LeafLayoutPanelDraggableContext.tsx
  • packages/layout/src/node-renderers/leaf-node-renderer/useIsDockable.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • examples/layout/src/components/DynamicChartPanel.tsx
  • packages/layout/src/node-renderers/leaf-node-renderer/LeafLayout.tsx

Comment thread examples/layout/src/components/DynamicChart.tsx
@dmitriy-kostianetskiy dmitriy-kostianetskiy changed the title feat: dnd-kit docking layout feat: dnd-kit docking layout and mosaic dashboards Apr 28, 2026
@dmitriy-kostianetskiy dmitriy-kostianetskiy changed the title feat: dnd-kit docking layout and mosaic dashboards feat: dnd-kit docking layout Apr 28, 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.

🧹 Nitpick comments (2)
packages/room-shell/src/RoomShellSidebarButtons.tsx (2)

69-83: Minor: Inline fallback function creates new reference each render.

The (() => null) fallback creates a new function on every render when info?.icon is undefined. Consider extracting a stable fallback.

♻️ Suggested refactor
+const NoIcon: FC<{className?: string}> = () => null;
+
 const RoomShellSidebarButton: FC<{roomPanelType: string}> = ({
   // ...
   return (
     <SidebarButton
       key={roomPanelType}
       title={info?.title ?? ''}
       isSelected={visibleRoomPanels.includes(roomPanelType)}
       isDisabled={!initialized}
-      icon={info?.icon ?? (() => null)}
+      icon={info?.icon ?? NoIcon}
       onClick={() => togglePanel(roomPanelType)}
     />
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/room-shell/src/RoomShellSidebarButtons.tsx` around lines 69 - 83,
The inline fallback (() => null) creates a new function each render; define a
stable NOOP_ICON function (e.g., const NOOP_ICON = () => null) outside the
component and replace the fallback in the SidebarButton prop from
icon={info?.icon ?? (() => null)} to icon={info?.icon ?? NOOP_ICON} so the icon
prop keeps a stable reference; update RoomShellSidebarButtons.tsx to declare
NOOP_ICON near the top of the module and use it in the render.

92-118: Memoize the filtered panel lists.

The filtering logic runs on every render, calling resolvePanelDefinition for each panel even when panels hasn't changed. Per coding guidelines, derived arrays should be computed with useMemo.

♻️ Suggested refactor
 const RoomShellSidebarButtons: FC<{className?: string}> = ({className}) => {
   const panels = useBaseRoomShellStore((state) => state.layout.panels);

+  const sidebarPanels = useMemo(() => {
+    if (!panels) return [];
+    return Object.keys(panels).filter((key) => {
+      const def = panels[key];
+      const resolved = def ? resolvePanelDefinition(def, {panelId: key}) : undefined;
+      return resolved?.placement === 'sidebar';
+    });
+  }, [panels]);
+
+  const sidebarBottomPanels = useMemo(() => {
+    if (!panels) return [];
+    return Object.keys(panels).filter((key) => {
+      const def = panels[key];
+      const resolved = def ? resolvePanelDefinition(def, {panelId: key}) : undefined;
+      return resolved?.placement === 'sidebar-bottom';
+    });
+  }, [panels]);
+
   return (
     <div className={cn('flex h-full grow flex-col', className)}>
       <div className="flex flex-col gap-2">
-        {panels
-          ? Object.keys(panels)
-              .filter((key) => {
-                const def = panels[key];
-                const resolved = def
-                  ? resolvePanelDefinition(def, {panelId: key})
-                  : undefined;
-                return resolved?.placement === 'sidebar';
-              })
-              .map((type) => (
-                <RoomShellSidebarButton key={type} roomPanelType={type} />
-              ))
-          : null}
+        {sidebarPanels.map((type) => (
+          <RoomShellSidebarButton key={type} roomPanelType={type} />
+        ))}
       </div>
       <div className="flex-1" />
       <div className="flex flex-col gap-2">
-        {Object.keys(panels)
-          .filter((key) => {
-            const def = panels[key];
-            const resolved = def
-              ? resolvePanelDefinition(def, {panelId: key})
-              : undefined;
-            return resolved?.placement === 'sidebar-bottom';
-          })
-          .map((type) => (
-            <RoomShellSidebarButton key={type} roomPanelType={type} />
-          ))}
+        {sidebarBottomPanels.map((type) => (
+          <RoomShellSidebarButton key={type} roomPanelType={type} />
+        ))}
       </div>
     </div>
   );
 };

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/room-shell/src/RoomShellSidebarButtons.tsx` around lines 92 - 118,
The component repeatedly filters `panels` and calls `resolvePanelDefinition` on
every render; wrap the two derived arrays (sidebar and sidebar-bottom) in
React.useMemo so they are recomputed only when `panels` changes. Specifically,
compute memoized arrays (e.g. sidebarPanelTypes and sidebarBottomPanelTypes)
using useMemo that iterate Object.keys(panels), call resolvePanelDefinition(def,
{panelId: key}) and filter by resolved.placement === 'sidebar' and
'sidebar-bottom' respectively, then map those memo values to render
<RoomShellSidebarButton roomPanelType={type} /> instead of filtering inline.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/room-shell/src/RoomShellSidebarButtons.tsx`:
- Around line 69-83: The inline fallback (() => null) creates a new function
each render; define a stable NOOP_ICON function (e.g., const NOOP_ICON = () =>
null) outside the component and replace the fallback in the SidebarButton prop
from icon={info?.icon ?? (() => null)} to icon={info?.icon ?? NOOP_ICON} so the
icon prop keeps a stable reference; update RoomShellSidebarButtons.tsx to
declare NOOP_ICON near the top of the module and use it in the render.
- Around line 92-118: The component repeatedly filters `panels` and calls
`resolvePanelDefinition` on every render; wrap the two derived arrays (sidebar
and sidebar-bottom) in React.useMemo so they are recomputed only when `panels`
changes. Specifically, compute memoized arrays (e.g. sidebarPanelTypes and
sidebarBottomPanelTypes) using useMemo that iterate Object.keys(panels), call
resolvePanelDefinition(def, {panelId: key}) and filter by resolved.placement ===
'sidebar' and 'sidebar-bottom' respectively, then map those memo values to
render <RoomShellSidebarButton roomPanelType={type} /> instead of filtering
inline.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 771af8d7-0dbf-4cc8-9cca-3dcc4d213236

📥 Commits

Reviewing files that changed from the base of the PR and between 8e57089 and 8e4d5fe.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (13)
  • apps/sqlrooms-cli-ui/package.json
  • docs/upgrade-guide.md
  • examples/deckgl/package.json
  • packages/deck/__tests__/prepareDeckDataset.test.ts
  • packages/deck/src/prepare/detectGeometryColumn.ts
  • packages/deck/src/prepare/geoarrow.ts
  • packages/layout-config/package.json
  • packages/layout/package.json
  • packages/mosaic/src/profiler/useMosaicProfiler.ts
  • packages/preset-jest/package.json
  • packages/room-shell/package.json
  • packages/room-shell/src/RoomShellSidebarButtons.tsx
  • packages/ui/package.json
💤 Files with no reviewable changes (2)
  • packages/preset-jest/package.json
  • apps/sqlrooms-cli-ui/package.json
✅ Files skipped from review due to trivial changes (7)
  • packages/mosaic/src/profiler/useMosaicProfiler.ts
  • packages/deck/src/prepare/geoarrow.ts
  • packages/deck/src/prepare/detectGeometryColumn.ts
  • packages/layout-config/package.json
  • packages/ui/package.json
  • packages/deck/tests/prepareDeckDataset.test.ts
  • examples/deckgl/package.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/room-shell/package.json
  • packages/layout/package.json
  • docs/upgrade-guide.md

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants