--- url: https://sqlrooms.org/overview.md --- # Overview SQLRooms provides a comprehensive foundation and rich set of building blocks for creating modern, interactive data-analytics applications that can run entirely in the browser. At its core is the concept of a ***Room*** — a self‑contained workspace where data lives, analysis happens, and (soon) collaborators meet. It combines essential components like a SQL query engine (DuckDB), data visualization tools, state management, and UI components into a cohesive toolkit, making it significantly easier to create powerful analytics tools with or without a backend. ![SQLRooms example apps](/media/overview/collage.webp) ## Why SQLRooms? SQLRooms is designed to empower developers and users with a modern, modular analytics toolkit that runs entirely in the browser. Here's what sets it apart: * **Performance & Scale:** Every user gets a dedicated in-browser DuckDB instance, delivering columnar analytics speed with zero backend load. * **Modular Architecture:** Mix and match packages, and combine state *slices* to include only the features you need—no bloat, just what your app requires. * **AI‑Powered Analytics:** Built-in support for agents that can write and execute SQL queries, and generate insights directly in your browser—no server roundtrips required. * **Developer Experience:** A composable, React-based framework with ready-to-use components, state management, and visualization tools, making it easy to build custom analytics solutions. ## Why Single-Node? SQLRooms is designed for single-node analytics: all computation happens on your device, whether in the browser or a desktop app (e.g. via [Electron](https://www.electronjs.org/)), with no backend required. Data can remain local if you choose, or be loaded from external sources like S3—always giving you full control over how and where your data is processed. If you are evaluating architecture options for your organization, see [Deployment Scenarios](/deployment-scenarios). * **Privacy:** All data remains on your device for simplified compliance and peace of mind—nothing leaves your browser unless you choose. * **Own Your Data:** You control your files and data, with no vendor lock-in or forced cloud storage. Your work is portable and future-proof. * **Offline Use:** SQLRooms [supports offline work](/offline-use)—query, analyze, and visualize your data even without an internet connection. * **Fast Local Querying:** Queries run instantly in your browser, with no network roundtrip or server lag—results are available as soon as you ask. * **Private AI Insights:** AI agents generate insights and run queries locally, so your data is never shared with external model providers. You get the power of AI-driven analytics without sacrificing privacy. ## Local-First Foundations This approach draws on [Local-First principles](https://www.inkandswitch.com/essay/local-first), which emphasize user ownership and seamless collaboration. In Local-First apps, users retain full control of their data — it lives on their device, remains accessible offline, and isn’t locked behind a remote server. By contrast, traditional cloud apps centralize both computation and storage, often reducing user agency. If the service goes down or is discontinued, the app may stop working entirely, and user data can become inaccessible. While SQLRooms does not yet implement sync or collaboration, it is already capable of delivering some of the key benefits of local-first software — your data and computation can stay private and accessible on your device. ## Next Steps * **Review the [Key Concepts](/key-concepts)** to understand the core ideas and architecture. * **Explore the [Modular Architecture](/modular-architecture)** to see how you can compose and extend your app. * **Check the [Example Applications](/examples)** to see what can be built with the framework. * **Compare [Deployment Scenarios](/deployment-scenarios)** to choose the right setup for your team. --- --- url: https://sqlrooms.org/key-concepts.md --- # Key Concepts ## What's a Room? A **Room** is a self-contained workspace where users can explore datasets, run queries, and view results. The term comes from [collaborative tools](https://en.wikipedia.org/wiki/Collaborative_software)—where users work in shared spaces—and SQLRooms is built with future real-time collaboration in mind. A Room consists of: * ``: a React component that renders the Room UI * `roomStore`: a Zustand-based state store for the Room *** ![SQLRooms example RoomShell diagram](/media/key-concepts/room-shell.png) *** ## Room Store The `roomStore` is a [composable](#composing-store-from-slices) [`Zustand`](/state-management#why-zustand) store created by calling `createRoomStore()`. The store holds: * `config`: the persistable part of the state that captures a Room's saveable settings and can be serialized to JSON for storage or sharing including: * the view configuration and the layout state * the user preferences * `room`: non-persistable state that holds runtime information like: * loaded DuckDB tables * transient UI state (like "query running") ```tsx const {roomStore, useRoomStore} = createRoomStore( (set, get, store) => ({ ...createRoomShellSlice({ config: { dataSources: [ { type: 'url', url: 'https://.../earthquakes.parquet', tableName: 'earthquakes', }, ], }, layout: { config: { // Layout configuration }, panels: { // Panel definitions }, }, })(set, get, store), }), ); ``` Check the [minimal example](https://github.com/sqlrooms/examples/blob/main/minimal/src/app.tsx) for the complete implementation. *** ## RoomShell `` is a React component that wraps your Room UI * It injects the `roomStore` into React context, accessible via the `useRoomStore()` hook * It sets up essential UI infrastructure including error boundaries, toast notifications, and tooltips, making it easy to use components from `@sqlrooms/ui` out of the box * It provides slots for the optional `LayoutComposer` (see [Layout](#layout-optional) section below), `Sidebar`, and `LoadingProgress` components ```tsx const App = () => ( ); ``` *** ## SQL and DuckDB Access SQLRooms includes a built-in DuckDB integration via the [`DuckDbSlice`](/api/duckdb/). The `DuckDbSlice` provides helper functions for managing and querying tables: * `findTableByName()` - Look up a table by name in the current schema * `addTable()` - Add a new table from Arrow data or records * `dropTable()` - Remove a table from the database * `refreshTableSchemas()` - Update the cached table schemas * `tables` - The cached list of tables from the last refreshTableSchemas() call * `getConnector()` - Access the underlying DuckDB connector You can query your datasets using the `useSql(query)` hook and work directly with Arrow tables in React. ```tsx function MyComponent() { const isTableReady = useRoomStore((state) => Boolean(state.db.findTableByName('earthquakes')), ); const queryResult = useSql<{maxMagnitude: number}>({ query: `SELECT max(Magnitude) AS maxMagnitude FROM earthquakes`, enabled: isTableReady, }); const row = queryResult.data?.toArray()[0]; return row ? `Max earthquake magnitude: ${row.maxMagnitude}` : ; } ``` For more details on DuckDB integration and available methods, see the [DuckDB API Reference](/api/duckdb/). *** ## Composing Store from Slices The store can be enhanced with **slices**—modular pieces of state and logic that can be added to your Room. You can use slices from the `@sqlrooms/*` packages or create your own custom slices. Each slice is a function that returns a partial state object along with methods to modify that state. Here's an example showing how to combine the default room shell with SQL editor functionality: ```tsx const {roomStore, useRoomStore} = createRoomStore( (set, get, store) => ({ // Default slice ...createRoomShellSlice({ config: { // Room configuration }, layout: { config: { // Layout configuration }, panels: { // Panel definitions }, }, })(set, get, store), // Mix in sql editor slice ...createSqlEditorSlice()(set, get, store), }), ); ``` You can access slices' namespaced config, state and functions in the store using selectors, for example: ```tsx const queries = useRoomStore((state) => state.sqlEditor.config.queries); const runQuery = useRoomStore((state) => state.sqlEditor.parseAndRunQuery); ``` Learn more about store and slices in [State Management](/state-management). *** ## Layout (Optional) The `LayoutComposer` provides a flexible panel layout for your Room's UI. * Panels are React components that can be plugged into the layout. They include metadata (`id`, `title`, `icon`) and a `component` to render. * Panels can be moved, resized, or hidden * Developers can add panels by registering them in the `roomStore`. * Layout state is persisted in the `roomStore` Configure the room layout and panels during store initialization: ```tsx const {roomStore, useRoomStore} = createRoomStore( (set, get, store) => ({ ...createRoomShellSlice({ config: { dataSources: [], }, layout: { config: { type: 'split', direction: 'row', children: [ {type: 'panel', id: 'data', defaultSize: '30%'}, {type: 'panel', id: 'main', defaultSize: '70%'}, ], }, panels: { data: { title: 'Data Sources', icon: DatabaseIcon, component: DataSourcesPanel, }, main: { title: 'Main view', icon: () => null, component: MainView, }, }, }, })(set, get, store), }), ); ``` Layout composer renders the layout with panels: ```tsx function App() { return ( ); } ``` For examples of panels, tabs, grids, and docking, see the [Layout guide](/layout). For the full API, see the [Layout API Reference](/api/layout/). --- --- url: https://sqlrooms.org/modular-architecture.md --- # Modular Architecture SQLRooms is designed with a modular architecture that allows developers to pick and choose exactly the functionality they need for their data analytics applications. This approach enables you to build custom solutions tailored to your specific requirements. ![SQLRooms Architecture](/media/overview/architecture.svg) The diagram above illustrates how SQLRooms is structured for composability and extensibility: * **Core packages** (green) provide the foundation: `roomShellStore` manages configuration and data sources, while `` offers a flexible UI shell and panel layout. DuckDB-WASM and utilities enable fast, in-browser analytics. * **Feature packages** (purple) are plug-and-play modules you can add as needed—such as DataTable views, SQL query editors, AI integration, Vega charts, S3 browser, and more. These integrate seamlessly with the core, letting you extend your app with popular data visualization and analytics tools. * **Custom App Code** (blue) is where you bring it all together: use `roomStore` to compose base and custom store logic, and build your own custom views and panels. You can mix and match core and feature packages, or add your own, to create a tailored analytics experience. A key part of SQLRooms' modularity is **store composability via slices**. Slices are modular pieces of state and logic that can be added to your Room. You can use slices from the `@sqlrooms/*` packages or create your own custom slices. Each slice is a function that returns a partial state object along with methods to modify that state. This makes it easy to extend and customize your Room's behavior. Learn more and see an example in [Composing Store from Slices](/key-concepts#composing-store-from-slices). This modular approach means you can start simple and grow your app as your needs evolve—adding only the components you want, and integrating with the libraries and tools your users need. --- --- url: https://sqlrooms.org/deployment-scenarios.md --- # Deployment Scenarios SQLRooms supports multiple deployment models, from fully browser-only apps to server-backed collaborative setups. This guide helps teams choose a scenario based on infrastructure constraints, collaboration needs, and data governance requirements. ## 1) Browser-only analytics for read-mostly use cases (DuckDB WASM + object storage) This is the simplest setup for traditional BI-style analytics with many managers using only a browser, especially when shared data is mostly consumed rather than collaboratively edited. * **How it works:** SQLRooms runs in the browser with DuckDB WASM, reading parquet (or other supported files) from object storage. * **Data storage:** S3-compatible object storage (AWS S3, Cloudflare R2, MinIO, etc.). * **App state storage:** Browser `localStorage` or IndexedDB; optionally OPFS for persisted DuckDB files. * **Access patterns:** Signed URLs, backend-issued short-lived credentials, or DuckDB Secrets Manager where applicable. * **Best fit:** Read-only or read-mostly self-service analytics with minimal backend operations. * **At a glance:** Lowest ops complexity; low collaboration on shared data; partial/strong offline depending on caching and OPFS usage (offline is reduced if you rely on cloud persistence like MotherDuck). Examples and references: * [AI example](/examples#ai-powered-analytics) * [SQL Query Editor Example](/examples#sql-query-editor) ## 2) Browser clients with writable shared datasets Use this scenario when teams need shared writable datasets with stronger governance and concurrency guarantees than individual parquet files. * **How it works:** Browser clients query and update shared datasets via a managed DuckDB backend (for example MotherDuck) or catalog-managed table formats such as Iceberg. * **Data storage:** MotherDuck-managed DuckDB storage, or Iceberg tables in object storage with a catalog layer. * **App state storage:** Browser local state and optionally backend metadata storage. * **Platform choice:** See [Backend platform options](#backend-platform-options-for-scenarios-2-and-3) for trade-offs between MotherDuck, Modal, Daytona, Cloudflare, and Plane. * **Best fit:** Teams that need managed table lifecycle, concurrent writes, and shared editable data assets. * **At a glance:** Moderate infra complexity; medium collaboration through shared writable tables; limited offline. Potential ecosystem options include [MotherDuck](https://motherduck.com/), [Amazon S3 Tables](https://aws.amazon.com/s3/features/tables/), and [Cloudflare R2 Data Catalog](https://developers.cloudflare.com/r2/data-catalog/). Browser write/read capabilities depend on current DuckDB + browser connector support. Reference: * [DuckDB: Iceberg in the Browser](https://duckdb.org/2025/12/16/iceberg-in-the-browser) * [MotherDuck example](/examples#motherduck-cloud-query-editor) ## 3) Collaborative shared rooms with `sqlrooms-server` (coming soon) Use server-backed sessions when many users need to see and edit the same analytical workspace in near real time. * **How it works:** A shared server runtime hosts DuckDB + sync endpoints; browser clients connect over WebSockets. * **Data storage:** Server DuckDB database (with optional remote sources/extensions). * **Alternative persistence:** MotherDuck can be used as a managed central DuckDB backend when you prefer hosted durability over self-managed database files. * **App state storage:** Server-side metadata in the default meta schema or dedicated `--meta-db`, plus optional browser persistence. * **Platform choice:** See [Backend platform options](#backend-platform-options-for-scenarios-2-and-3) for trade-offs between managed persistence and per-room runtime platforms. * **Best fit:** Team collaboration in a shared room, coordinated analysis, and synchronized state. * **At a glance:** Highest collaboration; backend required; low offline for shared sessions. A common deployment pattern is session-per-room on demand (for example, containerized workers). One practical option is [Cloudflare Containers](https://developers.cloudflare.com/containers/), which can spin up container instances on demand and route requests per session. Another option is [Daytona](https://www.daytona.io/), which provides API-driven isolated sandboxes suitable for per-room runtime isolation and agent/tool execution workflows. [Plane](https://plane.dev/) is another self-hostable option that can run stateful WebSocket backends with per-session process isolation. [Modal Sandbox](https://modal.com/docs/guide/sandboxes) is also a strong fit for isolated compute environments, especially when paired with bursty heavier jobs. Examples and references: * [Sync example](https://github.com/sqlrooms/examples/tree/main/sync) * [`sqlrooms-server` README](https://github.com/sqlrooms/sqlrooms/tree/main/python/sqlrooms-server) * [Build your own data warehouse with DuckDB, DBT, and Modal](https://modal.com/docs/examples/dbt_duckdb) ## Backend platform options for Scenarios 2 and 3 In practice, teams can choose between managed data persistence (MotherDuck) and per-room compute runtimes (Modal/Daytona/Cloudflare/Plane), or combine them. * **Use MotherDuck when:** You have a business/enterprise budget and want managed DuckDB persistence, sharing, and operational simplicity. It is best as the durable data layer rather than the room runtime itself. * **Use Modal when:** You want fast developer velocity for per-room isolated environments with persistent volumes and straightforward programmatic provisioning. * **Use Daytona when:** You want API-driven per-room isolated sandboxes, strong runtime isolation for untrusted code or agent tools, and workspace-style execution environments. * **Use Cloudflare Containers when:** You prioritize low baseline cost and edge proximity, and are comfortable implementing extra persistence orchestration (for example with R2) for sleeping/ephemeral instances. * **Use Plane when:** You want a self-hostable system for stateful per-session WebSocket backends with full control over infrastructure behavior. Pricing and limits can change frequently, so treat platform economics as a regularly reviewed decision input. ## 4) Local session backend from CLI This scenario is for single-user local workflows, similar to how Jupyter is often used: run a local backend process and connect from your browser. * **How it works:** Start `sqlrooms` from CLI on your laptop, then point a local SQLRooms app to that local server endpoint. * **Data storage:** Local DuckDB file (or `:memory:`), with optional remote sources/extensions. * **App state storage:** Primarily local browser state, with optional server-side metadata tables. * **Best fit:** Power users and developers who want local control, reproducibility, and a backend runtime without deploying shared infrastructure. * **At a glance:** Simple local setup; strong privacy; no built-in team collaboration unless you later promote it to shared deployment. Reference: * [`sqlrooms` README](https://github.com/sqlrooms/sqlrooms/tree/main/python/sqlrooms) ## 5) Offline-capable PWA Choose this for local-first experiences where users must continue working without network access. * **How it works:** SQLRooms is shipped as a PWA with service worker caching and local DuckDB persistence. * **Data storage:** Browser OPFS (DuckDB files), local file imports. * **App state storage:** `localStorage` or IndexedDB. * **Best fit:** Offline analysis, privacy-first workflows, and disconnected environments. * **At a glance:** Strong offline; simple architecture; collaboration is mostly asynchronous/manual. Examples and references: * [Offline Use](https://sqlrooms.org/offline-use) * [Query PWA example](https://github.com/sqlrooms/examples/tree/HEAD/query-pwa) ## 6) Desktop packaging (Electron/Tauri) SQLRooms can be packaged as a desktop app using Electron or Tauri. * **How it works:** A desktop shell hosts the SQLRooms web app. * **Runtime options:** In-app DuckDB WASM, or native DuckDB via a local backend such as `sqlrooms-server`. * **Data storage:** Local filesystem/embedded database, optionally with remote sources. * **Alternative persistence:** MotherDuck can back desktop deployments that need cloud-synced datasets across devices instead of only local files. * **App state storage:** Local files and browser-like storage inside the desktop shell. * **Best fit:** Organizations that prefer managed desktop distribution and local data residency. * **At a glance:** Good for managed desktop environments; offline-friendly in local mode; collaboration depends on backend connectivity. Current status: SQLRooms does not provide direct Electron integration and there is no first-party Electron example today. Reference: * [Foursquare Spatial Desktop case study](/case-studies.html#foursquare-spatial-desktop) ## Hybrid setups Hybrid setups are also possible: start with browser-only or PWA for most users, and add server-backed shared rooms for teams that need real-time collaboration. --- --- url: https://sqlrooms.org/whats-new.md --- # What's New New features, improvements, and notable changes in each SQLRooms release. For migration steps and breaking changes, see the [Upgrade Guide](/upgrade-guide). ## 0.29.0 (upcoming) SQLRooms 0.29 expands the project from a collection of analytics components into a more complete, composable workspace for documents, dashboards, maps, queries, and AI-assisted exploration. This is a large release with breaking changes; applications upgrading from 0.28 should review the [0.29 upgrade guide](/upgrade-guide#_0-29-0-upcoming). ### Documents, blocks, and artifacts The new `@sqlrooms/blocks` and `@sqlrooms/documents` packages provide reusable primitives for rich-text documents containing live SQL, Python, chart, map, and other stateful blocks. Documents can embed charts as image assets, expose block settings, and participate in AI editing through the same command and artifact mechanisms used by the rest of SQLRooms ([#666](https://github.com/sqlrooms/sqlrooms/pull/666), [#603](https://github.com/sqlrooms/sqlrooms/pull/603), [#612](https://github.com/sqlrooms/sqlrooms/pull/612)). Reusable query blocks and artifact tabs make these surfaces easier to compose inside applications ([#669](https://github.com/sqlrooms/sqlrooms/pull/669)). The former "worksheet" terminology now uses block documents. Structured artifacts and commands use `block-document` / `block-document.*`; Markdown artifacts and commands use `markdown-document` / `markdown-document.*`, with `MarkdownDocumentsSlice*` APIs and the `markdownDocuments` store key ([#878](https://github.com/sqlrooms/sqlrooms/pull/878), [#903](https://github.com/sqlrooms/sqlrooms/pull/903), [#904](https://github.com/sqlrooms/sqlrooms/pull/904)). See the [document naming migration guide](/upgrade-guide#sqlroomsdocuments-canonical-document-names-breaking) for API changes and local workspace preservation. The new [Artifacts](/artifacts) and [Blocks and Block Documents](/blocks-and-documents) developer guides explain how these layers compose and who owns persisted feature state. `@sqlrooms/artifacts` now models AI session/artifact relationships as many-to-many `sessionArtifactLinks`, allowing one chat to work across several artifacts while keeping workspace state separate from chat associations ([#844](https://github.com/sqlrooms/sqlrooms/pull/844)). The prerelease-only `aiSessionArtifacts` and `artifactCreators` representations were removed; see the [upgrade guide](/upgrade-guide#sqlroomsartifacts-artifact-ai-sessions-use-pure-many-to-many-associations-breaking). ### AI SDK v6 and composable chat The AI packages now use AI SDK v6 and `ToolLoopAgent`. SQLRooms tools use native AI SDK definitions, while tool rendering is registered separately so the same tool can be reused across different interfaces ([#497](https://github.com/sqlrooms/sqlrooms/pull/497), [#800](https://github.com/sqlrooms/sqlrooms/pull/800)). Chat is now exposed as a compound, composable UI API with unstyled composer and prompt-suggestion primitives ([#871](https://github.com/sqlrooms/sqlrooms/pull/871)). This release also adds chat history, transcript search, session forking, file attachments, persisted errors, active status rendering, and opt-in timeouts ([#698](https://github.com/sqlrooms/sqlrooms/pull/698), [#695](https://github.com/sqlrooms/sqlrooms/pull/695), [#716](https://github.com/sqlrooms/sqlrooms/pull/716), [#890](https://github.com/sqlrooms/sqlrooms/pull/890), [#814](https://github.com/sqlrooms/sqlrooms/pull/814)). ### Commands, skills, and agent capabilities Commands can declare keyboard shortcuts and run through middleware and telemetry hooks. `createRoomShellSlice` accepts the same command configuration, allowing the UI, application code, and agents to invoke a shared, inspectable command model. See the [Commands guide](/commands) for the full API. `@sqlrooms/ai` adds a skills subsystem and authoring wizard ([#574](https://github.com/sqlrooms/sqlrooms/pull/574)). The CLI builds on the same primitives with an MCP capability runtime, named capability profiles, separate chat and artifact navigation, and tools that can capture rendered charts, maps, and documents for visual inspection ([#845](https://github.com/sqlrooms/sqlrooms/pull/845), [#859](https://github.com/sqlrooms/sqlrooms/pull/859), [#891](https://github.com/sqlrooms/sqlrooms/pull/891), [#889](https://github.com/sqlrooms/sqlrooms/pull/889)). ### Mosaic dashboards and data exploration `@sqlrooms/mosaic` now includes chart builders, composable dashboards, AI dashboard tools, and a table profiler for building Quake-style cross-filtered data inspectors ([#473](https://github.com/sqlrooms/sqlrooms/pull/473), [#539](https://github.com/sqlrooms/sqlrooms/pull/539), [#527](https://github.com/sqlrooms/sqlrooms/pull/527)). Data table explorer blocks bring per-column summaries and paged Arrow rows into dashboards ([#668](https://github.com/sqlrooms/sqlrooms/pull/668)). The profiler API pairs `useMosaicProfiler` with `MosaicProfilerHeader`, `MosaicProfilerRows`, and `MosaicProfilerStatusBar`, keeping rendering React-driven while following Mosaic coordinator and cross-filter lifecycle. Charts gain box plots, configurable count metrics, multiple line-series and aggregations, better data-limit reporting, and row-count line charts ([#588](https://github.com/sqlrooms/sqlrooms/pull/588), [#591](https://github.com/sqlrooms/sqlrooms/pull/591), [#787](https://github.com/sqlrooms/sqlrooms/pull/787), [#900](https://github.com/sqlrooms/sqlrooms/pull/900)). ### Maps and spatial visualization Deck.gl maps gain GeoArrow layers, overlaid integration, split views, richer appearance controls, reusable AI map tools, and direct document blocks ([#549](https://github.com/sqlrooms/sqlrooms/pull/549), [#661](https://github.com/sqlrooms/sqlrooms/pull/661), [#701](https://github.com/sqlrooms/sqlrooms/pull/701), [#841](https://github.com/sqlrooms/sqlrooms/pull/841)). New maps use keyless OpenFreeMap vector basemaps by default, while Mapbox remains available when a token is configured ([#897](https://github.com/sqlrooms/sqlrooms/pull/897)). Kepler map selection and tab ownership now compose with artifacts instead of maintaining a separate host-level current-map state ([#595](https://github.com/sqlrooms/sqlrooms/pull/595)). ### Layout and workspace persistence The layout packages now use n-ary docking and grid primitives backed by `react-resizable-panels`, including per-panel sizing constraints and persisted resize state ([#552](https://github.com/sqlrooms/sqlrooms/pull/552), [#575](https://github.com/sqlrooms/sqlrooms/pull/575), [#594](https://github.com/sqlrooms/sqlrooms/pull/594), [#631](https://github.com/sqlrooms/sqlrooms/pull/631)). The configuration shape and several public types changed; consult the upgrade guide before loading persisted 0.28 layouts. ### DuckDB and query results The SQLRooms Python packages now target DuckDB 1.5.3. The 0.29.0 JavaScript runtimes use `@duckdb/node-api` 1.4.4-r.3 and `@duckdb/duckdb-wasm` 1.32.0. DuckDB integrations also have more consistent qualified-table and multi-schema handling ([#659](https://github.com/sqlrooms/sqlrooms/pull/659), [#734](https://github.com/sqlrooms/sqlrooms/pull/734)). `@sqlrooms/duckdb-node` converts results through DuckDB's Arrow IPC support, preserving declared Arrow types for values such as `BIGINT`, `DATE`, `DECIMAL`, and `BLOB` ([#887](https://github.com/sqlrooms/sqlrooms/pull/887)). This changes runtime value types and requires the DuckDB `nanoarrow` extension; see the [migration note](/upgrade-guide#sqlroomsduckdb-node-query-results-now-use-duckdb-arrow-ipc-breaking). ### UI notifications `Toaster` now renders [Sonner](https://sonner.emilkowal.ski/) with SQLRooms theme-aware styling, and `@sqlrooms/ui` exports Sonner's `toast` function for application notifications ([#397](https://github.com/sqlrooms/sqlrooms/pull/397)). ## 0.28.0 * **Tailwind v4**: SQLRooms now uses Tailwind v4, including the new CSS-first setup that simplifies project styling and configuration ([#324](https://github.com/sqlrooms/sqlrooms/pull/324)). For Tailwind migration details, jump to the [upgrade guide](/upgrade-guide#tailwind-v3-to-v4). * **Cosmos.gl upgrade**: updates the [Cosmos.gl](https://cosmos.gl) integration to include the latest improvements in this powerful graph visualization library ([#379](https://github.com/sqlrooms/sqlrooms/pull/379)) * **Command system implementation**: Command Palette UI added to shells (toggle with `Ctrl/Cmd+K`, sidebar button, searchable/grouped commands, per-command shortcuts, JSON input editor, and programmatic open/close controls). A global command system and tooling is also introduced to register, list, validate, and execute commands, with adapters for CLI/MCP and AI tool integrations, plus DB and editor command sets ([#382](https://github.com/sqlrooms/sqlrooms/pull/382)) ## 0.27.0 ### `@sqlrooms/data-table`: RowSelection API `DataTablePaginated` now includes a first-class row selection API with checkbox support. * `enableRowSelection`: enables the checkbox column * `rowSelection`: controlled row selection state * `onRowSelectionChange`: callback fired when selection changes Checkbox clicks are handled independently from row click handlers, so selecting via checkbox does not double-toggle rows. Example: ```tsx import {RowSelectionState} from '@sqlrooms/data-table'; import {useState} from 'react'; const [rowSelection, setRowSelection] = useState({}); { setRowSelection((prev) => ({ ...prev, [row.index]: !prev[row.index], })); }} />; ``` ### `@sqlrooms/room-store`: bound `useRoomStore` API + `useRoomStoreApi` `useRoomStore` now exposes imperative Zustand store methods (`getState`, `setState`, `subscribe`, `getInitialState`) in addition to selector usage. This makes event handlers and async callbacks more ergonomic while preserving existing reactive selector patterns. For context-based access, use the new `useRoomStoreApi()` hook to read/write state imperatively from components wrapped in `RoomStateProvider`. ### Introducing MosaicSlice A new centralized state management system for Mosaic integration. The `MosaicSlice` provides a unified way to manage Mosaic connections, coordinate cross-filtering between visualizations, and create reactive data queries that automatically update based on user selections. Key features: * Automatic connection management with DuckDB * Named selections for cross-filtering between multiple visualizations * `useMosaicClient` hook for custom visualization clients * Support for custom visualizations that respond to Mosaic selections See the [Mosaic API documentation](/api/mosaic/) for details and check out the [DeckGL + Mosaic example](examples#deck-gl-mosaic) for a complete implementation. ### Additional 0.27.0 highlights * **AI**: parallel sessions, persisted open session tabs, provider options, prompt suggestion improvements, inline API-key prompt in chat, and output copy-to-clipboard. * **Vega/Charts**: actions toolbar, chart sizing fixes, improved SQL error display, hover-only chart actions, and responsive chart labels. * **Kepler**: configurable injector with custom recipes, legend/timeline fixes, and stability improvements across integration edge cases. * **Room/store + persistence**: `storeKey` support in `createRoomStore` and `persistSliceConfigs` helper improvements. * **SQL/editor + query UX**: improved explain output, query panel/tab mapping fixes, and query cancellation support in create-table flows. ## 0.26.1-rc.7 (2025-12-05) ### Replaced barrel exports across all modules Barrel exports (i.e., `export * from ...`) were replaced across all modules to improve tree-shaking, reduce bundle size, and avoid import path ambiguities. Direct/explicit exports now ensure only the required symbols are included in consumers' builds, making dependencies clearer and preventing accidental re-exports or circular dependencies. Additionally, `"sideEffects": false` was added to all packages. This signals to bundlers that the modules are free of side effects, enabling better tree-shaking and further reducing the final bundle size. ### TabStrip component in `@sqlrooms/ui` A composable tab strip with drag-to-reorder, inline renaming, and a search dropdown for reopening closed tabs. Supports custom tab menus and flexible layouts via subcomponents (`TabStrip.Tabs`, `TabStrip.SearchDropdown`, `TabStrip.NewButton`). New: the search dropdown can optionally sort items by recent usage via `sortSearchItems="recent"` and an optional `getTabLastOpenedAt` accessor. ### Kepler integration Added [Kepler.gl](https://kepler.gl/) integration module for geospatial data visualization. Check the [Kepler example](https://github.com/sqlrooms/examples/tree/main/kepler) ### AI RAG module New `@sqlrooms/ai-rag` module for Retrieval Augmented Generation. Query your documentation using vector similarity search powered by DuckDB's native vector capabilities. Check the [AI RAG example](https://github.com/sqlrooms/examples/tree/main/ai-rag) ## 0.26.0 (2025-11-17) ### AI SDK v5 We migrated to Vercel AI SDK v5. Now supporting agents: check the [ai-agent example](https://github.com/sqlrooms/sqlrooms/tree/main/examples/ai-agent) --- --- url: https://sqlrooms.org/upgrade-guide.md --- # Upgrade Guide This document provides detailed guidance for upgrading between different versions of SQLRooms packages. Each section outlines breaking changes, required code modifications, and implementation examples to ensure a smooth upgrade process. When upgrading, please follow the version-specific instructions below that apply to your project. If you encounter any issues during the upgrade process, please refer to our [GitHub issues](https://github.com/sqlrooms/sqlrooms/issues) or contact support. ## 0.29.0 (upcoming) ### `@sqlrooms/documents`: canonical document names (breaking) The prerelease document APIs now distinguish structured block documents from Markdown documents. Block documents use the `block-document` artifact type and `block-document.*` command IDs. Markdown artifacts and embedded Markdown blocks use `markdown-document`, with `markdown-document.*` command IDs. Update Markdown imports, selectors, persistence schemas, artifact registries, and command callers: | Previous API or key | Replacement | | --------------------------------------------------- | ------------------------------------------------------------------- | | `createDocumentsSlice` | `createMarkdownDocumentsSlice` | | `createDefaultDocumentsConfig` | `createDefaultMarkdownDocumentsConfig` | | `DocumentsSliceConfig` / `DocumentsSliceConfigType` | `MarkdownDocumentsSliceConfig` / `MarkdownDocumentsSliceConfigType` | | `DocumentsSliceState` | `MarkdownDocumentsSliceState` | | `CreateDocumentsSliceProps` | `CreateMarkdownDocumentsSliceProps` | | `useStoreWithDocuments` | `useStoreWithMarkdownDocuments` | | `state.documents` | `state.markdownDocuments` | | `createMarkdownCommands` | `createMarkdownDocumentCommands` | | `markdown.*` command IDs | `markdown-document.*` command IDs | | `markdown` artifact and embedded block type | `markdown-document` | | `buildKnowledgeIndex({documents, artifacts})` | `buildKnowledgeIndex({markdownDocuments, artifacts})` | Persist `MarkdownDocumentsSliceConfig` under the `markdownDocuments` key. `DocumentAsset` and `createDocumentsCrdtMirror()` remain shared by both document families and keep their names. `createBlockDocumentCommands()` and `createPythonBlockCommands()` no longer accept `artifactType`, `artifactLabel`, or `commandNamespace`. Remove those options and use the canonical `block-document` type and `block-document.*` IDs. Keep product-specific labels in the artifact registry; `commandGroup` remains available for UI grouping. The block-document command factory also retains `defaultTitle`. `createBlockDocumentCommandIds()` no longer accepts a namespace argument. `createBlockDocumentCommandAiAdapter()` no longer accepts `isBlockDocumentArtifact`, and `createDocumentsCrdtMirror()` no longer accepts `blockDocumentArtifactTypes`. #### Persisted workspace data The CLI migrates local workspace snapshots from `documents` to `markdownDocuments` before schema validation, preserving Markdown bodies and owned assets. When both keys exist, it retains disjoint records and gives canonical records precedence for overlapping IDs. It also normalizes legacy `markdown` artifact and embedded block types to `markdown-document` and keeps its existing local `document`/`worksheet` migration to the appropriate family. Applications that persist their own workspaces must perform the equivalent migration before parsing with the new schema; the package schema does not rename the outer slice key automatically. Preserve both the Markdown content and the document-owned asset map when moving each record. The CRDT Markdown field is also named `markdownDocuments`. Experimental CRDT snapshots and saved AI context are not migrated. Reset incompatible development sync snapshots and saved sessions when upgrading; this does not replace the local workspace migration above. ### `@sqlrooms/artifacts`: artifact AI sessions use pure many-to-many associations (breaking) The prerelease-only one-to-one `artifactAi.aiSessionArtifacts` map, `artifactCreators` map, and provenance-bearing link shape were removed. `artifactAi.sessionArtifactLinks` is now the only persisted and runtime representation of relationships between AI sessions and artifacts: ```ts type ArtifactSessionLink = { sessionId: string; artifactId: string; linkedAt: number; }; ``` This is a clean prerelease break: `ArtifactAiConfigSchema` does not migrate the removed fields or the previous `{createdAt, linkType}` link shape. If you need to retain prerelease state, convert each relationship to an association and rename its relationship timestamp from `createdAt` to `linkedAt`. Creation provenance, when needed, should live in the artifact's domain metadata rather than in its chat associations. Helper APIs now require `sessionArtifactLinks`, and the deprecated one-to-one and creator-provenance slice methods were removed: * `setSessionArtifact` → `addSessionArtifactLink(sessionId, artifactId)` * `clearSessionArtifact` → `removeAllLinksForSession` * `getSessionArtifactId` → `getLatestArtifactForSession` * `setArtifactCreator`, `getArtifactCreatorSessionId`, and `getCreatedArtifactIds` have no association-layer replacement Artifact pinning is workspace state and has moved from the AI companion slice to the base artifacts slice: * `artifactAi.config.pinnedArtifactIds` → `artifacts.config.pinnedArtifactIds` * `artifactAi.togglePinArtifact(id)` → `artifacts.togglePinArtifact(id)` * `artifactAi.isPinnedArtifact(id)` → `artifacts.isPinnedArtifact(id)` ### `@sqlrooms/artifacts`: "Sheets" terminology migrated to "Artifacts" (breaking) The concept of "sheets" has been replaced with "artifacts" to better represent the variety of content types (app builders, charts, maps, etc.) that can be created and managed. #### API Changes **Store namespace:** * `state.sheets` → `state.artifacts` * `createSheetsSlice` → `createArtifactsSlice` * `SheetsSlice` → `ArtifactsSlice` **Component renames:** * `Sheets` → `Artifacts` * `SheetsTabs` → `ArtifactTabs` * `SheetsPanel` → `ArtifactsPanel` **Type renames:** * `Sheet` → `Artifact` * `SheetType` → `ArtifactType` #### Migration Example Before: ```tsx import {createSheetsSlice, SheetsSlice} from '@sqlrooms/sheets'; type RoomState = SheetsSlice & ...; const store = createRoomStore((set, get, store) => ({ ...createSheetsSlice()(set, get, store), })); const sheets = useRoomStore((state) => state.sheets.items); ``` After: ```tsx import {createArtifactsSlice, ArtifactsSlice} from '@sqlrooms/artifacts'; type RoomState = ArtifactsSlice & ...; const store = createRoomStore((set, get, store) => ({ ...createArtifactsSlice()(set, get, store), })); const artifacts = useRoomStore((state) => state.artifacts.items); ``` ### `@sqlrooms/kepler`: map tabs moved to `@sqlrooms/artifacts` (breaking) Kepler no longer owns host-level tab selection. If your app supports multiple user-managed maps, model each map as an artifact and let `ArtifactTabs` own the selected tab, ordering, close/reopen, rename, and delete lifecycle. #### API Changes * `KeplerSliceConfig.currentMapId` was removed. * Legacy Kepler tab state such as `openTabs` should move to layout/artifact tab state. * `state.kepler.getCurrentMap()` and `state.kepler.setCurrentMapId(...)` were removed. Pass explicit map ids to Kepler APIs instead. * `KeplerMapContainer`, `KeplerPlotContainer`, `KeplerSidePanels`, and Kepler slice actions should receive a `mapId` derived from the artifact panel or current artifact selection. #### Migration Helper Use `migrateKeplerTabsToArtifacts` when loading persisted Kepler configs that still contain `maps`, `openTabs`, and `currentMapId`. ```ts import {ArtifactsSliceConfig} from '@sqlrooms/artifacts'; import { KeplerSliceConfig, migrateKeplerTabsToArtifacts, } from '@sqlrooms/kepler-config'; const migrated = migrateKeplerTabsToArtifacts(rawKeplerConfig, { artifactType: 'kepler-map', }); const keplerConfig = KeplerSliceConfig.parse(migrated.keplerConfig); const artifactsConfig = ArtifactsSliceConfig.parse(migrated.artifactsConfig); ``` Then initialize the slices with the migrated configs and apply `migrated.hiddenArtifactIds` to the artifact tabs layout node if you need to preserve maps that were closed under the old Kepler tab model. #### Before ```ts const currentMap = useRoomStore((state) => state.kepler.getCurrentMap()); ; ``` #### After ```tsx const mapId = artifactIdFromPanelMeta; ; ``` ### `@sqlrooms/kepler`: `addTableToMap` now prefers an object parameter `state.kepler.addTableToMap` now accepts a single object parameter. The older positional signature remains supported for compatibility, but host apps should migrate to the object form because the API now separates the table reference, Kepler `addDataToMap` options, config, and optional dataset id override. #### Before ```ts await state.kepler.addTableToMap( mapId, tableName, { autoCreateLayers: false, centerMap: false, }, config, ); ``` #### After ```ts await state.kepler.addTableToMap({ mapId, tableName, options: { autoCreateLayers: false, centerMap: false, }, config, }); ``` If your app restores a previously saved Kepler layer or filter and must load the table under an existing `dataId`, pass `datasetId` explicitly: ```ts await state.kepler.addTableToMap({ mapId, tableName: savedDataId, options: { autoCreateLayers: false, centerMap: false, }, datasetId: savedDataId, }); ``` For normal add-table flows, omit `datasetId`. Kepler will derive the persisted dataset id from the configured `tableSelection.getDatasetIdForTable` policy. ### `@sqlrooms/duckdb-core`, `@sqlrooms/duckdb`: schema catalog loader and `createDbSchemaTrees()` input changed (breaking) `createDbSchemaTrees()` now takes a grouped `SchemaWithTables[]` instead of a flat `DataTable[]`, and the loader pair changed: * `loadSchemasWithTables()` → replaced by `loadSchemaCatalog()` (single metadata query, preserves empty schemas and empty `main` schemas of attached databases) * Filter API: instead of a single `(QualifiedTableName) => boolean` invoked with a fake `table === ''` for schemas, the new `loadSchemaCatalogFilter` receives a typed `SchemaCatalogFilterEntry` discriminated union (`{type: 'database' | 'schema' | 'table', ...}`) * `DuckDbSlice` accepts a new `loadSchemaCatalogFilter` prop alongside `loadTableSchemasFilter`. If you only set `loadTableSchemasFilter`, it is bridged for `entry.type === 'table'` only — schemas/databases fall through to `defaultLoadSchemaCatalogFilter`, so any reserved schemas/databases you previously hid via the table filter must move to a `loadSchemaCatalogFilter`. #### Signature change ```ts // Before function createDbSchemaTrees(tables: DataTable[]): DbSchemaNode[]; // After function createDbSchemaTrees(schemas: SchemaWithTables[]): DbSchemaNode[]; type SchemaWithTables = { database: string; schema: string; tables: DataTable[]; }; ``` `SchemaWithTables` is exported from both `@sqlrooms/duckdb-core` and `@sqlrooms/duckdb`. #### Before ```ts import {createDbSchemaTrees, type DataTable} from '@sqlrooms/duckdb-core'; const tables: DataTable[] = await loadTableSchemas(connector); const trees = createDbSchemaTrees(tables); ``` #### After Use the new `loadSchemaCatalog()` from `@sqlrooms/duckdb`, which returns `SchemaWithTables[]` directly: ```ts import {createDbSchemaTrees} from '@sqlrooms/duckdb-core'; import { defaultLoadSchemaCatalogFilter, loadSchemaCatalog, } from '@sqlrooms/duckdb'; const schemas = await loadSchemaCatalog(connector, { filterFunction: (entry) => entry.type === 'schema' && entry.schema === 'scratch' ? false : defaultLoadSchemaCatalogFilter(entry), }); const trees = createDbSchemaTrees(schemas); ``` If you only have a flat `DataTable[]`, group it before passing to `createDbSchemaTrees`: ```ts const grouped = new Map(); for (const t of tables) { const key = `${t.database}\x00${t.schema}`; let g = grouped.get(key); if (!g) { g = {database: t.database ?? '', schema: t.schema, tables: []}; grouped.set(key, g); } g.tables.push(t); } const trees = createDbSchemaTrees(Array.from(grouped.values())); ``` ### `@sqlrooms/duckdb-node`: query results now use DuckDB Arrow IPC (breaking) `@sqlrooms/duckdb-node` now converts query results with DuckDB's `nanoarrow` extension instead of reconstructing Arrow tables from JavaScript values. This preserves DuckDB's declared types and fixes lossy handling of timestamps, decimals, and binary data, but it changes both initialization requirements and the JavaScript values returned by `query()`. #### Make `nanoarrow` available during initialization The connector installs and loads DuckDB's `nanoarrow` community extension when it initializes. Initialization now fails if the extension cannot be installed or loaded. Environments without outbound network access must populate DuckDB's extension cache before creating the connector. If you use a restricted CI or production environment, exercise connector initialization in that environment before deployment. Do not treat `nanoarrow` as an optional enhancement: it is the conversion path used by the Node connector. #### Update code that consumes Arrow values Values returned by `query()` now follow their Arrow types instead of inferred JavaScript types. In particular: * `BIGINT` is Arrow `Int64` and is exposed as JavaScript `bigint`. * `DATE` remains Arrow `Date32` instead of being inferred from a JavaScript date value. * `DECIMAL` retains its declared precision and scale. * `BLOB` remains Arrow `Binary` and preserves arbitrary bytes. Code that converts `query()` results to plain objects or serializes them with `JSON.stringify` must handle values such as `bigint` explicitly. For JSON-facing code, prefer `queryJson()`. Its row accessor converts safe integers to JavaScript numbers, unsafe integers and decimals to strings, and applies the same conversion recursively inside lists, structs, and maps. #### Arrow loading is now supported `loadArrow()` now accepts both Arrow tables and IPC byte streams. The Node API does not currently register in-memory Arrow buffers directly, so the connector uses a short-lived local file while DuckDB reads the IPC stream. Environments that restrict temporary-file creation must provide a writable operating-system temporary directory. ### `@sqlrooms/ai-core`, `@sqlrooms/ai`: Upgraded to AI SDK v6 with `ToolLoopAgent` (breaking) The AI SDK dependency has been upgraded from v5 to v6. Tool execution now uses `ToolLoopAgent` instead of `streamText`. If you only use `createAiSlice` without customization, no changes are needed — the transport layer is updated internally. ### `@sqlrooms/ai-config`, `@sqlrooms/ai-core`, `@sqlrooms/ai`: chat session terminology The public AI session API now uses chat terminology. Existing analysis-named exports remain available as compatibility aliases during the migration window, but new code should use the chat-named APIs. #### API Changes * `AnalysisSessionSchema` is deprecated in favor of `ChatSessionSchema`. * `isAnalysisSessionEmpty` is deprecated in favor of `isChatSessionEmpty`. * `AnalysisResultsContainer` is deprecated in favor of `ChatMessagesContainer` or the preferred compound component API, `Chat.Messages`. * `AnalysisResult` is deprecated in favor of `ChatTurnView`. * `AnalysisAnswer` is deprecated in favor of `MessageContent`. * `processAnalysisAnswerContent` is deprecated in favor of `processMessageContent`. * `AnalysisResultSchema`, `getAnalysisResults`, `addAnalysisResult`, `deleteAnalysisResult`, and `cleanupPendingAnalysisResults` remain compatibility APIs for existing apps. * New code should prefer `uiMessages` and derived `ChatTurn` helpers such as `getChatTurnsFromUiMessages`. * Persisted legacy `analysisResults` is still accepted when loading old rooms, but `ChatSessionSchema` no longer emits `analysisResults` in parsed session state and new sessions no longer persist it. #### Migration Example Before: ```ts import {AnalysisSessionSchema, isAnalysisSessionEmpty} from '@sqlrooms/ai'; ``` After: ```ts import {ChatSessionSchema, isChatSessionEmpty} from '@sqlrooms/ai'; ``` If you render the built-in chat UI, prefer the compound component: ```tsx ``` For custom chat rendering, derive turns from `uiMessages`: ```ts import {getChatTurnsFromUiMessages} from '@sqlrooms/ai'; const turns = getChatTurnsFromUiMessages(session.uiMessages, { isRunning: session.isRunning, }); ``` #### Sub-agent composition The tool-as-agent pattern now uses `ToolLoopAgent` + `streamSubAgent`: ```ts // Before (v5) const result = await streamText({ model, system: instructions, messages: [{role: 'user', content: prompt}], tools, maxSteps: 10, }); // After (v6) import {ToolLoopAgent, stepCountIs} from 'ai'; import {streamSubAgent} from '@sqlrooms/ai'; const agent = new ToolLoopAgent({ model, instructions, tools, stopWhen: stepCountIs(10), temperature: 0, }); const resultText = await streamSubAgent(agent, prompt, abortSignal); ``` #### `addToolResult` → `addToolOutput` ```ts // Before addToolResult({toolCallId, result: {...}}); // After — note: `tool` is a new required field in v6 addToolOutput({tool: toolName, toolCallId, output: {...}}); ``` #### `ToolRendererProps` new states Tool renderers may now receive three additional states for approval workflows: `approval-requested`, `approval-responded`, and `output-denied`. Update any exhaustive switch/if-else on `state` in custom renderers. #### Remote transport If you use `createRemoteChatTransportFactory`, your server-side route must migrate from `streamText` to `ToolLoopAgent` + `createAgentUIStreamResponse`. The transport now sends `instructions`, `maxSteps`, and `temperature` in the request body. > **Note:** The [`ai-nextjs` example](https://github.com/sqlrooms/sqlrooms/blob/main/examples/ai-nextjs/src/app/api/chat/route.ts) shows a reference implementation that intentionally ignores these client-supplied fields and uses server-controlled defaults for security. Production endpoints should decide whether to trust client-supplied values for `instructions`, `maxSteps`, and `temperature` based on their security model. ### `@sqlrooms/kepler`: `initialKeplerState` was replaced with `createInitialMapKeplerState` (breaking) `createKeplerSlice()` no longer accepts a static `initialKeplerState` object. Use `createInitialMapKeplerState` instead. It is called whenever a map's kepler state is initialized, so consumers can override the default map state and, if needed, derive things like the basemap style from the current theme by calling `getTheme()`. #### Before ```ts createKeplerSlice({ initialKeplerState: { mapStyle: { styleType: 'positron', }, }, }); ``` #### After ```ts import {getTheme} from '@sqlrooms/ui'; createKeplerSlice({ createInitialMapKeplerState: ({defaultInitialMapKeplerState}) => ({ ...defaultInitialMapKeplerState, mapStyle: { ...defaultInitialMapKeplerState.mapStyle, styleType: getTheme() === 'dark' ? 'dark-matter' : 'positron', }, }), }); ``` ### `@sqlrooms/ui`: `toast` export now uses Sonner (breaking) The top-level `toast` export from `@sqlrooms/ui` now points to Sonner's API. * **Before**: `toast({...})` used SQLRooms' legacy Radix-based object API. * **After**: `toast.success(...)`, `toast.error(...)`, etc. use Sonner. If you still need the old API temporarily, import `legacyToast` from `@sqlrooms/ui`. #### Before ```tsx import {toast} from '@sqlrooms/ui'; toast({ variant: 'default', title: 'Table created', description: 'File loaded', }); ``` #### After (Sonner) ```tsx import {toast} from '@sqlrooms/ui'; toast.success('Table created', { description: 'File loaded', }); ``` #### Temporary compatibility option ```tsx import {legacyToast} from '@sqlrooms/ui'; legacyToast({ variant: 'default', title: 'Table created', description: 'File loaded', }); ``` ### `@sqlrooms/ai`, `@sqlrooms/vega`, `@sqlrooms/ai-rag`: Tools migrated to native AI SDK format (breaking) All built-in tools and the tool authoring API now use the AI SDK's `tool()` factory instead of the OpenAssistant format. The `@openassistant/utils` dependency has been removed. #### Custom tools: before ```ts import {z} from 'zod'; const myTool = { name: 'my_tool', description: 'Does something', parameters: z.object({text: z.string()}), execute: async ({text}) => ({ llmResult: {success: true, details: `Result: ${text}`}, additionalData: {processed: text}, }), component: MyToolResult, // renderer attached to tool }; ``` #### Custom tools: after ```ts import {tool} from 'ai'; import {z} from 'zod'; const myTool = tool({ description: 'Does something', inputSchema: z.object({text: z.string()}), execute: async ({text}) => ({ // flat output — no llmResult / additionalData nesting success: true, details: `Result: ${text}`, processed: text, }), // optional: control what the LLM sees (defaults to full JSON) toModelOutput: ({output}) => ({type: 'text', value: output.details}), }); // renderer is registered separately — see toolRenderers below ``` ### `@sqlrooms/ai-core`, `@sqlrooms/ai`: Tool renderers decoupled from tools (breaking) Tool renderers (`component`) are no longer attached to individual tools. They are now registered once in `createAiSlice` via the new `toolRenderers` option, typed against the tools map. #### Before ```ts createAiSlice({ tools: { query: createQueryTool(store), // had component: QueryToolResult chart: createVegaChartTool(), // had component: VegaChartToolResult }, // ... }); ``` #### After ```ts import {createDefaultAiTools, createDefaultAiToolRenderers} from '@sqlrooms/ai'; import {VegaChartToolResult} from '@sqlrooms/vega'; createAiSlice({ tools: { ...createDefaultAiTools(store), chart: createVegaChartTool(), }, toolRenderers: { ...createDefaultAiToolRenderers(), // includes QueryToolResult chart: VegaChartToolResult, // myCustomTool: MyCustomToolResult, }, // ... }); ``` ### `@sqlrooms/ai`: Tool output type renames (breaking) The `llmResult`/`additionalData` split has been replaced with a single flat output type per tool. | Package | Old type | New type | | ------------------ | -------------------------------------------------------- | --------------------------------------- | | `@sqlrooms/ai` | `QueryToolLlmResult` + `QueryToolAdditionalData` | `QueryToolOutput` | | `@sqlrooms/ai` | `QueryToolOutput.errorMessage` | `QueryToolOutput.error` | | `@sqlrooms/vega` | `VegaChartToolLlmResult` + `VegaChartToolAdditionalData` | `VegaChartToolOutput` | | `@sqlrooms/vega` | `VegaChartToolArgs` (type alias) | removed — use `VegaChartToolParameters` | | `@sqlrooms/vega` | `VegaChartToolContext` | removed | | `@sqlrooms/ai-rag` | `RagToolAdditionalData` + `RagToolContext` | removed — use `RagToolOutput` | ### `@sqlrooms/ai`: `QueryToolResult` props changed (breaking) `QueryToolResult` now receives `ToolRendererProps` instead of standalone props. If you render it directly, update the call-site: #### Before ```tsx ``` #### After Use `createQueryToolRenderer` and register it in `toolRenderers`: ```ts import {createQueryToolRenderer} from '@sqlrooms/ai'; toolRenderers: { query: createQueryToolRenderer({showSql: false, formatValue: myFormatter}), } ``` ### `@sqlrooms/vega`: `createVegaChartTool` options removed (breaking) The `embedOptions`, `editable`, and `editorMode` options have been removed from `createVegaChartTool`. Pass them directly as props to `VegaChartToolResult` instead. #### Before ```ts createVegaChartTool({editable: false, editorMode: 'sql'}); ``` #### After ```tsx import {VegaChartToolResult} from '@sqlrooms/vega'; // In your toolRenderers: toolRenderers: { chart: (props) => , } ``` ### `@sqlrooms/ai-rag`: `ragToolRenderer` exported separately (breaking) The RAG tool renderer is no longer attached to the tool. Import and register it explicitly. #### Before ```ts // renderer was bundled inside createRagTool() as `component` tools: { search_documentation: createRagTool(); } ``` #### After ```ts import {createRagTool, ragToolRenderer} from '@sqlrooms/ai-rag'; tools: {search_documentation: createRagTool()}, toolRenderers: {search_documentation: ragToolRenderer}, ``` ### `@sqlrooms/ai-core`: In-chat tool result editing removed (breaking) The `setSessionToolAdditionalData` API and the `toolAdditionalData` session field have been removed. In-chat editing of tool results (e.g. inline Vega chart spec editing) is no longer supported — charts and other tool outputs are now rendered read-only within the chat. ```ts // Before state.ai.setSessionToolAdditionalData(sessionId, toolCallId, data); // After — remove the call entirely; no replacement needed. ``` If you were using `toolAdditionalData` to persist user edits to charts, extract the chart into a first-class entity stored independently of the chat instead. Persisted sessions with `toolAdditionalData` are automatically cleaned up on load. ### `@sqlrooms/ai`: Remote transport — drop `data-tool-additional-output` custom chunk (breaking) If you have a custom Next.js (or other server-side) route that manually wrote `data-tool-additional-output` data chunks to ferry `additionalData` to the client, you can remove that code entirely. #### Before ```ts // app/api/chat/route.ts result.pipeThrough( new TransformStream({ async onChunk({chunk}) { if (chunk.type === 'tool-result') { writer.write({ type: 'data-tool-additional-output', transient: true, data: { toolCallId: chunk.toolCallId, toolName: chunk.toolName, output: getToolAdditionalData(chunk.toolCallId), }, }); } }, }), ); ``` #### After Remove the `onChunk` handler. `createAgentUIStreamResponse` embeds the full tool `execute()` output directly into the `UIMessage` stream as a `tool-result` part, which the renderer receives via `ToolRendererProps.output`. No side-channel is needed. ```ts // chatTransport.ts — inside the local transport factory return createAgentUIStreamResponse({ agent, uiMessages: sanitizeMessagesForLLM(fixIncompleteToolCalls(messagesCopy)), abortSignal, }); ``` **Why this works:** Previously, `execute()` returned `{llmResult, additionalData}` — the UI data (`additionalData`) was separate and had to be sent manually. Now `execute()` returns a single flat output object. The AI SDK propagates the full output to the client through the standard `UIMessage` parts, so `ToolRendererProps.output` is populated automatically without any custom data chunks. ### `@sqlrooms/ai-core`: Removed exports * `convertToAiSDKTools` — removed (tools are now native AI SDK tools) * `findToolComponent` — replaced by `findToolRenderer` * `VegaChartToolParametersType` from `@sqlrooms/vega` — removed (use `VegaChartToolParameters` directly) ### `@sqlrooms/ai-core`: Composer and prompt suggestions rebuilt on unstyled primitives (breaking) `Chat.Composer` and `Chat.PromptSuggestions` are now recipes built on a new public primitive layer — `useChatComposer()` / `usePromptSuggestions()` and a set of `asChild`-capable, unstyled components (`Input`, `Send`, `Stop`, `DropTarget` for the composer; `Root`, `Item`, `VisibilityToggle`, `Dismiss` for suggestions). See the "Composable composer and prompt-suggestions primitives" section of the [`@sqlrooms/ai-core` README](https://github.com/sqlrooms/sqlrooms/blob/main/packages/ai-core/README.md) for the full layering and API. Three behavior changes ship alongside the new primitives: * **`Chat.Composer`'s `onRun` is now a chat-wide pre-send veto, not a per-control one.** It is registered on the composer state rather than wired into the composer's own button and keymap, so it also runs for sends that originate elsewhere under the same `` root — clicking a prompt suggestion, or a host calling `useChatComposer().send()`. This is deliberate: a policy the composer enforces and a suggestion row bypasses is a policy two surfaces disagree about. Two consequences to check: `onRun` may now fire for a prompt the user never typed into the composer, and two `Chat.Composer`s under one root share one registry, so both `onRun`s run for either surface's sends (a duplicate warns in development). Give independent surfaces their own `` root. `onRun` is still skipped entirely when sending is not possible, so it never fires for a send that does not happen. * **Local-agent `Enter` while streaming no longer stops the run.** It is now a no-op, matching session mode: `Enter` sends when ready, and never cancels a run in flight. * **`Chat.PromptSuggestions` now defaults to a full-width vertical list** with click-to-send and CSS-ellipsis truncation (plus a native `title` for the full text), replacing the previous horizontal card carousel that filled the prompt for editing and truncated by character count. A horizontal layout is still available — build it directly from the suggestions primitives, as `examples/ai-rag` now does. ### `@sqlrooms/layout`, `@sqlrooms/layout-config`: Layout config refactored (breaking) The layout system now uses explicit panel identity and dock boundaries instead of path-based lookup. `LayoutConfig` is `LayoutNode | null` directly — the outer `{ type: 'mosaic', nodes: ... }` wrapper is gone. Type names have been renamed from `MosaicLayout*` to `Layout*`, and `react-resizable-panels` now handles all layout rendering. The following APIs and properties were removed: * `getPanelByPath` * `useGetPanelByPath` * `useGetPanelInfoByPath` * `draggable` on split and tabs nodes * `pathSegment` on split and tabs nodes **Limited automatic migration:** The Zod schema uses `z.preprocess` to detect and convert **only** legacy binary tree formats (`{first, second, direction, splitPercentage?}`) to the new n-ary format with `children` arrays. **Manual migration required for:** * The outer `{ type: 'mosaic', nodes: ... }` wrapper (must be removed) * N-ary `splitPercentages` arrays on nodes that already use `children` arrays (must be converted to per-child `defaultSize`) * Any other v1 layout formats not in binary tree shape #### Layout config: remove the outer wrapper The `{ type: 'mosaic', nodes: ... }` wrapper and `LayoutTypes` enum are no longer needed. ##### Before ```ts import {LayoutTypes} from '@sqlrooms/layout-config'; const layout = { type: LayoutTypes.enum.mosaic, nodes: { type: 'split', direction: 'row', children: ['data', 'main'], splitPercentages: [30, 70], }, }; ``` ##### After ```ts import {LayoutConfig} from '@sqlrooms/layout-config'; const layout: LayoutConfig = { type: 'split', direction: 'row', children: [ {type: 'panel', id: 'data', defaultSize: '30%'}, {type: 'panel', id: 'main', defaultSize: '70%'}, ], }; ``` #### `splitPercentages` replaced by per-node sizing `splitPercentages` and `savedPercentages` on split nodes have been removed. Sizing is now specified on individual child nodes via `defaultSize`, `minSize`, `maxSize`, `collapsedSize`, and `collapsible`. ##### Before ```ts { type: 'split', direction: 'row', children: ['sidebar', 'main'], splitPercentages: [25, 75], } ``` ##### After ```ts { type: 'split', direction: 'row', children: [ {type: 'panel', id: 'sidebar', defaultSize: '25%', minSize: '150px'}, 'main', ], } ``` Size values accept CSS units (`'200px'`, `'25%'`, `'1rem'`). A plain string without a unit suffix is treated as a percentage. #### New `type: 'panel'` leaf node A new `panel` node type allows specifying sizing constraints on individual panels: ```ts { type: 'panel', id: 'sidebar', defaultSize: '25%', minSize: '150px', maxSize: '50%', collapsedSize: '0px', collapsible: true, } ``` Plain string keys (e.g. `'main'`) still work as leaf nodes without sizing constraints. #### Type renames All `MosaicLayout*` types have been renamed to `Layout*`. The old names are still exported as deprecated aliases. | Old name | New name | | ----------------------------- | ----------------------- | | `MosaicLayoutConfig` | `LayoutConfig` | | `MosaicLayoutNode` | `LayoutNode` | | `MosaicLayoutSplitNode` | `LayoutSplitNode` | | `MosaicLayoutTabsNode` | `LayoutTabsNode` | | `MosaicLayoutMosaicNode` | `LayoutMosaicNode` | | `MosaicLayoutParent` | `LayoutSplitNode` | | `MosaicLayoutDirection` | `LayoutDirection` | | `MosaicLayoutNodeKey` | `LayoutNodeKey` | | `isMosaicLayoutParent()` | `isLayoutSplitNode()` | | `isMosaicLayoutSplitNode()` | `isLayoutSplitNode()` | | `isMosaicLayoutTabsNode()` | `isLayoutTabsNode()` | | `isMosaicLayoutMosaicNode()` | `isLayoutMosaicNode()` | | `createDefaultMosaicLayout()` | `createDefaultLayout()` | | `DEFAULT_MOSAIC_LAYOUT` | *(removed)* | | `LayoutTypes` | *(removed)* | Deprecated helper renames in `@sqlrooms/layout`: | Old name | New name | | ------------------------------ | ------------------------- | | `makeMosaicStack` | `makeLayoutStack` | | `visitMosaicLeafNodes` | `visitLayoutLeafNodes` | | `getVisibleMosaicLayoutPanels` | `getVisibleLayoutPanels` | | `findMosaicNodePathByKey` | `findLayoutNodePathByKey` | | `removeMosaicNodeByKey` | `removeLayoutNodeByKey` | #### Panel `placement` is deprecated The `placement` property on panel info (`'sidebar'`, `'main'`, etc.) is deprecated and no longer used. Panel location is now determined entirely by the layout tree structure, not by a property on the panel definition. ##### Before ```ts panels: { data: {title: 'Data', component: DataPanel, placement: 'sidebar'}, } ``` ##### After ```ts panels: { data: {title: 'Data', component: DataPanel}, } ``` Panel location is controlled by the layout configuration structure (e.g., which `split`, `tabs`, or `panel` node references the panel key). #### New `LayoutRenderer` component `LayoutRenderer` is the new top-level renderer that handles all node types (`split`, `tabs`, `mosaic`, `panel`, and string leaves). `MosaicLayout` is still available for rendering mosaic-only sub-trees. #### Render callbacks API `createLayoutSlice` now accepts `renderPanel` and `renderTabStrip` callbacks for custom rendering: ```ts createLayoutSlice({ config: { /* ... */ }, panels: { /* ... */ }, renderPanel: (context) => { // Return custom JSX or undefined to use default }, renderTabStrip: (context) => { // Return custom tab strip JSX or undefined for default }, }); ``` #### Panel padding removed from `LeafLayoutPanel` (breaking) `LeafLayoutPanel` no longer applies `p-2` padding by default. Panel components must now add their own padding. ##### Before Panel content inherited `p-2` padding from `LeafLayoutPanel`: ```tsx export const MyPanel: RoomPanelComponent = () => { return
My content
; }; ``` ##### After Add `p-2` to your panel component: ```tsx export const MyPanel: RoomPanelComponent = () => { return
My content
; }; ``` #### Area-based panel management Named `tabs` nodes (with an `id`) act as areas with new management methods: ```ts state.layout.setActivePanel(areaId, panelId); state.layout.addPanelToArea(areaId, panelId); state.layout.removePanelFromArea(areaId, panelId); state.layout.setAreaCollapsed(areaId, collapsed); state.layout.toggleAreaCollapsed(areaId); state.layout.getAreaPanels(areaId); state.layout.getActivePanel(areaId); state.layout.isAreaCollapsed(areaId); ``` #### `react-mosaic-component` removed `react-mosaic-component` has been removed and replaced with `react-resizable-panels` for all layout rendering. The layout tree structure changed from a binary format (`first`/`second`) to an n-ary format (`children[]`). Binary tree layouts are migrated automatically via `z.preprocess`, but other formats require manual migration (see above). ## 0.28.0 ### Tailwind v3 to v4 Tailwind in SQLRooms is now upgraded from v3 to v4. For the full migration checklist and additional breaking changes, see the official Tailwind upgrade guide: . You can use the official migration tool directly in your repository: ```sh npx @tailwindcss/upgrade ``` #### Manual steps The main migration step is moving template/content discovery from `tailwind.config.js` into your global CSS using `@source` directives (see `examples/query/src/index.css` for a complete example). ##### Step 1 Move content paths from `tailwind.config.js` to global css `index.css`. Also, add `index.html` and pay attention to relative paths since `index.css` is usually located under `src/` folder while `tailwind.config.js` is in the root. ```css /* index.css */ @import 'tailwindcss'; @import '@sqlrooms/ui/tailwind-preset.css'; @source '../index.html'; @source './**/*.{ts,tsx}'; @source '../node_modules/@sqlrooms/*/dist/'; /* styles */ ``` ##### Step 2 Remove `tailwind.config.js` ##### Step 3 Remove `@layer base { ... }` from `index.css` Before: ```css /* index.css */ @layer base { :root { --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; /* ... */ } .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; /* ... */ } } ``` After: ```css /* index.css */ :root { --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; /* ... */ } .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; /* ... */ } ``` ##### Step 4: For Vite projects * Install `@tailwindcss/vite` and add it to your `vite.config.js` file, ```bash pnpm add -D @tailwindcss/vite ``` ```javascript // vite.config.js import {defineConfig} from 'vite'; import tailwindcss from '@tailwindcss/vite'; import react from '@vitejs/plugin-react'; // https://vite.dev/config/ export default defineConfig({ plugins: [react(), tailwindcss()], }); ``` * Remove `autoprefixer` and `postcss` * Remove `postcss.config.js` ##### Step 4: For NextJS projects Update `postcss.config.js` Before: ```javascript // postcss.config.js const config = { plugins: ['@tailwindcss/postcss'], }; export default config; ``` After: ```javascript // postcss.config.js const config = { plugins: { '@tailwindcss/postcss': {}, }, }; export default config; ``` ## 0.27.0-rc.0 ### @sqlrooms/mosaic * `useMosaic` hook removed: Use `MosaicSlice` and `useMosaicClient` instead The `useMosaic` hook has been replaced with a more robust slice-based architecture. You now need to: 1. Add `MosaicSlice` to your room store 2. Check connection status via the store 3. Use `useMosaicClient` for reactive data queries #### Before ```tsx import {useMosaic} from '@sqlrooms/mosaic'; function MyComponent() { const {isMosaicLoading, mosaicConnector} = useMosaic(); if (isMosaicLoading) { return
Loading...
; } // Use mosaicConnector directly // ... } ``` #### After **Step 1: Add MosaicSlice to your store** ```tsx import {createMosaicSlice, MosaicSliceState} from '@sqlrooms/mosaic'; import {createRoomStore, RoomShellSliceState} from '@sqlrooms/room-shell'; export type RoomState = RoomShellSliceState & MosaicSliceState; export const {roomStore, useRoomStore} = createRoomStore( (set, get, store) => ({ // ... other slices ...createMosaicSlice()(set, get, store), }), ); ``` **Step 2: Check connection status via store** ```tsx import {useRoomStore} from './store'; function MyComponent() { const mosaicConn = useRoomStore((state) => state.mosaic.connection); if (mosaicConn.status === 'loading') { return
Loading...
; } if (mosaicConn.status === 'error') { return
Error: {mosaicConn.error.message}
; } // Mosaic is ready when status === 'ready' // Access connector via mosaicConn.connector if needed } ``` **Step 3: Use `useMosaicClient` for reactive queries** ```tsx import {Query, useMosaicClient} from '@sqlrooms/mosaic'; function MapView() { const {data, isLoading, client} = useMosaicClient({ selectionName: 'brush', query: (filter: any) => { return Query.from('earthquakes') .select('Latitude', 'Longitude', 'Magnitude') .where(filter); }, }); if (isLoading) { return
Loading data...
; } // Use data for visualization return
Data loaded: {data?.numRows} rows
; } ``` For more details, see the [Mosaic API documentation](/api/mosaic/) and the [DeckGL + Mosaic example](https://github.com/sqlrooms/examples/tree/main/deckgl-mosaic). ### @sqlrooms/ai #### Per-session chat + analysis state AI chat state is now **scoped per session** (instead of a single global chat instance). This enables multiple sessions to stream concurrently without overwriting each other when you switch sessions. * **Removed global state**: `state.ai.prompt`, `state.ai.isRunning` (now per-session) * **Breaking method signature changes**: * `startAnalysis(sendMessage)` → `startAnalysis(sessionId)` * `cancelAnalysis()` → `cancelAnalysis(sessionId)` * **New per-session accessors**: * `getPrompt(sessionId)` / `setPrompt(sessionId, prompt)` * `getIsRunning(sessionId)` / `setIsRunning(sessionId, isRunning)` * **New hook**: `useSessionChat(sessionId)` for session-scoped chat (replaces legacy single-instance patterns) * **Lifecycle**: session chat execution is owned by the AI slice. Starting a run does not require a mounted React chat provider. #### Before ```tsx const prompt = useRoomStore((s) => s.ai.prompt); const isRunning = useRoomStore((s) => s.ai.isRunning); // startAnalysis used to take a sendMessage fn (global chat instance) await useRoomStore.getState().ai.startAnalysis(sendMessage); ``` #### After ```tsx const currentSession = useRoomStore((s) => s.ai.getCurrentSession()); const sessionId = currentSession?.id; const prompt = useRoomStore((s) => sessionId ? s.ai.getPrompt(sessionId) : '', ); const isRunning = useRoomStore((s) => sessionId ? s.ai.getIsRunning(sessionId) : false, ); if (sessionId) { await useRoomStore.getState().ai.startAnalysis(sessionId); } ``` #### Recommended UI composition Use `Chat.Root` once at the top of your AI UI tree to provide the compound chat presentation context: ```tsx import {Chat} from '@sqlrooms/ai'; export function MyAiPanel() { return ( ); } ``` ## 0.26.0-rc.5 * There's no combined config in the store anymore. We decided to split the config into individual slices' configs to avoid confusion and simplify the store typing. ``` state.config.title -> state.room.config.title state.config.dataSources -> state.room.config.dataSources state.config.sqlEditor -> state.sqlEditor.config state.config.layout -> state.layout.config ... ``` If you were saving the combined config, make sure to update the persistence logic (check out the examples). * createStore, createSlice now only have one generic type parameter * room.setRoomConfig removed, use .setConfig in all individual slices * RoomState renamed to BaseRoomStoreState (meant to be internal) and RoomStore interface renamed to BaseRoomStore to avoid confusion with RoomState/RoomStore introduced in many of the examples * room.onSaveConfig, hasUnsavedChanges, lastSavedConfig were removed. ## 0.25.0-rc.1 * createAiSlice init parameters changed: * Instead of customTools and toolsOptions use tools + createDefaultAiTools(store, toolsOptions) * getInstructions must be provided, but can use createDefaultAiInstructions(store) ## 0.24.28-rc.1 * Discuss config separated from RoomConfig to make it easier to persist separately and to simplify typing (`state.discuss.config` instead of `state.config.discuss`) ```tsx const discussConfig = useRoomStore((state) => state.discuss.config); ``` After: ```tsx const discussConfig = useRoomStore((state) => state.config.discuss); ``` If you were persisting this state, you will likely need a migration. You should also remove `.merge(DiscussSliceConfig)` when defining your `RoomConfig` ## 0.19.0 We are trying to make the package structure more logical, especially, for new users of the SQLRooms framework. Sorry for the more renaming. * Package `@sqlrooms/core` (previously, `@sqlrooms/project`) renamed to `@sqlrooms/room-store`. * The layout-related state and functions were moved to the new `LayoutSlice` added to `@sqlrooms/layout` which is namespaced as `layout`: * `panels` * `setLayout` * `togglePanel` * `tooglePanelPin` Before: ```tsx const togglePanel = useRoomStore((state) => state.room.togglePanel); ``` After: ```tsx const togglePanel = useRoomStore((state) => state.layout.togglePanel); ``` ## 0.18.0 `QueryHandle` returned from `.query()` is now implementing `PromiseLike` and can be awaited. So adding `.result`, which was introduced in [0.16.0](#_0-16-0), is not necessary anymore. ### Old ```tsx const result = await connector.query('SELECT * FROM some_table').result; ``` ### New ```tsx const result = await connector.query('SELECT * FROM some_table'); ``` ## 0.17.0 This release focuses on standardizing terminology across the codebase and improving the developer experience for new users. We are replacing the concept of "project" with "room" to better align with the SQLRooms name. "Room" is an established concept in collaborative apps and fits well with the overall vision of the project. ### Package name changes * `@sqlrooms/project` renamed to `@sqlrooms/core` (renamed again to `@sqlrooms/room-store` in [0.19.0](#_0-19-0), sorry) * `@sqlrooms/project-config` renamed to `@sqlrooms/room-config` * `@sqlrooms/project-builder` renamed to `@sqlrooms/room-shell` ### Component name changes * `ProjectBuilder` is replaced by `RoomShell` * `ProjectBuilderProvider` is removed (in favor of `RoomShell`) * `ProjectBuilderState` renamed to `RoomShellSliceState` * `createProjectBuilderStore` renamed to `createRoomStore` * `createProjectBuilderSlice` renamed to `createRoomShellSlice` * `ProjectBuilderPanel` renamed to `RoomPanel` * `ProjectBuilderPanelHeader` renamed to `RoomPanelHeader` #### Old way to set up a project ```tsx
``` #### New ```tsx ``` ### State name changes * `state.project` namespace renamed to `state.room` #### Old ```tsx const dataSources = useProjectStore((state) => state.project.dataSources); ``` #### New ```tsx const dataSources = useRoomStore((state) => state.room.dataSources); ``` ## 0.16.3 ### @sqlrooms/duckdb The `BaseDuckDbConnector` and `WasmDuckDbConnector` are now provided as factory functions rather than classes. Use `createWasmDuckDbConnector()` or the generic `createDuckDbConnector({type: 'wasm'})` to obtain a connector instance. #### Before ```typescript import {WasmDuckDbConnector} from '@sqlrooms/duckdb'; const connector = new WasmDuckDbConnector(); ``` #### After ```typescript import {createWasmDuckDbConnector} from '@sqlrooms/duckdb'; const connector = createWasmDuckDbConnector(); ``` ## 0.16.0 ### @sqlrooms/duckdb The DuckDbConnector now supports query cancellation through a unified `QueryHandle` interface with full composability support. All query methods (`execute`, `query`, `queryJson`) now return a `QueryHandle` that provides immediate access to cancellation functionality and signal composability. [Read more…](https://sqlrooms.org/query-cancellation) #### Old ```tsx const result = await connector.query('SELECT * FROM some_table'); ``` #### New ::: warning Since [0.18.0](#_0-18-0) `QueryHandle` returned from `.query()` is implementing `PromiseLike` and can be awaited. So adding `.result` is not necessary anymore. ::: ```tsx const result = await connector.query('SELECT * FROM some_table').result; ``` ## 0.14.0 ### @sqlrooms/ui * `sqlroomsTailwindPreset` prefix parameter was removed ## 0.9.0 ### @sqlrooms/project-builder * `createProjectSlice` renamed into `createProjectBuilderSlice` * `createProjectStore` renamed into `createProjectBuilderStore` * `ProjectState` renamed into `ProjectBuilderState` * `projectId` and `setProjectId` removed: add custom state if necessary * `INITIAL_BASE_PROJECT_STATE` renamed into `INITIAL_PROJECT_BUILDER_STATE` * A number of project store props and moved from `.project` to `.db`: * `.tables` * `.addTable` * `.getTable` * `.getTables` * `.getTableRowCount` * `.getTableSchema` * `.getTableSchemas` * `.checkTableExists` * `.dropTable` * `.createTableFromQuery` * `.setTableRowCount` * `.findTableByName` * `.refreshTableSchemas` * `useBaseProjectStore` was renamed into `useBaseProjectBuilderStore`, but it's better to use `useProjectStore` returned by `createProjectBuilderStore` instead * `processDroppedFile()` is removed: Use `ProjectStore.addProjectFile` directly. * `ProjectStore.replaceProjectFile` is removed: Use `ProjectStore.addProjectFile` instead. * `ProjectStore.addProjectFile` parameter changes: The function now takes a File or a pathname instead of the result of `processDroppedFile()`. * `ProjectStore.addProjectFile` behavior changes: The function will no longer attempt to create unique table names, but will overwrite the created table. * `ProjectStore.areViewsReadyToRender` and `onDataUpdated` were removed * `ProjectStore.setTables` removed: use `state.db.refreshTableSchemas()` instead. * `ProjectStore.isReadOnly` was removed: pass `isReadOnly` as a prop to respective components instead ### @sqlrooms/duckdb * `useDuckDb()` now returns an instance of [`DuckDbConnector`](api/duckdb/interfaces/DuckDbConnector) to enable support for external DuckDB * `getDuckDb` was removed: Use `useDuckDb()` instead * `getDuckTableSchemas` was removed: use `const getTableSchemas = useProjectStore(state => state.db.getTableSchemas)` * `exportToCsv` was removed: Use `useExportToCsv` instead ### @sqlrooms/mosaic * `getMosaicConnector` removed: Use `useMosaic` instead ### @sqlrooms/ai * `TOOLS` is not exported anymore: use `useProjectStore(state => state.ai.tools)` instead ## 0.8.0 ### @sqlrooms/project-builder * `project.config` moved to top level of `ProjectStore` This was done to simplify persistence. To migrate you need to pull it up in your slice creation code. Before: ```typescript const {projectStore, useProjectStore} = createProjectStore< RoomConfig, RoomState >( (set, get, store) => ({ ...createProjectSlice({ project: { config: { ... }, ... } }) }) ); ``` After: ```typescript const {projectStore, useProjectStore} = createProjectStore< RoomConfig, RoomState >( (set, get, store) => ({ ...createProjectSlice({ config: { ... }, project: { ... } }) }) ); ``` Check the [AI example store code](https://github.com/sqlrooms/examples/blob/main/ai/src/store.ts). ### @sqlrooms/ai * Model provider in `getApiKey` `getApiKey` property of `createAiSlice` now takes `modelProvider`: ```typescript ...createAiSlice({ getApiKey: (modelProvider: string) => { return get()?.apiKeys[modelProvider] || ''; }, })(set, get, store), ``` * Combining `useScrollToBottom` and `useScrollToBottomButton` `useScrollToBottom` is now combined with `useScrollToBottomButton`. `useScrollToBottom` now takes `dataToObserve`, `containerRef`, `endRef`. When the data changes, the hook will scroll to the bottom of the container. * Vega Chart Tool is now a custom tool The Vega Chart Tool is no longer included by default and must be explicitly provided as a custom tool to `createAiSlice`. You need to import it from `@sqlrooms/vega` and add it to the `customTools` object: ```typescript import {createVegaChartTool} from '@sqlrooms/vega'; ...createAiSlice({ getApiKey: (modelProvider: string) => { return get()?.apiKeys[modelProvider] || ''; }, // Add custom tools customTools: { // Add the VegaChart tool from the vega package chart: createVegaChartTool(), // Other custom tools... }, })(set, get, store), ``` This change allows for more flexibility in configuring the chart tool and reduces bundle size for applications that don't need chart functionality. --- --- url: https://sqlrooms.org/getting-started.md --- # Getting Started with SQLRooms SQLRooms is a powerful framework and a set of building blocks for creating DuckDB-backed analytics applications in React. This guide will help you integrate SQLRooms into your application. For a detailed overview of the framework's architecture and core ideas, check out the [Key Concepts](/key-concepts) and [Modular Architecture](/modular-architecture) pages. ## Try the Minimal Example The [Minimal Example](https://github.com/sqlrooms/examples/tree/main/minimal) is the quickest way to see SQLRooms in action with the smallest possible setup. It demonstrates loading a CSV data source and running SQL queries with `useSql()` in a barebones Vite + React app. To create a new project from the minimal example, run: ```bash npx giget gh:sqlrooms/examples/minimal my-minimal-app/ cd my-minimal-app npm install npm run dev ``` *** ## Try the Get Started Example The [Get Started Example](https://github.com/sqlrooms/examples/tree/main/get-started) is a more feature-rich starter template that demonstrates a typical SQLRooms application structure, including panels, layout, and configuration. To create a new project from the get-started example, run: ```bash npx giget gh:sqlrooms/examples/get-started myapp/ cd myapp npm install npm run dev ``` This Vite application demonstrates loading a CSV data source and running SQL queries with `useSql()`, along with a more complete app shell and layout. ## Manual Setup ### Prerequisites Your application should have the following dependencies: * [React 18+](https://react.dev/) (React 19 is supported) * [Tailwind CSS](https://tailwindcss.com/) * [Node.js](https://nodejs.org/) >= 22 SQLRooms uses [Zustand](https://zustand.docs.pmnd.rs) for state management and [Zod](https://zod.dev) for schema validation internally, but you don't need to install them separately ### Installation Install the required SQLRooms packages: ::: code-group ```bash [npm] npm install @sqlrooms/room-shell @sqlrooms/duckdb @sqlrooms/ui ``` ```bash [pnpm] pnpm add @sqlrooms/room-shell @sqlrooms/duckdb @sqlrooms/ui ``` ```bash [yarn] yarn add @sqlrooms/room-shell @sqlrooms/duckdb @sqlrooms/ui ``` ::: ### Configure Tailwind CSS You can follow [this guide](https://tailwindcss.com/docs/installation/using-vite) to install and configure Tailwind 4. ::: code-group ```bash [npm] npm install -D tailwindcss@4 ``` ```bash [pnpm] pnpm add -D tailwindcss@4 ``` ```bash [yarn] yarn add -D tailwindcss@4 ``` ::: SQLRooms provides a Tailwind preset that includes all the necessary styles. Make sure to import the preset Tailwind styles in your main CSS file: ```css @import '@sqlrooms/ui/tailwind-preset.css'; ``` ### Setting Up the Room Store 1. Define your application state type: ```typescript import { createRoomShellSlice, createRoomStore, RoomShellSliceState, } from '@sqlrooms/room-shell'; /** * The whole app state. */ export type RoomState = RoomShellSliceState & { // Add your custom app state types here // If using additional slices: // & SqlEditorSliceState }; ``` 2. Create your room store: ```typescript import {DatabaseIcon} from 'lucide-react'; import {MainView} from './components/MainView'; import {DataSourcesPanel} from './components/DataSourcesPanel'; /** * Create the room store. You can combine your custom state and logic * with the slices from the SQLRooms modules. */ export const {roomStore, useRoomStore} = createRoomStore( (set, get, store) => ({ ...createRoomShellSlice({ config: { title: 'My SQLRooms App', dataSources: [ { tableName: 'earthquakes', type: 'url', url: 'https://huggingface.co/datasets/sqlrooms/earthquakes/resolve/main/earthquakes.parquet', }, ], }, layout: { config: { type: 'split', direction: 'row', children: [ {type: 'panel', id: 'data-sources', defaultSize: '30%'}, 'main', ], }, panels: { 'data-sources': { title: 'Data Sources', icon: DatabaseIcon, component: DataSourcesPanel, }, main: { title: 'Main view', icon: () => null, component: MainView, }, }, }, })(set, get, store), // Add additional slices if needed // ...createSqlEditorSlice()(set, get, store), }), ); ``` 3. Optionally add persistence using the `persistSliceConfigs` helper: ```typescript import { BaseRoomConfig, LayoutConfig, persistSliceConfigs, } from '@sqlrooms/room-shell'; export const {roomStore, useRoomStore} = createRoomStore( persistSliceConfigs( { name: 'app-state-storage', sliceConfigSchemas: { room: BaseRoomConfig, layout: LayoutConfig, // Add other slice configs as needed // sqlEditor: SqlEditorSliceConfig, }, }, (set, get, store) => ({ // Store configuration as shown above ...createRoomShellSlice({ config: { title: 'My SQLRooms App', dataSources: [], }, layout: { config: { type: 'split', direction: 'row', children: [ {type: 'panel', id: 'data-sources', defaultSize: '30%'}, 'main', ], }, panels: { // Panel definitions }, }, })(set, get, store), }), ), ); ``` ### Using the Room Store Wrap your application with a `RoomShell` which provides the room store context: ```typescript import {RoomShell} from '@sqlrooms/room-shell'; import {ThemeProvider} from '@sqlrooms/ui'; import {roomStore} from './store'; export const Room = () => ( ); ``` Access the store in your components: ```typescript import {useRoomStore} from './store'; function YourComponent() { // Access config from room const roomConfig = useRoomStore((state) => state.room.config); // Access database state const tables = useRoomStore((state) => state.db.tables); // Check if a table is ready const tableReady = useRoomStore((state) => state.db.findTableByName('earthquakes'), ); return ( // Your component JSX ); } ``` ### Querying Data Use the `useSql` hook from `@sqlrooms/duckdb` to run SQL queries: ```typescript import {useSql} from '@sqlrooms/duckdb'; import {useRoomStore} from './store'; function MainView() { const tableReady = useRoomStore((state) => state.db.findTableByName('earthquakes'), ); const {data, isLoading, error} = useSql<{ count: number; maxMag: number; }>({ query: ` SELECT COUNT(*)::int AS count, max(Magnitude) AS maxMag FROM earthquakes `, enabled: Boolean(tableReady), }); if (isLoading) return
Loading...
; if (error) return
Error: {error.message}
; const row = data?.toArray()[0]; return (
Total records: {row?.count}
Max magnitude: {row?.maxMag}
); } ``` The `useSql` hook automatically re-runs queries when the database state changes and provides loading/error states out of the box. ## Need Help? * Start or join a discussion on [GitHub Discussions](https://github.com/sqlrooms/sqlrooms/discussions) * File an issue on [GitHub](https://github.com/sqlrooms/sqlrooms/issues) --- --- url: https://sqlrooms.org/state-management.md --- # State Management SQLRooms uses a slice-based architecture powered by [Zustand](http://zustand.docs.pmnd.rs/) for state management. This approach allows you to compose different functionality slices into a unified application state. ## Why Zustand? [Zustand](https://zustand.docs.pmnd.rs/) is a small, fast, and scalable state management solution for React applications. SQLRooms chose Zustand for several key reasons: * **Simplicity**: Zustand has a minimal API that's easy to learn and use, with no boilerplate code. * **Performance**: It uses the React concurrent renderer and only re-renders components when their specific slice of state changes. * **Flexibility**: Zustand works well with TypeScript, supports middleware, and can be used outside of React components. * **Composability**: The slices pattern allows for modular state management that scales with application complexity. Unlike other state management libraries, Zustand doesn't require providers or context wrappers, making it lightweight and straightforward to integrate into any component. ## Understanding Slices A [slice](https://zustand.docs.pmnd.rs/guides/slices-pattern) is a modular piece of state and associated actions that can be combined with other slices to form a complete application state. Feature packages which manage their own state typically provide a slice that can be integrated into your application store. ### How to Combine Slices Slices are combined in the store creation process. Here's an example from the AI example application: ```typescript import {AiSliceState} from '@sqlrooms/ai'; import {RoomShellSliceState} from '@sqlrooms/room-shell'; import {SqlEditorSliceState} from '@sqlrooms/sql-editor'; // Combining multiple slices into a unified application state type export type RoomState = RoomShellSliceState & AiSliceState & SqlEditorSliceState & { // Custom application state types }; // Creating a store with multiple slices export const {roomStore, useRoomStore} = createRoomStore( (set, get, store) => ({ // Base room state ...createRoomShellSlice({ config: { // Room configuration }, layout: { config: { // Layout configuration }, panels: { // Panel definitions }, }, })(set, get, store), // SQL editor slice ...createSqlEditorSlice()(set, get, store), // AI slice with custom configuration ...createAiSlice({ // AI slice configuration })(set, get, store), // Custom application state // ... }), ); ``` This approach allows you to: 1. Include only the slices you need 2. Customize each slice with your own configuration 3. Extend slices with additional functionality 4. Create custom slices for application-specific features ### How to Access Store Data Once you've combined slices into a unified store, you can access different parts of the store using selectors. Here's an example: ```typescript // Import the store hook (returned from `createRoomStore`) import {useRoomStore} from '../store'; export const MyCustomView: React.FC = () => { // Access room slice data const isDataAvailable = useRoomStore((state) => state.room.isDataAvailable); // Access AI slice config (persistable state) const currentSessionId = useRoomStore((s) => s.ai.config.currentSessionId); // Access custom app state const apiKey = useRoomStore((s) => s.apiKey); // Access actions from custom app state const setApiKey = useRoomStore((s) => s.setApiKey); // Rest of component... }; ``` Each selector function receives the entire store state and returns only the specific piece of data needed, which helps optimize rendering performance by preventing unnecessary re-renders. ### Defining Configuration Types with Zod SQLRooms uses [Zod](https://zod.dev/) for runtime type validation. When combining slices, you'll often need to combine their configuration types as well. The `.merge` method from Zod makes this process straightforward. Here's an example from the AI example application showing how to combine configuration types: ```typescript import {BaseRoomConfig} from '@sqlrooms/room-config'; import {SqlEditorSliceConfig} from '@sqlrooms/sql-editor'; import {z} from 'zod'; /** * Room config for saving - combining multiple slice configs */ export const RoomConfig = BaseRoomConfig.merge(SqlEditorSliceConfig).merge( z.object({ // Custom app config }), ); export type RoomConfig = z.infer; ``` This approach offers several benefits: 1. **Type Safety**: The combined type is fully type-safe, with TypeScript inferring the correct type from the Zod schema. 2. **Runtime Validation**: The schema can validate data at runtime, ensuring configuration objects match the expected structure. 3. **Modularity**: Each slice provides its own configuration schema that can be combined with others. 4. **Documentation**: The schema serves as self-documenting code, clearly showing what configuration options are available. When using the combined configuration type in your store, you can ensure that all required configuration properties from each slice are properly included: ```typescript // Using the combined RoomConfig in the store ...createRoomShellSlice({ config: { // Base room slice config }, layout: { config: { // Layout configuration }, panels: { // Panel definitions }, }, })(set, get, store) ``` This pattern ensures that your application's configuration is both type-safe at compile time and validated at runtime. --- --- url: https://sqlrooms.org/layout.md --- # Layout SQLRooms layouts compose React panels into resizable splits, tabs, dashboard grids, and docking workspaces. The layout tree lives in `layout.config`; the React components and their titles live in the `layout.panels` registry. Explore the [layout example](https://github.com/sqlrooms/examples/tree/main/layout) for a complete app with collapsible sidebars, custom tab strips, and dynamically created dock and grid dashboards. In the SQLRooms repository, run it with: ```sh pnpm install pnpm build pnpm dev layout-example ``` ## Choose a layout | Node | Use it for | | ------- | -------------------------------------------------------------- | | `panel` | One React surface, such as a chart, editor, or document | | `split` | Side-by-side (`row`) or stacked (`column`) resizable children | | `tabs` | Switching between panels or entire nested layouts | | `grid` | A scrollable dashboard with draggable, resizable tiles | | `dock` | A workspace where users rearrange panels into resizable splits | Each container can contain other layout nodes. For example, a split can hold a sidebar and a tabs node whose children are a grid and a dock workspace. Documents are content rendered inside panels; there is no `doc` layout node. ## Register panels and render a split This example assumes an app with SQLRooms styling already configured, as in [Getting Started](/getting-started). `createRoomShellSlice()` includes the layout slice, and `RoomShell.LayoutComposer` connects rendering and layout changes to the store. ```tsx import { LeafLayout, type LayoutConfig, type RoomPanelComponent, } from '@sqlrooms/layout'; import { createRoomShellSlice, createRoomStore, RoomShell, type RoomShellSliceState, } from '@sqlrooms/room-shell'; const DataPanel: RoomPanelComponent = () => (
Data sources
); const ChartPanel: RoomPanelComponent = ({meta, panelInfo}) => ( <> {panelInfo.title}
Chart: {String(meta?.chartId ?? 'overview')}
); const splitLayout = { type: 'split', id: 'workspace', direction: 'row', children: [ { type: 'panel', id: 'data-panel', panel: 'data', defaultSize: '25%', minSize: '200px', }, { type: 'panel', id: 'overview-chart', panel: {key: 'chart', meta: {chartId: 'overview'}}, defaultSize: '75%', }, ], } satisfies LayoutConfig; export const {roomStore, useRoomStore} = createRoomStore( (set, get, store) => ({ ...createRoomShellSlice({ layout: { config: splitLayout, panels: { data: {title: 'Data', component: DataPanel}, chart: ({meta}) => ({ title: `Chart: ${meta?.chartId ?? 'overview'}`, component: ChartPanel, }), }, }, })(set, get, store), }), ); export function App() { return ( ); } ``` Give object nodes stable, unique `id` values. A panel's `id` identifies its position in the layout tree; `panel` selects a registry entry. Using `panel: {key, meta}` lets multiple instances share a renderer while receiving different metadata. The `panelId` prop is the registry key, not the node ID. A string leaf such as `'data'` is shorthand that uses the same value for both. An object panel node needs an explicit `panel` to render its component. Use percentage strings for proportional split sizes and pixel strings for constraints such as `minSize`. Give the composer a parent with a defined height. `LeafLayout.Header` shows the header in dock/grid contexts, and `LeafLayout.DragHandle` supplies the drag target for those layouts. The following configurations reuse this panel registry. To try one, pass it as `layout.config` at initialization or call `roomStore.getState().layout.setConfig(nextLayout)`. ## Tabs and collapsible areas Use a named tabs node for an area that users can switch, close, or collapse. Its `children` can be panels or nested layouts, and `activeTabIndex` selects the initial tab. ```ts const tabsLayout = { type: 'split', id: 'tabbed-workspace', direction: 'row', children: [ { type: 'tabs', id: 'sidebar', children: ['data'], activeTabIndex: 0, defaultSize: '25%', minSize: '200px', collapsible: true, collapsedSize: 0, }, { type: 'tabs', id: 'charts', activeTabIndex: 0, children: [ { type: 'panel', id: 'sales-chart', panel: {key: 'chart', meta: {chartId: 'sales'}}, }, { type: 'panel', id: 'traffic-chart', panel: {key: 'chart', meta: {chartId: 'traffic'}}, }, ], }, ], } satisfies LayoutConfig; ``` Use the tabs node ID and child node ID when changing the active panel: ```ts const layout = roomStore.getState().layout; layout.setActiveTab('charts', 'traffic-chart'); layout.setCollapsed('sidebar', true); layout.addTab('charts', { type: 'panel', id: 'retention-chart', panel: {key: 'chart', meta: {chartId: 'retention'}}, }); layout.removeTab('charts', 'sales-chart'); // Close; it can be reopened with addTab. ``` ### Customize a tab strip Register a component such as `chartTabs: {component: ChartTabs}`, then set `panel: 'chartTabs'` on the `charts` tabs node above. Compose the built-in tab parts inside that component to retain the layout context and store actions: ```tsx import {TabsLayout} from '@sqlrooms/layout'; function ChartTabs() { return ( <> ); } ``` `forceMount` keeps inactive visible tabs mounted, preserving component state and setup work at the cost of keeping those components in memory. Omit it when that is unnecessary. Wire `TabsLayout.NewButton` to the composer's `onTabCreate` callback as shown below. ## Add and remove panels at runtime Runtime changes have two parts: the **panel registry** defines what can render, and the **layout tree** defines which instances appear and where. Registering a renderer does not insert a node; removing a node does not unregister its renderer or delete the feature state behind it. | Action | Effect | | -------------------------------- | ------------------------------------------------ | | `registerPanel(key, definition)` | Add or replace a renderer in the registry | | `addTab(tabsId, node)` | Insert a new child and activate it | | `removeTab(tabsId, nodeId)` | Close a tab, retaining its node so it can reopen | | `deleteTab(tabsId, nodeId)` | Remove a tab's node from the layout tree | | `unregisterPanel(key)` | Remove a renderer from the registry | ### Create chart tabs from a shared renderer Keep one `chart` registry entry and create a unique layout node for each chart. This reuses `ChartPanel` and its metadata-driven title from the first example; there is no need to register a component for every chart instance. With `tabsLayout` as the store's initial config and the `chartTabs` component registered as above, connect the tab strip's new button to a creation handler: ```tsx function addChartTab(tabsId: string = 'charts') { const chartId = crypto.randomUUID(); const nodeId = `chart-${chartId}`; // If charts have their own store slice, create the backing chart here first. roomStore.getState().layout.addTab(tabsId, { type: 'panel', id: nodeId, panel: {key: 'chart', meta: {chartId}}, }); return nodeId; } function DynamicTabsApp() { return ( ); } ``` The callback receives the ID of the tabs container whose button was clicked. That container must already exist. If different tab areas create different content, dispatch to the appropriate creation handler using that ID. Use the returned **node ID**, rather than the shared `'chart'` registry key, for close, reopen, and delete actions: ```ts const newChartNodeId = addChartTab('charts'); const tabActions = roomStore.getState().layout; tabActions.removeTab('charts', newChartNodeId); // Close, keeping the node. tabActions.addTab('charts', newChartNodeId); // Reopen and activate that node. tabActions.deleteTab('charts', newChartNodeId); // Permanently remove the node. ``` These calls illustrate separate user actions. `addTab` with an existing child ID reopens it without duplicating it. After `deleteTab`, recreating a chart tab requires the full node, including its `panel` metadata. Closing a tab can unmount its component even with `forceMount`; keep chart configuration in a feature store if it must survive closing and reopening. Deleting a layout node does not delete chart data. Apply feature-specific cleanup through the chart or artifact API when the user intends to delete that content, and account for other views that might share it. Keep the shared `chart` renderer registered while any chart nodes still use it. ### Register a new panel type on demand Use `registerPanel` when the renderer itself becomes available at runtime, such as when loading an optional feature. Then add a node referencing that key: ```tsx const panelActions = roomStore.getState().layout; panelActions.registerPanel('session-info', { title: 'Session info', component: () =>
Session details
, }); panelActions.addTab('charts', 'session-info'); // When unloading this feature, remove its layout references before its renderer. panelActions.deleteTab('charts', 'session-info'); panelActions.unregisterPanel('session-info'); ``` For multiple instances, use distinct node IDs with the same `panel` key and remove every reference, including closed tabs, before unregistering that key. Unregistering alone leaves the layout nodes in place without their renderer. ## Grid dashboards A grid positions its children using column and row units. Each `layouts` entry describes a breakpoint, and each item's `i` must match a child node's `id`. This example puts charts side by side on wide screens and stacks them on small screens: ```ts const gridLayout = { type: 'grid', id: 'dashboard', breakpoints: {lg: 768, sm: 0}, cols: {lg: 12, sm: 6}, rowHeight: 160, margin: [12, 12], compactType: 'vertical', resizeHandles: ['e', 's', 'se'], children: [ { type: 'panel', id: 'sales-chart', panel: {key: 'chart', meta: {chartId: 'sales'}}, }, { type: 'panel', id: 'traffic-chart', panel: {key: 'chart', meta: {chartId: 'traffic'}}, }, ], layouts: { lg: [ {i: 'sales-chart', x: 0, y: 0, w: 6, h: 2}, {i: 'traffic-chart', x: 6, y: 0, w: 6, h: 2}, ], sm: [ {i: 'sales-chart', x: 0, y: 0, w: 6, h: 2}, {i: 'traffic-chart', x: 0, y: 2, w: 6, h: 2}, ], }, } satisfies LayoutConfig; ``` `rowHeight` is in pixels; `w` and `h` are column and row counts. The renderer provides scrolling and writes drag/resize changes back through the composer. Omit `layouts` to use automatically generated initial positions. When adding children to a grid with explicit positions, update the corresponding breakpoint layouts too. For chart dashboards with built-in add/remove operations, see [adding and removing Mosaic dashboard panels](/api/mosaic/#add-and-remove-chart-panels). For a custom grid, the [layout example](https://github.com/sqlrooms/examples/blob/main/layout/src/store.tsx) shows how to update children and saved positions through `layout.setConfig()`. ## Docking workspaces Wrap a layout tree in a `dock` node to let users rearrange its panels by dragging their handles to the left, right, top, or bottom of another panel. These drops create or update splits within the dock. A dock stores its nested layout in `root`, rather than `children`: ```ts const dockLayout = { type: 'dock', id: 'analysis-dock', root: { type: 'split', id: 'analysis-split', direction: 'row', children: [ { type: 'panel', id: 'sales-chart', panel: {key: 'chart', meta: {chartId: 'sales'}}, defaultSize: '50%', }, { type: 'panel', id: 'traffic-chart', panel: {key: 'chart', meta: {chartId: 'traffic'}}, defaultSize: '50%', }, ], }, } satisfies LayoutConfig; ``` `RoomShell.LayoutComposer` supplies the drag-and-drop infrastructure. Reuse the `ChartPanel` with its `LeafLayout.DragHandle` from the first example to make these panels draggable. ## Documents inside layouts A document can occupy a panel, a tab, or a dashboard tile just like any other React surface. For a document workspace, use `ArtifactTabs` and `createArtifactPanelDefinition()` to connect layout panels to artifact IDs, then render `BlockDocumentArtifact` inside the document panel. See [Blocks and Block Documents](/blocks-and-documents#set-up-a-block-document-artifact) for a complete store and panel example, including document state and embedded stateful blocks. See [Artifacts](/artifacts) for top-level workspace tabs and artifact lifecycle. ## Save and restore layouts Layout interactions update `roomStore.getState().layout.config`. To preserve them across reloads, include `layout: LayoutConfig` from `@sqlrooms/layout` in your persistence `sliceConfigSchemas`. Persist the config tree and serializable metadata, and register React components again when creating the store. See [Persistence](/persistence) for storage setup. For lower-level integration without `RoomShell`, use `createLayoutSlice()` and `LayoutRenderer` from the [Layout API reference](/api/layout/). --- --- url: https://sqlrooms.org/persistence.md --- # Persistence SQLRooms persistence is designed for local-first analytics apps where the host application owns durable storage. The storage might be a DuckDB table, a project file, IndexedDB, a server endpoint, or another workspace-specific location. The persistence API separates three concerns: * What part of room state is durable. * How durable snapshots are loaded, saved, and removed. * When a changed snapshot should be treated as dirty and flushed. SQLRooms room state is built from composable slices on top of a [Zustand room store](/state-management#why-zustand). Persistence uses the same store and slice model: slice configs define the durable shape, and the persistence helpers connect that shape to host-owned storage. Most apps should start with `createRoomStorePersistence()`. It composes the lower-level controller with [Zustand persist middleware](https://zustand.docs.pmnd.rs/reference/integrations/persisting-store-data) storage and room-store subscription glue. ## API Layers ```mermaid %%{init: {"themeCSS": ".nodeLabel p { margin: 0; line-height: 1.2; }"}}%% flowchart TD Store["Zustand room store"] Helpers["createPersistHelpers()\npartialize + merge"] RoomPersistence["createRoomStorePersistence()\nstorage + controller"] Controller["createPersistenceController()\ndirty state + autosave + flush"] Adapter["Host adapter\nload/save/remove"] Durable["Durable storage\nfiles, DBs, or servers"] Store --> Helpers Helpers --> RoomPersistence RoomPersistence --> Controller Controller --> Adapter Adapter --> Durable ``` Use the layers this way: * `createPersistHelpers()` maps slice config schemas to a typed persisted state shape and merges persisted configs back into runtime state. * `createRoomStorePersistence()` is the usual app integration point. It exposes `storage` for Zustand persist, save status through `controller`, and flush helpers for close or navigation events. * `createPersistenceController()` is the storage-agnostic save policy. Use it directly only when you are not integrating a Zustand room store. ## Recommended Room Store Setup For a normal SQLRooms app, define the persistable slice configs, create helpers, then pass the same partialization function to both Zustand persist and the persistence helper. ```ts import { createPersistHelpers, createRoomStore, createRoomStorePersistence, persistSliceConfigs, } from '@sqlrooms/room-store'; import {BaseRoomConfig} from '@sqlrooms/room-config'; import {LayoutConfig} from '@sqlrooms/layout-config'; const sliceConfigSchemas = { room: BaseRoomConfig, layout: LayoutConfig, } as const; const persistHelpers = createPersistHelpers(sliceConfigSchemas); const persistence = createRoomStorePersistence({ partialize: persistHelpers.partialize, autosaveDelayMs: 300, load: async () => loadWorkspaceState(), save: async (snapshot, metadata) => { await saveWorkspaceState(snapshot, metadata?.reason); }, }); export const {roomStore, useRoomStore} = createRoomStore( persistSliceConfigs( { name: 'workspace-state', sliceConfigSchemas, storage: persistence.storage, partialize: persistence.partialize, merge: persistHelpers.merge, onRehydrateStorage: persistence.onRehydrateStorage, }, (set, get, store) => ({ // Compose room slices here. }), ), ); ``` The important invariant is that `storage` receives the already-partialized persisted state shape. That is why `createRoomStorePersistence()` returns `PersistStorage`, not `PersistStorage`. ## Hydration Flow When using Zustand persist, hydration has two phases: the durable snapshot is loaded by SQLRooms persistence, then Zustand merges the persisted state into the runtime store. ```mermaid sequenceDiagram participant App participant Zustand as Zustand persist participant Storage as persistence.storage participant Controller as PersistenceController participant Durable as Durable storage App->>Zustand: create room store Zustand->>Storage: getItem(name) Storage->>Controller: hydrate() Controller->>Durable: load() Durable-->>Controller: snapshot or null Controller-->>Storage: clean snapshot Storage-->>Zustand: { state, version } Zustand->>App: merge persisted state Zustand->>Storage: onRehydrateStorage(state) Storage->>Controller: markSnapshotSaved(partialize(state)) ``` `onRehydrateStorage()` matters because the runtime state after merge might not be byte-for-byte identical to the loaded snapshot. Defaults, migrations, and schema normalization can all change the runtime shape. Marking the post-merge state as saved prevents the app from treating hydration as a user edit. ## Save Flow Zustand persist calls `storage.setItem()` when its persisted state changes. The storage adapter does not write immediately. It hands the serialized snapshot to the controller, which marks it dirty and schedules a save when autosave is enabled. ```mermaid sequenceDiagram participant Store as Room store participant Zustand as Zustand persist participant Storage as persistence.storage participant Controller as PersistenceController participant Durable as Durable storage Store->>Zustand: state changed Zustand->>Storage: setItem(name, { state: persisted }) Storage->>Controller: setSnapshot(serialized, "setItem") Controller->>Controller: dirty = true Controller->>Controller: debounce autosave Controller->>Durable: save(snapshot, { reason: "autosave" }) Durable-->>Controller: saved Controller->>Controller: dirty = false ``` Call `persistence.flush('final-flush')` before unload, close, project switch, or any other operation where pending state must be durable before continuing. ## Direct Store Subscription Some hosts want persistence to observe the room store directly rather than only responding to Zustand persist storage calls. Pass `store` when creating persistence, or call `bindStore()` later. ```ts const persistence = createRoomStorePersistence({ store: roomStore, partialize: persistHelpers.partialize, autosaveDelayMs: 300, load, save, }); ``` Direct binding is useful when: * You are not using Zustand persist middleware. * You need explicit control over initial binding. * You want `shouldPersistChange` guards for host lifecycle states. ### Initial Binding By default, an initially-bound store snapshot is treated as already saved: ```ts createRoomStorePersistence({ store, partialize, load, save, markInitialSnapshotSaved: true, }); ``` Set `markInitialSnapshotSaved: false` when the initial state should be persisted as a new durable snapshot. If persistence is created before `store.getState()` returns the completed initial state, pass `initialState`: ```ts const persistence = createRoomStorePersistence({ store, initialState, partialize, load, save, }); ``` ### Skipping Changes Use `shouldPersistChange` when a store change should update the last observed snapshot but should not be saved. ```ts const persistence = createRoomStorePersistence({ store, partialize, load, save, shouldPersistChange: (state) => state.room.initialized, }); ``` This is useful during startup, teardown, restore flows, or failed initialization states. ## Snapshot Equivalence The controller compares snapshots by strict equality by default. This is ideal for JSON string snapshots. For structured snapshots, provide either `compareSnapshots` or `getSnapshotRevision`. ```ts createRoomStorePersistence({ partialize, load, save, serialize: (state) => ({ revision: state.revision, payload: state, }), deserialize: (snapshot) => snapshot.payload, getSnapshotRevision: (snapshot) => snapshot.revision, }); ``` Use `compareSnapshots` when revision equality is not enough: ```ts createRoomStorePersistence({ partialize, load, save, compareSnapshots: (next, previous) => next.contentHash === previous.contentHash && next.layoutHash === previous.layoutHash, }); ``` ## Low-Level Controller Use `createPersistenceController()` directly when you need persistence policy without Zustand. ```ts import {createPersistenceController} from '@sqlrooms/room-store'; const controller = createPersistenceController({ autosaveDelayMs: 300, getSnapshot: () => serializeCurrentWorkspace(), adapter: { load: () => loadWorkspaceSnapshot(), save: (snapshot, metadata) => saveWorkspaceSnapshot(snapshot, metadata?.reason), }, }); const snapshot = await controller.hydrate(); restoreWorkspace(snapshot); controller.markSnapshotSaved(serializeCurrentWorkspace()); controller.markDirty('manual'); await controller.flush('final-flush'); ``` The controller owns: * `hydrating`, `dirty`, `saving`, and `pendingSave` state. * Autosave scheduling. * Final flush behavior. * Coalescing in-flight saves so the latest snapshot wins. * Error reporting through `controller.getState().error` and subscribers. It does not own: * Which fields are durable. * How snapshots are serialized. * Where snapshots are stored. * How loaded snapshots are merged into runtime state. ## Remove Flow Remove flow is for deleting or clearing the durable workspace snapshot, not for ordinary edits. It is relevant for workflows such as deleting a project, resetting a saved workspace, clearing local app data, or replacing a workspace with a freshly imported one. If you pass `remove`, `persistence.storage.removeItem(name)` first flushes pending state so the host does not lose an in-flight save. It then pauses dirty tracking, calls your remove adapter, and marks the saved snapshot as `null`. ```ts const persistence = createRoomStorePersistence({ partialize, load, save, remove: async (name) => { await deleteWorkspaceState(name); }, }); ``` Without a `remove` option, `removeItem()` throws. This makes accidental durable deletes explicit. ## Practical Checklist When integrating persistence in an app: 1. Decide the durable state shape. Prefer persisted slice configs first. 2. Define Zod schemas for persisted slice configs. 3. Use `createPersistHelpers()` for `partialize` and `merge`. 4. Use `createRoomStorePersistence()` for storage, dirty tracking, autosave, and final flush. 5. Pass `persistence.storage`, `persistence.partialize`, and `persistence.onRehydrateStorage` to Zustand persist. 6. Register `persistence.flush('final-flush')` for unload, close, or project switch. 7. Use `createPersistenceController()` directly only for non-Zustand persistence flows. --- --- url: https://sqlrooms.org/artifacts.md --- # Artifacts Artifacts are durable, top-level entries in a SQLRooms workspace. A dashboard, notebook, canvas, pivot table, or document can all be represented as artifacts and opened through the same workspace navigation. The artifact registry owns workspace metadata and navigation state. Feature packages continue to own their domain state: | Artifact registry | Feature slice | | ----------------------------------------- | ---------------------------------------------------------------- | | ID, type, and title | Queries, cells, charts, document content, and other domain state | | Order, pinning, and current selection | Feature-specific runtime status | | Open, close, rename, and delete lifecycle | Create, ensure, rename, close, and cleanup behavior | This separation keeps artifact APIs small while allowing feature state to be used in other hosts. For example, the same pivot definition can be a top-level artifact or an embedded block in a document. ## Define artifact types An artifact type connects workspace actions to a feature's component and lifecycle. Keep these definitions in runtime configuration; only artifact metadata is persisted. ```tsx import { ArtifactTabs, ArtifactsSliceConfig, createArtifactPanelDefinition, createArtifactsSlice, defineArtifactTypes, useArtifactWorkspace, type ArtifactTypeDefinition, type ArtifactsSliceState, } from '@sqlrooms/artifacts'; import { PivotSliceConfig, PivotView, createPivotSlice, type PivotSliceState, } from '@sqlrooms/pivot'; import type {RoomPanelComponent} from '@sqlrooms/layout'; import { createRoomShellSlice, createRoomStore, persistSliceConfigs, type RoomShellSliceState, } from '@sqlrooms/room-shell'; import {TablePropertiesIcon} from 'lucide-react'; type RoomState = RoomShellSliceState & ArtifactsSliceState & PivotSliceState; const PivotArtifactPanel: RoomPanelComponent = ({panelId, meta}) => { const artifactId = typeof meta?.artifactId === 'string' ? meta.artifactId : panelId; if (!artifactId) return null; return ; }; const PivotWorkspacePanel: RoomPanelComponent = () => ( ); const artifactTypes = defineArtifactTypes({ pivot: { label: 'Pivot table', defaultTitle: 'Pivot table', icon: TablePropertiesIcon, component: PivotArtifactPanel, onCreate: ({artifactId, artifact, store}) => { store.getState().pivot.ensurePivot(artifactId, { title: artifact.title, }); }, onEnsure: ({artifactId, artifact, store}) => { store.getState().pivot.ensurePivot(artifactId, { title: artifact.title, }); }, onRename: ({artifactId, artifact, store}) => { store.getState().pivot.renamePivot(artifactId, artifact.title); }, onDelete: ({artifactId, store}) => { store.getState().pivot.removePivot(artifactId); }, }, } satisfies Record<'pivot', ArtifactTypeDefinition>); export const {roomStore} = createRoomStore( persistSliceConfigs( { name: 'my-workspace', sliceConfigSchemas: { artifacts: ArtifactsSliceConfig, pivot: PivotSliceConfig, }, }, (set, get, store) => ({ ...createRoomShellSlice({ layout: { config: { id: 'workspace', type: 'tabs', panel: 'workspace', children: [], activeTabIndex: 0, }, panels: { workspace: { title: 'Pivots', icon: TablePropertiesIcon, component: PivotWorkspacePanel, }, artifact: createArtifactPanelDefinition(artifactTypes, store), }, }, })(set, get, store), ...createArtifactsSlice({artifactTypes})(set, get, store), ...createPivotSlice()(set, get, store), }), ), ); ``` Use the lifecycle callbacks consistently: * `onCreate` initializes backing state for a new registry entry. * `onEnsure` repairs or initializes backing state while restoring a known artifact. * `onRename` mirrors a workspace title only when the feature owns a related display name. * `onClose` releases temporary runtime resources without deleting durable state. * `onDelete` removes feature-owned durable state. `closeArtifact()` is intentionally non-destructive. The tab adapter removes the artifact from the active layout while leaving it available to reopen. `deleteArtifact()` runs close and delete lifecycle hooks, then removes the registry entry. ## Persist the registry Add `ArtifactsSliceConfig` to the room's persisted slice schemas. The config contains only serializable workspace state. The setup above persists both `artifacts` and `pivot`, so every registry entry can restore its backing state. Persist the feature slice alongside the registry. Persisting an artifact entry without its backing state can restore a tab that has nothing to render. See [Persistence](/persistence) for storage adapters, hydration, and autosave. ## Render an artifact workspace `ArtifactTabs` is the standard layout adapter. Its compound API keeps custom controls and tab content on one shared workspace model. It must render inside a layout node with `type: 'tabs'`; the setup above mounts `PivotWorkspacePanel` as the fallback panel for the `workspace` tabs node. `ArtifactTabs` then adds and removes artifact panel children in that container. For a sidebar, home screen, or other surface that does not use layout tabs, use `useArtifactWorkspace()` directly: ```tsx const workspace = useArtifactWorkspace({ types: ['pivot', 'dashboard'], }); return workspace.selectedArtifact ? ( ) : ( ); ``` ## Reuse stateful blocks as artifacts Feature packages can expose a `StatefulBlockDefinition` from `@sqlrooms/blocks`. Wrap that definition when the same feature should also be available as a top-level artifact: ```ts import {createArtifactTypeFromStatefulBlock} from '@sqlrooms/artifacts'; import {createPivotBlockDefinition} from '@sqlrooms/pivot'; const artifactTypes = defineArtifactTypes({ pivot: createArtifactTypeFromStatefulBlock(createPivotBlockDefinition()), }); ``` The artifact still owns the workspace title, selection, and navigation. The stateful block owns rendering and feature-state lifecycle. This is preferable to creating separate artifact-only and embedded implementations. ## Server and tooling entry points Use the smallest package entry point that fits the caller: * `@sqlrooms/artifacts` includes the store slice and React helpers. * `@sqlrooms/artifacts/config` contains serializable schemas without React. * `@sqlrooms/artifacts/ai` contains artifact context tools and optional artifact-owned AI sessions. See the [`@sqlrooms/artifacts` API reference](/api/artifacts/) for the complete API, [Blocks and Block Documents](/blocks-and-documents) for embedding feature state inside structured documents, and [Commands](/commands) for exposing artifact actions across UI and agent surfaces. --- --- url: https://sqlrooms.org/blocks-and-documents.md --- # Blocks and Block Documents For arranging document panels alongside other workspace content, see the [Layout guide](/layout), with examples of panels, tabs, grids, and docking. Blocks are composable units of workspace content or behavior. Block documents are ordered, rich-text containers that mix ordinary content with interactive SQLRooms features such as charts, pivots, and dashboards. Use the layers according to the job they own: | Layer | Responsibility | Package | | -------------- | ----------------------------------------------------------------------- | --------------------- | | Artifact | Top-level workspace identity, title, selection, and navigation | `@sqlrooms/artifacts` | | Block contract | Portable identity, attributes, references, and ownership | `@sqlrooms/blocks` | | Feature state | Queries, dashboards, pivots, maps, or other interactive state | The feature package | | Block document | Ordered Tiptap content, editor UI, mutations, commands, and AI adapters | `@sqlrooms/documents` | A block document is usually itself a top-level artifact. Its embedded stateful blocks are not child artifacts: they refer directly to feature-owned state. ## Document families and canonical names `@sqlrooms/documents` provides two document families: | Family | Artifact type | Store key | Command IDs | | ------------------------- | ------------------- | ------------------- | --------------------- | | Structured block document | `block-document` | `blockDocuments` | `block-document.*` | | Markdown document | `markdown-document` | `markdownDocuments` | `markdown-document.*` | For Markdown content, compose `createMarkdownDocumentsSlice()` and persist `MarkdownDocumentsSliceConfig` under `markdownDocuments`. Use `createMarkdownDocumentBlockDefinition()` for an embeddable Markdown document and `createMarkdownDocumentCommands()` for its commands. Embedded Markdown blocks also use the `markdown-document` type. The [documents package README](https://github.com/sqlrooms/sqlrooms/tree/main/packages/documents#usage) shows both families composed together. Artifact registry labels remain customizable. For example, a UI can call a `block-document` artifact “Document” while the shared commands and AI instructions consistently use “block document”. The command factories do not accept artifact type, label, or command namespace overrides. ## Block kinds `@sqlrooms/documents` supports text and interactive block DTOs that map to its canonical Tiptap/ProseMirror JSON: * headings, paragraphs, lists, and todos * images and chart images backed by document assets * standalone `chart` blocks for a focused chart bound to a table * `statefulBlock` blocks for feature-owned surfaces such as a dashboard or pivot table Use a standalone chart for one visualization with document-local context. Use a stateful block when the feature has its own substantial configuration, runtime, or lifecycle. ## Set up a block document artifact Compose both artifact and block-document state, and persist both configs: ```tsx import { ArtifactTabs, ArtifactsSliceConfig, createArtifactPanelDefinition, createArtifactsSlice, defineArtifactTypes, type ArtifactTypeDefinition, type ArtifactsSliceState, } from '@sqlrooms/artifacts'; import { BlockDocumentArtifact, BlockDocumentStatefulBlockRendererProvider, BlockDocumentsSliceConfig, createBlockDocumentFeatureSlices, type BlockDocumentFeatureSlicesState, type BlockDocumentStatefulBlockRenderer, } from '@sqlrooms/documents'; import type {RoomPanelComponent} from '@sqlrooms/layout'; import { PivotSliceConfig, PivotBlock, createPivotSlice, type PivotSliceState, } from '@sqlrooms/pivot'; import { createRoomShellSlice, createRoomStore, persistSliceConfigs, type RoomShellSliceState, } from '@sqlrooms/room-shell'; type RoomState = RoomShellSliceState & ArtifactsSliceState & BlockDocumentFeatureSlicesState & PivotSliceState; const PivotBlockRenderer: BlockDocumentStatefulBlockRenderer = ({ blockInstanceId, readOnly, }) => ( ); const pivotBlockTypes = [ { blockType: 'pivot', label: 'Pivot table', description: 'Explore a table by dimensions and measures', createNode: (blockId: string) => ({ type: 'blockDocumentStatefulBlock', attrs: { id: blockId, blockType: 'pivot', blockInstanceId: blockId, ownership: 'owned', caption: '', }, }), }, ]; const BlockDocumentPanel: RoomPanelComponent = ({panelId, meta}) => { const artifactId = typeof meta?.artifactId === 'string' ? meta.artifactId : panelId; const artifact = useRoomStore((state) => artifactId ? state.artifacts.getArtifact(artifactId) : undefined, ); const renameArtifact = useRoomStore( (state) => state.artifacts.renameArtifact, ); if (!artifactId || !artifact) return null; return ( renameArtifact(artifactId, title)} /> ); }; const DocumentWorkspacePanel: RoomPanelComponent = () => ( ); const artifactTypes = defineArtifactTypes({ 'block-document': { label: 'Document', defaultTitle: 'Untitled document', component: BlockDocumentPanel, onCreate: ({artifactId, store}) => { store.getState().blockDocuments.ensureBlockDocument(artifactId); }, onEnsure: ({artifactId, store}) => { store.getState().blockDocuments.ensureBlockDocument(artifactId); }, onDelete: ({artifactId, store}) => { store.getState().blockDocuments.removeBlockDocument(artifactId); }, }, } satisfies Record<'block-document', ArtifactTypeDefinition>); export const {roomStore, useRoomStore} = createRoomStore( persistSliceConfigs( { name: 'my-workspace', sliceConfigSchemas: { artifacts: ArtifactsSliceConfig, blockDocuments: BlockDocumentsSliceConfig, pivot: PivotSliceConfig, }, }, (set, get, store) => ({ ...createRoomShellSlice({ layout: { config: { id: 'workspace', type: 'tabs', panel: 'workspace', children: [], activeTabIndex: 0, }, panels: { workspace: { title: 'Documents', component: DocumentWorkspacePanel, }, artifact: createArtifactPanelDefinition(artifactTypes, store), }, }, })(set, get, store), ...createArtifactsSlice({artifactTypes})(set, get, store), ...createBlockDocumentFeatureSlices({ onCreateOwnedStatefulBlock: ({ blockType, blockInstanceId, getState, }) => { if (blockType === 'pivot') { getState().pivot.ensurePivot(blockInstanceId); } }, onDeleteOwnedStatefulBlock: ({ blockType, blockInstanceId, getState, }) => { if (blockType === 'pivot') { getState().pivot.removePivot(blockInstanceId); } }, })(set, get, store), ...createPivotSlice()(set, get, store), }), ), ); ``` The artifact panel wrapper resolves the artifact metadata and keeps the title in sync with the editor. `createBlockDocumentFeatureSlices()` combines document content with the shared block-settings slice. If another composed feature already installs block settings, use `createBlockDocumentsSlice()` directly so the shared slice is installed only once. ## Mutate documents through block DTOs The slice exposes a small set of ordered mutations. These are the same primitives used by the editor, commands, and generic AI adapters: ```ts const {blockDocuments} = roomStore.getState(); blockDocuments.appendBlocks(documentId, [ { id: 'summary', type: 'heading', level: 2, text: [{type: 'text', text: 'Summary'}], }, { id: 'conclusion', type: 'paragraph', text: [ { type: 'text', text: 'Revenue increased during the selected period.', }, ], }, ]); blockDocuments.moveBlock(documentId, 'conclusion', 0); blockDocuments.removeBlock(documentId, 'summary'); ``` Use `setContent()` when synchronizing a complete Tiptap document. Prefer the block DTO operations for commands and agent tools because they are smaller, easier to validate, and preserve the same visible mutation path as the UI. ## Host stateful blocks A stateful block reference identifies the feature state without copying that state into the document: ```ts blockDocuments.appendBlocks(documentId, [ { id: 'sales-pivot-block', type: 'statefulBlock', blockType: 'pivot', blockInstanceId: 'sales-pivot', ownership: 'owned', caption: 'Sales by region', }, ]); ``` Register renderers at the document surface: ```tsx ``` If a renderer is unavailable, the document preserves the block JSON and shows an unsupported state. This lets workspaces round-trip content even when a host does not enable every feature. ## Choose an ownership mode Ownership controls lifecycle, not visual nesting: | Ownership | Meaning | Delete behavior | | ---------- | -------------------------------------------------------------- | -------------------------------------------------------------- | | `owned` | The backing instance belongs to this document | Remove backing state after its last owned reference disappears | | `shared` | The document refers to state shared elsewhere in the workspace | Keep backing state | | `external` | The reference resolves outside the document's managed state | Keep backing state | Wire lifecycle callbacks when composing the slice. The complete setup above installs them like this: ```ts ...createBlockDocumentFeatureSlices({ onCreateOwnedStatefulBlock: ({blockType, blockInstanceId, getState}) => { if (blockType === 'pivot') { getState().pivot.ensurePivot(blockInstanceId); } }, onDeleteOwnedStatefulBlock: ({blockType, blockInstanceId, getState}) => { if (blockType === 'pivot') { getState().pivot.removePivot(blockInstanceId); } }, })(set, get, store), ``` Captions belong to the document reference. A feature's own display name belongs to its backing instance and should be changed through that feature's UI or commands. ## Commands, AI, and collaboration `createBlockDocumentCommands()` exposes validated append, move, update, and remove operations under `block-document.*` for palettes and other command surfaces. Python block commands from `createPythonBlockCommands()` in `@sqlrooms/python` use the same prefix. AI integrations can use `createBlockDocumentCommandAiAdapter()` so tools invoke those same commands instead of maintaining a separate mutation path. For collaborative workspaces, `createDocumentsCrdtMirror()` from `@sqlrooms/documents/crdt` mirrors document configs to Loro. The room store remains the application-facing state model; the mirror handles synchronization and loop prevention. The shared mirror covers both document families and uses `markdownDocuments` for its Markdown state field. Experimental CRDT snapshots and saved AI context are not migrated across these renamings. For local workspace migration requirements, see the [Upgrade Guide](/upgrade-guide#sqlroomsdocuments-canonical-document-names-breaking). See the [`@sqlrooms/blocks` API reference](/api/blocks/), the [`@sqlrooms/documents` API reference](/api/documents/), and [Artifacts](/artifacts) for the top-level workspace model. See [Commands](/commands) for exposing the same document mutations to palettes, agents, and external integrations. --- --- url: https://sqlrooms.org/commands.md --- # Commands Commands are SQLRooms' typed action layer. They give the command palette, UI controls, AI tools, CLI, MCP, and other API clients a shared vocabulary for discovering and invoking workspace actions. A command does not own application state. It validates an action, delegates to the slice or feature that owns the state, and returns a structured result. The registry and command definitions are runtime configuration; the owning feature slice remains responsible for persistence. ## Decide what should be a command Use a command for a stable, user-visible action that should be available from more than one surface. Creating or renaming an artifact, adding a dashboard panel, running a query, and appending document blocks are good examples. Keep lower-level operations in their owning layer: | Operation | Prefer | | -------------------------------------------------------- | ----------------- | | Durable product action shared by UI, AI, or integrations | Room command | | State mutation or invariant used internally by a feature | Slice method | | Model-only context lookup, planning, or summarization | AI tool | | Multi-step generate, observe, and repair loop | Specialized agent | Commands should call existing slice methods instead of implementing a second state model. AI tools that perform durable writes should invoke commands when a matching command exists. ## Add the command registry `createRoomShellSlice()` already includes the command registry and registers the room shell's built-in commands during room initialization. Most SQLRooms apps do not need to compose the registry separately. When building a store directly from `@sqlrooms/room-store`, add `createCommandSlice()` alongside the base slice: ```ts import { createBaseRoomSlice, createCommandSlice, createRoomStore, createSlice, type BaseRoomStoreState, type CommandSliceState, } from '@sqlrooms/room-store'; type Report = {title: string}; type ReportsSliceState = { reports: { byId: Record; getReport: (reportId: string) => Report | undefined; renameReport: (reportId: string, title: string) => void; }; }; interface RoomState extends BaseRoomStoreState, ReportsSliceState, CommandSliceState {} const createReportsSlice = createSlice( (set, get) => ({ reports: { byId: {'quarterly-sales': {title: 'Quarterly sales'}}, getReport: (reportId) => get().reports.byId[reportId], renameReport: (reportId, title) => { const report = get().reports.byId[reportId]; if (!report) return; set((state) => ({ reports: { ...state.reports, byId: { ...state.reports.byId, [reportId]: {...report, title}, }, }, })); }, }, }), ); export const {roomStore} = createRoomStore((set, get, store) => ({ ...createBaseRoomSlice()(set, get, store), ...createCommandSlice()(set, get, store), ...createReportsSlice(set, get, store), })); ``` ## Define a command family Feature packages should expose factories that return `RoomCommand[]`. Keep IDs stable and namespace them by feature, such as `report.rename` or `block-document.append-blocks`. ```ts import type {BaseRoomStoreState, RoomCommand} from '@sqlrooms/room-store'; import {z} from 'zod'; type ReportCommandState = BaseRoomStoreState & { reports: { getReport: (reportId: string) => {title: string} | undefined; renameReport: (reportId: string, title: string) => void; }; }; const RenameReportInput = z.object({ reportId: z.string().describe('ID of the report to rename.'), title: z.string().trim().min(1).describe('New report title.'), }); type RenameReportInput = z.infer; export function createReportCommands< TRoomState extends ReportCommandState, >(): RoomCommand[] { return [ { id: 'report.rename', name: 'Rename report', description: 'Change the title of a report.', group: 'Reports', keywords: ['report', 'title', 'rename'], inputSchema: RenameReportInput, inputDescription: 'Report ID and a non-empty title.', metadata: { readOnly: false, idempotent: true, riskLevel: 'low', }, execute: ({getState}, input) => { const {reportId, title} = input as RenameReportInput; const report = getState().reports.getReport(reportId); if (!report) { return { success: false, commandId: 'report.rename', code: 'report-not-found', error: `Unknown report "${reportId}".`, }; } const previousTitle = report.title; getState().reports.renameReport(reportId, title); return { success: true, commandId: 'report.rename', message: `Renamed report to "${title}".`, data: {reportId, title, previousTitle}, }; }, }, ]; } ``` Zod parses the input before middleware or `execute()` runs. Use `validateInput()` for checks that need current store state and use `isEnabled()` or `isVisible()` when availability depends on the execution context. ### Describe discovery, safety, and UI behavior Command metadata is consumed by more than the palette: | Field | Purpose | | ------------------------------------------ | ---------------------------------------------------------------- | | `name`, `description`, `group`, `keywords` | Human and intent-based discovery | | `inputSchema`, `inputDescription` | Validation, palette input, and portable tool schemas | | `isVisible`, `isEnabled` | Context-sensitive discovery and availability | | `metadata.readOnly` | Declares whether execution can change state | | `metadata.idempotent` | Declares whether repeated calls have the same effect | | `metadata.riskLevel` | Classifies the consequence as `low`, `medium`, or `high` | | `metadata.requiresConfirmation` | Requires an explicit confirmation on guarded surfaces | | `ui.keystrokes` | Adds one or more palette keyboard bindings | | `ui.inputComponent` | Replaces the palette's generic JSON input UI | | `ui.hidden` | Hides an internal command from the palette and default discovery | Always set the policy metadata deliberately. The conservative defaults are mutating, non-idempotent, medium risk, and no explicit confirmation flag. A high-risk command is confirmation-gated by guarded invocation even when `requiresConfirmation` is omitted. Use descriptions on Zod fields. SQLRooms converts supported Zod schemas to a portable JSON-schema representation for AI, CLI, and MCP discovery. ## Register and clean up commands Register a complete command family under one owner: ```ts import { registerCommandsForOwner, unregisterCommandsForOwner, } from '@sqlrooms/room-store'; const REPORT_COMMAND_OWNER = '@acme/reports'; registerCommandsForOwner( roomStore, REPORT_COMMAND_OWNER, createReportCommands(), ); // When the feature is removed or its slice is destroyed: unregisterCommandsForOwner(roomStore, REPORT_COMMAND_OWNER); ``` Feature slices normally register commands in their `initialize()` lifecycle and unregister them in `destroy()`. Calling `registerCommands()` for an existing owner replaces that owner's previous command set, which makes re-registration and hot reload deterministic. Pass the whole family each time; do not register several commands one at a time with the same owner. ## Return useful results Return a `RoomCommandResult` when callers need a meaningful outcome: | Field | Convention | | ----------- | ------------------------------------------------------------------ | | `success` | Whether the requested outcome was achieved | | `commandId` | Stable ID of the invoked command | | `message` | Short summary for people, chat transcripts, and traces | | `code` | Stable outcome for callers that need to branch | | `data` | IDs, chosen defaults, and other values needed by follow-up actions | | `error` | Concise explanation of an unsuccessful result | An `execute()` handler may instead return data directly or return nothing; SQLRooms normalizes either form into a successful result. Prefer an explicit result for mutations so callers can identify what changed without rereading the whole store. ## Add the command palette Mount the palette once inside `RoomShell`. The optional compound button opens the same palette and fits naturally in the shell sidebar. Because the low-level store above intentionally omits layout and database state, use a RoomShell-backed store for this UI: ```tsx import { RoomShell, createRoomShellSlice, createRoomStore, type RoomShellSliceState, } from '@sqlrooms/room-shell'; const {roomStore: shellRoomStore} = createRoomStore( (set, get, store) => ({ ...createRoomShellSlice({})(set, get, store), }), ); ; ``` Users can also open it with Cmd+K on macOS or Ctrl+K elsewhere. A command with required input opens its custom `ui.inputComponent` or the default JSON input editor. Commands without required input run immediately. Define keyboard bindings with `ui.keystrokes`, for example `Mod+Shift+R`; conflicting bindings are not invoked directly. ## Invoke commands from application code `invokeCommand()` returns unsuccessful outcomes as values. It is convenient when the caller wants to display or inspect a failure: ```ts const result = await roomStore.getState().commands.invokeCommand( 'report.rename', {reportId: 'quarterly-sales', title: 'Q3 sales'}, { surface: 'api', actor: 'report-settings-form', traceId: requestId, target: {kind: 'report', id: 'quarterly-sales'}, }, ); if (!result.success) { showError(result.error); } ``` `executeCommand()` uses the same path but throws when the normalized result is unsuccessful. Use it where the surrounding control flow is already exception based. Invocation metadata is available to predicates, validation, middleware, execution, and telemetry callbacks. `surface` can be `palette`, `ai`, `cli`, `mcp`, `api`, or `unknown`. Handlers can also read `context.signal` and pass it to cancellable work; cancellation is cooperative. ### Guard agent and external invocation Direct `invokeCommand()` does not enforce confirmation metadata. Agent-facing and external surfaces must use `invokeCommandWithPolicy()` or one of the guarded CLI/MCP adapters: ```ts import {invokeCommandWithPolicy} from '@sqlrooms/room-store'; const result = await invokeCommandWithPolicy( roomStore, 'room.remove-data-source', {tableName: 'old_sales'}, { surface: 'api', actor: 'workspace-service', traceId: requestId, signal: abortController.signal, }, {confirmed: userConfirmedRemoval}, ); ``` The guard rechecks that the command exists and is currently enabled immediately before execution. It rejects high-risk and confirmation-required commands unless the caller records explicit user confirmation. ## Expose commands to other surfaces The same registry can drive several adapters: | Surface | Integration | | --------------- | -------------------------------------------------- | | Command palette | `` | | AI | `createDefaultAiTools()` or `createCommandTools()` | | CLI | `createCommandCliAdapter()` | | MCP | `createCommandMcpAdapter()` | | Custom API | `invokeCommandWithPolicy()` | | Custom UI | `invokeCommand()` | The default AI tools expose `search_commands`, `get_command`, `execute_command`, and `list_commands`. Model-facing flows should normally use `search_commands`, inspect the selected command with `get_command` when its input schema is needed, and then call `execute_command`. This keeps routine discovery compact while preserving validation and confirmation policy. CLI and MCP adapters derive portable descriptors from the registry and use the same guarded invocation semantics: ```ts import { createCommandCliAdapter, createCommandMcpAdapter, } from '@sqlrooms/room-store'; const cli = createCommandCliAdapter(roomStore, { defaultActor: 'sqlrooms-cli', }); const mcp = createCommandMcpAdapter(roomStore, { defaultActor: 'sqlrooms-mcp', toolNamePrefix: 'room.', }); ``` ## Add middleware and telemetry Pass `createCommandProps` through `createRoomShellSlice()` to apply middleware and observe every registered command without changing feature implementations: ```ts const createCommandProps = { middleware: [ async (command, input, context, next) => { audit.debug('command requested', { commandId: command.id, surface: context.invocation.surface, }); return await next(); }, ], onCommandInvokeSuccess: ({command, result, durationMs}) => { telemetry.track('command_success', { commandId: command.id, code: result.code, durationMs, }); }, onCommandInvokeFailure: ({command, result, durationMs}) => { telemetry.track('command_failure', { commandId: command.id, code: result.code, durationMs, }); }, onCommandInvokeError: ({command, error, durationMs}) => { telemetry.track('command_error', { commandId: command.id, message: String(error), durationMs, }); }, }; export const {roomStore} = createRoomStore((set, get, store) => ({ ...createRoomShellSlice({createCommandProps})(set, get, store), // Compose feature slices here. })); ``` Middleware runs after input validation and may transform the result, wrap execution, or stop the chain by returning without calling `next()`. Each middleware function may call `next()` only once. ## Command authoring checklist * Use a stable, namespaced ID and register the complete family under one owner. * Delegate state changes to the owning slice. * Describe the command and every input field for non-UI discovery. * Set read-only, idempotency, risk, and confirmation metadata explicitly. * Return stable IDs and chosen defaults needed by follow-up actions. * Use guarded invocation for agents and external clients. * Register and unregister feature-owned commands with the feature lifecycle. See the [`@sqlrooms/room-store` API reference](/api/room-store/) for registry and adapter types, the [`@sqlrooms/room-shell` API reference](/api/room-shell/) for the palette, [Artifacts](/artifacts), and [Blocks and Block Documents](/blocks-and-documents). --- --- url: https://sqlrooms.org/query-cancellation.md --- # Query Cancellation in DuckDbConnector The DuckDbConnector now supports query cancellation through a unified `QueryHandle` interface with full composability support. All query methods (`execute`, `query`, `queryJson`) now return a `QueryHandle` that provides immediate access to cancellation functionality and signal composability. ## QueryHandle Interface ```typescript interface QueryOptions { signal?: AbortSignal; // Optional external abort signal } // Promise-like intersection – can be awaited directly *or* via .result type QueryHandle = PromiseLike & { result: Promise; // Underlying promise (kept for backwards-compatibility) cancel: () => Promise; // Method to cancel the query signal: AbortSignal; // Read-only abort signal for composability }; ``` ## Usage Examples ### Basic Query with Cancellation ```typescript import {createWasmDuckDbConnector} from './connectors/createDuckDbConnector'; const connector = createWasmDuckDbConnector(); await connector.initialize(); // Start a query and get immediate access to cancellation const queryHandle = connector.query('SELECT * FROM large_table'); console.log('Query started'); // Cancel the query if needed (e.g., user clicks cancel button) setTimeout(() => { queryHandle.cancel(); }, 5000); try { const result = await queryHandle; console.log('Query completed:', result.numRows); } catch (error) { console.log('Query was cancelled or failed:', error.message); } ``` ### Composable Cancellation - Multiple Queries with Shared Controller ```typescript // Create a master abort controller for a series of operations const masterController = new AbortController(); // Start multiple queries that can all be cancelled together const query1 = connector.query('SELECT COUNT(*) FROM table1', { signal: masterController.signal, }); const query2 = connector.query('SELECT AVG(price) FROM products', { signal: masterController.signal, }); const query3 = connector.queryJson('SELECT * FROM users LIMIT 100', { signal: masterController.signal, }); // Cancel all queries at once setTimeout(() => { console.log('Cancelling all queries...'); masterController.abort(); // This cancels all three queries }, 3000); try { const results = await Promise.allSettled([query1, query2, query3]); results.forEach((result, index) => { if (result.status === 'fulfilled') { console.log(`Query ${index + 1} completed`); } else { console.log(`Query ${index + 1} failed:`, result.reason.message); } }); } catch (error) { console.log('Error in query execution:', error.message); } ``` ### Integration with Other Cancellable Operations ```typescript // Create a shared abort controller for the entire operation const operationController = new AbortController(); async function performComplexOperation() { try { // Step 1: Run a query const queryHandle = connector.query( 'SELECT id, data FROM source_table WHERE condition = ?', {signal: operationController.signal}, ); const queryResult = await queryHandle; // Step 2: Make HTTP requests using the same signal const response = await fetch('/api/process-data', { method: 'POST', body: JSON.stringify(queryResult), signal: operationController.signal, // Same signal! }); // Step 3: Another query with the same cancellation const finalQuery = connector.execute( 'INSERT INTO results SELECT * FROM processed_data', {signal: operationController.signal}, ); await finalQuery; console.log('Complex operation completed'); } catch (error) { if (error.name === 'AbortError') { console.log('Operation was cancelled'); } else { console.log('Operation failed:', error.message); } } } // Start the operation performComplexOperation(); // Cancel the entire operation (queries + HTTP requests) after 10 seconds setTimeout(() => { operationController.abort(); }, 10000); ``` ### Advanced Signal Composition ```typescript // Create timeout-based cancellation function createTimeoutSignal(ms: number): AbortSignal { const controller = new AbortController(); setTimeout(() => controller.abort(), ms); return controller.signal; } // Combine multiple signals function combineSignals(...signals: AbortSignal[]): AbortSignal { const controller = new AbortController(); signals.forEach((signal) => { if (signal.aborted) { controller.abort(); } else { signal.addEventListener('abort', () => controller.abort()); } }); return controller.signal; } // Usage: Query with both user cancellation and timeout const userController = new AbortController(); const timeoutSignal = createTimeoutSignal(30000); // 30 second timeout const combinedSignal = combineSignals(userController.signal, timeoutSignal); const queryHandle = connector.query('SELECT * FROM very_large_table', { signal: combinedSignal, }); // User can still cancel manually document.getElementById('cancel-btn').onclick = () => { userController.abort(); }; try { const result = await queryHandle; console.log('Query completed within timeout'); } catch (error) { console.log('Query cancelled or timed out:', error.message); } ``` ### Listening to Cancellation Events ```typescript const queryHandle = connector.query('SELECT * FROM table'); // Listen for cancellation queryHandle.signal.addEventListener('abort', () => { console.log('Query was cancelled'); // Update UI, clean up resources, etc. }); // Check if already cancelled if (queryHandle.signal.aborted) { console.log('Query was already cancelled'); } // Cancel after some condition if (someCondition) { await queryHandle.cancel(); } ``` ## Migration from Old API ### Before (Old API) ```typescript const {data, qid} = await connector.query('SELECT * FROM table'); console.log('Query ID:', qid); console.log('Results:', data.numRows); ``` ### After (New API) ```typescript // Simple usage (no external signal) const queryHandle = connector.query('SELECT * FROM table'); console.log('Query started'); const data = await queryHandle; console.log('Results:', data.numRows); // With external cancellation control const controller = new AbortController(); const queryHandle = connector.query('SELECT * FROM table', { signal: controller.signal, }); // controller.abort() to cancel const data = await queryHandle; ``` ## Implementation Notes * **Hybrid Approach**: Combines the simplicity of `.cancel()` with the composability of `AbortSignal` * **Optional External Control**: Pass your own `AbortSignal` for coordinated cancellation across multiple operations * **Automatic Internal Management**: If no signal is provided, one is created internally * **Signal Chaining**: External signals are chained to internal controllers for proper cleanup * **Web Standards Compliant**: Uses standard `AbortController`/`AbortSignal` APIs * **Composable**: Signals can be shared across queries, HTTP requests, and other cancellable operations * **Event-Driven**: Listen for abort events to update UI or perform cleanup --- --- url: https://sqlrooms.org/theming.md --- # Theming SQLRooms uses [shadcn's](https://ui.shadcn.com/) CSS variables approach for theming, providing a flexible and maintainable way to manage color schemes and design tokens across the application. ## Theme Provider The application uses `ThemeProvider` to manage theme state: ```tsx ``` ### Props * `defaultTheme`: Initial theme ("light" or "dark") * `storageKey`: localStorage key for persisting theme preference ## Using Themes in Components You can either use the pre-built `ThemeSwitch` component or implement one yourself like here: ```tsx import {ThemeSwitch} from '@sqlrooms/ui'; function MyNavBarComponent() { return (
...
); } ``` Or with a custom implementation using `Button` and `useTheme`: ```tsx import {useTheme, Button} from '@sqlrooms/ui'; function ThemeToggle() { const {theme, setTheme} = useTheme(); return ( ); } ``` ## CSS Variables The theming system uses CSS custom properties in HSL format. These variables are defined in the global CSS: ```css :root { --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; --card: 0 0% 100%; --card-foreground: 222.2 84% 4.9%; --popover: 0 0% 100%; --popover-foreground: 222.2 84% 4.9%; --primary: 221.2 83.2% 53.3%; --primary-foreground: 210 40% 98%; --secondary: 210 40% 96.1%; --secondary-foreground: 222.2 47.4% 11.2%; --muted: 210 40% 96.1%; --muted-foreground: 215.4 16.3% 46.9%; --accent: 210 40% 96.1%; --accent-foreground: 222.2 47.4% 11.2%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 210 40% 98%; --border: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%; --ring: 221.2 83.2% 53.3%; --radius: 0.5rem; --chart-1: 12 76% 61%; --chart-2: 173 58% 39%; --chart-3: 197 37% 24%; --chart-4: 43 74% 66%; --chart-5: 27 87% 67%; } .dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; --card: 222.2 84% 4.9%; --card-foreground: 210 40% 98%; --popover: 222.2 84% 4.9%; --popover-foreground: 210 40% 98%; --primary: 217.2 91.2% 59.8%; --primary-foreground: 222.2 47.4% 11.2%; --secondary: 217.2 32.6% 17.5%; --secondary-foreground: 210 40% 98%; --muted: 217.2 32.6% 17.5%; --muted-foreground: 215 20.2% 65.1%; --accent: 217.2 32.6% 17.5%; --accent-foreground: 210 40% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 40% 98%; --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; --ring: 224.3 76.3% 48%; --chart-1: 220 70% 50%; --chart-2: 160 60% 45%; --chart-3: 30 80% 55%; --chart-4: 280 65% 60%; --chart-5: 340 75% 55%; } ``` ### Variable Categories * **Base Colors** * `--background` / `--foreground`: Main background and text colors * `--card` / `--card-foreground`: Card component colors * `--popover` / `--popover-foreground`: Popover/dropdown colors * **Semantic Colors** * `--primary` / `--primary-foreground`: Primary action colors * `--secondary` / `--secondary-foreground`: Secondary action colors * `--muted` / `--muted-foreground`: Subdued UI elements * `--accent` / `--accent-foreground`: Emphasis and highlights * `--destructive` / `--destructive-foreground`: Error and deletion actions * **UI Elements** * `--border`: Border colors * `--input`: Form input borders * `--ring`: Focus ring color * `--radius`: Border radius for components * **Chart Colors** * `--chart-1` through `--chart-5`: Data visualization colors ### Using Variables in CSS To use these variables in your components: ```css .my-component { background-color: hsl(var(--background)); color: hsl(var(--foreground)); border: 1px solid hsl(var(--border)); border-radius: var(--radius); } ``` #### Using with Tailwind Classes The theme variables are mapped to Tailwind's color system, allowing you to use them directly in className props: ```tsx import { Button, Input } from '@sqlrooms/ui'; // Basic usage
// With hover states // With opacity modifiers
Semi-transparent background
// Border and ring utilities // Destructive actions ``` These class names automatically adapt to the current theme, switching between light and dark mode values as appropriate. #### Dark Mode Variants You can explicitly specify different styles for light and dark modes using Tailwind's `dark:` modifier: ```tsx import { Button } from '@sqlrooms/ui'; // Basic dark mode override
Light and dark specific background
// Combining with theme variables
Card with dark mode opacity
// Complex component example // Multiple dark mode modifiers
Complex Container
``` Note: The `dark:` modifier works automatically with our theme system - it will apply when the theme is set to "dark" through the ThemeProvider. ## Customizing Themes To create or modify themes: 1. Visit the [shadcn theme generator](https://ui.shadcn.com/themes) 2. Customize colors interactively 3. Copy the generated CSS 4. Update your global CSS file with the new variables ## API Reference For detailed API documentation, refer to: * [ThemeProvider API](/api/ui/functions/ThemeProvider) * [ThemeSwitch API](/api/ui/functions/ThemeSwitch) * [useTheme Hook API](/api/ui/functions/useTheme) --- --- url: https://sqlrooms.org/offline-use.md --- # Offline Use with SQLRooms SQLRooms can be integrated into a [Progressive Web App (PWA)](https://web.dev/progressive-web-apps/) capable of working offline: ![SQLRooms Query Workbench progressive web app](/media/offline/sqlrooms-query-pwa.png) All computation happens on your device, whether in the browser or a desktop app, with no backend required. This enables privacy, speed, and user control, even when you're completely offline. Here's how you can implement an offline-first experience with SQLRooms: ## 1. Persisting State in localStorage SQLRooms uses [Zustand](https://docs.pmnd.rs/zustand/getting-started/introduction) for state management. You can persist your app's state in the browser's `localStorage` using the `persistSliceConfigs` helper. This ensures user settings, layouts, and other state survive reloads and work offline. **Example:** ```ts import { createRoomStore, createRoomShellSlice, RoomShellSliceState, BaseRoomConfig, LayoutConfig, persistSliceConfigs, } from '@sqlrooms/room-shell'; type RoomState = RoomShellSliceState; const {roomStore, useRoomStore} = createRoomStore( persistSliceConfigs( { name: 'sql-editor-example-app-state-storage', // localStorage key sliceConfigSchemas: { room: BaseRoomConfig, layout: LayoutConfig, }, }, (set, get, store) => ({ ...createRoomShellSlice({ config: { // Room configuration }, layout: { config: { // Layout configuration }, panels: { // Panel definitions }, }, })(set, get, store), }), ), ); ``` See [`examples/query/src/store.ts`](https://github.com/sqlrooms/examples/blob/main/query/src/store.ts) for a full example. ## 2. Using OPFS for DuckDB Storage SQLRooms leverages [DuckDB-Wasm](https://duckdb.org/docs/wasm/overview.html) for in-browser SQL analytics. To persist your database between sessions, use the `opfs://` path, which stores the DuckDB database in the browser's [Origin Private File System (OPFS)](https://web.dev/origin-private-file-system/). **Example:** ```ts import {createWasmDuckDbConnector, DuckDBAccessMode} from '@sqlrooms/duckdb'; const connector = createWasmDuckDbConnector({ path: 'opfs://database.db', accessMode: DuckDBAccessMode.READ_WRITE, }); ``` This allows users to keep their data local, persistent, and private. ## 3. Enabling Offline Support with PWA To make your app work offline and provide a native-like experience, enable PWA support using [`vite-plugin-pwa`](https://vite-pwa-org.netlify.app/). **Example vite.config.ts:** ```ts import {VitePWA} from 'vite-plugin-pwa'; export default defineConfig({ plugins: [ VitePWA({ registerType: 'autoUpdate', manifest: { name: 'SQLRooms Query Workbench', short_name: 'SQLRooms', start_url: '.', display: 'standalone', background_color: '#ffffff', description: 'Query Workbench example for SQLRooms', icons: [ {src: 'icon.png', sizes: '192x192', type: 'image/png'}, {src: 'icon.png', sizes: '512x512', type: 'image/png'}, ], }, }), ], }); ``` See [`examples/query/vite.config.ts`](https://github.com/sqlrooms/examples/blob/main/query/vite.config.ts) for a real-world config. ## 4. Example: SQL Query Editor The [PWA SQL Query Editor example](https://github.com/sqlrooms/examples/tree/main/query-pwa) demonstrates all of these offline techniques in a real app. It persists state, stores data in OPFS, and works offline as a PWA. *** By combining these techniques, you can build analytics applications with SQLRooms that are fast, private, and fully offline—empowering your users to own their data and work anywhere, anytime. --- --- url: https://sqlrooms.org/examples.md --- # Example Applications All example applications are available in our [Examples Repository](https://github.com/sqlrooms/examples). Here's a list of featured examples: ## Basic examples ### [Getting Started](https://github.com/sqlrooms/examples/tree/main/get-started) [GitHub repo](https://github.com/sqlrooms/examples/tree/main/get-started) | [Open in StackBlitz](https://stackblitz.com/github/sqlrooms/examples/tree/main/get-started?embed=1) A minimal Vite application demonstrating the basic usage of SQLRooms. Features include: * Sets up an app store and a single main panel using SQLRooms' project builder utilities * Loads a CSV file of California earthquakes as a data source * Runs a SQL query in the browser (DuckDB WASM) to show summary statistics * Simple UI with loading, error, and result states To create a new project from the get-started example run this: ```bash npx giget gh:sqlrooms/examples/get-started my-new-app/ ``` ### [SQL Query Editor](https://query.sqlrooms.org/) [Try live](https://query.sqlrooms.org/) | [GitHub repo](https://github.com/sqlrooms/examples/tree/main/query) | [Open in StackBlitz](https://stackblitz.com/github/sqlrooms/examples/tree/main/query?embed=1) [![Netlify Status](https://api.netlify.com/api/v1/badges/779ab00f-9f8f-4c12-92d2-a75426ac0315/deploy-status)](https://app.netlify.com/projects/sqlrooms-query/deploys) A comprehensive SQL query editor demonstrating SQLRooms' DuckDB integration. Features include: * Interactive SQL editor with syntax highlighting * File dropzone for adding data tables to DuckDB * Schema tree for browsing database tables and columns * Tabbed interface for working with multiple queries * Query execution with results data table * Support for query cancellation * There is a [version of the example with offline functionality](https://github.com/sqlrooms/examples/tree/main/query-pwa) which supports Progressive Web App (PWA) features, persistent database storage with OPFS, and state persistence via local storage To create a new project from the query example run this: ```bash npx giget gh:sqlrooms/examples/query my-new-app/ ``` #### Running locally ```sh npm install npm run dev ``` ### [Layout](https://github.com/sqlrooms/examples/tree/main/layout) An app demonstrating collapsible panels, custom tab strips, and dynamically created dock and grid dashboards. Start with the [Layout developer guide](/layout) for configuration examples and links to the layout APIs. ### Multi-Room [Try live](https://sqlrooms-multi-room.netlify.app/) | [GitHub repo](https://github.com/sqlrooms/sqlrooms/tree/main/examples/multi-room) 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: * TanStack Router with room list (`/`) and room detail (`/room/:id`) pages * Team-style room switcher in the Sidebar header with icon-collapsible navigation * Sidebar groups for platform navigation and live table schema tree exploration * Pre-seeded with two sample rooms: Earthquakes and BIXI bike locations * Paginated data table preview using `QueryDataTable` * Persistent storage for room configs in local storage * Room CRUD operations (create, rename, delete) * Proper store initialization and destruction on room navigation To create a new project from the query example run this: ```bash npx giget gh:sqlrooms/examples/multi-room my-new-app/ ``` #### Running locally ```sh pnpm install pnpm dev ``` ## AI Assistant ### [AI-Powered Analytics](https://ai.sqlrooms.org/) [Try live](https://ai.sqlrooms.org/) | [GitHub repo](https://github.com/sqlrooms/examples/tree/main/ai) | [Open in StackBlitz](https://stackblitz.com/github/sqlrooms/examples/tree/main/ai?embed=1\&file=components/app-shell.tsx) [![Netlify Status](https://api.netlify.com/api/v1/badges/031f0d4f-c2a3-44f8-adf1-6429164bb0c7/deploy-status)](https://app.netlify.com/projects/sqlrooms-ai/deploys) An advanced example showing how to build an AI-powered analytics application with SQLRooms. Features include: * Natural language data exploration * AI-driven data analysis * Integration with [SQLRooms AI assistant](/api/ai/) * Custom visualization components * Room state persistence To create a new project from the AI example run this: ```bash npx giget gh:sqlrooms/examples/ai my-new-app/ ``` #### Running locally ```sh npm install npm run dev ``` ### [AI App Builder](https://sqlrooms-ai.netlify.app/) [GitHub repo](https://github.com/sqlrooms/examples/tree/main/app-builder) | [Open in StackBlitz](https://stackblitz.com/github/sqlrooms/examples/tree/main/app-builder?embed=1\&file=src/main.tsx) A SQLRooms app that builds SQLRooms apps—demonstrating recursive bootstrapping. The outer app runs an AI assistant on the left and a code editor in the middle, while the right third hosts the inner app which compiles on the fly and executes in a browser-based virtual environment powered by [StackBlitz WebContainer](https://github.com/stackblitz/webcontainer-core). Features: * AI-assisted app generation via [SQLRooms AI assistant](/api/ai/) * Live code editing with instant preview * In-browser compilation and execution (no server required, except for the model) * Recursive bootstrapping pattern To create a new project from this example: ```bash npx giget gh:sqlrooms/examples/app-builder my-new-app/ ``` #### Running locally ```sh npm install npm run dev ``` ## Geospatial ### [Deck.gl + Mosaic](https://sqlrooms-deckgl-mosaic.netlify.app/) [Try live](https://sqlrooms-deckgl-mosaic.netlify.app/) | [GitHub repo](https://github.com/sqlrooms/examples/tree/main/deckgl-mosaic) | [Open in StackBlitz](https://stackblitz.com/github/sqlrooms/examples/tree/main/deckgl-mosaic?embed=1\&file=src/app.tsx) [![Netlify Status](https://api.netlify.com/api/v1/badges/e4571f95-9e51-4d4a-8e68-98d6f7c99980/deploy-status)](https://app.netlify.com/projects/sqlrooms-deckgl-mosaic/deploys) This example is based on the [original demo app](https://github.com/dzole0311/deckgl-duckdb-geoarrow) by [Gjore Milevski](https://github.com/dzole0311). An example showcasing integration with [deck.gl](https://deck.gl/) and the [UWData Mosaic](https://github.com/uwdata/mosaic) package for performant cross-filtering, now routed through [`@sqlrooms/deck`](../../packages/deck/README.md). The architecture uses Mosaic’s global Coordinator to manage state between linked views using SQL predicates. The map spec stays separate from the data, the current Mosaic-filtered Arrow result is passed into `DeckJsonMap`, and multiple JSON layers reuse that same prepared dataset instead of maintaining a local GeoArrow bridge utility. To create a new project from the deckgl-mosaic example run this: ```bash npx giget gh:sqlrooms/examples/deckgl-mosaic my-new-app/ ``` #### Running locally ```sh npm install npm run dev ``` ### [Mosaic + DataFusion-WASM + Zarr](https://sqlrooms-deckgl-mosaic-datafusion.netlify.app/) [Try live](https://sqlrooms-deckgl-mosaic-datafusion.netlify.app/) | [GitHub repo](https://github.com/sqlrooms/sqlrooms/tree/main/examples/deckgl-mosaic-datafusion) Preview of the SQLRooms Deck.gl, Mosaic, and DataFusion example app. This example ports [Gjore Milevski](https://github.com/dzole0311)'s [mosaic-datafusion-zarr-deckgl](https://github.com/dzole0311/mosaic-datafusion-zarr-deckgl) experiment (read the [original write-up](https://gjoremilevski.com/posts/mosaic-datafusion-zarr-deckgl/)) into the SQLRooms shell, alongside the [Deck.gl + Mosaic example](https://github.com/sqlrooms/examples/tree/main/deckgl-mosaic) it is structurally closest to. ECMWF IFS ENS temperature is streamed client-side from a public Zarr store ([dynamical.org](https://dynamical.org)) with zarrita, queried with [DataFusion compiled to WASM](https://github.com/apache/datafusion) through a Mosaic crossfilter, and rendered with [@developmentseed/deck.gl-zarr](https://github.com/developmentseed/deck.gl-raster). This room is hand-composed from the base room, layout, Mosaic, and forecast slices. It does not include SQLRooms' DuckDB slice, so loading the example does not initialize or download DuckDB-WASM. Its full query path is: ```text Mosaic clients → supplied Mosaic Coordinator → DataFusion connector → DataFusion-WASM ``` The DataFusion-WASM wrapper's `query({type, sql})` method already matches Mosaic's `Connector` interface. It returns [flechette](https://github.com/uwdata/flechette) tables directly because that is what DataFusion's Arrow IPC output decodes into, so the wrapper is handed to the supplied `Coordinator` as-is. Room shell chrome (sidebar, theme, layout panels), the map, the raster shaders, and the crossfilter hooks are otherwise unchanged from the source app. Because the Coordinator can only be built once the DataFusion tables exist (which needs the first streamed Zarr chunk), the room store here is not a static module export like other examples' `store.ts`; it's built by `createForecastRoomStore(lab)` once boot finishes, see `src/App.tsx`. The DataFusion-WASM bindings ship with `execute_ipc`, `register_ipc` and `materialize_table`, which the published upstream package doesn't have, so this example depends on [`@dzole0311/datafusion-wasm`](https://www.npmjs.com/package/@dzole0311/datafusion-wasm), a patched build published from [dzole0311/datafusion-wasm-bindings](https://github.com/dzole0311/datafusion-wasm-bindings) (a fork of [datafusion-contrib/datafusion-wasm-bindings](https://github.com/datafusion-contrib/datafusion-wasm-bindings)). #### Running locally Build the workspace packages first, then run this example: ```sh pnpm build pnpm dev deckgl-mosaic-datafusion-example ``` ### [Kepler.gl Geospatial Visualization](https://kepler.sqlrooms.org/) [Try live](https://kepler.sqlrooms.org/) | [GitHub repo](https://github.com/sqlrooms/examples/tree/main/kepler) | [Open in StackBlitz](https://stackblitz.com/github/sqlrooms/examples/tree/main/kepler?embed=1\&file=src/app.tsx) [![Netlify Status](https://api.netlify.com/api/v1/badges/888420a3-33e4-4142-a3b5-03a61c44e09a/deploy-status)](https://app.netlify.com/projects/sqlrooms-kepler/deploys) An example demonstrating [Kepler.gl](https://kepler.gl/) integration for geospatial data visualization. Features include: * Load earthquakes dataset into DuckDB * Add data as a Kepler layer for map visualization * Interactive map controls and filtering * Rich styling options for geospatial layers To create a new project from the kepler example run this: ```sh npx giget gh:sqlrooms/examples/kepler my-new-app/ ``` #### Running locally ```sh npm install npm dev ``` ### [Deck.gl Geospatial Visualization](https://sqlrooms-deckgl.netlify.app/) [Try live](https://sqlrooms-deckgl.netlify.app/) | [GitHub repo](https://github.com/sqlrooms/examples/tree/main/deckgl) | [Open in StackBlitz](https://stackblitz.com/github/sqlrooms/examples/tree/main/deckgl?embed=1\&file=src/app.tsx) [![Netlify Status](https://api.netlify.com/api/v1/badges/b507fcea-e5ec-4822-988d-77857944cf48/deploy-status)](https://app.netlify.com/projects/sqlrooms-deckgl/deploys) An example demonstrating [deck.gl](https://deck.gl/) integration for geospatial data visualization through [`@sqlrooms/deck`](../../packages/deck/README.md). It renders ~48k Overture Maps building footprints for the Zurich city centre (currently 48,451 rows), extruded in 3D and colored by height using a sequential color scale. Features: * Query a Hugging Face-hosted Parquet file with DuckDB WASM via `httpfs` * Load airports data file into DuckDB * Define a serializable deck.gl JSON layer spec separately from the data * Bind multiple named DuckDB-backed datasets into one map * Visualize airport locations on an interactive map with GeoArrow-backed point layers * WKB geometry decoded directly to GeoArrow — no GeoJSON intermediate * 3D extruded `GeoArrowPolygonLayer` with height-based color scale * Legend title includes units (`Height (m)`) with domain matching loaded data min/max * Tooltip with building name, class, and height * Toggle between airports and Zurich buildings in the same map UI To create a new project from the deckgl example run this: ```sh npx giget gh:sqlrooms/examples/deckgl my-new-app/ ``` #### Running Locally ```sh pnpm install pnpm build pnpm dev deckgl-example ``` #### Regenerating the dataset The Zurich buildings dataset is hosted at [`sqlrooms/buildings`](https://huggingface.co/datasets/sqlrooms/buildings) on Hugging Face. It was generated from [Overture Maps](https://overturemaps.org/) using DuckDB. Run in the DuckDB CLI or any SQL client with `httpfs` and `spatial` extensions: ```sql INSTALL httpfs; LOAD httpfs; INSTALL spatial; LOAD spatial; SET s3_region = 'us-west-2'; COPY ( SELECT names.primary AS name, class, COALESCE(height, num_floors * 3.2, 5) AS height, ST_AsWKB(geometry) AS geometry FROM read_parquet( 's3://overturemaps-us-west-2/release/2026-04-15.0/theme=buildings/type=building/*.zstd.parquet', hive_partitioning = 1 ) WHERE bbox.xmin BETWEEN 8.47 AND 8.59 AND bbox.ymin BETWEEN 47.335 AND 47.415 LIMIT 50000 ) TO 'zurich_buildings.parquet'; ``` Adjust the bounding box or the release date to target a different area or a newer Overture snapshot, then upload the resulting Parquet file to the Hugging Face dataset. ### [Deck.gl + Commenting & Annotation](https://sqlrooms-deckgl-discuss.netlify.app/) [Try live](https://sqlrooms-deckgl-discuss.netlify.app/) | [GitHub repo](https://github.com/sqlrooms/examples/tree/main/deckgl-discuss) | [Open in StackBlitz](https://stackblitz.com/github/sqlrooms/examples/tree/main/deckgl-discuss?embed=1\&file=src/app.tsx) [![Netlify Status](https://api.netlify.com/api/v1/badges/9c32bdac-f2b1-4cf3-b48b-fa197e0986e3/deploy-status)](https://app.netlify.com/projects/sqlrooms-deckgl-discuss/deploys) An example showcasing integration with [deck.gl](https://deck.gl/) for geospatial data visualization combined with the [@sqlrooms/discuss](/api/discuss) module for collaborative features. Features include: * High-performance WebGL-based geospatial visualizations * Real-time commenting and annotation system * Contextual discussions tied to specific data points To create a new project from the deckgl-discuss example run this: ```bash npx giget gh:sqlrooms/examples/deckgl-discuss my-new-app/ ``` #### Running locally ```sh npm install npm run dev ``` ## Graph and embedding visualization ### [Cosmos – Graph Visualization](http://sqlrooms-cosmos.netlify.app/) [Try live](http://sqlrooms-cosmos.netlify.app/) | [GitHub repo](https://github.com/sqlrooms/examples/tree/main/cosmos) | [Open in StackBlitz](https://stackblitz.com/github/sqlrooms/examples/tree/main/cosmos?embed=1\&file=src/app.tsx) [![Netlify Status](https://api.netlify.com/api/v1/badges/9e7cb117-0355-406d-88f8-54bf6d9050a0/deploy-status)](https://app.netlify.com/projects/sqlrooms-cosmos/deploys) An example demonstrating integration with the [Cosmos](https://github.com/cosmograph-org/cosmos) GPU-accelerated graph visualization library. Features include: * WebGL-based force-directed layout computation * High-performance rendering of large networks * Real-time interaction and filtering capabilities * Customizable visual attributes and physics parameters * Event handling for node/edge interactions To create a new project from the cosmos example run this: ```bash npx giget gh:sqlrooms/examples/cosmos my-new-app/ ``` #### Running locally ```sh npm install npm dev ``` ### [Cosmos – 2D Embedding Visualization](http://sqlrooms-cosmos-embedding.netlify.app/) [Try live](http://sqlrooms-cosmos-embedding.netlify.app/) | [GitHub repo](https://github.com/sqlrooms/examples/tree/main/cosmos-embedding) | [Open in StackBlitz](https://stackblitz.com/github/sqlrooms/examples/tree/main/cosmos-embedding?embed=1\&file=src/app.tsx) [![Netlify Status](https://api.netlify.com/api/v1/badges/da9fa044-3770-40c1-80cb-224db20de6d4/deploy-status)](https://app.netlify.com/projects/sqlrooms-cosmos-embedding/deploys) An example showcasing integration with Cosmos for visualizing high-dimensional data in 2D space. Features include: * WebGL-powered rendering of 2D embeddings * GPU-accelerated positioning and transitions * Dynamic mapping of data attributes to visual properties * Efficient handling of large-scale embedding datasets * Interactive exploration with pan, zoom, and filtering To create a new project from the cosmos-embedding example run this: ```bash npx giget gh:sqlrooms/examples/cosmos-embedding my-new-app/ ``` #### Running locally ```sh npm install npm dev ``` ## Charts ### [Next.js + Recharts Example](https://sqlrooms-nextjs.netlify.app/) [Try live](https://sqlrooms-nextjs.netlify.app/) | [GitHub repo](https://github.com/sqlrooms/examples/tree/main/nextjs) | [Open in StackBlitz](https://stackblitz.com/github/sqlrooms/examples/tree/main/nextjs?embed=1) [![Netlify Status](https://api.netlify.com/api/v1/badges/3b7e32f9-b8f0-4da1-8ae7-6fa7c0fd9589/deploy-status)](https://app.netlify.com/projects/sqlrooms-nextjs/deploys) A minimalistic [Next.js](https://nextjs.org/) app example featuring: * [Recharts module](/api/recharts) for data visualization * [Tailwind 4](https://tailwindcss.com/blog/tailwindcss-v4) for styling To create a new project from the Next.js example run this: ```bash npx giget gh:sqlrooms/examples/nextjs my-new-app/ ``` #### Running locally ```sh npm install npm dev ``` ### [Mosaic Interactive Visualization Example](https://sqlrooms-mosaic.netlify.app/) [Try live](https://sqlrooms-mosaic.netlify.app/) | [GitHub repo](https://github.com/sqlrooms/examples/tree/main/mosaic) | [Open in StackBlitz](https://stackblitz.com/github/sqlrooms/examples/tree/main/mosaic?embed=1\&file=src/app.tsx) [![Netlify Status](https://api.netlify.com/api/v1/badges/e67a893c-87ac-409d-ac54-3d31e431bb0b/deploy-status)](https://app.netlify.com/projects/sqlrooms-mosaic/deploys) An example demonstrating integration with [Mosaic](https://idl.uw.edu/mosaic/), a powerful interactive visualization framework utilizing DuckDB and high-performance cross-filtering. Features include: * Complete project setup using Vite and TypeScript * Comprehensive data source management and configuration * Seamless integration with Mosaic for interactive visualizations * Real-time cross-filtering capabilities across multiple views * Example dashboards with common visualization types To create a new project from the mosaic example run this: ```bash npx giget gh:sqlrooms/examples/mosaic my-new-app/ ``` #### Running locally ```sh npm install npm dev ``` ## Other examples ### [MotherDuck Cloud Query Editor](https://motherduck.sqlrooms.org/) [Try live](https://motherduck.sqlrooms.org/) | [GitHub repo](https://github.com/sqlrooms/examples/tree/main/query-motherduck) | [Open in StackBlitz](https://stackblitz.com/github/sqlrooms/examples/tree/main/query-motherduck?embed=1) [![Netlify Status](https://api.netlify.com/api/v1/badges/92d69716-a7b3-4051-9b31-2016584d4d5e/deploy-status)](https://app.netlify.com/projects/sqlrooms-motherduck/deploys) A browser-based SQL query editor that connects directly to MotherDuck's cloud-hosted DuckDB using the WASM connector. Features include: * Example of using the `WasmMotherDuckDbConnector` from [`@sqlrooms/motherduck`](api/motherduck) * Connect to MotherDuck from the browser using DuckDB WASM * Run SQL queries against local and cloud datasets * Attach and query [DuckLake data lake and catalog](https://motherduck.com/docs/integrations/file-formats/ducklake/) To create a new project from the query-motherduck example run this: ```bash npx giget gh:sqlrooms/examples/query-motherduck my-new-app/ ``` ### AI RAG Example (Retrieval Augmented Generation) [GitHub repo](https://github.com/sqlrooms/examples/tree/main/ai-rag) | [Open in StackBlitz](https://stackblitz.com/github/sqlrooms/examples/tree/main/ai-rag?embed=1\&file=src/app.tsx) An example demonstrating Retrieval Augmented Generation (RAG) using SQLRooms and DuckDB for vector search. Features include: * AI chat with RAG: ask questions and get answers based on relevant documentation * Direct RAG search UI to query embedded documentation * Vector embeddings stored in DuckDB with native vector similarity search * Integration with OpenAI for embeddings and chat responses To create a new project from the ai-rag example run this: ```bash npx giget gh:sqlrooms/examples/ai-rag my-new-app/ ``` #### Setup ##### 1. Generate DuckDB Documentation Embeddings First, generate vector embeddings of the DuckDB documentation using the [sqlrooms-rag](https://pypi.org/project/sqlrooms-rag/) package: ```bash # Download DuckDB docs npx giget gh:duckdb/duckdb-web/docs ./duckdb-docs # Generate embeddings with OpenAI (requires OPENAI_API_KEY env var) OPENAI_API_KEY=your-key uvx --from sqlrooms-rag prepare-embeddings ./duckdb-docs -o public/rag/duckdb_docs.duckdb --provider openai ``` This will process all markdown files and create a DuckDB database with 1536-dim OpenAI embeddings at `public/rag/duckdb_docs.duckdb`. ##### 2. Set Your OpenAI API Key The app requires an OpenAI API key for: * Generating embeddings for your search queries (on the fly) * Powering the AI chat responses You'll be prompted to enter your API key when you start the app, or you can set it in the settings. #### Running Locally ```bash npm install npm run dev ``` Then open the app and: 1. Enter your OpenAI API key in the settings 2. Click the search icon to test RAG search directly 3. Use the AI chat to ask questions about DuckDB ## Looking for More? You can find even more example applications in our [Examples Repository](https://github.com/sqlrooms/examples). Also, check out our [Case Studies](/case-studies) page for real-world applications using SQLRooms. --- --- url: https://sqlrooms.org/case-studies.md --- # Case Studies Built something with SQLRooms? We'd love to feature it! [Submit your app](https://github.com/sqlrooms/sqlrooms/discussions/categories/case-studies) to be included on this page. ## [Foursquare Spatial Desktop](https://foursquare.com/products/spatial-desktop) [Foursquare Spatial Desktop](https://foursquare.com/products/spatial-desktop) is a powerful geospatial computing tool that transforms your desktop into a comprehensive spatial analysis environment. Built on SQLRooms, it delivers native DuckDB query performance and real-time visualization rendering—all powered natively on your machine without requiring server infrastructure. [\](https://foursquare.com/products/spatial-desktop/) Key features include: * **Native DuckDB Performance**: Run complex spatial queries on multi-GB datasets with native DuckDB integration without cloud compute dependence * **Real-time Rendering**: Harness Kepler.gl's visualization excellence to render millions of points with interactive filtering and smooth animations * **Modern Spatial Formats**: Native support for GeoParquet, PMTiles, and other formats GIS professionals need * **Flexible Data Management**: Save projects locally or to personal cloud storage without internet connectivity requirements * **Offline Capability**: Full analytical power available without cloud dependencies ## [Flowmap City](https://www.flowmap.city/) [Flowmap City](https://www.flowmap.city/) is a powerful web-based platform for visualizing and analyzing mobility data and origin-destination flows. The application helps urban planners, transportation analysts, and researchers understand movement patterns in cities and regions. The platform enables users to upload their own mobility datasets and create interactive visualizations that can be shared with stakeholders or embedded in other applications. Key features include: * **Flow Visualization**: Analyze origin-destination patterns with interactive flow maps * **Mobility Analysis**: Study commuting patterns, transportation demand, and traffic flows * **Temporal Patterns**: Explore how movement patterns change over time and seasons * **Multi-modal Analysis**: Compare different transportation modes and their usage * **Infrastructure Planning**: Make data-driven decisions for transportation infrastructure * **Interactive Filtering**: Filter and analyze specific routes, regions, or time periods ## [Cosmograph](https://cosmograph.app/) [Cosmograph](https://cosmograph.app/) is a powerful web-based application for visualizing and analyzing large graph datasets and machine learning embeddings. The application runs entirely in the browser, leveraging your GPU for all computations while keeping your data private and secure. The upcoming version of Cosmograph is being built using SQLRooms to enhance its data processing capabilities and analytical features. Key features include: * **Network Graph Visualization**: Analyze complex relationships and patterns in graph data * **ML Embeddings Analysis**: Visualize and explore machine learning embeddings in 2D space * **Temporal Analysis**: Study how relationships and patterns evolve over time * **Community Detection**: Identify clusters and anomalies within your data * **Interactive Analysis**: Use filters and histograms to explore data distributions * **GPU-Accelerated**: Performs all calculations locally using your GPU for optimal performance ## [Transcality](https://www.transcality.com/) [Transcality](https://www.transcality.com/) is a Swiss company building transport modeling software using SQLRooms. Their platform creates digital twins of transportation systems, enabling planners and engineers to simulate infrastructure changes—like adding or closing a road—and immediately see the effects on traffic flow. By leveraging SQLRooms, Transcality enables visualization, filtering, and aggregation of simulation results to run directly on end-users' machines, providing fast and interactive exploration of traffic scenarios. Key features include: * **Traffic Flow Modeling**: Simulate and analyze traffic patterns at various resolutions * **Infrastructure Scenarios**: Model the impact of road closures, new routes, or construction * **Digital Transportation Twins**: Build comprehensive models of transportation systems * **Local Data Exploration**: Visualize, filter, and aggregate simulation results directly in the browser ## [ChordShell.com](https://www.chordshell.com/) {#chordshellcom} [ChordShell.com](https://www.chordshell.com/) is a harmony workspace for musicians built with SQLRooms. It adapts SQLRooms' room, artifact, layout, document, and AI primitives for music theory workflows, bringing chord exploration, scale exploration, chord sheets, recordings, notes, and an assistant into one composable workspace. ChordShell demonstrates SQLRooms beyond traditional analytics dashboards: musical structures become inspectable workspace state, and the assistant can use the same application tools as the UI to analyze theory and make edits. Key features include: * **Domain-specific Artifacts**: Compose chords explorers, scales explorers, chord sheets, recordings, and documents inside a shared SQLRooms workspace * **Interactive Harmony Exploration**: Explore guitar and piano voicings, tunings, note spellings, chord tones, scales, and circle-of-fifths relationships * **AI-Assisted Music Theory**: Parse chord symbols, analyze notes, harmonize scales, suggest scales over chords, and update workspace artifacts through app-level tools * **Composable Workspace Model**: Keep musical context visible across panels so chord sheets, theory views, recordings, notes, and assistant conversations can work together * **Musician-Friendly Playback**: Audition selected chords and scales while changing voicings, instruments, and harmonic context --- --- url: https://sqlrooms.org/api/artifacts.md --- # @sqlrooms/artifacts `@sqlrooms/artifacts` provides a room-store slice and React/layout helpers for workspace artifacts such as dashboards, notebooks, canvas documents, pivot tables, and apps. Artifacts are durable workspace entries. Artifact tabs are the layout/UI adapter for opening, closing, renaming, reordering, searching, and deleting those entries. Artifacts are workspace-level entries. Embedded document content should be modeled as blocks, usually hosted stateful blocks, rather than as hidden child artifacts in the artifact registry. See the [Artifacts developer guide](https://sqlrooms.org/artifacts) for the workspace model, lifecycle guidance, and end-to-end setup. ## Usage ```tsx import { ArtifactTabs, ArtifactsSliceConfig, createArtifactTypeFromStatefulBlock, createArtifactPanelDefinition, createArtifactsSlice, defineArtifactTypes, useArtifactWorkspace, } from '@sqlrooms/artifacts'; const artifactTypes = defineArtifactTypes({ notebook: { label: 'Notebook', defaultTitle: 'Notebook', icon: FileTextIcon, component: NotebookPanel, onCreate: ({artifactId, store}) => { store.getState().notebook.ensureArtifact(artifactId); }, onEnsure: ({artifactId, store}) => { store.getState().notebook.ensureArtifact(artifactId); }, onDelete: ({artifactId, store}) => { store.getState().notebook.removeArtifact(artifactId); }, }, }); const store = createRoomStore( persistSliceConfigs( { name: 'my-room', sliceConfigSchemas: { artifacts: ArtifactsSliceConfig, }, }, (set, get, store) => ({ ...createArtifactsSlice({artifactTypes})(set, get, store), layout: { panels: { artifact: createArtifactPanelDefinition(artifactTypes, store), }, }, }), ), ); ``` ```tsx ``` Use `ArtifactTabs.useActions()` from custom subcomponents when you need access to the tab adapter actions, and use `overlay` for dialogs or other elements that need that context without being rendered inside the tab strip. For non-tab artifact surfaces, use `useArtifactWorkspace()` directly: ```tsx const artifacts = useArtifactWorkspace({ types: ['notebook', 'dashboard'], }); return artifacts.selectedArtifact ? ( ) : ( artifacts.createArtifact('notebook')} /> ); ``` ## Slice API Config uses artifact terminology throughout: * `artifacts.config.artifactsById` * `artifacts.config.artifactOrder` * `artifacts.config.pinnedArtifactIds` * `artifacts.config.currentArtifactId` * `artifacts.createArtifact({type, title?, id?})` * `artifacts.ensureArtifact(id, {type, title?})` * `artifacts.renameArtifact(id, title)` * `artifacts.closeArtifact(id)` * `artifacts.deleteArtifact(id)` * `artifacts.setCurrentArtifact(id?)` * `artifacts.setArtifactOrder(order)` * `artifacts.togglePinArtifact(id)` * `artifacts.isPinnedArtifact(id)` * `artifacts.getArtifact(id)` `closeArtifact` is non-destructive. It runs close lifecycle cleanup, while the tab adapter hides the layout tab so it can be reopened from search. `deleteArtifact` is destructive. It runs close and delete lifecycle hooks, then removes the artifact registry entry. ## Artifact Tabs * `useArtifactWorkspace({types?, selectFallback?})` returns tab-free artifact ids, descriptors, current selection, type definitions, and create/delete/ rename/select actions. It is useful for single-content hosts, sidebars, and search/create surfaces that should not adopt layout-tab behavior. * `useArtifactTabs({tabsId?, types?, panelKey?})` returns TabStrip-compatible descriptors, open tab ids, selected id, and handlers. It builds on `useArtifactWorkspace()` and adds the layout-tabs adapter. * `ArtifactTabs` is a compound component over `TabStrip` and `TabsLayout.TabContent`. * Pass `forceMountContent` to `ArtifactTabs` to keep visible artifact tab panels mounted while hiding inactive panels. * `ArtifactTabs.useActions()` exposes the current tab adapter actions to custom subcomponents rendered under `ArtifactTabs`. * `createArtifactLayoutNode(artifactId, panelKey?)` creates a stable layout panel node for an artifact. * `createArtifactPanelDefinition(artifactTypes, store)` resolves artifact panel titles, icons, and components from the runtime type registry. Type definitions are runtime configuration and are not persisted. Set `canCreate: false` on a type definition when an app needs to render an existing artifact as a read-only compatibility surface without showing it in creation menus or allowing `createArtifact()` calls for that type. ## Stateful Block Bridge Feature packages can expose reusable stateful block definitions from `@sqlrooms/blocks`. Use `createArtifactTypeFromStatefulBlock()` when a stateful block should also be available as a top-level artifact shell: ```tsx const artifactTypes = defineArtifactTypes({ dashboard: createArtifactTypeFromStatefulBlock(dashboardBlockDefinition), }); ``` The artifact shell still owns workspace metadata such as id, title, tabs, current selection, and AI context. The stateful block definition owns the feature-specific rendering and backing-state lifecycle. ## Entry Points | Import | Contains | Pulls React | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------- | ----------- | | `@sqlrooms/artifacts` | Slices, components, artifact types — the full package | Yes | | `@sqlrooms/artifacts/config` | Serializable shapes only: `ArtifactMetadata`, `ArtifactsSliceConfig`, `ArtifactType`, `ArtifactSessionLink(Schema)` | No | | `@sqlrooms/artifacts/ai` | Assistant tools for artifact context | No | Prefer `@sqlrooms/artifacts/config` when you only need the persisted data model and want to stay out of the React dependency — a test runner, a config migration, or server-side code: ```ts import { ArtifactMetadata, ArtifactSessionLinkSchema, } from '@sqlrooms/artifacts/config'; const link = ArtifactSessionLinkSchema.parse(row); ``` Import these shapes from `/config` rather than from an internal module path such as `@sqlrooms/artifacts/dist/ArtifactsSliceConfig`. The subpath is the supported surface and will keep working when the underlying modules are reorganized; internal paths have moved before. All entries target bundlers and transpilers (`moduleResolution: "bundler"`), so emitted re-exports are extensionless. Being React-free is what makes `/config` and `/ai` usable from Node toolchains, not native `node` resolution of the published files. ## AI Context Tools `@sqlrooms/artifacts/ai` provides reusable assistant tools for artifact context: * `list_context_artifacts` * `read_context_artifact` * `set_primary_context_artifact` Use `createArtifactContextAiTools({store, readArtifact})` in apps that combine `@sqlrooms/artifacts` with `@sqlrooms/ai`. The factory handles primary artifact selection and run-context updates; the app supplies artifact payload readers for domain-specific types such as documents or dashboards. Apps with capability profiles can pass `isArtifactAllowed` to apply the same eligibility rule when listing, reading, or selecting a primary context artifact. Artifact-aware room commands can use `resolveArtifactTargetId()` to preserve the same per-turn target. Its precedence is an explicit command artifact ID, then an AI invocation's captured artifact target, then the live current artifact for non-AI and compatibility fallback behavior. The helper depends only on room command invocation data; it does not require artifact-owned sessions or a context-selector UI. ## Artifact-Owned AI Sessions `@sqlrooms/artifacts/ai` also provides `createArtifactAiSlice()` for apps that want AI chats to belong to the current artifact without changing the generic chat session schema. Add `ArtifactAiConfigSchema` to persistence and compose the slice after `createArtifactsSlice()` and the app's AI slice: ```tsx import { ArtifactAiConfigSchema, createArtifactAiSlice, } from '@sqlrooms/artifacts/ai'; const store = createRoomStore( persistSliceConfigs( { name: 'my-room', sliceConfigSchemas: { artifacts: ArtifactsSliceConfig, artifactAi: ArtifactAiConfigSchema, }, }, (set, get, store) => ({ ...createArtifactsSlice({artifactTypes})(set, get, store), ...createAiSlice(aiOptions)(set, get, store), ...createArtifactAiSlice()(set, get, store), }), ), ); ``` The slice stores a list of links between sessions and artifacts: ```ts sessionArtifactLinks: ArtifactSessionLink[]; ``` `sessionArtifactLinks` is the only supported persisted and runtime representation. The prerelease-only `aiSessionArtifacts` and `artifactCreators` fields were removed without an automatic migration. Update persisted prerelease configs before parsing them with `ArtifactAiConfigSchema`. The link type is exported from `@sqlrooms/artifacts`: * `ArtifactSessionLink` — a single association: `{sessionId, artifactId, linkedAt}`. `linkedAt` is a Unix timestamp in milliseconds. A session may be associated with multiple artifacts. * `ArtifactSessionLinkSchema` — the Zod schema used to validate a link (for example when persisting `ArtifactAiConfigSchema`). Links intentionally describe association only. If an app needs creation provenance, store it with the artifact's domain metadata instead of overloading the chat association. All artifact AI session helpers accept `sessionArtifactLinks`; they do not accept a parallel one-to-one association map. Use `artifactAi.createArtifactScopedSession()` when creating chats from an artifact-scoped assistant. It creates a fresh session and associates it with the current artifact. `artifactAi.selectLatestSessionForArtifact()` and `artifactAi.syncCurrentArtifactAiSession()` keep the current AI session aligned with `artifacts.config.currentArtifactId`. Sessions without an explicit artifact association are ignored by artifact-scoped history. Reusable helpers include: * `isAiSessionVisibleForArtifact` * `getLatestAiSessionIdForArtifact` * `getEmptyAiSessionIdForArtifact` * `getAiSessionIdsForArtifact` * `getAiSessionGroupsByArtifact` * `getRunningAiSessionCountsByArtifact` * `getOwningArtifactRunContextItems` `getEmptyAiSessionIdForArtifact()` requires session objects that include `prompt` and `uiMessages`; summary-only session rows cannot prove that a chat is empty. `Chat.History` should remain generic: pass a `filterSession` callback built with `isAiSessionVisibleForArtifact()` and keep artifact-specific labels in the host app. For run context, use `getOwningArtifactRunContextItems()` to prepend the owning artifact as the implicit primary context item. Artifact-owned AI sessions attach to artifact shells. If a stateful block is wrapped as a top-level artifact, use that artifact id. Stateful blocks hosted directly inside a block document should use the containing artifact's chat unless the host app intentionally introduces a block-scoped chat model. ## Type Aliases * [ArtifactLifecycleContext](type-aliases/ArtifactLifecycleContext.md) * [ArtifactRenameLifecycleContext](type-aliases/ArtifactRenameLifecycleContext.md) * [ArtifactTypeDefinition](type-aliases/ArtifactTypeDefinition.md) * [ArtifactTypeDefinitions](type-aliases/ArtifactTypeDefinitions.md) * [ArtifactsSliceState](type-aliases/ArtifactsSliceState.md) * [CreateArtifactsSliceProps](type-aliases/CreateArtifactsSliceProps.md) * [RoomStateWithArtifacts](type-aliases/RoomStateWithArtifacts.md) * [RoomStateWithArtifactsAndLayout](type-aliases/RoomStateWithArtifactsAndLayout.md) * [ArtifactType](type-aliases/ArtifactType.md) * [ArtifactMetadata](type-aliases/ArtifactMetadata.md) * [ArtifactsSliceConfig](type-aliases/ArtifactsSliceConfig.md) * [ArtifactSessionLink](type-aliases/ArtifactSessionLink.md) * [ArtifactTabDescriptor](type-aliases/ArtifactTabDescriptor.md) * [UseArtifactTabsOptions](type-aliases/UseArtifactTabsOptions.md) * [UseArtifactTabsResult](type-aliases/UseArtifactTabsResult.md) * [ArtifactTabsProps](type-aliases/ArtifactTabsProps.md) * [ArtifactWorkspaceSelectFallback](type-aliases/ArtifactWorkspaceSelectFallback.md) * [ArtifactWorkspaceDescriptor](type-aliases/ArtifactWorkspaceDescriptor.md) * [UseArtifactWorkspaceOptions](type-aliases/UseArtifactWorkspaceOptions.md) * [UseArtifactWorkspaceResult](type-aliases/UseArtifactWorkspaceResult.md) ## Variables * [ArtifactType](variables/ArtifactType.md) * [ArtifactMetadata](variables/ArtifactMetadata.md) * [ArtifactsSliceConfig](variables/ArtifactsSliceConfig.md) * [ArtifactSessionLinkSchema](variables/ArtifactSessionLinkSchema.md) * [ArtifactTabs](variables/ArtifactTabs.md) ## Functions * [defineArtifactTypes](functions/defineArtifactTypes.md) * [createArtifactTypeFromStatefulBlock](functions/createArtifactTypeFromStatefulBlock.md) * [createArtifactsSlice](functions/createArtifactsSlice.md) * [useStoreWithArtifacts](functions/useStoreWithArtifacts.md) * [useStoreWithArtifactsAndLayout](functions/useStoreWithArtifactsAndLayout.md) * [createArtifactLayoutNode](functions/createArtifactLayoutNode.md) * [createArtifactPanelDefinition](functions/createArtifactPanelDefinition.md) * [useArtifactTabs](functions/useArtifactTabs.md) * [resolveArtifactTargetId](functions/resolveArtifactTargetId.md) * [useArtifactWorkspace](functions/useArtifactWorkspace.md) ## References ### ArtifactMetadataType Renames and re-exports [ArtifactMetadata](variables/ArtifactMetadata.md) *** ### ArtifactsSliceConfigType Renames and re-exports [ArtifactsSliceConfig](variables/ArtifactsSliceConfig.md) *** ### ArtifactTypeType Renames and re-exports [ArtifactType](variables/ArtifactType.md) --- --- url: https://sqlrooms.org/api/blocks.md --- # @sqlrooms/blocks Shared block contracts for SQLRooms packages. This package owns vocabulary and type shapes only. Concrete block implementations remain in the feature packages that own their state. ## Type Aliases * [BlockId](type-aliases/BlockId.md) * [BlockType](type-aliases/BlockType.md) * [BlockInstance](type-aliases/BlockInstance.md) * [BlockCapability](type-aliases/BlockCapability.md) * [BlockCapabilities](type-aliases/BlockCapabilities.md) * [BlockOwnership](type-aliases/BlockOwnership.md) * [BlockReference](type-aliases/BlockReference.md) * [OrderedBlockContainer](type-aliases/OrderedBlockContainer.md) * [GraphBlockEdgeKind](type-aliases/GraphBlockEdgeKind.md) * [GraphBlockEdge](type-aliases/GraphBlockEdge.md) * [GraphBlockContainer](type-aliases/GraphBlockContainer.md) * [StatefulBlockContext](type-aliases/StatefulBlockContext.md) * [StatefulBlockRenameContext](type-aliases/StatefulBlockRenameContext.md) * [StatefulBlockRenderProps](type-aliases/StatefulBlockRenderProps.md) * [StatefulBlockSettingsProps](type-aliases/StatefulBlockSettingsProps.md) * [StatefulBlockDefinition](type-aliases/StatefulBlockDefinition.md) --- --- url: https://sqlrooms.org/api/db.md --- # @sqlrooms/db DuckDB-centered orchestration for SQLRooms multi-database execution. Most applications receive this slice through `createRoomShellSlice()`. Use `createDbSlice()` directly when building a custom room store or connector host. ## Purpose * Keep DuckDB as the core runtime for SQL execution DAG semantics. * Register and route connector execution for external engines. * Aggregate connector catalogs/schemas into one explorer view. * Materialize non-DuckDB results into core DuckDB with a configurable policy. ## Basic setup ```ts import {createDbSlice} from '@sqlrooms/db'; import {createBaseRoomSlice, createRoomStore} from '@sqlrooms/room-store'; const {roomStore} = createRoomStore((set, get, store) => ({ ...createBaseRoomSlice()(set, get, store), ...createDbSlice()(set, get, store), })); await roomStore.getState().db.initialize(); const result = await roomStore.getState().db.connectors.runQuery({ sql: 'select 42 as answer', queryType: 'arrow', }); ``` The core DuckDB connection is registered automatically. Existing DuckDB APIs, including `useSql()` and `useDataTable()`, are re-exported from this package. ## Add an external connection A direct connector runs in the current JavaScript runtime. A bridge delegates execution to a server when a driver cannot run in the browser. ```ts import {createHttpDbBridge} from '@sqlrooms/db'; const {db} = roomStore.getState(); db.connectors.registerBridge(createHttpDbBridge({id: 'server', baseUrl: '/'})); db.connectors.registerConnection({ id: 'warehouse', engineId: 'postgres', title: 'Warehouse', runtimeSupport: 'server', requiresBridge: true, bridgeId: 'server', }); const result = await db.connectors.runQuery({ connectionId: 'warehouse', sql: 'select * from orders', queryType: 'arrow', materialize: true, materializedName: 'orders', }); ``` Arrow results from external connections are materialized into core DuckDB by default, allowing downstream SQLRooms features to query them through one local execution graph. Set `materialize: false` when the caller will consume the returned Arrow table directly. Use `registerConnector(connectionId, connector)` instead of a bridge when the connector implements `DbConnector` in the current runtime. ## Notes * This package is intentionally additive and keeps `@sqlrooms/duckdb` APIs intact. * Default materialization strategy is strict ephemeral attached database mode. * `@sqlrooms/db/bridge` and `@sqlrooms/db/connectors/duckdb` are supported focused entry points for hosts that do not need the complete root export. ## Interfaces * [BaseDuckDbConnectorOptions](interfaces/BaseDuckDbConnectorOptions.md) * [BaseDuckDbConnectorImpl](interfaces/BaseDuckDbConnectorImpl.md) * [QueryOptions](interfaces/QueryOptions.md) * [DuckDbConnector](interfaces/DuckDbConnector.md) * [TypedRowAccessor](interfaces/TypedRowAccessor.md) ## Type Aliases * [CreateDbSliceProps](type-aliases/CreateDbSliceProps.md) * [RuntimeSupport](type-aliases/RuntimeSupport.md) * [DbEngineId](type-aliases/DbEngineId.md) * [CoreMaterializationStrategy](type-aliases/CoreMaterializationStrategy.md) * [CoreMaterializationStrategy](type-aliases/CoreMaterializationStrategy-1.md) * [CoreMaterializationConfig](type-aliases/CoreMaterializationConfig.md) * [CoreMaterializationConfig](type-aliases/CoreMaterializationConfig-1.md) * [DbConnection](type-aliases/DbConnection.md) * [CatalogDatabase](type-aliases/CatalogDatabase.md) * [CatalogSchema](type-aliases/CatalogSchema.md) * [CatalogTable](type-aliases/CatalogTable.md) * [CatalogColumn](type-aliases/CatalogColumn.md) * [CatalogTableDetails](type-aliases/CatalogTableDetails.md) * [DbConnectorCapabilities](type-aliases/DbConnectorCapabilities.md) * [DbConnector](type-aliases/DbConnector.md) * [DbBridge](type-aliases/DbBridge.md) * [QueryExecutionRequest](type-aliases/QueryExecutionRequest.md) * [QueryExecutionResult](type-aliases/QueryExecutionResult.md) * [CatalogEntry](type-aliases/CatalogEntry.md) * [DbSliceConfig](type-aliases/DbSliceConfig.md) * [DbSliceState](type-aliases/DbSliceState.md) * [DbRootState](type-aliases/DbRootState.md) * [QueryHandle](type-aliases/QueryHandle.md) * [FunctionSuggestion](type-aliases/FunctionSuggestion.md) * [GroupedFunctionSuggestion](type-aliases/GroupedFunctionSuggestion.md) * [QualifiedTableName](type-aliases/QualifiedTableName.md) * [TableIdentity](type-aliases/TableIdentity.md) * [FullTableIdentity](type-aliases/FullTableIdentity.md) * [RawSqlTableReference](type-aliases/RawSqlTableReference.md) * [ResolveTableReferenceResult](type-aliases/ResolveTableReferenceResult.md) * [SplitSqlStatementsOptions](type-aliases/SplitSqlStatementsOptions.md) * [SeparatedStatements](type-aliases/SeparatedStatements.md) * [ColumnTypeCategory](type-aliases/ColumnTypeCategory.md) * [ColumnTypeLike](type-aliases/ColumnTypeLike.md) * [DbSchemaNode](type-aliases/DbSchemaNode.md) * [NodeObject](type-aliases/NodeObject.md) * [ColumnNodeObject](type-aliases/ColumnNodeObject.md) * [TableNodeObject](type-aliases/TableNodeObject.md) * [SchemaNodeObject](type-aliases/SchemaNodeObject.md) * [DatabaseNodeObject](type-aliases/DatabaseNodeObject.md) * [SchemaWithTables](type-aliases/SchemaWithTables.md) * [TableColumn](type-aliases/TableColumn.md) * [DataTable](type-aliases/DataTable.md) ## Variables * [RuntimeSupport](variables/RuntimeSupport.md) * [DbEngineId](variables/DbEngineId.md) * [DbConnection](variables/DbConnection.md) * [escapeVal](variables/escapeVal.md) * [escapeId](variables/escapeId.md) * [isNumericDuckType](variables/isNumericDuckType.md) * [getSqlErrorWithPointer](variables/getSqlErrorWithPointer.md) * [getFunctionDocumentation](variables/getFunctionDocumentation.md) * [getFunctionSuggestions](variables/getFunctionSuggestions.md) ## Functions * [createDbSlice](functions/createDbSlice.md) * [useStoreWithDb](functions/useStoreWithDb.md) * [createHttpDbBridge](functions/createHttpDbBridge.md) * [createCoreDuckDbConnection](functions/createCoreDuckDbConnection.md) * [isCoreDuckDbConnection](functions/isCoreDuckDbConnection.md) * [getCoreDuckDbConnectionId](functions/getCoreDuckDbConnectionId.md) * [useDataTable](functions/useDataTable.md) * [useSql](functions/useSql.md) * [createBaseDuckDbConnector](functions/createBaseDuckDbConnector.md) * [arrowTableToJson](functions/arrowTableToJson.md) * [isQualifiedTableName](functions/isQualifiedTableName.md) * [makeQualifiedTableName](functions/makeQualifiedTableName.md) * [getTableIdentity](functions/getTableIdentity.md) * [getFullTableIdentity](functions/getFullTableIdentity.md) * [parseQualifiedSqlIdentifier](functions/parseQualifiedSqlIdentifier.md) * [parseTableIdentity](functions/parseTableIdentity.md) * [parseFullTableIdentity](functions/parseFullTableIdentity.md) * [parseTableIdentityToQualifiedName](functions/parseTableIdentityToQualifiedName.md) * [getUnqualifiedSqlIdentifier](functions/getUnqualifiedSqlIdentifier.md) * [getRawSqlTableReference](functions/getRawSqlTableReference.md) * [quoteParsedRawSqlTableReference](functions/quoteParsedRawSqlTableReference.md) * [~~quoteTableReference~~](functions/quoteTableReference.md) * [getTableDisplayName](functions/getTableDisplayName.md) * [resolveTableReference](functions/resolveTableReference.md) * [getColValAsNumber](functions/getColValAsNumber.md) * [splitSqlStatements](functions/splitSqlStatements.md) * [sanitizeQuery](functions/sanitizeQuery.md) * [makeLimitQuery](functions/makeLimitQuery.md) * [separateLastStatement](functions/separateLastStatement.md) * [joinStatements](functions/joinStatements.md) * [load](functions/load.md) * [loadCSV](functions/loadCSV.md) * [loadJSON](functions/loadJSON.md) * [loadParquet](functions/loadParquet.md) * [loadSpatial](functions/loadSpatial.md) * [loadObjects](functions/loadObjects.md) * [sqlFrom](functions/sqlFrom.md) * [literalToSQL](functions/literalToSQL.md) * [createDbSchemaTrees](functions/createDbSchemaTrees.md) * [~~getAllTablesFromSchemaTrees~~](functions/getAllTablesFromSchemaTrees.md) * [findTableInSchemaTrees](functions/findTableInSchemaTrees.md) * [getDuckDbTypeCategory](functions/getDuckDbTypeCategory.md) * [getArrowColumnTypeCategory](functions/getArrowColumnTypeCategory.md) * [getColumnTypeCategory](functions/getColumnTypeCategory.md) * [isColumnNumeric](functions/isColumnNumeric.md) * [isColumnTemporal](functions/isColumnTemporal.md) * [isColumnQuantitative](functions/isColumnQuantitative.md) * [isColumnCategorical](functions/isColumnCategorical.md) * [columnTypeCategoryToSelectorType](functions/columnTypeCategoryToSelectorType.md) * [createTypedRowAccessor](functions/createTypedRowAccessor.md) --- --- url: https://sqlrooms.org/api/duckdb.md --- # @sqlrooms/duckdb A powerful wrapper around DuckDB-WASM that provides React hooks and utilities for working with DuckDB in browser environments. ## Features ### React Integration & Type Safety * **React Hooks**: Seamless integration with React applications via `useSql` and `useDataTable` * **Runtime Validation**: Optional Zod schema validation for query results with type transformations * **Typed Row Accessors**: Type-safe row access with validation and multiple iteration methods ### Data Management * **File Operations**: Import data from various file formats (CSV, JSON, Parquet) with auto-detection * **Arrow Integration**: Work directly with Apache Arrow tables for efficient columnar data processing * **Schema Management**: Comprehensive database, schema, and table discovery and management * **Qualified Table Names**: Full support for `database.schema.table` naming convention ### Performance & Operations * **Query Deduplication**: Automatic deduplication of identical running queries to prevent duplicate execution * **Query Cancellation**: Cancel running queries with full composability support via `QueryHandle` interface ([learn more](https://sqlrooms.org/query-cancellation)) * **Data Export**: Export query results to CSV files with pagination for large datasets * **Batch Processing**: Handle large datasets efficiently with built-in pagination support ### SQL Statement Utilities `splitSqlStatements` splits DuckDB SQL without treating semicolons inside quoted strings, dollar-quoted strings, or comments as statement boundaries. Comments are removed safely by default; preserve them when the original SQL will be rewritten or re-executed: ```ts import {splitSqlStatements} from '@sqlrooms/duckdb'; const statements = splitSqlStatements(sql, {removeComments: false}); ``` ## Installation ```bash npm install @sqlrooms/duckdb ``` ## Basic Usage ### Using the SQL Hook ```tsx import {useSql} from '@sqlrooms/duckdb'; function UserList() { // Basic usage with TypeScript types const {data, isLoading, error} = useSql<{id: number; name: string}>({ query: 'SELECT id, name FROM users', }); if (isLoading) return
Loading...
; if (error) return
Error: {error.message}
; if (!data) return null; return (
    {Array.from(data.rows()).map((user) => (
  • {user.name}
  • ))}
); } ``` For more information and examples on using the `useSql` hook, see the [useSql API documentation](/api/duckdb/functions/useSql). ### Monitoring WebSocket DuckDB Connections `createWebSocketDuckDbConnector()` exposes the persistent WebSocket lifecycle through `connectionStatus`, `subscribeConnectionStatus()`, and the optional `onConnectionStatusChange` callback. Use this for live UI affordances such as lost-connection dialogs. Call `reconnect()` to reopen the socket and rerun the connector initialization SQL without destroying the connector instance. ```tsx import {createWebSocketDuckDbConnector} from '@sqlrooms/duckdb'; const connector = createWebSocketDuckDbConnector({ wsUrl: 'ws://localhost:4000', onConnectionStatusChange: (status) => { console.log('DuckDB WebSocket status:', status); }, }); const unsubscribe = connector.subscribeConnectionStatus((status) => { if (status === 'disconnected') { console.warn('DuckDB WebSocket disconnected'); } }); await connector.reconnect(); ``` ### Looking up Table Metadata Use `useDataTable()` in React components or `db.findTable()` from the room store. String references are parsed like SQL identifiers, so use quotes for literal dots in table names. ```tsx import {useDataTable} from '@sqlrooms/duckdb'; function TableColumns() { const table = useDataTable('"memory"."main"."earthquakes"'); return (
    {table?.columns.map((column) => (
  • {column.name}
  • ))}
); } ``` ### Using Zod for Runtime Validation ```tsx import {useSql} from '@sqlrooms/duckdb'; import {z} from 'zod'; const userSchema = z.object({ id: z.number(), name: z.string(), email: z.string().email(), created_at: z.string().transform((str) => new Date(str)), }); function ValidatedUserList() { const {data, isLoading, error} = useSql(userSchema, { query: 'SELECT id, name, email, created_at FROM users', }); if (isLoading) return
Loading...
; if (error) { if (error instanceof z.ZodError) { return
Validation Error: {error.errors[0].message}
; } return
Error: {error.message}
; } if (!data) return null; return (
    {data.toArray().map((user) => (
  • {user.name} ({user.email}) - Joined:{' '} {user.created_at.toLocaleDateString()}
  • ))}
); } ``` ### Accessing the Underlying Arrow Table and Schema You can access the underlying Arrow table and schema of a `useSql()` query result. This is especially useful if you want to pass the data to a library that expect an Apache Arrow Table as input without additional data transformation: ```tsx import {useSql} from '@sqlrooms/duckdb'; function ArrowTableSchemaExample() { const {data, isLoading, error} = useSql({ query: 'SELECT id, name FROM users', }); if (isLoading) return
Loading...
; if (error) return
Error: {error.message}
; if (!data || !data.arrowTable) return null; const {arrowTable} = data; const fields = arrowTable.schema.fields; const numRows = arrowTable.numRows; return ( {fields.map((field) => ( ))} {Array.from({length: numRows}).map((_, rowIdx) => ( {fields.map((field, colIdx) => ( ))} ))}
{field.name}
{String(arrowTable.getChildAt(colIdx)?.get(rowIdx) ?? '')}
); } ``` ## Working with Tables ### Using the Store for Direct Database Operations ```tsx import {useRoomStore} from './store'; import {Button} from '@sqlrooms/ui'; function DatabaseManager() { const createTableFromQuery = useRoomStore( (state) => state.db.createTableFromQuery, ); const addTable = useRoomStore((state) => state.db.addTable); const dropTable = useRoomStore((state) => state.db.dropTable); const tables = useRoomStore((state) => state.db.tables); const refreshTableSchemas = useRoomStore( (state) => state.db.refreshTableSchemas, ); // Create a table from a query const handleCreateTable = async () => { const result = await createTableFromQuery( 'filtered_users', 'SELECT * FROM users WHERE active = true', ); console.log(`Created table with ${result.rowCount} rows`); }; // Add a table from JavaScript objects const handleAddTable = async () => { const users = [ {id: 1, name: 'Alice', email: 'alice@example.com'}, {id: 2, name: 'Bob', email: 'bob@example.com'}, ]; await addTable('new_users', users); }; // Drop a table const handleDropTable = async () => { await dropTable('old_table'); }; return (

Available Tables:

    {tables.map((table) => (
  • {table.table.toString()} ({table.columns.length} columns)
  • ))}
); } ``` ### Working with Qualified Table Names ```tsx import {quoteTableReference, resolveTableReference} from '@sqlrooms/duckdb'; import {useRoomStore} from './store'; import {Button} from '@sqlrooms/ui'; function QualifiedTableOps() { const qualifyTableName = useRoomStore((state) => state.db.qualifyTableName); const createTableFromQuery = useRoomStore( (state) => state.db.createTableFromQuery, ); const dropTable = useRoomStore((state) => state.db.dropTable); const checkTableExists = useRoomStore((state) => state.db.checkTableExists); const run = async () => { // Store-aware qualification knows which database is the default. const qualifiedTable = qualifyTableName({ database: 'mydb', schema: 'public', table: 'users', }); // toString() is the canonical portable table ID; toFullString() includes // the database when explicit catalog qualification is needed. const tableSql = quoteTableReference(qualifiedTable.toString()); const resolved = resolveTableReference([{table: qualifiedTable}], 'users'); await createTableFromQuery(qualifiedTable, 'SELECT * FROM source_table'); console.log('Quoted table reference:', tableSql); console.log('Fully qualified reference:', qualifiedTable.toFullString()); console.log('Resolved table:', resolved.table?.table.toString()); const tableExists = await checkTableExists(qualifiedTable); console.log('Table exists after create:', tableExists); await dropTable(qualifiedTable); }; return ; } ``` ### Loading the Schema Catalog Use `loadSchemaCatalog()` when you need the database/schema/table hierarchy, including empty schemas and attached databases whose `main` schema has no tables yet. The catalog filter receives typed entries for databases, schemas, and tables, so schema visibility does not depend on fake table names. Related option and filter types are exported for callers that wrap these helpers in their own APIs. ```ts import { defaultLoadSchemaCatalogFilter, loadSchemaCatalog, } from '@sqlrooms/duckdb'; const catalog = await loadSchemaCatalog(connector, { filterFunction: (entry) => entry.type === 'schema' && entry.schema === 'scratch' ? false : defaultLoadSchemaCatalogFilter(entry), }); ``` ## Loading Data from Files ### Using Load Functions Directly ```tsx import {loadCSV, loadJSON, loadParquet, loadObjects} from '@sqlrooms/duckdb'; import {useRoomStore} from './store'; import {Button} from '@sqlrooms/ui'; function DataLoader() { const getConnector = useRoomStore((state) => state.db.getConnector); const handleLoadCSV = async (file: File) => { const connector = await getConnector(); // Generate SQL to load CSV file const sql = loadCSV('my_table', file.name, { auto_detect: true, replace: true, }); // Execute the load operation await connector.query(sql).result; }; const handleLoadObjects = async () => { const connector = await getConnector(); const data = [ {id: 1, name: 'Alice'}, {id: 2, name: 'Bob'}, ]; // Generate SQL to load objects const sql = loadObjects('users', data, {replace: true}); await connector.query(sql).result; }; return (
{ if (e.target.files?.[0]) handleLoadCSV(e.target.files[0]); }} />
); } ``` ### Using the Connector Directly ```tsx import * as arrow from 'apache-arrow'; import {useRoomStore} from './store'; function AdvancedDataLoader() { const getConnector = useRoomStore((state) => state.db.getConnector); const handleFileUpload = async (file: File) => { try { const connector = await getConnector(); await connector.loadFile(file, 'uploaded_data', { method: 'auto', // Auto-detect file type replace: true, temp: false, }); } catch (error) { console.error('Failed to load uploaded file:', error); } }; const handleLoadArrowTable = async (arrowTable: arrow.Table) => { try { const connector = await getConnector(); await connector.loadArrow(arrowTable, 'arrow_data'); } catch (error) { console.error('Failed to load Arrow table:', error); } }; return ( { if (e.target.files?.[0]) { void handleFileUpload(e.target.files[0]); } }} /> ); } ``` ## Exporting Data to CSV ```tsx import {useExportToCsv} from '@sqlrooms/duckdb'; import {Button} from '@sqlrooms/ui'; function ExportButton() { const {exportToCsv} = useExportToCsv(); const handleExport = async () => { await exportToCsv('SELECT * FROM users ORDER BY name', 'users_export.csv'); }; return ; } ``` ## Low-Level DuckDB Access ### Basic direct usage ```tsx import {roomStore} from './store'; async function executeCustomQuery() { // Plain TS/JS usage: read connector from the store API directly. const connector = roomStore.getState().db.connector; // QueryHandle is promise-like – await it directly const result = await connector.query('SELECT COUNT(*) AS count FROM users'); // Inspect Arrow table const count = result.getChildAt(0)?.get(0); console.log(`Total users: ${count}`); } ``` ### Cancellation examples ```tsx import {roomStore} from './store'; async function cancelExample() { const connector = roomStore.getState().db.connector; // 1. Manual cancel via the handle const query = connector.query('SELECT * FROM large_table'); setTimeout(() => query.cancel(), 2000); // cancel after 2 s await query; // throws if cancelled // 2. Composable cancellation – many queries, one controller const controller = new AbortController(); const q1 = connector.query('SELECT 1', {signal: controller.signal}); const q2 = connector.query('SELECT 2', {signal: controller.signal}); controller.abort(); // cancels q1 & q2 await Promise.allSettled([q1, q2]); } ``` ### Advanced operations with the Zustand store ```tsx import {Button} from '@sqlrooms/ui'; function AdvancedOperations() { const executeSql = useRoomStore((s) => s.db.executeSql); const sqlSelectToJson = useRoomStore((s) => s.db.sqlSelectToJson); const checkTableExists = useRoomStore((s) => s.db.checkTableExists); const handleAdvancedQuery = async () => { // Cached execution with deduplication const query = await executeSql('SELECT * FROM users LIMIT 10'); if (query) { const rows = await query; // await handle directly console.log('Query result:', rows); } // Parse SQL to JSON (analysis tool) const parsed = await sqlSelectToJson('SELECT id, name FROM users'); console.log('Parsed query:', parsed); // Safety check before destructive operations const exists = await checkTableExists('users'); console.log('Table exists:', exists); }; return ; } ``` For more information, visit the SQLRooms documentation. ## Interfaces * [WasmDuckDbConnectorOptions](interfaces/WasmDuckDbConnectorOptions.md) * [WasmDuckDbConnector](interfaces/WasmDuckDbConnector.md) * [WebSocketDuckDbConnectorOptions](interfaces/WebSocketDuckDbConnectorOptions.md) * [WebSocketDuckDbConnector](interfaces/WebSocketDuckDbConnector.md) * [CopyAsTsvResult](interfaces/CopyAsTsvResult.md) * [CopyAsTsvOptions](interfaces/CopyAsTsvOptions.md) * [UseCopyAsTsvReturn](interfaces/UseCopyAsTsvReturn.md) * [UseExportToCsvReturn](interfaces/UseExportToCsvReturn.md) * [UseSqlQueryResult](interfaces/UseSqlQueryResult.md) * [BaseDuckDbConnectorOptions](interfaces/BaseDuckDbConnectorOptions.md) * [BaseDuckDbConnectorImpl](interfaces/BaseDuckDbConnectorImpl.md) * [QueryOptions](interfaces/QueryOptions.md) * [DuckDbConnector](interfaces/DuckDbConnector.md) * [TypedRowAccessor](interfaces/TypedRowAccessor.md) ## Type Aliases * [DuckDbSliceState](type-aliases/DuckDbSliceState.md) * [CreateDuckDbSliceProps](type-aliases/CreateDuckDbSliceProps.md) * [WebSocketDuckDbConnectionStatus](type-aliases/WebSocketDuckDbConnectionStatus.md) * [DuckDbConnectorType](type-aliases/DuckDbConnectorType.md) * [~~DuckDbConnectorOptions~~](type-aliases/DuckDbConnectorOptions.md) * [LoadTableSchemasFilterFunction](type-aliases/LoadTableSchemasFilterFunction.md) * [LoadTableSchemasFilter](type-aliases/LoadTableSchemasFilter.md) * [LoadTableSchemasOptions](type-aliases/LoadTableSchemasOptions.md) * [SchemaCatalogFilterEntry](type-aliases/SchemaCatalogFilterEntry.md) * [LoadSchemaCatalogFilterFunction](type-aliases/LoadSchemaCatalogFilterFunction.md) * [LoadSchemaCatalogOptions](type-aliases/LoadSchemaCatalogOptions.md) * [~~DuckDbQueryResult~~](type-aliases/DuckDbQueryResult.md) * [QueryHandle](type-aliases/QueryHandle.md) * [FunctionSuggestion](type-aliases/FunctionSuggestion.md) * [GroupedFunctionSuggestion](type-aliases/GroupedFunctionSuggestion.md) * [QualifiedTableName](type-aliases/QualifiedTableName.md) * [TableIdentity](type-aliases/TableIdentity.md) * [FullTableIdentity](type-aliases/FullTableIdentity.md) * [RawSqlTableReference](type-aliases/RawSqlTableReference.md) * [ResolveTableReferenceResult](type-aliases/ResolveTableReferenceResult.md) * [SplitSqlStatementsOptions](type-aliases/SplitSqlStatementsOptions.md) * [SeparatedStatements](type-aliases/SeparatedStatements.md) * [ColumnTypeCategory](type-aliases/ColumnTypeCategory.md) * [ColumnTypeLike](type-aliases/ColumnTypeLike.md) * [DbSchemaNode](type-aliases/DbSchemaNode.md) * [NodeObject](type-aliases/NodeObject.md) * [ColumnNodeObject](type-aliases/ColumnNodeObject.md) * [TableNodeObject](type-aliases/TableNodeObject.md) * [SchemaNodeObject](type-aliases/SchemaNodeObject.md) * [DatabaseNodeObject](type-aliases/DatabaseNodeObject.md) * [SchemaWithTables](type-aliases/SchemaWithTables.md) * [TableColumn](type-aliases/TableColumn.md) * [DataTable](type-aliases/DataTable.md) * [SpatialLoadFileOptions](type-aliases/SpatialLoadFileOptions.md) * [LoadFileOptions](type-aliases/LoadFileOptions.md) ## Variables * [defaultLoadTableSchemasFilter](variables/defaultLoadTableSchemasFilter.md) * [defaultLoadSchemaCatalogFilter](variables/defaultLoadSchemaCatalogFilter.md) * [~~useDuckDbQuery~~](variables/useDuckDbQuery.md) * [escapeVal](variables/escapeVal.md) * [escapeId](variables/escapeId.md) * [isNumericDuckType](variables/isNumericDuckType.md) * [getSqlErrorWithPointer](variables/getSqlErrorWithPointer.md) * [getFunctionDocumentation](variables/getFunctionDocumentation.md) * [getFunctionSuggestions](variables/getFunctionSuggestions.md) * [SpatialLoadFileOptions](variables/SpatialLoadFileOptions.md) * [isSpatialLoadFileOptions](variables/isSpatialLoadFileOptions.md) * [LoadFileOptions](variables/LoadFileOptions.md) ## Functions * [createDefaultLoadTableSchemasFilter](functions/createDefaultLoadTableSchemasFilter.md) * [createDuckDbSlice](functions/createDuckDbSlice.md) * [createWasmDuckDbConnector](functions/createWasmDuckDbConnector.md) * [createWebSocketDuckDbConnector](functions/createWebSocketDuckDbConnector.md) * [createDuckDbConnector](functions/createDuckDbConnector.md) * [isWasmDuckDbConnector](functions/isWasmDuckDbConnector.md) * [loadSchemaCatalog](functions/loadSchemaCatalog.md) * [useCopyAsTsv](functions/useCopyAsTsv.md) * [useExportToCsv](functions/useExportToCsv.md) * [useDataTable](functions/useDataTable.md) * [useDuckDb](functions/useDuckDb.md) * [useSql](functions/useSql.md) * [createBaseDuckDbConnector](functions/createBaseDuckDbConnector.md) * [arrowTableToJson](functions/arrowTableToJson.md) * [isQualifiedTableName](functions/isQualifiedTableName.md) * [makeQualifiedTableName](functions/makeQualifiedTableName.md) * [getTableIdentity](functions/getTableIdentity.md) * [getFullTableIdentity](functions/getFullTableIdentity.md) * [parseQualifiedSqlIdentifier](functions/parseQualifiedSqlIdentifier.md) * [parseTableIdentity](functions/parseTableIdentity.md) * [parseFullTableIdentity](functions/parseFullTableIdentity.md) * [parseTableIdentityToQualifiedName](functions/parseTableIdentityToQualifiedName.md) * [getUnqualifiedSqlIdentifier](functions/getUnqualifiedSqlIdentifier.md) * [getRawSqlTableReference](functions/getRawSqlTableReference.md) * [quoteParsedRawSqlTableReference](functions/quoteParsedRawSqlTableReference.md) * [~~quoteTableReference~~](functions/quoteTableReference.md) * [getTableDisplayName](functions/getTableDisplayName.md) * [resolveTableReference](functions/resolveTableReference.md) * [getColValAsNumber](functions/getColValAsNumber.md) * [splitSqlStatements](functions/splitSqlStatements.md) * [sanitizeQuery](functions/sanitizeQuery.md) * [makeLimitQuery](functions/makeLimitQuery.md) * [separateLastStatement](functions/separateLastStatement.md) * [joinStatements](functions/joinStatements.md) * [load](functions/load.md) * [loadCSV](functions/loadCSV.md) * [loadJSON](functions/loadJSON.md) * [loadParquet](functions/loadParquet.md) * [loadSpatial](functions/loadSpatial.md) * [loadObjects](functions/loadObjects.md) * [sqlFrom](functions/sqlFrom.md) * [literalToSQL](functions/literalToSQL.md) * [createDbSchemaTrees](functions/createDbSchemaTrees.md) * [~~getAllTablesFromSchemaTrees~~](functions/getAllTablesFromSchemaTrees.md) * [findTableInSchemaTrees](functions/findTableInSchemaTrees.md) * [getDuckDbTypeCategory](functions/getDuckDbTypeCategory.md) * [getArrowColumnTypeCategory](functions/getArrowColumnTypeCategory.md) * [getColumnTypeCategory](functions/getColumnTypeCategory.md) * [isColumnNumeric](functions/isColumnNumeric.md) * [isColumnTemporal](functions/isColumnTemporal.md) * [isColumnQuantitative](functions/isColumnQuantitative.md) * [isColumnCategorical](functions/isColumnCategorical.md) * [columnTypeCategoryToSelectorType](functions/columnTypeCategoryToSelectorType.md) * [createTypedRowAccessor](functions/createTypedRowAccessor.md) --- --- url: https://sqlrooms.org/api/duckdb-core.md --- # @sqlrooms/duckdb-core ## Interfaces * [BaseDuckDbConnectorOptions](interfaces/BaseDuckDbConnectorOptions.md) * [BaseDuckDbConnectorImpl](interfaces/BaseDuckDbConnectorImpl.md) * [QueryOptions](interfaces/QueryOptions.md) * [DuckDbConnector](interfaces/DuckDbConnector.md) * [TypedRowAccessor](interfaces/TypedRowAccessor.md) ## Type Aliases * [QueryHandle](type-aliases/QueryHandle.md) * [FunctionSuggestion](type-aliases/FunctionSuggestion.md) * [GroupedFunctionSuggestion](type-aliases/GroupedFunctionSuggestion.md) * [QualifiedTableName](type-aliases/QualifiedTableName.md) * [TableIdentity](type-aliases/TableIdentity.md) * [FullTableIdentity](type-aliases/FullTableIdentity.md) * [RawSqlTableReference](type-aliases/RawSqlTableReference.md) * [ResolveTableReferenceResult](type-aliases/ResolveTableReferenceResult.md) * [SplitSqlStatementsOptions](type-aliases/SplitSqlStatementsOptions.md) * [SeparatedStatements](type-aliases/SeparatedStatements.md) * [ColumnTypeCategory](type-aliases/ColumnTypeCategory.md) * [ColumnTypeLike](type-aliases/ColumnTypeLike.md) * [DbSchemaNode](type-aliases/DbSchemaNode.md) * [NodeObject](type-aliases/NodeObject.md) * [ColumnNodeObject](type-aliases/ColumnNodeObject.md) * [TableNodeObject](type-aliases/TableNodeObject.md) * [SchemaNodeObject](type-aliases/SchemaNodeObject.md) * [DatabaseNodeObject](type-aliases/DatabaseNodeObject.md) * [SchemaWithTables](type-aliases/SchemaWithTables.md) * [TableColumn](type-aliases/TableColumn.md) * [DataTable](type-aliases/DataTable.md) ## Functions * [createBaseDuckDbConnector](functions/createBaseDuckDbConnector.md) * [arrowTableToJson](functions/arrowTableToJson.md) * [isQualifiedTableName](functions/isQualifiedTableName.md) * [makeQualifiedTableName](functions/makeQualifiedTableName.md) * [getTableIdentity](functions/getTableIdentity.md) * [getFullTableIdentity](functions/getFullTableIdentity.md) * [parseQualifiedSqlIdentifier](functions/parseQualifiedSqlIdentifier.md) * [parseTableIdentity](functions/parseTableIdentity.md) * [parseFullTableIdentity](functions/parseFullTableIdentity.md) * [parseTableIdentityToQualifiedName](functions/parseTableIdentityToQualifiedName.md) * [getUnqualifiedSqlIdentifier](functions/getUnqualifiedSqlIdentifier.md) * [getRawSqlTableReference](functions/getRawSqlTableReference.md) * [quoteParsedRawSqlTableReference](functions/quoteParsedRawSqlTableReference.md) * [~~quoteTableReference~~](functions/quoteTableReference.md) * [getTableDisplayName](functions/getTableDisplayName.md) * [resolveTableReference](functions/resolveTableReference.md) * [escapeVal](functions/escapeVal.md) * [escapeId](functions/escapeId.md) * [isNumericDuckType](functions/isNumericDuckType.md) * [getColValAsNumber](functions/getColValAsNumber.md) * [getSqlErrorWithPointer](functions/getSqlErrorWithPointer.md) * [splitSqlStatements](functions/splitSqlStatements.md) * [sanitizeQuery](functions/sanitizeQuery.md) * [makeLimitQuery](functions/makeLimitQuery.md) * [separateLastStatement](functions/separateLastStatement.md) * [joinStatements](functions/joinStatements.md) * [getFunctionDocumentation](functions/getFunctionDocumentation.md) * [getFunctionSuggestions](functions/getFunctionSuggestions.md) * [load](functions/load.md) * [loadCSV](functions/loadCSV.md) * [loadJSON](functions/loadJSON.md) * [loadParquet](functions/loadParquet.md) * [loadSpatial](functions/loadSpatial.md) * [loadObjects](functions/loadObjects.md) * [sqlFrom](functions/sqlFrom.md) * [literalToSQL](functions/literalToSQL.md) * [createDbSchemaTrees](functions/createDbSchemaTrees.md) * [~~getAllTablesFromSchemaTrees~~](functions/getAllTablesFromSchemaTrees.md) * [findTableInSchemaTrees](functions/findTableInSchemaTrees.md) * [getDuckDbTypeCategory](functions/getDuckDbTypeCategory.md) * [getArrowColumnTypeCategory](functions/getArrowColumnTypeCategory.md) * [getColumnTypeCategory](functions/getColumnTypeCategory.md) * [isColumnNumeric](functions/isColumnNumeric.md) * [isColumnTemporal](functions/isColumnTemporal.md) * [isColumnQuantitative](functions/isColumnQuantitative.md) * [isColumnCategorical](functions/isColumnCategorical.md) * [columnTypeCategoryToSelectorType](functions/columnTypeCategoryToSelectorType.md) * [createTypedRowAccessor](functions/createTypedRowAccessor.md) --- --- url: https://sqlrooms.org/api/layout.md --- # @sqlrooms/layout Layout slice and renderer for SQLRooms panel-based UIs. See the [Layout developer guide](https://sqlrooms.org/layout.html) for examples of panels, tabs, grids, and docking, and the [layout example app](https://github.com/sqlrooms/examples/tree/main/layout) for a complete implementation. This package renders layout trees using `react-resizable-panels` for split layouts, `dnd-kit` for dockable panel rearrangement, and `react-grid-layout` for scrollable grid dashboard layouts. ## Installation ```bash npm install @sqlrooms/layout ``` ## Main exports * `createLayoutSlice()`, `useStoreWithLayout()` * `LayoutRenderer` component — renders a `LayoutNode` tree using resizable panels, tabs, and generic docking * `useExpandGridPanel()` — expands a grid child panel horizontally to available row space * Grid layout defaults/helpers: `DEFAULT_GRID_COLS`, `DEFAULT_GRID_BREAKPOINTS`, `getResponsiveGridCols()`, `getGridColsForBreakpoint()` * Layout helpers: * `visitLayoutLeafNodes` * `getVisibleLayoutPanels` * `removeLayoutNodeByKey` * `findNodeById`, `findTabsNodeForPanel` * `movePanel` * Layout config schemas/types re-exported from `@sqlrooms/layout-config` ## Store usage ```tsx import {LayoutSliceState, createLayoutSlice} from '@sqlrooms/layout'; import { BaseRoomStoreState, createBaseRoomSlice, createRoomStore, } from '@sqlrooms/room-store'; function DataPanel() { return
Data
; } function MainPanel() { return
Main
; } type State = BaseRoomStoreState & LayoutSliceState; export const {roomStore, useRoomStore} = createRoomStore( (set, get, store) => ({ ...createBaseRoomSlice()(set, get, store), ...createLayoutSlice({ config: { type: 'split', direction: 'row', children: [ {type: 'panel', id: 'data', defaultSize: '30%'}, {type: 'panel', id: 'main', defaultSize: '70%'}, ], }, panels: { data: { title: 'Data', component: DataPanel, }, main: { title: 'Main', component: MainPanel, }, }, })(set, get, store), }), ); ``` ## Render callbacks `createLayoutSlice` accepts optional render callbacks for custom panel and tab strip rendering: ```ts createLayoutSlice({ config: { /* ... */ }, panels: { /* ... */ }, renderPanel: (context) => { // Return custom JSX or undefined to fall back to the default renderer }, }); ``` ## Tabs layout composition `TabsLayout.TabContent` accepts `forceMount` to keep all visible tab contents mounted while hiding inactive tabs. This is useful for expensive panels that should preserve local state or setup work during tab changes: ```tsx ``` ## Area-based panel management Named `tabs` nodes (with an `id`) act as **areas** that can be managed programmatically: ```tsx import {Button} from '@sqlrooms/ui'; function PanelButtons() { const setActiveTab = useRoomStore((state) => state.layout.setActiveTab); const addTab = useRoomStore((state) => state.layout.addTab); const setCollapsed = useRoomStore((state) => state.layout.setCollapsed); return (
); } ``` ## Note `@sqlrooms/layout` (panel layout system) is different from `@sqlrooms/mosaic` (UW IDL data visualization package). ## Interfaces * [TabsLayoutTabContentProps](interfaces/TabsLayoutTabContentProps.md) ## Type Aliases * [LayoutNodeContextTabs](type-aliases/LayoutNodeContextTabs.md) * [LayoutNodeContextSplit](type-aliases/LayoutNodeContextSplit.md) * [LayoutNodeContextDock](type-aliases/LayoutNodeContextDock.md) * [LayoutNodeContextGrid](type-aliases/LayoutNodeContextGrid.md) * [LayoutNodeContextPanel](type-aliases/LayoutNodeContextPanel.md) * [LayoutNodeContextLeaf](type-aliases/LayoutNodeContextLeaf.md) * [LayoutNodeContextValue](type-aliases/LayoutNodeContextValue.md) * [LayoutRendererProps](type-aliases/LayoutRendererProps.md) * [DockDirection](type-aliases/DockDirection.md) * [DockAxis](type-aliases/DockAxis.md) * [ParentDirection](type-aliases/ParentDirection.md) * [LayoutSliceConfig](type-aliases/LayoutSliceConfig.md) * [LayoutSliceConfig](type-aliases/LayoutSliceConfig-1.md) * [LayoutSliceState](type-aliases/LayoutSliceState.md) * [CreateLayoutSliceProps](type-aliases/CreateLayoutSliceProps.md) * [PanelIdentityResult](type-aliases/PanelIdentityResult.md) * [LayoutPath](type-aliases/LayoutPath.md) * [PanelContainerType](type-aliases/PanelContainerType.md) * [RoomPanelComponent](type-aliases/RoomPanelComponent.md) * [RoomPanelInfo](type-aliases/RoomPanelInfo.md) * [PanelDefinitionContext](type-aliases/PanelDefinitionContext.md) * [PanelDefinition](type-aliases/PanelDefinition.md) * [Panels](type-aliases/Panels.md) * [LayoutNodeKey](type-aliases/LayoutNodeKey.md) * [LayoutPanelNode](type-aliases/LayoutPanelNode.md) * [LayoutSplitNode](type-aliases/LayoutSplitNode.md) * [LayoutTabsNode](type-aliases/LayoutTabsNode.md) * [LayoutDockNode](type-aliases/LayoutDockNode.md) * [LayoutGridItem](type-aliases/LayoutGridItem.md) * [LayoutGridNode](type-aliases/LayoutGridNode.md) * [LayoutNode](type-aliases/LayoutNode.md) * [LayoutConfig](type-aliases/LayoutConfig.md) * [LayoutDirection](type-aliases/LayoutDirection.md) * [LayoutDirection](type-aliases/LayoutDirection-1.md) ## Variables * [LayoutNodeProvider](variables/LayoutNodeProvider.md) * [LayoutRenderer](variables/LayoutRenderer.md) * [RoomDndProvider](variables/RoomDndProvider.md) * [DEFAULT\_GRID\_BREAKPOINTS](variables/DEFAULT_GRID_BREAKPOINTS.md) * [DEFAULT\_GRID\_COLS](variables/DEFAULT_GRID_COLS.md) * [DockLayout](variables/DockLayout.md) * [GridLayout](variables/GridLayout.md) * [LeafLayout](variables/LeafLayout.md) * [SplitLayout](variables/SplitLayout.md) * [TabsLayout](variables/TabsLayout.md) * [MAIN\_VIEW](variables/MAIN_VIEW.md) * [LayoutNodeKey](variables/LayoutNodeKey.md) * [LayoutPanelNode](variables/LayoutPanelNode.md) * [LayoutSplitNode](variables/LayoutSplitNode.md) * [LayoutTabsNode](variables/LayoutTabsNode.md) * [LayoutDockNode](variables/LayoutDockNode.md) * [LayoutGridNode](variables/LayoutGridNode.md) * [LayoutNode](variables/LayoutNode.md) * [LayoutConfig](variables/LayoutConfig.md) ## Functions * [useLayoutNodeContext](functions/useLayoutNodeContext.md) * [useTabsNodeContext](functions/useTabsNodeContext.md) * [useSplitNodeContext](functions/useSplitNodeContext.md) * [useDockNodeContext](functions/useDockNodeContext.md) * [useGridNodeContext](functions/useGridNodeContext.md) * [getLayoutNodeContextValue](functions/getLayoutNodeContextValue.md) * [createDefaultLayoutConfig](functions/createDefaultLayoutConfig.md) * [createLayoutSlice](functions/createLayoutSlice.md) * [useStoreWithLayout](functions/useStoreWithLayout.md) * [movePanel](functions/movePanel.md) * [getGridColsForBreakpoint](functions/getGridColsForBreakpoint.md) * [getResponsiveGridCols](functions/getResponsiveGridCols.md) * [createLayoutId](functions/createLayoutId.md) * [visitLayoutLeafNodes](functions/visitLayoutLeafNodes.md) * [getVisibleLayoutPanels](functions/getVisibleLayoutPanels.md) * [findNodeById](functions/findNodeById.md) * [findTabsNodeForPanel](functions/findTabsNodeForPanel.md) * [findNearestDockAncestor](functions/findNearestDockAncestor.md) * [isDockablePanel](functions/isDockablePanel.md) * [removeLayoutNodeByKey](functions/removeLayoutNodeByKey.md) * [useLeafLayoutPanelDraggable](functions/useLeafLayoutPanelDraggable.md) * [useExpandGridPanel](functions/useExpandGridPanel.md) * [resolvePanelDefinition](functions/resolvePanelDefinition.md) * [resolvePanelIdentity](functions/resolvePanelIdentity.md) * [useGetPanel](functions/useGetPanel.md) * [isLayoutNodeKey](functions/isLayoutNodeKey.md) * [isLayoutPanelNode](functions/isLayoutPanelNode.md) * [isLayoutSplitNode](functions/isLayoutSplitNode.md) * [isLayoutTabsNode](functions/isLayoutTabsNode.md) * [isLayoutDockNode](functions/isLayoutDockNode.md) * [isLayoutGridNode](functions/isLayoutGridNode.md) * [createDefaultLayout](functions/createDefaultLayout.md) * [getLayoutNodeId](functions/getLayoutNodeId.md) * [getChildrenIds](functions/getChildrenIds.md) * [getVisibleTabChildren](functions/getVisibleTabChildren.md) * [getHiddenTabChildren](functions/getHiddenTabChildren.md) --- --- url: https://sqlrooms.org/api/room-shell.md --- # @sqlrooms/room-shell Main SQLRooms application shell and default Room slice composition. `@sqlrooms/room-shell` bundles: * base room lifecycle (`room-store`) * DuckDB slice (`@sqlrooms/duckdb`) * layout slice (`@sqlrooms/layout`) * React shell UI (`RoomShell`, sidebar/layout/loading components) Use this package as the default entry point for most SQLRooms apps. ## Installation ```bash npm install @sqlrooms/room-shell @sqlrooms/duckdb @sqlrooms/ui ``` ## Quick start ```tsx import { createRoomShellSlice, createRoomStore, RoomShell, RoomShellSliceState, } from '@sqlrooms/room-shell'; import {DatabaseIcon} from 'lucide-react'; function DataPanel() { return
Data panel
; } function MainPanel() { return
Main panel
; } type RoomState = RoomShellSliceState; export const {roomStore, useRoomStore} = createRoomStore( (set, get, store) => ({ ...createRoomShellSlice({ config: { title: 'My SQLRooms App', dataSources: [ { type: 'url', tableName: 'earthquakes', url: 'https://huggingface.co/datasets/sqlrooms/earthquakes/resolve/main/earthquakes.parquet', }, ], }, layout: { config: { type: 'split', direction: 'row', children: [{type: 'panel', id: 'data', defaultSize: '28%'}, 'main'], }, panels: { data: { title: 'Data', icon: DatabaseIcon, component: DataPanel, }, main: { title: 'Main', icon: () => null, component: MainPanel, }, }, }, })(set, get, store), }), ); export function App() { return ( ); } ``` ## Common room actions ```tsx import {useRoomStore} from './store'; import {Button} from '@sqlrooms/ui'; function RoomActions() { const setRoomTitle = useRoomStore((state) => state.room.setRoomTitle); const addDataSource = useRoomStore((state) => state.room.addDataSource); const removeDataSource = useRoomStore((state) => state.room.removeDataSource); const addRoomFile = useRoomStore((state) => state.room.addRoomFile); return (
); } ``` ## Persistence Use `persistSliceConfigs` with schemas: ```tsx import { BaseRoomConfig, LayoutConfig, createRoomStore, persistSliceConfigs, } from '@sqlrooms/room-shell'; const persistence = { name: 'my-room-storage', sliceConfigSchemas: { room: BaseRoomConfig, layout: LayoutConfig, }, }; createRoomStore( persistSliceConfigs(persistence, (set, get, store) => ({ // compose slices here })), ); ``` For host-owned storage such as DuckDB-backed project files, prefer `createRoomStorePersistence` alongside these schema helpers. It is the default room-store integration for explicit hydration, dirty tracking, save scheduling, final flush, and save status without repeating Zustand subscription and saved-snapshot wiring in every app. Use `createPersistenceController` directly only for lower-level integrations that need the same save policy outside a Zustand room store. ## Guarded command invocation `@sqlrooms/room-shell` re-exports `invokeCommandWithPolicy()`, `createCommandCliAdapter()`, and `createCommandMcpAdapter()` from `@sqlrooms/room-store`. External and agent-facing integrations must use these guarded APIs rather than invoking commands directly. They re-check command availability and require explicit confirmation for high-risk or confirmation-gated commands. See the [room-store command guide](_media/README.md#guarded-command-invocation) for usage and fail-closed semantics. ## Related packages * `@sqlrooms/sql-editor` * `@sqlrooms/ai` * `@sqlrooms/mosaic` * `@sqlrooms/vega` ## Enumerations * [DataSourceStatus](enumerations/DataSourceStatus.md) ## Type Aliases * [DbSliceState](type-aliases/DbSliceState.md) * [LayoutRendererProps](type-aliases/LayoutRendererProps.md) * [LayoutPath](type-aliases/LayoutPath.md) * [RoomPanelComponent](type-aliases/RoomPanelComponent.md) * [RoomPanelInfo](type-aliases/RoomPanelInfo.md) * [LayoutNodeKey](type-aliases/LayoutNodeKey.md) * [LayoutPanelNode](type-aliases/LayoutPanelNode.md) * [LayoutSplitNode](type-aliases/LayoutSplitNode.md) * [LayoutTabsNode](type-aliases/LayoutTabsNode.md) * [LayoutNode](type-aliases/LayoutNode.md) * [LayoutConfig](type-aliases/LayoutConfig.md) * [LayoutDirection](type-aliases/LayoutDirection.md) * [LayoutDirection](type-aliases/LayoutDirection-1.md) * [BaseRoomConfig](type-aliases/BaseRoomConfig.md) * [DataSourceTypes](type-aliases/DataSourceTypes.md) * [BaseDataSource](type-aliases/BaseDataSource.md) * [FileDataSource](type-aliases/FileDataSource.md) * [UrlDataSource](type-aliases/UrlDataSource.md) * [SqlQueryDataSource](type-aliases/SqlQueryDataSource.md) * [DataSource](type-aliases/DataSource.md) * [LoadFile](type-aliases/LoadFile.md) * [StandardLoadOptions](type-aliases/StandardLoadOptions.md) * [SpatialLoadOptions](type-aliases/SpatialLoadOptions.md) * [SpatialLoadFileOptions](type-aliases/SpatialLoadFileOptions.md) * [StandardLoadFileOptions](type-aliases/StandardLoadFileOptions.md) * [LoadFileOptions](type-aliases/LoadFileOptions.md) * [RoomShellBaseState](type-aliases/RoomShellBaseState.md) * [RoomShellCommandPaletteProps](type-aliases/RoomShellCommandPaletteProps.md) * [RoomShellCommandPaletteButtonProps](type-aliases/RoomShellCommandPaletteButtonProps.md) * [RoomShellSliceState](type-aliases/RoomShellSliceState.md) * [TableAction](type-aliases/TableAction.md) * [RoomFileState](type-aliases/RoomFileState.md) * [RoomFileInfo](type-aliases/RoomFileInfo.md) * [DataSourceState](type-aliases/DataSourceState.md) * [BaseRoomStoreState](type-aliases/BaseRoomStoreState.md) * [BaseRoomStore](type-aliases/BaseRoomStore.md) * [UseRoomStore](type-aliases/UseRoomStore.md) * [CreateBaseRoomSliceProps](type-aliases/CreateBaseRoomSliceProps.md) * [CommandCliAdapterOptions](type-aliases/CommandCliAdapterOptions.md) * [CommandInvocationPolicyOptions](type-aliases/CommandInvocationPolicyOptions.md) * [GuardedCommandInvocationOptions](type-aliases/GuardedCommandInvocationOptions.md) * [CommandCliAdapter](type-aliases/CommandCliAdapter.md) * [CommandMcpToolDescriptor](type-aliases/CommandMcpToolDescriptor.md) * [CommandMcpAdapterOptions](type-aliases/CommandMcpAdapterOptions.md) * [CommandMcpAdapter](type-aliases/CommandMcpAdapter.md) * [RoomCommandSurface](type-aliases/RoomCommandSurface.md) * [RoomCommandInvocation](type-aliases/RoomCommandInvocation.md) * [RoomCommandInvocationOptions](type-aliases/RoomCommandInvocationOptions.md) * [RoomCommandExecutionContext](type-aliases/RoomCommandExecutionContext.md) * [RoomCommandPredicate](type-aliases/RoomCommandPredicate.md) * [RoomCommandInputComponentProps](type-aliases/RoomCommandInputComponentProps.md) * [RoomCommandInputComponent](type-aliases/RoomCommandInputComponent.md) * [RoomCommandRiskLevel](type-aliases/RoomCommandRiskLevel.md) * [RoomCommandKeystrokes](type-aliases/RoomCommandKeystrokes.md) * [RoomCommandPolicyMetadata](type-aliases/RoomCommandPolicyMetadata.md) * [RoomCommandUiMetadata](type-aliases/RoomCommandUiMetadata.md) * [RoomCommandResult](type-aliases/RoomCommandResult.md) * [RoomCommandExecuteOutput](type-aliases/RoomCommandExecuteOutput.md) * [RoomCommandMiddlewareNext](type-aliases/RoomCommandMiddlewareNext.md) * [RoomCommandMiddleware](type-aliases/RoomCommandMiddleware.md) * [RoomCommandInvokeStartEvent](type-aliases/RoomCommandInvokeStartEvent.md) * [RoomCommandInvokeSuccessEvent](type-aliases/RoomCommandInvokeSuccessEvent.md) * [RoomCommandInvokeFailureEvent](type-aliases/RoomCommandInvokeFailureEvent.md) * [RoomCommandInvokeErrorEvent](type-aliases/RoomCommandInvokeErrorEvent.md) * [CreateCommandSliceProps](type-aliases/CreateCommandSliceProps.md) * [RoomCommand](type-aliases/RoomCommand.md) * [RegisteredRoomCommand](type-aliases/RegisteredRoomCommand.md) * [RoomCommandDescriptor](type-aliases/RoomCommandDescriptor.md) * [RoomCommandListOptions](type-aliases/RoomCommandListOptions.md) * [CommandSliceState](type-aliases/CommandSliceState.md) * [PersistenceSaveReason](type-aliases/PersistenceSaveReason.md) * [PersistenceSaveMetadata](type-aliases/PersistenceSaveMetadata.md) * [PersistenceAdapter](type-aliases/PersistenceAdapter.md) * [PersistenceControllerState](type-aliases/PersistenceControllerState.md) * [PersistenceControllerListener](type-aliases/PersistenceControllerListener.md) * [PersistenceController](type-aliases/PersistenceController.md) * [CreatePersistenceControllerOptions](type-aliases/CreatePersistenceControllerOptions.md) * [RoomCommandPortableSchema](type-aliases/RoomCommandPortableSchema.md) * [RoomStateProviderProps](type-aliases/RoomStateProviderProps.md) * [RoomStorePersistenceSnapshotCodec](type-aliases/RoomStorePersistenceSnapshotCodec.md) * [RoomStorePersistenceSnapshotEquivalence](type-aliases/RoomStorePersistenceSnapshotEquivalence.md) * [RoomStorePersistenceChangePredicate](type-aliases/RoomStorePersistenceChangePredicate.md) * [CreateRoomStorePersistenceOptions](type-aliases/CreateRoomStorePersistenceOptions.md) * [RoomStorePersistence](type-aliases/RoomStorePersistence.md) ## Variables * [LayoutRenderer](variables/LayoutRenderer.md) * [RoomDndProvider](variables/RoomDndProvider.md) * [MAIN\_VIEW](variables/MAIN_VIEW.md) * [LayoutNodeKey](variables/LayoutNodeKey.md) * [LayoutPanelNode](variables/LayoutPanelNode.md) * [LayoutSplitNode](variables/LayoutSplitNode.md) * [LayoutTabsNode](variables/LayoutTabsNode.md) * [LayoutNode](variables/LayoutNode.md) * [LayoutConfig](variables/LayoutConfig.md) * [DEFAULT\_ROOM\_TITLE](variables/DEFAULT_ROOM_TITLE.md) * [BaseRoomConfig](variables/BaseRoomConfig.md) * [DataSourceTypes](variables/DataSourceTypes.md) * [BaseDataSource](variables/BaseDataSource.md) * [FileDataSource](variables/FileDataSource.md) * [UrlDataSource](variables/UrlDataSource.md) * [SqlQueryDataSource](variables/SqlQueryDataSource.md) * [DataSource](variables/DataSource.md) * [LoadFile](variables/LoadFile.md) * [StandardLoadOptions](variables/StandardLoadOptions.md) * [SpatialLoadOptions](variables/SpatialLoadOptions.md) * [SpatialLoadFileOptions](variables/SpatialLoadFileOptions.md) * [isSpatialLoadFileOptions](variables/isSpatialLoadFileOptions.md) * [StandardLoadFileOptions](variables/StandardLoadFileOptions.md) * [LoadFileOptions](variables/LoadFileOptions.md) * [RoomShell](variables/RoomShell.md) * [RoomShellCommandPalette](variables/RoomShellCommandPalette.md) * [SidebarButton](variables/SidebarButton.md) * [RoomShellSidebarButton](variables/RoomShellSidebarButton.md) * [RoomShellSidebarButtons](variables/RoomShellSidebarButtons.md) * [TabButtons](variables/TabButtons.md) * [~~AreaPanelButtons~~](variables/AreaPanelButtons.md) * [FileDataSourceCard](variables/FileDataSourceCard.md) * [FileDataSourcesPanel](variables/FileDataSourcesPanel.md) * [TableCard](variables/TableCard.md) * [TablesListPanel](variables/TablesListPanel.md) * [PanelHeaderButton](variables/PanelHeaderButton.md) * [RoomPanel](variables/RoomPanel.md) * [RoomPanelHeader](variables/RoomPanelHeader.md) * [~~createRoomSlice~~](variables/createRoomSlice.md) * [~~createBaseSlice~~](variables/createBaseSlice.md) * [RoomStateContext](variables/RoomStateContext.md) ## Functions * [createDbSlice](functions/createDbSlice.md) * [isLayoutPanelNode](functions/isLayoutPanelNode.md) * [isLayoutSplitNode](functions/isLayoutSplitNode.md) * [isLayoutTabsNode](functions/isLayoutTabsNode.md) * [createDefaultLayout](functions/createDefaultLayout.md) * [createDefaultBaseRoomConfig](functions/createDefaultBaseRoomConfig.md) * [isFileDataSource](functions/isFileDataSource.md) * [isUrlDataSource](functions/isUrlDataSource.md) * [isSqlQueryDataSource](functions/isSqlQueryDataSource.md) * [createRoomShellSlice](functions/createRoomShellSlice.md) * [useBaseRoomShellStore](functions/useBaseRoomShellStore.md) * [createBaseRoomSlice](functions/createBaseRoomSlice.md) * [createSlice](functions/createSlice.md) * [createRoomStore](functions/createRoomStore.md) * [createRoomStoreCreator](functions/createRoomStoreCreator.md) * [isRoomSliceWithInitialize](functions/isRoomSliceWithInitialize.md) * [isRoomSliceWithDestroy](functions/isRoomSliceWithDestroy.md) * [createCommandCliAdapter](functions/createCommandCliAdapter.md) * [createCommandMcpAdapter](functions/createCommandMcpAdapter.md) * [invokeCommandWithPolicy](functions/invokeCommandWithPolicy.md) * [createCommandSlice](functions/createCommandSlice.md) * [createRoomCommandExecutionContext](functions/createRoomCommandExecutionContext.md) * [hasCommandSliceState](functions/hasCommandSliceState.md) * [registerCommandsForOwner](functions/registerCommandsForOwner.md) * [unregisterCommandsForOwner](functions/unregisterCommandsForOwner.md) * [listCommandsFromStore](functions/listCommandsFromStore.md) * [invokeCommandFromStore](functions/invokeCommandFromStore.md) * [validateCommandInput](functions/validateCommandInput.md) * [doesCommandRequireInput](functions/doesCommandRequireInput.md) * [getCommandShortcut](functions/getCommandShortcut.md) * [getCommandKeystrokes](functions/getCommandKeystrokes.md) * [getCommandInputComponent](functions/getCommandInputComponent.md) * [resolveCommandPolicyMetadata](functions/resolveCommandPolicyMetadata.md) * [exportCommandInputSchema](functions/exportCommandInputSchema.md) * [createPersistenceController](functions/createPersistenceController.md) * [RoomStateProvider](functions/RoomStateProvider.md) * [useRoomStoreApi](functions/useRoomStoreApi.md) * [useBaseRoomStore](functions/useBaseRoomStore.md) * [createPersistHelpers](functions/createPersistHelpers.md) * [persistSliceConfigs](functions/persistSliceConfigs.md) * [createRoomStorePersistence](functions/createRoomStorePersistence.md) --- --- url: https://sqlrooms.org/api/room-store.md --- # @sqlrooms/room-store Low-level state management primitives for SQLRooms, built on Zustand. Use this package when you want to build custom room state from scratch.\ If you want DuckDB + layout + room shell out of the box, use `@sqlrooms/room-shell`. ## Installation ```bash npm install @sqlrooms/room-store ``` ## What this package provides * `createRoomStore()` and `createRoomStoreCreator()` * base lifecycle slice: `createBaseRoomSlice()` * generic slice helper: `createSlice()` * React context/hooks: `RoomStateProvider`, `useBaseRoomStore`, `useRoomStoreApi` * persistence helpers: `persistSliceConfigs()`, `createPersistHelpers()` * room-store persistence glue: `createRoomStorePersistence()` * persistence controller: `createPersistenceController()` ## Quick start ```tsx import { BaseRoomStoreState, createBaseRoomSlice, createRoomStore, createSlice, type StateCreator, } from '@sqlrooms/room-store'; type CounterSliceState = { counter: { value: number; increment: () => void; }; }; function createCounterSlice(): StateCreator { return createSlice((set, get) => ({ counter: { value: 0, increment: () => set((state) => ({ counter: { ...state.counter, value: get().counter.value + 1, }, })), }, })); } type RoomState = BaseRoomStoreState & CounterSliceState; export const {roomStore, useRoomStore} = createRoomStore( (set, get, store) => ({ ...createBaseRoomSlice()(set, get, store), ...createCounterSlice()(set, get, store), }), ); ``` ## React integration ```tsx import {RoomStateProvider} from '@sqlrooms/room-store'; import {roomStore} from './store'; export function App() { return ( ); } ``` ```tsx import {useRoomStore} from './store'; import {Button} from '@sqlrooms/ui'; function Dashboard() { const value = useRoomStore((state) => state.counter.value); const increment = useRoomStore((state) => state.counter.increment); return ; } ``` ## Imperative access Use `roomStore.getState()` for non-reactive code (events, timers, async jobs). ```ts import {roomStore} from './store'; export function incrementLater() { setTimeout(() => { roomStore.getState().counter.increment(); }, 500); } ``` ## Guarded command invocation External and agent-facing integrations must use `invokeCommandWithPolicy()` instead of calling `roomStore.getState().commands.invokeCommand()` directly. The guarded helper re-checks that the command exists and is enabled immediately before execution. It also blocks high-risk or `requiresConfirmation` commands unless the caller supplies confirmation obtained from the user. ```ts import {invokeCommandWithPolicy} from '@sqlrooms/room-store'; const result = await invokeCommandWithPolicy( roomStore, 'workspace.refresh', undefined, { surface: 'mcp', actor: 'assistant', traceId: requestId, metadata: {clientName: 'Example client'}, signal: abortController.signal, }, {confirmed: false}, ); ``` Set `confirmed: true` only after explicit user confirmation. Omitting it, or passing `false`, fails closed with `command-confirmation-required` when the command requires confirmation. `createCommandCliAdapter()` and `createCommandMcpAdapter()` use the same guard and therefore have the same execution semantics. ## Persistence For a Zustand room store with host-owned storage, prefer `createRoomStorePersistence()`. It composes `createPersistHelpers()` with a controller-backed `PersistStorage`, rehydrate saved-snapshot marking, optional room-store subscription, autosave, and final flush helpers. This is the default entry point for SQLRooms apps that persist room state to DuckDB, files, or another project-owned store. See the [Persistence developer guide](https://sqlrooms.org/persistence.html) for the full integration model, data flow, and examples. `persistSliceConfigs()` defaults to browser `localStorage`. If browser storage is `null`, cannot be accessed, or a raw storage operation fails, the room state continues to work in memory and persistence is skipped. Malformed persisted JSON and read failures from an explicit custom storage adapter still propagate through Zustand's `onRehydrateStorage` callback so hosts can distinguish a failed load from an empty store; custom write and removal failures are logged and skipped. Use the lower-level `createPersistenceController()` only when you need the same persistence policy outside a room store or Zustand persist. The controller is storage-agnostic: hosts provide `load()` and `save()` adapter functions, while SQLRooms handles hydration state, dirty tracking, scheduled saves, final flush, in-flight save coalescing, and observable save status. `createPersistHelpers()` still only handles schema-based partialization and rehydrate merging. Let `createRoomStorePersistence()` combine those helpers with save policy unless you have a custom integration that does not fit the room-store helper. ```ts import {createRoomStorePersistence} from '@sqlrooms/room-store'; const persistence = createRoomStorePersistence({ partialize: (state) => ({room: state.room.config}), autosaveDelayMs: 300, load: async () => loadProjectSnapshot(), save: async (snapshot, metadata) => { await saveProjectSnapshot(snapshot, metadata?.reason); }, }); await persistence.hydrate(); await persistence.flush('final-flush'); ``` Inside components, `useRoomStoreApi()` gives you the raw store API: ```tsx import {useRoomStoreApi} from '@sqlrooms/room-store'; import {Button} from '@sqlrooms/ui'; function ResetButton() { const store = useRoomStoreApi(); return ( ); } ``` ## Interfaces * [SliceFunctions](interfaces/SliceFunctions.md) ## Type Aliases * [LayoutNodeKey](type-aliases/LayoutNodeKey.md) * [LayoutPanelNode](type-aliases/LayoutPanelNode.md) * [LayoutSplitNode](type-aliases/LayoutSplitNode.md) * [LayoutTabsNode](type-aliases/LayoutTabsNode.md) * [LayoutNode](type-aliases/LayoutNode.md) * [LayoutConfig](type-aliases/LayoutConfig.md) * [LayoutDirection](type-aliases/LayoutDirection.md) * [LayoutDirection](type-aliases/LayoutDirection-1.md) * [BaseRoomConfig](type-aliases/BaseRoomConfig.md) * [DataSourceTypes](type-aliases/DataSourceTypes.md) * [BaseDataSource](type-aliases/BaseDataSource.md) * [FileDataSource](type-aliases/FileDataSource.md) * [UrlDataSource](type-aliases/UrlDataSource.md) * [SqlQueryDataSource](type-aliases/SqlQueryDataSource.md) * [DataSource](type-aliases/DataSource.md) * [LoadFile](type-aliases/LoadFile.md) * [StandardLoadOptions](type-aliases/StandardLoadOptions.md) * [SpatialLoadOptions](type-aliases/SpatialLoadOptions.md) * [SpatialLoadFileOptions](type-aliases/SpatialLoadFileOptions.md) * [StandardLoadFileOptions](type-aliases/StandardLoadFileOptions.md) * [LoadFileOptions](type-aliases/LoadFileOptions.md) * [BaseRoomStoreState](type-aliases/BaseRoomStoreState.md) * [BaseRoomStore](type-aliases/BaseRoomStore.md) * [UseRoomStore](type-aliases/UseRoomStore.md) * [CreateBaseRoomSliceProps](type-aliases/CreateBaseRoomSliceProps.md) * [CommandCliAdapterOptions](type-aliases/CommandCliAdapterOptions.md) * [CommandInvocationPolicyOptions](type-aliases/CommandInvocationPolicyOptions.md) * [GuardedCommandInvocationOptions](type-aliases/GuardedCommandInvocationOptions.md) * [CommandCliAdapter](type-aliases/CommandCliAdapter.md) * [CommandMcpToolDescriptor](type-aliases/CommandMcpToolDescriptor.md) * [CommandMcpAdapterOptions](type-aliases/CommandMcpAdapterOptions.md) * [CommandMcpAdapter](type-aliases/CommandMcpAdapter.md) * [RoomCommandSurface](type-aliases/RoomCommandSurface.md) * [RoomCommandInvocation](type-aliases/RoomCommandInvocation.md) * [RoomCommandInvocationOptions](type-aliases/RoomCommandInvocationOptions.md) * [RoomCommandExecutionContext](type-aliases/RoomCommandExecutionContext.md) * [RoomCommandPredicate](type-aliases/RoomCommandPredicate.md) * [RoomCommandInputComponentProps](type-aliases/RoomCommandInputComponentProps.md) * [RoomCommandInputComponent](type-aliases/RoomCommandInputComponent.md) * [RoomCommandRiskLevel](type-aliases/RoomCommandRiskLevel.md) * [RoomCommandKeystrokes](type-aliases/RoomCommandKeystrokes.md) * [RoomCommandPolicyMetadata](type-aliases/RoomCommandPolicyMetadata.md) * [RoomCommandUiMetadata](type-aliases/RoomCommandUiMetadata.md) * [RoomCommandResult](type-aliases/RoomCommandResult.md) * [RoomCommandExecuteOutput](type-aliases/RoomCommandExecuteOutput.md) * [RoomCommandMiddlewareNext](type-aliases/RoomCommandMiddlewareNext.md) * [RoomCommandMiddleware](type-aliases/RoomCommandMiddleware.md) * [RoomCommandInvokeStartEvent](type-aliases/RoomCommandInvokeStartEvent.md) * [RoomCommandInvokeSuccessEvent](type-aliases/RoomCommandInvokeSuccessEvent.md) * [RoomCommandInvokeFailureEvent](type-aliases/RoomCommandInvokeFailureEvent.md) * [RoomCommandInvokeErrorEvent](type-aliases/RoomCommandInvokeErrorEvent.md) * [CreateCommandSliceProps](type-aliases/CreateCommandSliceProps.md) * [RoomCommand](type-aliases/RoomCommand.md) * [RegisteredRoomCommand](type-aliases/RegisteredRoomCommand.md) * [RoomCommandDescriptor](type-aliases/RoomCommandDescriptor.md) * [RoomCommandListOptions](type-aliases/RoomCommandListOptions.md) * [CommandSliceState](type-aliases/CommandSliceState.md) * [PersistenceSaveReason](type-aliases/PersistenceSaveReason.md) * [PersistenceSaveMetadata](type-aliases/PersistenceSaveMetadata.md) * [PersistenceAdapter](type-aliases/PersistenceAdapter.md) * [PersistenceControllerState](type-aliases/PersistenceControllerState.md) * [PersistenceControllerListener](type-aliases/PersistenceControllerListener.md) * [PersistenceController](type-aliases/PersistenceController.md) * [CreatePersistenceControllerOptions](type-aliases/CreatePersistenceControllerOptions.md) * [RoomCommandPortableSchema](type-aliases/RoomCommandPortableSchema.md) * [RoomStateProviderProps](type-aliases/RoomStateProviderProps.md) * [RoomStorePersistenceSnapshotCodec](type-aliases/RoomStorePersistenceSnapshotCodec.md) * [RoomStorePersistenceSnapshotEquivalence](type-aliases/RoomStorePersistenceSnapshotEquivalence.md) * [RoomStorePersistenceChangePredicate](type-aliases/RoomStorePersistenceChangePredicate.md) * [CreateRoomStorePersistenceOptions](type-aliases/CreateRoomStorePersistenceOptions.md) * [RoomStorePersistence](type-aliases/RoomStorePersistence.md) ## Variables * [MAIN\_VIEW](variables/MAIN_VIEW.md) * [LayoutNodeKey](variables/LayoutNodeKey.md) * [LayoutPanelNode](variables/LayoutPanelNode.md) * [LayoutSplitNode](variables/LayoutSplitNode.md) * [LayoutTabsNode](variables/LayoutTabsNode.md) * [LayoutNode](variables/LayoutNode.md) * [LayoutConfig](variables/LayoutConfig.md) * [DEFAULT\_ROOM\_TITLE](variables/DEFAULT_ROOM_TITLE.md) * [BaseRoomConfig](variables/BaseRoomConfig.md) * [DataSourceTypes](variables/DataSourceTypes.md) * [BaseDataSource](variables/BaseDataSource.md) * [FileDataSource](variables/FileDataSource.md) * [UrlDataSource](variables/UrlDataSource.md) * [SqlQueryDataSource](variables/SqlQueryDataSource.md) * [DataSource](variables/DataSource.md) * [LoadFile](variables/LoadFile.md) * [StandardLoadOptions](variables/StandardLoadOptions.md) * [SpatialLoadOptions](variables/SpatialLoadOptions.md) * [SpatialLoadFileOptions](variables/SpatialLoadFileOptions.md) * [isSpatialLoadFileOptions](variables/isSpatialLoadFileOptions.md) * [StandardLoadFileOptions](variables/StandardLoadFileOptions.md) * [LoadFileOptions](variables/LoadFileOptions.md) * [~~createRoomSlice~~](variables/createRoomSlice.md) * [~~createBaseSlice~~](variables/createBaseSlice.md) * [RoomStateContext](variables/RoomStateContext.md) ## Functions * [isLayoutPanelNode](functions/isLayoutPanelNode.md) * [isLayoutSplitNode](functions/isLayoutSplitNode.md) * [isLayoutTabsNode](functions/isLayoutTabsNode.md) * [createDefaultLayout](functions/createDefaultLayout.md) * [createDefaultBaseRoomConfig](functions/createDefaultBaseRoomConfig.md) * [isFileDataSource](functions/isFileDataSource.md) * [isUrlDataSource](functions/isUrlDataSource.md) * [isSqlQueryDataSource](functions/isSqlQueryDataSource.md) * [createBaseRoomSlice](functions/createBaseRoomSlice.md) * [createSlice](functions/createSlice.md) * [createRoomStore](functions/createRoomStore.md) * [createRoomStoreCreator](functions/createRoomStoreCreator.md) * [isRoomSliceWithInitialize](functions/isRoomSliceWithInitialize.md) * [isRoomSliceWithDestroy](functions/isRoomSliceWithDestroy.md) * [createCommandCliAdapter](functions/createCommandCliAdapter.md) * [createCommandMcpAdapter](functions/createCommandMcpAdapter.md) * [invokeCommandWithPolicy](functions/invokeCommandWithPolicy.md) * [createCommandSlice](functions/createCommandSlice.md) * [createRoomCommandExecutionContext](functions/createRoomCommandExecutionContext.md) * [hasCommandSliceState](functions/hasCommandSliceState.md) * [registerCommandsForOwner](functions/registerCommandsForOwner.md) * [unregisterCommandsForOwner](functions/unregisterCommandsForOwner.md) * [listCommandsFromStore](functions/listCommandsFromStore.md) * [invokeCommandFromStore](functions/invokeCommandFromStore.md) * [validateCommandInput](functions/validateCommandInput.md) * [doesCommandRequireInput](functions/doesCommandRequireInput.md) * [getCommandShortcut](functions/getCommandShortcut.md) * [getCommandKeystrokes](functions/getCommandKeystrokes.md) * [getCommandInputComponent](functions/getCommandInputComponent.md) * [resolveCommandPolicyMetadata](functions/resolveCommandPolicyMetadata.md) * [exportCommandInputSchema](functions/exportCommandInputSchema.md) * [createPersistenceController](functions/createPersistenceController.md) * [RoomStateProvider](functions/RoomStateProvider.md) * [useRoomStoreApi](functions/useRoomStoreApi.md) * [useBaseRoomStore](functions/useBaseRoomStore.md) * [createPersistHelpers](functions/createPersistHelpers.md) * [persistSliceConfigs](functions/persistSliceConfigs.md) * [createRoomStorePersistence](functions/createRoomStorePersistence.md) --- --- url: https://sqlrooms.org/api/ui.md --- # @sqlrooms/ui A comprehensive UI component library for SQLRooms applications, built on top of React and Tailwind CSS. This package provides a collection of reusable, accessible, and customizable components designed to create consistent and beautiful user interfaces. This library is based on [shadcn/ui](https://ui.shadcn.com/), a collection of beautifully designed, accessible components that can be copied and pasted into your apps. ## Features * 🎨 **Modern Design**: Clean, modern components following design best practices * ♿ **Accessibility**: Components built with accessibility in mind * 🌗 **Theming**: Support for light and dark modes * 📱 **Responsive**: Mobile-friendly components that adapt to different screen sizes * 🧩 **Composable**: Components designed to work together seamlessly * 🔄 **React Hooks**: Useful hooks for common UI patterns ## Installation ```bash npm install @sqlrooms/ui # or yarn add @sqlrooms/ui ``` ## Basic Usage ### Using Components ```tsx import {Button, Card, Input} from '@sqlrooms/ui'; function LoginForm() { return (

Login

); } ``` ### Using Hooks ```tsx import {toast, useDisclosure} from '@sqlrooms/ui'; function MyComponent() { const {isOpen, onOpen, onClose} = useDisclosure(); const handleAction = () => { // Perform some action toast.success('Success!', { description: 'Your action was completed successfully.', }); onClose(); }; return (
Confirm Action Are you sure you want to perform this action?
); } ``` ## Available Components * **Layout**: Card, Resizable, SettingsPanelHeader, Tabs * **Forms**: Button, Checkbox, Combobox, Input, Select, Slider, Switch, Textarea * **Feedback**: Alert, Progress, Spinner, Toast * **Navigation**: Accordion, Breadcrumb, Dropdown Menu, TabStrip * **Overlay**: Dialog, ModifierScrollOverlay, Popover, Tooltip * **Data Display**: Badge, Table * **Utility**: Error Boundary, Theme Switch ## Combobox Use the compound `Combobox` component for searchable select dropdowns built on the package's Popover and Command primitives. ```tsx import {Combobox} from '@sqlrooms/ui'; function MySelector() { const [value, setValue] = useState(''); const options = [ {value: 'option1', label: 'Option 1'}, {value: 'option2', label: 'Option 2'}, {value: 'option3', label: 'Option 3'}, ]; const selectedLabel = options.find((option) => option.value === value)?.label ?? 'Select option'; return ( {selectedLabel} {options.map((option) => ( {option.label} ))} ); } ``` Available compound components: * `Combobox` (root) - Manages state and provides context * `Combobox.Trigger` - Button to open the dropdown * `Combobox.Content` - Popover content wrapper * `Combobox.Item` - Individual selectable item Pass `disabled` to the root `Combobox` to disable opening the dropdown and selecting items. For advanced composition, the lower-level `useCombobox` hook is also exported. ## Settings Panel Header Use `SettingsPanelHeader` for compact settings surfaces that should share the standard settings icon and optional close affordance. ```tsx import {Button, SettingsPanelHeader} from '@sqlrooms/ui'; import {CodeIcon} from 'lucide-react'; function SettingsPanel({onClose}: {onClose: () => void}) { return (
} onClose={onClose} /> {/* settings controls */}
); } ``` ## Advanced Features * **Component Composition**: Build complex UIs by composing simple components * **Form Handling**: Integrated with React Hook Form for easy form management * **Custom Styling**: Extend components with custom styles using Tailwind CSS * **Animation**: Smooth transitions and animations for interactive elements * **`ScrollableRow` forwards its ref and passes through extra props** (e.g. `data-*`, `aria-*`, event handlers) to its outermost element, so it can be wrapped by a slot component (such as Radix's `Slot`, re-exported from this package) without silently losing the ref or those props. Note the two refs point at different elements: the forwarded ref is the outer wrapper (the one that also takes `className`), while `scrollRef` is the inner scrolling container, for reading or setting `scrollLeft`. ## Auto-Resize for Textareas `useAutoResizeTextarea` is the hook behind `Textarea`'s `autoResize` prop, exported so it can be applied to a textarea element you did not render yourself — for example one rendered by a host application's own text-input component. Give it a ref to the textarea and it grows the element's height to fit its content, tracks whether the content now exceeds the element's `max-height`, and re-measures on container resize. `resizeToFitContent` schedules the measurement on the next animation frame, so the element's height is not yet updated when the call returns. ```tsx import {useAutoResizeTextarea} from '@sqlrooms/ui'; import {useRef} from 'react'; function MyTextarea({ value, onChange, }: { value: string; onChange: (value: string) => void; }) { const textareaRef = useRef(null); const {hasOverflow, resizeToFitContent} = useAutoResizeTextarea({ autoResize: true, textareaRef, value, }); return (