Skip to content

feat: Add shadcn Sidebar component - #400

Merged
ilyabo merged 6 commits into
mainfrom
cursor/project-schema-sidebar-a7a0
Feb 28, 2026
Merged

ilyabo merged 6 commits into
mainfrom
cursor/project-schema-sidebar-a7a0

Conversation

@ilyabo

@ilyabo ilyabo commented Feb 27, 2026 •

Copy link
Copy Markdown
Collaborator

Add a shadcn-style sidebar to @sqlrooms/ui and integrate it into the multi-room example to enable project switching and display the schema tree.


Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Redesigned room page with a persistent, accessible sidebar and room-focused workflow.
    • Schema tree panel in the sidebar with refresh state and automatic table selection.
    • Room selector to switch/create rooms; table preview moved to a responsive header selector with improved loading/empty states.
    • Responsive sidebar with mobile/offcanvas support and keyboard toggle; mobile breakpoint detection.
  • Chores

    • Added icon and schema-tree libraries to example dependencies.
  • Documentation

    • Updated multi-room README and examples index to describe the sidebar-driven demo.

@cursor

cursor Bot commented Feb 27, 2026

Copy link
Copy Markdown

Cursor Agent can help with this pull request. Just @cursor in comments and I'll start working on changes in this branch.
Learn more about Cursor Agents

@coderabbitai

coderabbitai Bot commented Feb 27, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a responsive, stateful Sidebar component suite and a useIsMobile hook to the UI package, updates theme tokens, and refactors the multi-room example to use a Sidebar-driven layout with room switching and a TableSchemaTree; RoomContent now accepts roomId.

Changes

Cohort / File(s) Summary
Sidebar UI system
packages/ui/src/components/sidebar.tsx, packages/ui/src/index.ts, packages/ui/src/hooks/use-mobile.ts
Adds a full Sidebar implementation (provider, context, hooks, 23+ subcomponents), a useIsMobile hook, and re-exports Sidebar APIs from the UI package.
Sidebar styling
packages/ui/tailwind-preset.css
Introduces new CSS custom properties and theme tokens for sidebar colors (background, foreground, primary, accent, border, ring) across light/dark themes.
Multi-room example code
examples/multi-room/src/RoomPage.tsx
Refactors RoomPage to a Sidebar-driven layout: persistent SidebarProvider, team/room switcher, table Select picker, schema tree panel, navigation via useNavigate, schema refresh/loading states; RoomContent now accepts roomId.
Multi-room deps & docs
examples/multi-room/package.json, examples/multi-room/README.md, docs/examples.md
Adds dependencies @sqlrooms/schema-tree and lucide-react; updates README and adds a comment in docs/examples referencing the multi-room sidebar demo.

Sequence Diagram(s)

sequenceDiagram
  participant User as User
  participant RoomPage as RoomPage
  participant SidebarProv as SidebarProvider
  participant RoomsAPI as getRoomsList/getRoom
  participant Schema as TableSchemaTree

  User->>RoomPage: Open room page (roomId)
  RoomPage->>SidebarProv: Mount with roomId (provide context)
  SidebarProv->>RoomsAPI: getRoomsList()
  RoomsAPI-->>SidebarProv: rooms list
  SidebarProv->>RoomPage: expose selectedRoom, onSelect, onAdd
  User->>SidebarProv: select room / add room
  SidebarProv->>RoomPage: navigate(roomId)
  RoomPage->>Schema: refreshTableSchemas(roomId)
  Schema-->>RoomPage: schemaTrees / loading state
  RoomPage->>User: render sidebar, schema tree, table UI
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • feat: Tanstack multi-room example #385 — Modifies the same multi-room example files (examples/multi-room/src/RoomPage.tsx, examples/multi-room/package.json) and appears related to the RoomPage/sidebar refactor.

Poem

🐰 I hopped through code, a tiny guide,
A rail of menus, open wide,
Rooms I switch with gentle cheer,
Schema branches drawing near,
Hop in, explorer — the sidebar's here 🌿

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the primary change: adding a shadcn-style Sidebar component to the @sqlrooms/ui package and integrating it into the multi-room example.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch cursor/project-schema-sidebar-a7a0

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@ilyabo ilyabo changed the title Project schema sidebar feat: Project schema sidebar Feb 27, 2026
@ilyabo
ilyabo marked this pull request as ready for review February 27, 2026 18:40
@ilyabo ilyabo changed the title feat: Project schema sidebar feat: Add shadcn Sidebar component Feb 27, 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 (1)
packages/ui/src/components/sidebar.tsx (1)

503-518: Consider avoiding parameter reassignment.

The tooltip parameter is reassigned from a string to an object. While this works, it can be slightly cleaner to use a separate variable for the normalized value:

♻️ Optional refactor
   if (!tooltip) {
     return button;
   }

-  if (typeof tooltip === 'string') {
-    tooltip = {
-      children: tooltip,
-    };
-  }
+  const tooltipProps = typeof tooltip === 'string' ? { children: tooltip } : tooltip;

   return (
     <Tooltip>
       <TooltipTrigger asChild>{button}</TooltipTrigger>
-      <TooltipContent side="right" align="center" hidden={state !== 'collapsed' || isMobile} {...tooltip} />
+      <TooltipContent side="right" align="center" hidden={state !== 'collapsed' || isMobile} {...tooltipProps} />
     </Tooltip>
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/components/sidebar.tsx` around lines 503 - 518, The code
reassigns the incoming tooltip param (string -> object); instead, create a new
normalized variable (e.g., const normalizedTooltip or tooltipProps) to hold the
normalized object and leave the original tooltip untouched. In the component
around the Tooltip return path (references: tooltip param, button,
TooltipTrigger, TooltipContent, state, isMobile), replace the reassignment with
a conditional that sets normalizedTooltip = typeof tooltip === 'string' ? {
children: tooltip } : tooltip and then spread normalizedTooltip into
<TooltipContent ... {...normalizedTooltip} /> while keeping the early return for
falsy tooltip.
🤖 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/ui/src/components/sidebar.tsx`:
- Around line 503-518: The code reassigns the incoming tooltip param (string ->
object); instead, create a new normalized variable (e.g., const
normalizedTooltip or tooltipProps) to hold the normalized object and leave the
original tooltip untouched. In the component around the Tooltip return path
(references: tooltip param, button, TooltipTrigger, TooltipContent, state,
isMobile), replace the reassignment with a conditional that sets
normalizedTooltip = typeof tooltip === 'string' ? { children: tooltip } :
tooltip and then spread normalizedTooltip into <TooltipContent ...
{...normalizedTooltip} /> while keeping the early return for falsy tooltip.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0692aac and a7fcd75.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • examples/multi-room/package.json
  • examples/multi-room/src/RoomPage.tsx
  • packages/ui/src/components/sidebar.tsx
  • packages/ui/src/hooks/use-mobile.ts
  • packages/ui/src/index.ts
  • packages/ui/tailwind-preset.css

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

🤖 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/multi-room/src/RoomPage.tsx`:
- Around line 120-131: The room naming is off-by-one because onAddRoom uses
roomOptions.length (which includes the synthetic fallback) to compute the new
name; change the logic in onAddRoom to base the name on the real rooms array
(use rooms.length + 1) so the first created room is "Room 1" even when
roomOptions contains the fallback; update the name generation in onAddRoom to
reference rooms rather than roomOptions (symbols: onAddRoom, roomOptions,
rooms).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a7fcd75 and f13ea74.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (2)
  • examples/multi-room/package.json
  • examples/multi-room/src/RoomPage.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/multi-room/package.json

Comment thread examples/multi-room/src/RoomPage.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: 1

Caution

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

⚠️ Outside diff range comments (1)
examples/multi-room/src/RoomPage.tsx (1)

462-465: ⚠️ Potential issue | 🟠 Major

Escape table identifiers in SQL query.

selectedTable is directly interpolated into the SQL query. If a table name contains ", the query breaks and opens identifier-injection risk.

Use the existing escapeId function from @sqlrooms/duckdb to properly escape identifiers:

🔧 Proposed fix
-import {QueryDataTable} from '@sqlrooms/data-table';
+import {QueryDataTable} from '@sqlrooms/data-table';
+import {escapeId} from '@sqlrooms/duckdb';

 function TableBrowser({
@@
         {selectedTable ? (
           <QueryDataTable
             key={selectedTable}
-            query={`SELECT * FROM "${selectedTable}"`}
+            query={`SELECT * FROM ${escapeId(selectedTable)}`}
             fontSize="text-xs"
           />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/multi-room/src/RoomPage.tsx` around lines 462 - 465, selectedTable
is interpolated directly into the SQL string passed to QueryDataTable, which
allows identifier-breaking characters and identifier-injection; fix it by
importing and using the escapeId function from `@sqlrooms/duckdb` to wrap the
selectedTable when building the query (update the query prop passed to
QueryDataTable to use escapeId(selectedTable) instead of raw selectedTable) so
table identifiers are properly quoted/escaped and injection is prevented.
🤖 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/multi-room/src/RoomPage.tsx`:
- Around line 94-118: RoomContent never calls room.initialize(), so it may never
become initialized; add a useEffect that calls the room.initialize() method on
mount (and when roomId changes) before relying on the initialized flag. Grab the
initialize function from the same store (e.g., const initialize = useRoomStore(s
=> s.room.initialize)) and invoke it inside a useEffect with [roomId,
initialize] (use void/async handling and optional catch/logging) so the room
lifecycle is triggered prior to calling refreshTableSchemas or inspecting
tables.

---

Outside diff comments:
In `@examples/multi-room/src/RoomPage.tsx`:
- Around line 462-465: selectedTable is interpolated directly into the SQL
string passed to QueryDataTable, which allows identifier-breaking characters and
identifier-injection; fix it by importing and using the escapeId function from
`@sqlrooms/duckdb` to wrap the selectedTable when building the query (update the
query prop passed to QueryDataTable to use escapeId(selectedTable) instead of
raw selectedTable) so table identifiers are properly quoted/escaped and
injection is prevented.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f13ea74 and ef4bb52.

📒 Files selected for processing (1)
  • examples/multi-room/src/RoomPage.tsx

Comment on lines +94 to 118
function RoomContent({roomId, roomName}: {roomId: string; roomName: string}) {
const navigate = useNavigate();
const rooms = getRoomsList();
const initialized = useRoomStore((s) => s.room.initialized);
const tables = useRoomStore((s) => s.db.tables);
const schemaTrees = useRoomStore((s) => s.db.schemaTrees);
const isRefreshing = useRoomStore((s) => s.db.isRefreshingTableSchemas);
const refreshTableSchemas = useRoomStore((s) => s.db.refreshTableSchemas);
const [selectedTable, setSelectedTable] = useState<string | null>(null);

useEffect(() => {
if (initialized && tables.length > 0 && !selectedTable) {
if (!initialized) return;
void refreshTableSchemas();
}, [initialized, refreshTableSchemas, roomId]);

useEffect(() => {
if (!initialized) return;
if (tables.length === 0) {
setSelectedTable(null);
return;
}
if (!selectedTable || !tables.some((table) => table.table.table === selectedTable)) {
setSelectedTable(tables[0].table.table);
}
}, [initialized, tables, selectedTable]);

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 | 🟠 Major

Call room.initialize() on mount before relying on initialized.

RoomContent gates schema refresh on initialized, but this component never triggers room.initialize(). That can leave the room stuck uninitialized depending on lifecycle timing.

🔧 Proposed fix
 function RoomContent({roomId, roomName}: {roomId: string; roomName: string}) {
   const navigate = useNavigate();
   const rooms = getRoomsList();
   const initialized = useRoomStore((s) => s.room.initialized);
+  const initializeRoom = useRoomStore((s) => s.room.initialize);
   const tables = useRoomStore((s) => s.db.tables);
   const schemaTrees = useRoomStore((s) => s.db.schemaTrees);
   const isRefreshing = useRoomStore((s) => s.db.isRefreshingTableSchemas);
   const refreshTableSchemas = useRoomStore((s) => s.db.refreshTableSchemas);
   const [selectedTable, setSelectedTable] = useState<string | null>(null);

+  useEffect(() => {
+    void initializeRoom();
+  }, [initializeRoom, roomId]);
+
   useEffect(() => {
     if (!initialized) return;
     void refreshTableSchemas();
   }, [initialized, refreshTableSchemas, roomId]);

As per coding guidelines, "Always call room.initialize() during component mount to orchestrate automatic slice initialization with lifecycle hooks".

📝 Committable suggestion

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

Suggested change
function RoomContent({roomId, roomName}: {roomId: string; roomName: string}) {
const navigate = useNavigate();
const rooms = getRoomsList();
const initialized = useRoomStore((s) => s.room.initialized);
const tables = useRoomStore((s) => s.db.tables);
const schemaTrees = useRoomStore((s) => s.db.schemaTrees);
const isRefreshing = useRoomStore((s) => s.db.isRefreshingTableSchemas);
const refreshTableSchemas = useRoomStore((s) => s.db.refreshTableSchemas);
const [selectedTable, setSelectedTable] = useState<string | null>(null);
useEffect(() => {
if (initialized && tables.length > 0 && !selectedTable) {
if (!initialized) return;
void refreshTableSchemas();
}, [initialized, refreshTableSchemas, roomId]);
useEffect(() => {
if (!initialized) return;
if (tables.length === 0) {
setSelectedTable(null);
return;
}
if (!selectedTable || !tables.some((table) => table.table.table === selectedTable)) {
setSelectedTable(tables[0].table.table);
}
}, [initialized, tables, selectedTable]);
function RoomContent({roomId, roomName}: {roomId: string; roomName: string}) {
const navigate = useNavigate();
const rooms = getRoomsList();
const initialized = useRoomStore((s) => s.room.initialized);
const initializeRoom = useRoomStore((s) => s.room.initialize);
const tables = useRoomStore((s) => s.db.tables);
const schemaTrees = useRoomStore((s) => s.db.schemaTrees);
const isRefreshing = useRoomStore((s) => s.db.isRefreshingTableSchemas);
const refreshTableSchemas = useRoomStore((s) => s.db.refreshTableSchemas);
const [selectedTable, setSelectedTable] = useState<string | null>(null);
useEffect(() => {
void initializeRoom();
}, [initializeRoom, roomId]);
useEffect(() => {
if (!initialized) return;
void refreshTableSchemas();
}, [initialized, refreshTableSchemas, roomId]);
useEffect(() => {
if (!initialized) return;
if (tables.length === 0) {
setSelectedTable(null);
return;
}
if (!selectedTable || !tables.some((table) => table.table.table === selectedTable)) {
setSelectedTable(tables[0].table.table);
}
}, [initialized, tables, selectedTable]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/multi-room/src/RoomPage.tsx` around lines 94 - 118, RoomContent
never calls room.initialize(), so it may never become initialized; add a
useEffect that calls the room.initialize() method on mount (and when roomId
changes) before relying on the initialized flag. Grab the initialize function
from the same store (e.g., const initialize = useRoomStore(s =>
s.room.initialize)) and invoke it inside a useEffect with [roomId, initialize]
(use void/async handling and optional catch/logging) so the room lifecycle is
triggered prior to calling refreshTableSchemas or inspecting tables.

@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 (1)
examples/multi-room/README.md (1)

8-8: Consider more neutral, evergreen documentation language.

The phrase "powerful new Sidebar component pattern" uses subjective marketing language. Technical documentation typically benefits from more neutral, factual descriptions. The word "new" will also become outdated over time.

📝 Suggested improvement
-A multi-room application demonstrating how to manage multiple independent data workspaces with proper room store lifecycle management and the powerful new Sidebar component pattern. Features include:
+A multi-room application demonstrating how to manage multiple independent data workspaces with proper room store lifecycle management and a Sidebar component for navigation. Features include:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/multi-room/README.md` at line 8, Replace the subjective marketing
phrase "powerful new Sidebar component pattern" with neutral, evergreen wording:
describe the Sidebar as a component or pattern and state its function factually
(e.g., "the Sidebar component pattern for managing workspace navigation and
context"), removing "powerful" and "new" and keeping the rest of the sentence
intact to read: "A multi-room application demonstrating how to manage multiple
independent data workspaces with proper room store lifecycle management and the
Sidebar component pattern."
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@examples/multi-room/README.md`:
- Line 8: Replace the subjective marketing phrase "powerful new Sidebar
component pattern" with neutral, evergreen wording: describe the Sidebar as a
component or pattern and state its function factually (e.g., "the Sidebar
component pattern for managing workspace navigation and context"), removing
"powerful" and "new" and keeping the rest of the sentence intact to read: "A
multi-room application demonstrating how to manage multiple independent data
workspaces with proper room store lifecycle management and the Sidebar component
pattern."

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ef4bb52 and f9c0d16.

⛔ Files ignored due to path filters (1)
  • docs/media/examples/multi-room.mp4 is excluded by !**/*.mp4
📒 Files selected for processing (2)
  • docs/examples.md
  • examples/multi-room/README.md
✅ Files skipped from review due to trivial changes (1)
  • docs/examples.md

@ilyabo
ilyabo merged commit b44ac17 into main Feb 28, 2026
5 checks passed
@ilyabo
ilyabo deleted the cursor/project-schema-sidebar-a7a0 branch February 28, 2026 22:20
@coderabbitai coderabbitai Bot mentioned this pull request Jun 13, 2026
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.

2 participants