@svgrid/mcp 2.3.3 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/data.js +38 -38
- package/package.json +3 -2
- package/server.json +25 -25
package/dist/data.js
CHANGED
|
@@ -2648,13 +2648,13 @@ export const docs = [
|
|
|
2648
2648
|
"slug": "compliance/hipaa",
|
|
2649
2649
|
"path": "docs/compliance/hipaa.md",
|
|
2650
2650
|
"title": "HIPAA posture",
|
|
2651
|
-
"markdown": "# HIPAA posture\
|
|
2651
|
+
"markdown": "# HIPAA posture\n\nsv-grid is **HIPAA-neutral**: the library does not transmit, store,\nor process PHI on its own. If your app is HIPAA-compliant before\nadding sv-grid, adding sv-grid does not break that compliance.\n\nThis page documents the four configuration choices that matter for\nhealthcare deployments.\n\n> **Live example:** [demo 41 (Healthcare EMR - inpatient board)](https://svgrid.com/demos/41-healthcare-emr/)\n> shows a role-based ICU census - the same patterns this page describes.\n\n## 1. Disable persistent saved-views\n\nThe default `<SvGrid>` writes nothing to disk. Saved views (an opt-in\nfeature) writes view layouts to localStorage. If you don't want PHI\nto ever land in localStorage, the simplest answer is: don't use\nsaved views, OR make sure the view payload contains no PHI.\n\n```ts\n// Save the LAYOUT but never the filter VALUES (which could leak PHI\n// like 'Patient: Jane Doe').\nconst view = {\n widths: api.getColumnWidths(),\n pinning: api.getColumnPinning(),\n // intentionally do NOT save api.getFilters()\n}\nlocalStorage.setItem('view', JSON.stringify(view))\n```\n\n## 2. Disable clipboard copy/paste for PHI columns\n\nThe grid's copy/paste serialises selected cells to the OS clipboard\nas TSV. Clipboards are not under your app's control - browsers,\nextensions, OS-level sync (e.g. Apple Universal Clipboard) all read\nfrom them.\n\nThe simplest mitigation: drop the cell-selection feature, which\nremoves the keyboard surface that triggers copy:\n\n```svelte\n<SvGrid {data} {columns} features={features}\n enableCellSelection={false}\n selectionMode=\"row\" />\n```\n\nFor finer control, intercept the `onActiveCellChange` callback and\nblock the column you don't want copyable - see [accessibility](../help/accessibility.md#keyboard-map)\nfor the keyboard map.\n\n## 3. Disable AI helpers for PHI rows (or route to a HIPAA-BAA provider)\n\nThe [AI assistant](../help/ai.md) helpers (`aiFilter`, `aiSmartFill`,\n`aiSummarize`, `aiClassify`) send row data to whatever model provider\nyou registered via `setAIProvider(...)`. For PHI:\n\n- Use a model hosted on a HIPAA-eligible service (AWS Bedrock with\n Anthropic / Cohere / Mistral; Azure OpenAI with a signed BAA;\n Google Vertex AI with a signed BAA).\n- OR redact PHI fields BEFORE calling the helper.\n\nExample redaction in the provider adapter:\n\n```ts\nimport { setAIProvider, type AIProvider } from '@svgrid/grid'\n\nconst redactingProvider: AIProvider = async ({ prompt, ...rest }) => {\n const safe = prompt.replace(/MRN-?\\d{8}/g, '<<MRN>>').replace(/SSN[:\\s-]*\\d{3}-?\\d{2}-?\\d{4}/g, '<<SSN>>')\n return fetch('/api/ai', { ... body: JSON.stringify({ prompt: safe, ...rest }) }).then((r) => r.text())\n}\n\nsetAIProvider(redactingProvider)\n```\n\nThe library hands the prompt to the provider VERBATIM - any\nredaction happens in the wrapper you author, where it's auditable.\n\n## 4. Audit every cell edit\n\nMost HIPAA reviewers care more about the audit trail than the\ndisplay surface. The grid's `onCellValueChange` fires on every\ncommitted edit with `{ rowIndex, columnId, oldValue, newValue, row }`.\nPipe it to your audit pipeline:\n\n```svelte\n<SvGrid {data} {columns} features={features}\n onCellValueChange={async (e) => {\n await fetch('/api/audit', {\n method: 'POST',\n body: JSON.stringify({\n actor: currentUser.id,\n action: 'cell-edit',\n resource: `patient/${e.row.id}/${e.columnId}`,\n before: e.oldValue,\n after: e.newValue,\n ts: new Date().toISOString(),\n }),\n })\n }}\n/>\n```\n\nSee [audit log integration](./audit-log.md) for the full pattern,\nincluding a CryptoSign-the-row trick that makes the audit log\ntamper-evident.\n\n## What the library does NOT do\n\n- It does not transmit row data anywhere on its own.\n- It does not store row data on disk.\n- It does not phone home with telemetry.\n- It does not log to the console at runtime in production builds.\n\nYou can confirm with DevTools: `<SvGrid>` produces zero network\nrequests of its own.\n\n## Browser-level mitigations to know about\n\nThese are general HIPAA-in-the-browser concerns; the grid doesn't\nmake them worse, but you should be aware:\n\n- **Browser auto-fill**: disable on PHI fields with `autocomplete=\"off\"`.\n- **Browser screenshot APIs** (`getDisplayMedia`): browsers ask the\n user; your app can't fully block. Combine with `Cache-Control:\n no-store` on your origin so screenshots don't end up in browser\n history thumbnails.\n- **Browser back-forward cache** caches the DOM. Use `Cache-Control:\n no-store` and `Pragma: no-cache` on PHI pages.\n\n## See also\n\n- [SOC 2 posture](./soc2.md)\n- [GDPR + data residency](./gdpr.md)\n- [Audit log integration](./audit-log.md)\n- [Security & supply chain](../help/security.md)\n- [Demo 41 - Healthcare EMR](https://svgrid.com/demos/41-healthcare-emr/) - role-based cell editing in practice\n"
|
|
2652
2652
|
},
|
|
2653
2653
|
{
|
|
2654
2654
|
"slug": "compliance/index",
|
|
2655
2655
|
"path": "docs/compliance/index.md",
|
|
2656
2656
|
"title": "Compliance",
|
|
2657
|
-
"markdown": "# Compliance\r\n\r\nsv-grid is a client-side UI library - **all data stays in the\r\nbrowser**, the library never makes a network call of its own, no\r\ntelemetry phones home. The compliance story is therefore short, but\r\nbecause enterprise procurement asks the same questions every time,\r\nthis section answers each one directly.\r\n\r\n> If your reviewer wants a one-pager: jump to the\r\n> [vendor-questionnaire shortlist](#vendor-questionnaire-shortlist)\r\n> at the bottom.\r\n\r\n## Pages\r\n\r\n- [SOC 2 posture](./soc2.md) - what the library covers, what your\r\n hosting / build pipeline must cover\r\n- [GDPR + data residency](./gdpr.md) - personal-data handling, where\r\n data physically sits, the user-rights surface\r\n- [HIPAA posture](./hipaa.md) - PHI handling in the browser, what\r\n \"no PHI on disk\" requires you to wire\r\n- [Audit log integration](./audit-log.md) - turn the grid's callbacks\r\n into an immutable audit trail with one adapter\r\n\r\n## Vendor-questionnaire shortlist\r\n\r\n| Question | Answer |\r\n| ------------------------------------------------- | --------------------------------------------------------------- |\r\n| Does the library transmit any data? | **No.** Zero outbound network calls. Inspect with DevTools. |\r\n| Does the library write to localStorage? | **Only when you opt in.** [Saved views](../help/saved-views.md) writes when you tell it to. |\r\n| Does the library evaluate user input as code? | **No.** CSP-compliant; no `eval` / `new Function`. |\r\n| Does the library include third-party trackers? | **No.** Verify the bundle - ~
|
|
2657
|
+
"markdown": "# Compliance\r\n\r\nsv-grid is a client-side UI library - **all data stays in the\r\nbrowser**, the library never makes a network call of its own, no\r\ntelemetry phones home. The compliance story is therefore short, but\r\nbecause enterprise procurement asks the same questions every time,\r\nthis section answers each one directly.\r\n\r\n> If your reviewer wants a one-pager: jump to the\r\n> [vendor-questionnaire shortlist](#vendor-questionnaire-shortlist)\r\n> at the bottom.\r\n\r\n## Pages\r\n\r\n- [SOC 2 posture](./soc2.md) - what the library covers, what your\r\n hosting / build pipeline must cover\r\n- [GDPR + data residency](./gdpr.md) - personal-data handling, where\r\n data physically sits, the user-rights surface\r\n- [HIPAA posture](./hipaa.md) - PHI handling in the browser, what\r\n \"no PHI on disk\" requires you to wire\r\n- [Audit log integration](./audit-log.md) - turn the grid's callbacks\r\n into an immutable audit trail with one adapter\r\n\r\n## Vendor-questionnaire shortlist\r\n\r\n| Question | Answer |\r\n| ------------------------------------------------- | --------------------------------------------------------------- |\r\n| Does the library transmit any data? | **No.** Zero outbound network calls. Inspect with DevTools. |\r\n| Does the library write to localStorage? | **Only when you opt in.** [Saved views](../help/saved-views.md) writes when you tell it to. |\r\n| Does the library evaluate user input as code? | **No.** CSP-compliant; no `eval` / `new Function`. |\r\n| Does the library include third-party trackers? | **No.** Verify the bundle - ~78 kB gzip, no analytics SDK. |\r\n| Is the library SOC 2 / ISO 27001 certified? | The LIBRARY can't be certified - it's not a service. Your hosted app gets certified; the library is in-scope as a dependency. See [SOC 2 posture](./soc2.md). |\r\n| Is the library GDPR-compliant? | The library is GDPR-neutral: it never processes data the user didn't already see. See [GDPR + data residency](./gdpr.md). |\r\n| Is the library HIPAA-compliant? | Same: HIPAA-neutral. PHI handling is a property of your app, not the grid. See [HIPAA posture](./hipaa.md). |\r\n| Is the source code auditable? | **Yes.** MIT-licensed; published as readable source (no minified obfuscation). |\r\n| Where is data stored? | **In your app's memory.** Never on a sv-grid server. There is no sv-grid server. |\r\n| Is there a security disclosure policy? | Yes - email `support@jqwidgets.com`. Patches typically ship within 7 days for high-severity issues. |\r\n| Is the library tested for accessibility? | Yes - WAI-ARIA 1.2 grid pattern + axe-core in CI. See [accessibility](../help/accessibility.md). |\r\n| Are dependencies vetted? | Yes - 0 runtime dependencies in `@svgrid/grid`. `@svgrid/enterprise` lazy-loads `jszip` + `pdfmake` as peers. See [security](../help/security.md) for the dep table. |\r\n| Is there an SBOM? | Yes - `pnpm run sbom` emits CycloneDX 1.5. See [security](../help/security.md#sbom-generation). |\r\n\r\n## See also\r\n\r\n- [Security & supply chain](../help/security.md) - the parent posture\r\n- [Observability](../help/observability.md) - the audit log seam\r\n- [API stability](../help/api-stability.md) - the deprecation promise\r\n"
|
|
2658
2658
|
},
|
|
2659
2659
|
{
|
|
2660
2660
|
"slug": "compliance/soc2",
|
|
@@ -2690,7 +2690,7 @@ export const docs = [
|
|
|
2690
2690
|
"slug": "enterprise/studio/access-control",
|
|
2691
2691
|
"path": "docs/enterprise/studio/access-control.md",
|
|
2692
2692
|
"title": "Access control (RBAC)",
|
|
2693
|
-
"markdown": "# Access control (RBAC)\r\n\r\nStudio has **authentication** (who you are, via [`SvAuthGate`](./auth.md) +\r\nSupabase) and, separately, **authorization** - what a signed-in user may do.\r\nRole-based access control (RBAC) gates **screens** and **write actions**\r\n(create / update / delete) per role, and it is enforced in **two** places: the\r\ngenerated UI *and* the generated API route. Server enforcement is the point - a\r\ntampered client can hide the buttons all it likes; the route still rejects the\r\nwrite.\r\n\r\n\r\n\r\n> Reads are implied by screen access: if a role can open a screen, it can read\r\n> that entity. The three gated actions are **create**, **update**, **delete**.\r\n\r\n## Turn it on\r\n\r\nIn the [visual designer](./app-designer.md), open the inspector with no block\r\nselected and expand **Access control (app-wide)**. Tick **Enable role-based\r\naccess** and you get three starter roles - `admin`, `editor`, `viewer` - which\r\nyou can rename, remove, or add to. For each role, choose:\r\n\r\n- **Actions** - create / update / delete checkboxes (all three ticked = full write).\r\n- **Screens** - *All screens*, or a specific subset.\r\n\r\nPick a **Default role** - the fallback when the app can't resolve one from the\r\nsession (it default-denies writes, so make it your least-privileged role).\r\n\r\nThe same policy lives in the project model, so it round-trips through\r\n`studio.config.json`:\r\n\r\n```ts\r\nimport type { StudioProject } from '@svgrid/enterprise'\r\n\r\nconst access: StudioProject['access'] = {\r\n enabled: true,\r\n defaultRole: 'viewer',\r\n roles: [\r\n { role: 'admin', screens: '*', actions: '*' },\r\n { role: 'editor', screens: '*', actions: ['create', 'update', 'delete'] },\r\n { role: 'viewer', screens: ['customers'], actions: [] },\r\n ],\r\n}\r\n```\r\n\r\n## What gets generated\r\n\r\nWith RBAC on, **Generate app** emits `src/lib/access.ts` - the single policy\r\nmodule shared by every screen and route:\r\n\r\n```ts\r\nimport { writable } from 'svelte/store'\r\n\r\nexport type AppRole = 'admin' | 'editor' | 'viewer'\r\nexport const currentRole = writable<AppRole>('viewer') // set this after login\r\n\r\nexport function canScreen(role: AppRole, screenId: string): boolean { /* ... */ }\r\nexport function can(role: AppRole, action: 'create' | 'update' | 'delete'): boolean { /* ... */ }\r\n\r\n// server-side\r\nexport function getServerRole(event: { locals?: Record<string, unknown> }): AppRole { /* reads event.locals.role */ }\r\nexport function authorizeAction(role: AppRole, action: 'read' | 'create' | 'update' | 'delete'): boolean { /* ... */ }\r\n```\r\n\r\n- The **layout** hides nav links a role can't open (`canScreen($currentRole, id)`).\r\n- Each **screen** hides the **+ New** button without `create` and blocks the edit\r\n form / inline edits without `update`.\r\n- Each SQL / Supabase **API route** passes an `authorize` hook to\r\n `createKitHandlers`, so the server rejects unauthorized writes with `403`.\r\n\r\n## Wire the role\r\n\r\nTwo ends to connect, both one-liners:\r\n\r\n**Client** - set `currentRole` once you know the user (after `SvAuthGate` signs\r\nthem in, or from `+layout`'s data):\r\n\r\n```svelte\r\n<script lang=\"ts\">\r\n import { currentRole } from '$lib/access'\r\n import { onMount } from 'svelte'\r\n onMount(() => currentRole.set(mySession.role)) // 'admin' | 'editor' | 'viewer'\r\n</script>\r\n```\r\n\r\n**Server** - put the role on `event.locals` in `hooks.server.ts` so\r\n`getServerRole` finds it (this is what actually enforces access):\r\n\r\n```ts\r\n// src/hooks.server.ts\r\nexport const handle = async ({ event, resolve }) => {\r\n const session = await getSession(event) // your auth\r\n event.locals.role = session?.role ?? 'viewer'\r\n return resolve(event)\r\n}\r\n```\r\n\r\n## The `authorize` hook (hand-written apps too)\r\n\r\nRBAC is built on a hook you can use without Studio. `createKitHandlers` accepts\r\nan `authorize` callback run before every op; return `false` (or throw) to reject:\r\n\r\n```ts\r\nimport { createKitHandlers } from '@svgrid/enterprise'\r\n\r\nexport const { POST } = createKitHandlers({\r\n schema: customersSchema,\r\n source,\r\n authorize: ({ action, event }) => {\r\n const role = event.locals?.role\r\n if (action === 'read') return true\r\n return role === 'admin' || role === 'editor'\r\n },\r\n})\r\n```\r\n\r\n`action` is `'read' | 'create' | 'update' | 'delete'`, and `event` carries the\r\nSvelteKit `locals` so you can read the session. It runs **before** the data\r\nsource is touched, so an unauthorized request never reaches your database.\r\n\r\n## Layering with Row-Level Security\r\n\r\nRBAC decides *which actions* a role may perform. To also scope *which rows* a\r\nuser sees, combine it with database [Row-Level Security](./auth.md#scope-data-per-user-with-rls) -\r\nRBAC in the app for actions + screens, RLS in Postgres for row visibility. The\r\ntwo are complementary: keep both on for defense in depth.\r\n\r\n## Multi-tenancy\r\n\r\nRBAC answers \"what may this role do?\". Multi-tenancy answers a different\r\nquestion - \"whose rows are these?\" - and the two compose: a role gates the\r\naction, the tenant gates the data.\r\n\r\n```ts\r\nproject.tenancy = { enabled: true } // column: tenantId\r\nproject.tenancy = { enabled: true, field: 'orgId' } // custom column\r\nproject.tenancy = { enabled: true, sharedEntities: ['currencies'] }\r\n```\r\n\r\nOne database, one deployment, rows partitioned by a tenant column. The tenant\r\ncomes from the signed-in user's session - never from anything the client sends.\r\n\r\n**It is enforced on the server, on all four paths.** Scoping reads alone is not\r\nisolation, so the generated API route also:\r\n\r\n| Operation | What the route does |\r\n| --- | --- |\r\n| read | merges the tenant predicate into the query, written **last** so a client-supplied `tenantId` filter cannot widen it |\r\n| create | stamps the tenant onto the row, overriding whatever was sent, and **after** any business-rule hook |\r\n| update / delete | re-reads the target row under the scope first and returns `403` if it isn't yours - otherwise guessing an id would reach across tenants |\r\n| update (patch) | re-stamps the tenant, so a patch cannot hand a row to someone else |\r\n\r\nIf the tenant cannot be resolved, `requireTenant` **throws** and the request\r\nfails with `403`. That is deliberate: returning \"no tenant\" would run the query\r\nunscoped, which is the one failure mode multi-tenancy cannot have.\r\n\r\n### What it generates\r\n\r\n- `src/lib/server/tenant.ts` - `getTenant` / `requireTenant` off `event.locals`\r\n- `scope: ...` on every scoped entity's `+server.ts`\r\n- a not-null tenant column on each scoped table **and** on `auth_users`, in the\r\n same migration as everything else\r\n- the tenant on `event.locals` in `hooks.server.ts`\r\n\r\nThe column is added to the database schema, not to the entity's field list, so\r\nit stays out of forms and grids - it is infrastructure, not data your users edit.\r\n\r\n### Requirements\r\n\r\nNeeds the [auth starter](./auth.md) (to know the tenant), the\r\n[typed data layer](./drizzle.md) (so the column exists), and at least one\r\nSQL-bound entity. Missing any of them it degrades to **off** rather than\r\nemitting a half-enforced scope; the `studio_set_tenancy` MCP tool says so\r\nexplicitly rather than letting you believe an unscoped app is scoped.\r\n\r\n`sharedEntities` stay global - reference tables like currencies or countries\r\nthat every tenant reads get no column and no scope.\r\n\r\n### Using the primitive directly\r\n\r\nOutside Studio, the same mechanism is one option on `createKitHandlers`:\r\n\r\n```ts\r\nexport const { POST } = createKitHandlers({\r\n schema, source,\r\n scope: ({ event }) => ({ field: 'tenantId', value: requireTenant(event) }),\r\n})\r\n```\r\n\r\nReturn `null` to skip scoping for a caller (a super-admin), or throw to reject.\r\n\r\n## See also\r\n\r\n- [Auth & secured screens](./auth.md) - authentication + RLS\r\n- [Code generation](./code-generation.md) · [Databases](./databases.md) - the API routes RBAC guards\r\n- [The visual designer](./app-designer.md) - where you author the policy\r\n"
|
|
2693
|
+
"markdown": "# Access control (RBAC)\r\n\r\nStudio has **authentication** (who you are, via [`SvAuthGate`](./auth.md) +\r\nSupabase) and, separately, **authorization** - what a signed-in user may do.\r\nRole-based access control (RBAC) gates **screens** and **write actions**\r\n(create / update / delete) per role, and it is enforced in **two** places: the\r\ngenerated UI *and* the generated API route. Server enforcement is the point - a\r\ntampered client can hide the buttons all it likes; the route still rejects the\r\nwrite.\r\n\r\n\r\n\r\n> Reads are implied by screen access: if a role can open a screen, it can read\r\n> that entity. The three gated actions are **create**, **update**, **delete**.\r\n\r\n## Turn it on\r\n\r\nIn the [visual designer](./app-designer.md), open the inspector with no block\r\nselected and expand **Access control (app-wide)**. Tick **Enable role-based\r\naccess** and you get three starter roles - `admin`, `editor`, `viewer` - which\r\nyou can rename, remove, or add to. For each role, choose:\r\n\r\n- **Actions** - create / update / delete checkboxes (all three ticked = full write).\r\n- **Screens** - *All screens*, or a specific subset.\r\n\r\nPick a **Default role** - the fallback when the app can't resolve one from the\r\nsession (it default-denies writes, so make it your least-privileged role).\r\n\r\nThe same policy lives in the project model, so it round-trips through\r\n`studio.config.json`:\r\n\r\n```ts\r\nimport type { StudioProject } from '@svgrid/enterprise'\r\n\r\nconst access: StudioProject['access'] = {\r\n enabled: true,\r\n defaultRole: 'viewer',\r\n roles: [\r\n { role: 'admin', screens: '*', actions: '*' },\r\n { role: 'editor', screens: '*', actions: ['create', 'update', 'delete'] },\r\n { role: 'viewer', screens: ['customers'], actions: [] },\r\n ],\r\n}\r\n```\r\n\r\n## What gets generated\r\n\r\nWith RBAC on, **Generate app** emits `src/lib/access.ts` - the single policy\r\nmodule shared by every screen and route:\r\n\r\n```ts {nocheck}\r\nimport { writable } from 'svelte/store'\r\n\r\nexport type AppRole = 'admin' | 'editor' | 'viewer'\r\nexport const currentRole = writable<AppRole>('viewer') // set this after login\r\n\r\nexport function canScreen(role: AppRole, screenId: string): boolean { /* ... */ }\r\nexport function can(role: AppRole, action: 'create' | 'update' | 'delete'): boolean { /* ... */ }\r\n\r\n// server-side\r\nexport function getServerRole(event: { locals?: Record<string, unknown> }): AppRole { /* reads event.locals.role */ }\r\nexport function authorizeAction(role: AppRole, action: 'read' | 'create' | 'update' | 'delete'): boolean { /* ... */ }\r\n```\r\n\r\n- The **layout** hides nav links a role can't open (`canScreen($currentRole, id)`).\r\n- Each **screen** hides the **+ New** button without `create` and blocks the edit\r\n form / inline edits without `update`.\r\n- Each SQL / Supabase **API route** passes an `authorize` hook to\r\n `createKitHandlers`, so the server rejects unauthorized writes with `403`.\r\n\r\n## Wire the role\r\n\r\nTwo ends to connect, both one-liners:\r\n\r\n**Client** - set `currentRole` once you know the user (after `SvAuthGate` signs\r\nthem in, or from `+layout`'s data):\r\n\r\n```svelte\r\n<script lang=\"ts\">\r\n import { currentRole } from '$lib/access'\r\n import { onMount } from 'svelte'\r\n onMount(() => currentRole.set(mySession.role)) // 'admin' | 'editor' | 'viewer'\r\n</script>\r\n```\r\n\r\n**Server** - put the role on `event.locals` in `hooks.server.ts` so\r\n`getServerRole` finds it (this is what actually enforces access):\r\n\r\n```ts\r\n// src/hooks.server.ts\r\nexport const handle = async ({ event, resolve }) => {\r\n const session = await getSession(event) // your auth\r\n event.locals.role = session?.role ?? 'viewer'\r\n return resolve(event)\r\n}\r\n```\r\n\r\n## The `authorize` hook (hand-written apps too)\r\n\r\nRBAC is built on a hook you can use without Studio. `createKitHandlers` accepts\r\nan `authorize` callback run before every op; return `false` (or throw) to reject:\r\n\r\n```ts\r\nimport { createKitHandlers } from '@svgrid/enterprise'\r\n\r\nexport const { POST } = createKitHandlers({\r\n schema: customersSchema,\r\n source,\r\n authorize: ({ action, event }) => {\r\n const role = event.locals?.role\r\n if (action === 'read') return true\r\n return role === 'admin' || role === 'editor'\r\n },\r\n})\r\n```\r\n\r\n`action` is `'read' | 'create' | 'update' | 'delete'`, and `event` carries the\r\nSvelteKit `locals` so you can read the session. It runs **before** the data\r\nsource is touched, so an unauthorized request never reaches your database.\r\n\r\n## Layering with Row-Level Security\r\n\r\nRBAC decides *which actions* a role may perform. To also scope *which rows* a\r\nuser sees, combine it with database [Row-Level Security](./auth.md#scope-data-per-user-with-rls) -\r\nRBAC in the app for actions + screens, RLS in Postgres for row visibility. The\r\ntwo are complementary: keep both on for defense in depth.\r\n\r\n## Multi-tenancy\r\n\r\nRBAC answers \"what may this role do?\". Multi-tenancy answers a different\r\nquestion - \"whose rows are these?\" - and the two compose: a role gates the\r\naction, the tenant gates the data.\r\n\r\n```ts\r\nproject.tenancy = { enabled: true } // column: tenantId\r\nproject.tenancy = { enabled: true, field: 'orgId' } // custom column\r\nproject.tenancy = { enabled: true, sharedEntities: ['currencies'] }\r\n```\r\n\r\nOne database, one deployment, rows partitioned by a tenant column. The tenant\r\ncomes from the signed-in user's session - never from anything the client sends.\r\n\r\n**It is enforced on the server, on all four paths.** Scoping reads alone is not\r\nisolation, so the generated API route also:\r\n\r\n| Operation | What the route does |\r\n| --- | --- |\r\n| read | merges the tenant predicate into the query, written **last** so a client-supplied `tenantId` filter cannot widen it |\r\n| create | stamps the tenant onto the row, overriding whatever was sent, and **after** any business-rule hook |\r\n| update / delete | re-reads the target row under the scope first and returns `403` if it isn't yours - otherwise guessing an id would reach across tenants |\r\n| update (patch) | re-stamps the tenant, so a patch cannot hand a row to someone else |\r\n\r\nIf the tenant cannot be resolved, `requireTenant` **throws** and the request\r\nfails with `403`. That is deliberate: returning \"no tenant\" would run the query\r\nunscoped, which is the one failure mode multi-tenancy cannot have.\r\n\r\n### What it generates\r\n\r\n- `src/lib/server/tenant.ts` - `getTenant` / `requireTenant` off `event.locals`\r\n- `scope: ...` on every scoped entity's `+server.ts`\r\n- a not-null tenant column on each scoped table **and** on `auth_users`, in the\r\n same migration as everything else\r\n- the tenant on `event.locals` in `hooks.server.ts`\r\n\r\nThe column is added to the database schema, not to the entity's field list, so\r\nit stays out of forms and grids - it is infrastructure, not data your users edit.\r\n\r\n### Requirements\r\n\r\nNeeds the [auth starter](./auth.md) (to know the tenant), the\r\n[typed data layer](./drizzle.md) (so the column exists), and at least one\r\nSQL-bound entity. Missing any of them it degrades to **off** rather than\r\nemitting a half-enforced scope; the `studio_set_tenancy` MCP tool says so\r\nexplicitly rather than letting you believe an unscoped app is scoped.\r\n\r\n`sharedEntities` stay global - reference tables like currencies or countries\r\nthat every tenant reads get no column and no scope.\r\n\r\n### Using the primitive directly\r\n\r\nOutside Studio, the same mechanism is one option on `createKitHandlers`:\r\n\r\n```ts\r\nexport const { POST } = createKitHandlers({\r\n schema, source,\r\n scope: ({ event }) => ({ field: 'tenantId', value: requireTenant(event) }),\r\n})\r\n```\r\n\r\nReturn `null` to skip scoping for a caller (a super-admin), or throw to reject.\r\n\r\n## See also\r\n\r\n- [Auth & secured screens](./auth.md) - authentication + RLS\r\n- [Code generation](./code-generation.md) · [Databases](./databases.md) - the API routes RBAC guards\r\n- [The visual designer](./app-designer.md) - where you author the policy\r\n"
|
|
2694
2694
|
},
|
|
2695
2695
|
{
|
|
2696
2696
|
"slug": "enterprise/studio/accessibility",
|
|
@@ -2714,7 +2714,7 @@ export const docs = [
|
|
|
2714
2714
|
"slug": "enterprise/studio/app-designer",
|
|
2715
2715
|
"path": "docs/enterprise/studio/app-designer.md",
|
|
2716
2716
|
"title": "Visual app designer",
|
|
2717
|
-
"markdown": "# Visual app designer\r\n\r\n`SvStudioDesigner` is the grid-centric visual **data-app** designer - compose a\r\nmulti-entity app by arranging data-bound blocks on a canvas, then generate a\r\nrunnable SvelteKit project. It's the app-level companion to the single-entity\r\n[schema designer](./designer.md): where that authors one `EntitySchema`, this\r\ncomposes **screens** across **many entities**.\r\n\r\nGrid-centric by design: the blocks are schema-driven and data-bound (a grid, a\r\nchart, a pivot, a dashboard, a KPI, master-detail, a faceted filter panel, a\r\nrecord panel, a lookup) - not arbitrary layout components. Point it at a\r\ndatabase, get a CRUD app - kept to data views.\r\n\r\n> **Just want to open it?** `npx @svgrid/studio designer` launches this designer\r\n> in your browser, auto-saves your work to `studio.config.json`, and writes the\r\n> generated app to a folder - no host app needed. See\r\n> [Launch the designer](./launch.md).\r\n\r\n## Starting a new app\r\n\r\n**New app** in the top bar walks you from nothing to a working CRUD app: pick\r\nwhere the data comes from, choose the tables, choose the pages, open the result.\r\n\r\n1. **Start** - sample data, your own data, or a blank set of tables you name.\r\n2. **Data** - connect a database (the table picker shows row counts and lets you\r\n preview rows before importing), read a **Supabase** project, pick a starter\r\n dataset, point at a REST endpoint, or paste an OpenAPI document.\r\n3. **Screens** - tick which pages each table gets (list, form, record page,\r\n dashboard) and how rows are edited: a popup form, in the grid, or on the\r\n record page.\r\n4. **Done** - name it and open it. It arrives as one undo step, so Ctrl+Z puts\r\n the previous design back.\r\n\r\nConnecting to a live SQL database needs the local designer\r\n(`npx @svgrid/studio dev`) because database drivers run on your machine, not in a\r\nbrowser tab. **Supabase is the exception**: it serves its own REST API, so the\r\nwizard reads your tables with just the project URL and the anon key - it is the\r\none real database that works from\r\n[svgrid.com/studio](https://svgrid.com/studio) with nothing installed. Row-level\r\nsecurity still applies, so the app sees exactly what the browser may see. The\r\nother paths work there too, and you can rebind to any database later with\r\n**Use my data**.\r\n\r\nThe terminal equivalent is [`svgrid-studio init`](./cli.md#init) - same\r\nquestions, same generator, same app.\r\n\r\n## How the screen is laid out\r\n\r\nHere is the real designer with a small Sales App open:\r\n\r\n\r\n\r\nYou do not need to understand the internals to use it. The same layout, labelled:\r\n\r\n\r\n\r\n- **Screens** (far left) - the pages of your app. Click one to edit it; **+ Add\r\n screen** makes a new one.\r\n- **Blocks** - the pieces you drop onto a screen: a **grid** (a table of records),\r\n a **chart**, a **pivot**, a **KPI** number, a **dashboard**, a **filter panel**,\r\n and a **record panel**. Click or drag one onto the preview.\r\n- **Live preview** (middle) - your screen with **real data**, updating as you\r\n change things. What you see is what the app will look like.\r\n- **Properties** (right) - tune the selected block, or - with nothing selected -\r\n edit the entity's **fields** and pick its **data source**.\r\n- **Generate app** (top right) - when it looks right, one click writes the whole,\r\n runnable app.\r\n\r\nThe rest of this page is the detailed reference for each area, aimed at developers\r\nembedding or scripting the designer. If you just want to build an app, everything\r\nabove is done by pointing and clicking - see\r\n[Launch the designer](./launch.md).\r\n\r\n## What it edits: the project model\r\n\r\nThe designer reads and writes a `StudioProject` - the declarative model behind\r\nthe whole app:\r\n\r\n```ts\r\nimport { createProject } from '@svgrid/enterprise'\r\n\r\n// One default screen (grid + edit form) per entity, in-memory.\r\nlet project = $state(createProject([customerSchema, orderSchema], { title: 'Sales App' }))\r\n```\r\n\r\n```svelte\r\n<script lang=\"ts\">\r\n import { SvStudioDesigner } from '@svgrid/enterprise'\r\n</script>\r\n\r\n<SvStudioDesigner {project} onChange={(p) => (project = p)} />\r\n```\r\n\r\nThe designer is a single IDE-style frame: a **title bar** (app name + accent +\r\nundo/redo + Import/Load/Save/Generate), a **screen tab strip** (switch, close, or\r\nadd a screen), the three work panels, and a **status bar** (validity, entity /\r\nscreen / block counts, current selection, data source).\r\n\r\n- **Rail** (left) - switch and add screens; each screen is bound to an entity.\r\n- **Screen tabs** - the open screens as a document strip; click to switch, the\r\n **x** to remove one, the **+** to add one (from the current template).\r\n- **Palette** - the block kinds. **Drag** one onto the canvas to add it (or click).\r\n- **Canvas** - the screen's blocks in a responsive **12-column** grid, previewed\r\n live with the real components. **Drag a block** to reorder it; **drag its right\r\n edge** to set its width (1-12 columns), use the ⅓ / ½ / ⅔ / full quick buttons\r\n in its header, or the **Layout > Width** slider. **Drag a block's bottom edge**\r\n to make its region taller or shorter - grid, chart, pivot, and master-detail\r\n blocks are height-resizable, and the chosen height flows through to the generated app\r\n (also set it precisely under **Layout > Height** in the inspector).\r\n- **Inspector** (right) - edit the selected block: a grid's **editing mode** +\r\n behavior + **column config** (see below), a chart's group-by / measure / reduce\r\n / type. With no block selected, edit the **page** (title / route /\r\n **nav** settings), the entity's **data source** (see below), and the **entity's\r\n fields** - add, rename, retype, flag (PK / required / read-only), pick a\r\n **relation's target entity + label field**, and **drag to reorder**. The rail\r\n sets the **default source kind** for new entities, the **app layout**, and adds\r\n screens from a **template** (CRUD, dashboard, master-detail, empty).\r\n- **Top bar** - rename the app, set an **accent color** (themes the whole app),\r\n add a **New entity** from scratch, **Import CSV** (drop in a spreadsheet - see\r\n below), **Connect DB** (the launcher's live-database wizard - see\r\n [Launch the designer](./launch.md)), **Import schema** (paste a Drizzle / Prisma\r\n schema to add its entities), **Save / Load** the design as `studio.config.json`,\r\n and **Generate app**. With no entities yet, the canvas shows an **onboarding**\r\n screen offering the same ways to start.\r\n- **✨ Copilot** (when the host wires it) - describe a change in plain English\r\n (\"add an orders screen with a revenue chart\", \"make mrr required\") and the AI\r\n edits your project. It's a host hook: `<SvStudioDesigner onCopilot={...} />`\r\n receives `{ prompt, project }` and returns the edited `StudioProject` - your AI\r\n keys stay server-side. The result is validated before it applies, and it's one\r\n **Ctrl/Cmd+Z** away.\r\n\r\nThe three panels are **resizable** (drag the dividers). Every edit is **undoable**\r\n(Ctrl/Cmd+Z, Ctrl+Shift+Z / Ctrl+Y to redo); **Delete** removes the selected\r\nblock, **Ctrl/Cmd+D** (or the **⧉** header button) **duplicates** it, and\r\n**Escape** deselects. **Preview app** opens the whole app full-screen with a\r\n**Desktop / Tablet / Mobile** device-width toggle to check responsiveness. **Generate app** opens the output in a\r\n**file-tree viewer** modal (scrollable, Copy per file) and a **Download .zip** of\r\nthe **complete runnable SvelteKit + Vite project** - unzip, `npm install`,\r\n`npm run dev`. The zip includes `package.json` (with the right driver deps),\r\n`vite.config.ts`, `svelte.config.js`, `tsconfig.json`, the app shell, and every\r\ngenerated screen.\r\n\r\n## The grid (and how it edits)\r\n\r\nThe grid is the core block, so **editing is a grid property**, not a separate\r\nblock. Select a grid and set its **Editing mode**:\r\n\r\n- **Popup form** - double-click a row to edit it in a modal / drawer / inline\r\n panel (pick the **Form style**); a **+ New** button adds rows. This is the\r\n default.\r\n- **Inline** - edit cells right in the grid (Excel-style); each change saves via\r\n the data source.\r\n- **Read-only** - no editing.\r\n\r\nThe grid's property editor also covers **Behavior** (Sortable, Filtering + search,\r\nRow selection, Cell range selection, Striped rows, Totals footer row, Density),\r\n**Paging** (Paginate on / off, Page size, **Pager position** - bottom / top / both,\r\nand the **Page size options** for the selector), and per-**Column** settings -\r\nexpand a column to set its **header**, **width**, **alignment**, **pin** (left /\r\nright), plus show / hide + reorder. There is no standalone \"Edit form\" block - the\r\ngrid owns editing end to end.\r\n\r\n### Export toolbar\r\n\r\n**Export toolbar** adds a button bar above the grid. Six options, in two groups:\r\n\r\n| Button | Runs through | Adds to the generated app |\r\n| ------ | ------------ | ------------------------- |\r\n| Export CSV / Export JSON / Copy | the free grid API | nothing |\r\n| Export Excel (.xlsx) | `@svgrid/enterprise` | `jszip` |\r\n| Export PDF | `@svgrid/enterprise` | `pdfmake` |\r\n| Print | `@svgrid/enterprise` | nothing |\r\n\r\nThe Excel export is real OOXML - typed number and date cells, styled headers, a\r\nfrozen header row - not a renamed CSV. PDF is paginated with a repeating header,\r\nand Print opens the browser's print dialog on a paginated layout.\r\n\r\nAll six export what the user currently sees: the visible columns, in their\r\ncurrent order, over the filtered and sorted rows. The optional dependencies are\r\ndeclared only for the buttons you switch on, so a CSV-only app installs neither.\r\nThe canvas preview runs the same code the generated app does, so you can try a\r\nreal export before generating.\r\n\r\n## The analytical + companion blocks\r\n\r\nBeyond the grid, every block is still bound to the `EntitySchema` - these are data\r\nviews, not generic widgets:\r\n\r\n| Block | What it renders | Inspector |\r\n| --- | --- | --- |\r\n| **Chart** | A chart (`SvSchemaChart`) - bar, pie, line, area, radar, funnel, waterfall, or treemap. | Group-by dimension, measure, reduce, type. |\r\n| **Pivot** | A full pivot table (`SvPivotDesigner`) the end user can re-pivot live. | Row + column dimensions (checkboxes), a measure, and its aggregate. |\r\n| **Dashboard** | A schema-driven KPI + chart board (`SvSchemaDashboard`). | - |\r\n| **KPI** | A single reduced metric tile. | Label, measure, reduce. |\r\n| **Gauge** | A radial gauge (`SvGauge`) of one reduced measure within a range - utilization, progress, scores. | Label, measure, reduce, min / max, unit. |\r\n| **Tree** | A hierarchical tree (`SvTree`) built from the entity's own rows via a self-referential parent. | A label field + a parent field (a row's link to its parent row). |\r\n| **Tabs** | A tabbed container (`SvTabs`) that **groups display blocks** into tabs - e.g. an Overview tab of KPIs + a Details tab with a chart. | Add / rename / remove tabs; per tab, add child blocks (charts, KPIs, gauges, pivots, trees). |\r\n| **Accordion** | A collapsible-sections container (like Tabs, stacked vertically). | Add / rename / remove sections; child blocks per section. |\r\n| **Master / detail** | A row that expands into a nested grid of related records. | Child entity + foreign key. |\r\n| **Board** | A kanban board of the entity's rows, one lane per value of a group-by field, with drag between lanes. | Group-by, card title / subtitle / badge fields, open-screen drill. |\r\n| **Calendar** | A month event-calendar: each row with a date lands on its day, labelled and optionally color-coded. | Date field, title field, color field, open-screen drill. |\r\n| **Detail** | A full record \"detail page\": header, metric row, field sections, and related-record tabs - the 360 view a row action or drill-through opens. | Title / subtitle / status / metric fields, sections, related child entities. |\r\n| **Form** | A standalone create / edit form for the entity. | Presentation (drawer / modal / inline). |\r\n| **Filter panel** | A faceted sidebar that **filters the screen's grid** - enum / boolean facets pick a value, text facets search. | Title + which fields become facets. |\r\n| **Record panel** | Shows the row **selected in the grid** - a read-only field list, or an inline edit form. | Editable on / off, and (read-only) which fields to show. |\r\n| **Lookup** | Marks a relation field as a searchable picker in the edit form. | The relation field. |\r\n| **UI component** | A component from the SvGrid UI kit dropped from the toolbox - entity-agnostic, works on freestanding pages too. Grouped as Actions, Inputs, Display, Feedback, Layout, and Navigation, and covering headings and prose (heading, text, link, quote, code, keyboard key, list) as well as controls, pickers, and date/time inputs. | The component's own props (extracted from the component's own types, with its JSDoc as the tooltip), plus data bindings. |\r\n\r\nThe **filter** and **record** panels wire to the grid on the same screen: the\r\nfilter panel calls the grid controller's `setFilter`, and clicking a grid row\r\npublishes it to the record panel. So a common layout is a **filter panel + grid +\r\nrecord panel** three-up - list, narrow, inspect - all generated for you.\r\n\r\nThe grid, chart, pivot, and master-detail blocks are **height-resizable** (drag\r\nthe block's bottom edge, or set **Layout > Height**), and the chosen height flows\r\ninto the generated app. The filter and record panels size to their content.\r\n\r\n### Conditional formatting\r\n\r\nA grid's inspector has a **Conditional formatting** section: add no-code rules\r\nthat style a cell by its value - pick a field, a comparison (`=`, `<`, `>`,\r\n`contains`, `is empty`, ...), a value, and a **text color / fill / bold**. Rules\r\nrender **live in the canvas** and compile to the grid's built-in\r\n`conditionalFormats` rule engine in the generated app (e.g. negative `mrr` red,\r\n`status = overdue` filled). It's the same engine you'd use by hand - the designer\r\njust authors the rules.\r\n\r\n### Navigation & row actions\r\n\r\nThe grid's **Navigation & actions** section wires flow between screens:\r\n**drill-through** (row click opens another screen, filtered to the clicked value)\r\nand **row action buttons** (Edit / Delete / Open). A chart can drill too. See\r\n[Navigation & row actions](./navigation.md) for the full picture.\r\n\r\n## Data sources (per entity)\r\n\r\nEach entity binds to **its own backend** - the designer is not limited to one\r\ndata source per app. In the inspector (no block selected), the **Data source**\r\nsection binds the screen's entity to:\r\n\r\n- **In-memory** - seeded sample rows, runs with no backend (the default).\r\n- **Local database (no setup)** - a real, persistent Postgres ([PGlite](https://pglite.dev))\r\n running in the browser and saved to IndexedDB, so rows survive reloads with zero\r\n backend. Same SQL as production - swap to a hosted **SQL** source later without\r\n touching the schema. See [Local database](./local-database.md).\r\n The builder is a draggable, resizable, maximizable panel, and you can open it for\r\n any entity straight from the **Data model** dialog's **Configure** button.\r\n\r\n- **REST API** - a request builder: **method**, **base URL**, **path** (path\r\n params auto-derive from `{tokens}`), and **Query / Path / Header** tabs. **Send**\r\n runs it live and shows a **response table** plus the real rows in the canvas\r\n grid; **Import fields** rewrites the entity's schema to match the response keys.\r\n An **API format** picker (Manual / Offset + Limit / DummyJSON / json-server)\r\n wires a wire-format adapter so the grid does **real server-side paging and sort**;\r\n Manual keeps the rows-path / total-path mapping for a single fetched page.\r\n- **Supabase** - **List tables** reads your project's tables so you pick one, and\r\n **Import schema** pulls its real columns, primary key, foreign keys, and enums\r\n into the entity; **Preview** shows live rows (works in the online designer, since\r\n Supabase is HTTP). The project **URL + anon key are a shared connection** you set\r\n once for every Supabase entity. A **Live updates (Realtime)** toggle emits a live\r\n subscription. **Generate app** emits `createClient` in `connections.ts` reading\r\n `PUBLIC_SUPABASE_URL` / `PUBLIC_SUPABASE_ANON_KEY` from `.env` (the key is never\r\n inlined) and adds `@supabase/supabase-js`; access is protected by your RLS\r\n policies.\r\n- **SQL** - paste a connection string (the dialect is auto-detected) or fill the\r\n guided form, set the **Schema** (Postgres search path). In the **local** designer\r\n (`npx @svgrid/studio designer`) **Preview data** runs a real `SELECT` and shows\r\n your actual rows on the canvas, and a missing `pg` / `mysql2` / ... driver is a\r\n **one-click install**. **Generate app** emits a connected\r\n `src/routes/api/<table>/+server.ts` (the dialect's driver, reading\r\n `DATABASE_URL`) and points the grid at it; the driver dep is added for you. In\r\n the **online** designer, bind the entity and generate - the app connects for real\r\n at runtime. To scaffold from an existing DB via CLI: `npx @svgrid/studio add\r\n <table> --db <dialect> --url …`.\r\n\r\n**Generate app** then emits the matching adapter per entity in `src/lib/data.ts`\r\n(`createRestDataSource` / `createSqlDataSource` / `createSupabaseDataSource` /\r\n`createInMemoryDataSource`). SQL and Supabase entities read their connection from a\r\ngenerated `src/lib/connections.ts` (or the `+server.ts` route) - the SQL driver and\r\nthe Supabase client are wired for you; the only manual step is setting the\r\nconnection in `.env` (the bundle ships a `.env.example`). See\r\n[Databases](./databases.md) and [REST API](./rest-api.md).\r\n\r\n## Import a CSV / spreadsheet\r\n\r\nThe fastest way to start from **your own data**: **Import CSV** in the top bar\r\ntakes a `.csv` file and turns it into a running screen. The designer parses the\r\nfile (quoted fields, embedded newlines, and CRLF included), **infers a type per\r\ncolumn** from its values (number, boolean, date, or text - thousands separators\r\nand `yes/no/true/false` are understood), ensures a **primary key** (it reuses an\r\n`id` column or synthesizes one), and adds an entity with a full CRUD screen,\r\n**seeded with the real rows**. Unsafe headers (`First Name`, `E-mail`) become safe\r\nfield keys and the note tells you what was renamed.\r\n\r\nImported rows ship **in-memory** by default (no dependencies), so the app runs\r\nimmediately. Switch that entity's **Data source** to **Local database** to make\r\nthe same imported rows **persist** across reloads - the seed carries over. This is\r\nall client-side: `csvToEntity(name, text)` is a pure function exported from\r\n`@svgrid/enterprise`, so the same import works in the CLI and your own tools.\r\n\r\n> **Large files:** the imported rows are stored as the entity's seed, so they are\r\n> embedded in `studio.config.json` when you **Save** the design. That is fine for\r\n> reference data and samples; for a large dataset, import a representative sample\r\n> and point the entity at a **database** (Local database / SQL) for the full data.\r\n\r\n## Pages and layout\r\n\r\n- **Pages** - each screen is a route. In the inspector's **Page** section, toggle\r\n **Show in navigation**, set a **nav label** and **nav order**, or start a page\r\n from the **Empty** template. Hidden pages stay routable but drop out of the nav.\r\n- **Render mode** - eligible screens (a single plain grid, or read-only block\r\n screens, on a memory / SQL source) can switch from the default client page to\r\n **SSR**: an idiomatic `+page.server.ts` with `load` + form actions. The rules\r\n are in [Code generation](./code-generation.md#render-mode-spa-or-ssr-per-screen).\r\n- **App layout** - the rail's **App layout** section themes the generated shell:\r\n **Sidebar** or **Top navigation**, a **brand** name, a **company logo**\r\n (uploaded - stored inline and shown in the nav in place of the brand text), a\r\n **footer**, and (for the sidebar) the **nav position** (left / right). This\r\n drives the generated `src/routes/+layout.svelte`. The generated shell is\r\n **responsive**: on phones the sidebar collapses to a hamburger drawer, the\r\n top-nav links scroll, and each screen's block grid stacks to one column.\r\n\r\n## Save, regenerate, round-trip\r\n\r\nThe `StudioProject` is the persisted design. **Save config** exports a\r\n`studio.config.json`; regenerate the app from it any time:\r\n\r\n```bash\r\nnpm create @svgrid/studio@latest my-app -- --project ./studio.config.json\r\n```\r\n\r\nOr programmatically:\r\n\r\n```ts\r\nimport { serializeProject, parseProject, emitStudioProject } from '@svgrid/enterprise'\r\n\r\nconst json = serializeProject(project) // save the design\r\nconst project2 = parseProject(json) // reopen it\r\nconst files = emitStudioProject(project2) // -> the app's source files\r\n```\r\n\r\n**The exported app carries its own design.** The downloaded zip includes a\r\n`studio.config.json` at its root. To keep editing the app visually after you've\r\nworked on it locally, open the designer and **Load** that file - entities,\r\nscreens, blocks, theme, RBAC, i18n, and now the logo all come back exactly as\r\ngenerated. Because the designer regenerates the files under `src/`, keep any\r\nhand-written code in **new** files/modules you import, so a re-generate never\r\noverwrites it (or use the CLI's `svgrid:managed` markers - see\r\n[Code generation](./code-generation.md)).\r\n\r\n## Generate the app\r\n\r\n**Generate app** emits `src/lib/schemas.ts`, `src/lib/data.ts` (the right adapter\r\nper entity, plus `src/lib/connections.ts` when any entity is SQL / Supabase-bound),\r\nand one `src/routes/<route>/+page.svelte` **per screen** that composes that\r\nscreen's blocks with their config - a grid (with your visible columns, in order) +\r\nedit modal, plus any charts / pivots / dashboard / KPI tiles, and filter / record\r\npanels wired to the grid, all bound to the data - and the\r\nnav layout (sidebar or top-nav) + home. The pages are self-contained (they use\r\n`@svgrid/grid` + `@svgrid/enterprise` directly), so the output runs as a standard\r\nSvelteKit app.\r\n\r\n```ts\r\nimport { emitStudioProject } from '@svgrid/enterprise'\r\nconst files = emitStudioProject(project) // [{ path, contents, description }, ...]\r\n```\r\n\r\n## See also\r\n\r\n- [Code behind (Code view)](./code-behind.md) - write TypeScript against a typed `ctx` (grid API, events, `ctx.grid.sortable = true`, lifecycle)\r\n- [Sample apps + bind your data](./samples.md) - start from a ready-made app, then point it at your database\r\n- [Launch the designer](./launch.md) - `npx @svgrid/studio designer` (auto-save + generate to a folder)\r\n- [Schema designer](./designer.md) - author a single entity\r\n- [Dashboards](./dashboards.md) · [Databases](./databases.md) - the blocks + data sources\r\n- [CLI](./cli.md) / [Drizzle](./drizzle.md) / [Prisma](./prisma.md) - import a schema to design from\r\n"
|
|
2717
|
+
"markdown": "# Visual app designer\r\n\r\n`SvStudioDesigner` is the grid-centric visual **data-app** designer - compose a\r\nmulti-entity app by arranging data-bound blocks on a canvas, then generate a\r\nrunnable SvelteKit project. It's the app-level companion to the single-entity\r\n[schema designer](./designer.md): where that authors one `EntitySchema`, this\r\ncomposes **screens** across **many entities**.\r\n\r\nGrid-centric by design: the blocks are schema-driven and data-bound (a grid, a\r\nchart, a pivot, a dashboard, a KPI, master-detail, a faceted filter panel, a\r\nrecord panel, a lookup) - not arbitrary layout components. Point it at a\r\ndatabase, get a CRUD app - kept to data views.\r\n\r\n> **Just want to open it?** `npx @svgrid/studio designer` launches this designer\r\n> in your browser, auto-saves your work to `studio.config.json`, and writes the\r\n> generated app to a folder - no host app needed. See\r\n> [Launch the designer](./launch.md).\r\n\r\n## Starting a new app\r\n\r\n**New app** in the top bar walks you from nothing to a working CRUD app: pick\r\nwhere the data comes from, choose the tables, choose the pages, open the result.\r\n\r\n1. **Start** - sample data, your own data, or a blank set of tables you name.\r\n2. **Data** - connect a database (the table picker shows row counts and lets you\r\n preview rows before importing), read a **Supabase** project, pick a starter\r\n dataset, point at a REST endpoint, or paste an OpenAPI document.\r\n3. **Screens** - tick which pages each table gets (list, form, record page,\r\n dashboard) and how rows are edited: a popup form, in the grid, or on the\r\n record page.\r\n4. **Done** - name it and open it. It arrives as one undo step, so Ctrl+Z puts\r\n the previous design back.\r\n\r\nConnecting to a live SQL database needs the local designer\r\n(`npx @svgrid/studio dev`) because database drivers run on your machine, not in a\r\nbrowser tab. **Supabase is the exception**: it serves its own REST API, so the\r\nwizard reads your tables with just the project URL and the anon key - it is the\r\none real database that works from\r\n[svgrid.com/studio](https://svgrid.com/studio) with nothing installed. Row-level\r\nsecurity still applies, so the app sees exactly what the browser may see. The\r\nother paths work there too, and you can rebind to any database later with\r\n**Use my data**.\r\n\r\nThe terminal equivalent is [`svgrid-studio init`](./cli.md#init) - the same\r\ngenerator, the same app. The questions differ slightly: the CLI also asks for a\r\ntheme and light/dark, while the wizard offers a dashboard page per table (pick\r\nyour theme in the designer afterwards).\r\n\r\n## How the screen is laid out\r\n\r\nHere is the real designer with a small Sales App open:\r\n\r\n\r\n\r\nYou do not need to understand the internals to use it. The same layout, labelled:\r\n\r\n\r\n\r\n- **Screens** (far left) - the pages of your app. Click one to edit it; **+ Add\r\n screen** makes a new one.\r\n- **Blocks** - the pieces you drop onto a screen: a **grid** (a table of records),\r\n a **chart**, a **pivot**, a **KPI** number, a **dashboard**, a **filter panel**,\r\n and a **record panel**. Click or drag one onto the preview.\r\n- **Live preview** (middle) - your screen with **real data**, updating as you\r\n change things. What you see is what the app will look like.\r\n- **Properties** (right) - tune the selected block, or - with nothing selected -\r\n edit the entity's **fields** and pick its **data source**.\r\n- **Generate app** (top right) - when it looks right, one click writes the whole,\r\n runnable app.\r\n\r\nThe rest of this page is the detailed reference for each area, aimed at developers\r\nembedding or scripting the designer. If you just want to build an app, everything\r\nabove is done by pointing and clicking - see\r\n[Launch the designer](./launch.md).\r\n\r\n## What it edits: the project model\r\n\r\nThe designer reads and writes a `StudioProject` - the declarative model behind\r\nthe whole app:\r\n\r\n```ts\r\nimport { createProject } from '@svgrid/enterprise'\r\n\r\n// One default screen (grid + edit form) per entity, in-memory.\r\nlet project = $state(createProject([customerSchema, orderSchema], { title: 'Sales App' }))\r\n```\r\n\r\n```svelte {nocheck}\r\n<script lang=\"ts\">\r\n import { SvStudioDesigner } from '@svgrid/enterprise'\r\n</script>\r\n\r\n<SvStudioDesigner {project} onChange={(p) => (project = p)} />\r\n```\r\n\r\nThe designer is a single IDE-style frame: a **title bar** (app name + accent +\r\nundo/redo + Import/Load/Save/Generate), a **screen tab strip** (switch, close, or\r\nadd a screen), the three work panels, and a **status bar** (validity, entity /\r\nscreen / block counts, current selection, data source).\r\n\r\n- **Rail** (left) - switch and add screens; each screen is bound to an entity.\r\n- **Screen tabs** - the open screens as a document strip; click to switch, the\r\n **x** to remove one, the **+** to add one (from the current template).\r\n- **Palette** - the block kinds. **Drag** one onto the canvas to add it (or click).\r\n- **Canvas** - the screen's blocks in a responsive **12-column** grid, previewed\r\n live with the real components. **Drag a block** to reorder it; **drag its right\r\n edge** to set its width (1-12 columns), use the ⅓ / ½ / ⅔ / full quick buttons\r\n in its header, or the **Layout > Width** slider. **Drag a block's bottom edge**\r\n to make its region taller or shorter - grid, chart, pivot, and master-detail\r\n blocks are height-resizable, and the chosen height flows through to the generated app\r\n (also set it precisely under **Layout > Height** in the inspector).\r\n- **Inspector** (right) - edit the selected block: a grid's **editing mode** +\r\n behavior + **column config** (see below), a chart's group-by / measure / reduce\r\n / type. With no block selected, edit the **page** (title / route /\r\n **nav** settings), the entity's **data source** (see below), and the **entity's\r\n fields** - add, rename, retype, flag (PK / required / read-only), pick a\r\n **relation's target entity + label field**, and **drag to reorder**. The rail\r\n sets the **default source kind** for new entities, the **app layout**, and adds\r\n screens from a **template** (CRUD, dashboard, master-detail, empty).\r\n- **Top bar** - rename the app, set an **accent color** (themes the whole app),\r\n add a **New entity** from scratch, **Import CSV** (drop in a spreadsheet - see\r\n below), **Connect DB** (the launcher's live-database wizard - see\r\n [Launch the designer](./launch.md)), **Import schema** (paste a Drizzle / Prisma\r\n schema to add its entities), **Save / Load** the design as `studio.config.json`,\r\n and **Generate app**. With no entities yet, the canvas shows an **onboarding**\r\n screen offering the same ways to start.\r\n- **✨ Copilot** (when the host wires it) - describe a change in plain English\r\n (\"add an orders screen with a revenue chart\", \"make mrr required\") and the AI\r\n edits your project. It's a host hook: `<SvStudioDesigner onCopilot={...} />`\r\n receives `{ prompt, project }` and returns the edited `StudioProject` - your AI\r\n keys stay server-side. The result is validated before it applies, and it's one\r\n **Ctrl/Cmd+Z** away.\r\n\r\nThe three panels are **resizable** (drag the dividers). Every edit is **undoable**\r\n(Ctrl/Cmd+Z, Ctrl+Shift+Z / Ctrl+Y to redo); **Delete** removes the selected\r\nblock, **Ctrl/Cmd+D** (or the **⧉** header button) **duplicates** it, and\r\n**Escape** deselects. **Preview app** opens the whole app full-screen with a\r\n**Desktop / Tablet / Mobile** device-width toggle to check responsiveness. **Generate app** opens the output in a\r\n**file-tree viewer** modal (scrollable, Copy per file) and a **Download .zip** of\r\nthe **complete runnable SvelteKit + Vite project** - unzip, `npm install`,\r\n`npm run dev`. The zip includes `package.json` (with the right driver deps),\r\n`vite.config.ts`, `svelte.config.js`, `tsconfig.json`, the app shell, and every\r\ngenerated screen.\r\n\r\n## The grid (and how it edits)\r\n\r\nThe grid is the core block, so **editing is a grid property**, not a separate\r\nblock. Select a grid and set its **Editing mode**:\r\n\r\n- **Popup form** - double-click a row to edit it in a modal / drawer / inline\r\n panel (pick the **Form style**); a **+ New** button adds rows. This is the\r\n default.\r\n- **Inline** - edit cells right in the grid (Excel-style); each change saves via\r\n the data source.\r\n- **Read-only** - no editing.\r\n\r\nThe grid's property editor also covers **Behavior** (Sortable, Filtering + search,\r\nRow selection, Cell range selection, Striped rows, Totals footer row, Density),\r\n**Paging** (Paginate on / off, Page size, **Pager position** - bottom / top / both,\r\nand the **Page size options** for the selector), and per-**Column** settings -\r\nexpand a column to set its **header**, **width**, **alignment**, **pin** (left /\r\nright), plus show / hide + reorder. There is no standalone \"Edit form\" block - the\r\ngrid owns editing end to end.\r\n\r\n### Export toolbar\r\n\r\n**Export toolbar** adds a button bar above the grid. Six options, in two groups:\r\n\r\n| Button | Runs through | Adds to the generated app |\r\n| ------ | ------------ | ------------------------- |\r\n| Export CSV / Export JSON / Copy | the free grid API | nothing |\r\n| Export Excel (.xlsx) | `@svgrid/enterprise` | `jszip` |\r\n| Export PDF | `@svgrid/enterprise` | `pdfmake` |\r\n| Print | `@svgrid/enterprise` | nothing |\r\n\r\nThe Excel export is real OOXML - typed number and date cells, styled headers, a\r\nfrozen header row - not a renamed CSV. PDF is paginated with a repeating header,\r\nand Print opens the browser's print dialog on a paginated layout.\r\n\r\nAll six export what the user currently sees: the visible columns, in their\r\ncurrent order, over the filtered and sorted rows. The optional dependencies are\r\ndeclared only for the buttons you switch on, so a CSV-only app installs neither.\r\nThe canvas preview runs the same code the generated app does, so you can try a\r\nreal export before generating.\r\n\r\n## The analytical + companion blocks\r\n\r\nBeyond the grid, every block is still bound to the `EntitySchema` - these are data\r\nviews, not generic widgets:\r\n\r\n| Block | What it renders | Inspector |\r\n| --- | --- | --- |\r\n| **Chart** | A chart (`SvSchemaChart`) - bar, pie, line, area, radar, funnel, waterfall, or treemap. | Group-by dimension, measure, reduce, type. |\r\n| **Pivot** | A full pivot table (`SvPivotDesigner`) the end user can re-pivot live. | Row + column dimensions (checkboxes), a measure, and its aggregate. |\r\n| **Dashboard** | A schema-driven KPI + chart board (`SvSchemaDashboard`). | - |\r\n| **KPI** | A single reduced metric tile. | Label, measure, reduce. |\r\n| **Gauge** | A radial gauge (`SvGauge`) of one reduced measure within a range - utilization, progress, scores. | Label, measure, reduce, min / max, unit. |\r\n| **Tree** | A hierarchical tree (`SvTree`) built from the entity's own rows via a self-referential parent. | A label field + a parent field (a row's link to its parent row). |\r\n| **Tabs** | A tabbed container (`SvTabs`) that **groups display blocks** into tabs - e.g. an Overview tab of KPIs + a Details tab with a chart. | Add / rename / remove tabs; per tab, add child blocks (charts, KPIs, gauges, pivots, trees). |\r\n| **Accordion** | A collapsible-sections container (like Tabs, stacked vertically). | Add / rename / remove sections; child blocks per section. |\r\n| **Master / detail** | A row that expands into a nested grid of related records. | Child entity + foreign key. |\r\n| **Board** | A kanban board of the entity's rows, one lane per value of a group-by field, with drag between lanes. | Group-by, card title / subtitle / badge fields, open-screen drill. |\r\n| **Calendar** | A month event-calendar: each row with a date lands on its day, labelled and optionally color-coded. | Date field, title field, color field, open-screen drill. |\r\n| **Detail** | A full record \"detail page\": header, metric row, field sections, and related-record tabs - the 360 view a row action or drill-through opens. | Title / subtitle / status / metric fields, sections, related child entities. |\r\n| **Form** | A standalone create / edit form for the entity. | Presentation (drawer / modal / inline). |\r\n| **Filter panel** | A faceted sidebar that **filters the screen's grid** - enum / boolean facets pick a value, text facets search. | Title + which fields become facets. |\r\n| **Record panel** | Shows the row **selected in the grid** - a read-only field list, or an inline edit form. | Editable on / off, and (read-only) which fields to show. |\r\n| **Lookup** | Marks a relation field as a searchable picker in the edit form. | The relation field. |\r\n| **UI component** | A component from the SvGrid UI kit dropped from the toolbox - entity-agnostic, works on freestanding pages too. Grouped as Actions, Inputs, Display, Feedback, Layout, and Navigation, and covering headings and prose (heading, text, link, quote, code, keyboard key, list) as well as controls, pickers, and date/time inputs. | The component's own props (extracted from the component's own types, with its JSDoc as the tooltip), plus data bindings. |\r\n\r\nThe **filter** and **record** panels wire to the grid on the same screen: the\r\nfilter panel calls the grid controller's `setFilter`, and clicking a grid row\r\npublishes it to the record panel. So a common layout is a **filter panel + grid +\r\nrecord panel** three-up - list, narrow, inspect - all generated for you.\r\n\r\nThe grid, chart, pivot, and master-detail blocks are **height-resizable** (drag\r\nthe block's bottom edge, or set **Layout > Height**), and the chosen height flows\r\ninto the generated app. The filter and record panels size to their content.\r\n\r\n### Conditional formatting\r\n\r\nA grid's inspector has a **Conditional formatting** section: add no-code rules\r\nthat style a cell by its value - pick a field, a comparison (`=`, `<`, `>`,\r\n`contains`, `is empty`, ...), a value, and a **text color / fill / bold**. Rules\r\nrender **live in the canvas** and compile to the grid's built-in\r\n`conditionalFormats` rule engine in the generated app (e.g. negative `mrr` red,\r\n`status = overdue` filled). It's the same engine you'd use by hand - the designer\r\njust authors the rules.\r\n\r\n### Navigation & row actions\r\n\r\nThe grid's **Navigation & actions** section wires flow between screens:\r\n**drill-through** (row click opens another screen, filtered to the clicked value)\r\nand **row action buttons** (Edit / Delete / Open). A chart can drill too. See\r\n[Navigation & row actions](./navigation.md) for the full picture.\r\n\r\n## Data sources (per entity)\r\n\r\nEach entity binds to **its own backend** - the designer is not limited to one\r\ndata source per app. In the inspector (no block selected), the **Data source**\r\nsection binds the screen's entity to:\r\n\r\n- **In-memory** - seeded sample rows, runs with no backend (the default).\r\n- **Local database (no setup)** - a real, persistent Postgres ([PGlite](https://pglite.dev))\r\n running in the browser and saved to IndexedDB, so rows survive reloads with zero\r\n backend. Same SQL as production - swap to a hosted **SQL** source later without\r\n touching the schema. See [Local database](./local-database.md).\r\n The builder is a draggable, resizable, maximizable panel, and you can open it for\r\n any entity straight from the **Data model** dialog's **Configure** button.\r\n\r\n- **REST API** - a request builder: **method**, **base URL**, **path** (path\r\n params auto-derive from `{tokens}`), and **Query / Path / Header** tabs. **Send**\r\n runs it live and shows a **response table** plus the real rows in the canvas\r\n grid; **Import fields** rewrites the entity's schema to match the response keys.\r\n An **API format** picker (Manual / Offset + Limit / DummyJSON / json-server)\r\n wires a wire-format adapter so the grid does **real server-side paging and sort**;\r\n Manual keeps the rows-path / total-path mapping for a single fetched page.\r\n- **Supabase** - **List tables** reads your project's tables so you pick one, and\r\n **Import schema** pulls its real columns, primary key, foreign keys, and enums\r\n into the entity; **Preview** shows live rows (works in the online designer, since\r\n Supabase is HTTP). The project **URL + anon key are a shared connection** you set\r\n once for every Supabase entity. A **Live updates (Realtime)** toggle emits a live\r\n subscription. **Generate app** emits `createClient` in `connections.ts` reading\r\n `PUBLIC_SUPABASE_URL` / `PUBLIC_SUPABASE_ANON_KEY` from `.env` (the key is never\r\n inlined) and adds `@supabase/supabase-js`; access is protected by your RLS\r\n policies.\r\n- **SQL** - paste a connection string (the dialect is auto-detected) or fill the\r\n guided form, set the **Schema** (Postgres search path). In the **local** designer\r\n (`npx @svgrid/studio designer`) **Preview data** runs a real `SELECT` and shows\r\n your actual rows on the canvas, and a missing `pg` / `mysql2` / ... driver is a\r\n **one-click install**. **Generate app** emits a connected\r\n `src/routes/api/<table>/+server.ts` (the dialect's driver, reading\r\n `DATABASE_URL`) and points the grid at it; the driver dep is added for you. In\r\n the **online** designer, bind the entity and generate - the app connects for real\r\n at runtime. To scaffold from an existing DB via CLI: `npx @svgrid/studio add\r\n <table> --db <dialect> --url …`.\r\n\r\n**Generate app** then emits the matching adapter per entity in `src/lib/data.ts`\r\n(`createRestDataSource` / `createSqlDataSource` / `createSupabaseDataSource` /\r\n`createInMemoryDataSource`). SQL and Supabase entities read their connection from a\r\ngenerated `src/lib/connections.ts` (or the `+server.ts` route) - the SQL driver and\r\nthe Supabase client are wired for you; the only manual step is setting the\r\nconnection in `.env` (the bundle ships a `.env.example`). See\r\n[Databases](./databases.md) and [REST API](./rest-api.md).\r\n\r\n## Import a CSV / spreadsheet\r\n\r\nThe fastest way to start from **your own data**: **Import CSV** in the top bar\r\ntakes a `.csv` file and turns it into a running screen. The designer parses the\r\nfile (quoted fields, embedded newlines, and CRLF included), **infers a type per\r\ncolumn** from its values (number, boolean, date, or text - thousands separators\r\nand `yes/no/true/false` are understood), ensures a **primary key** (it reuses an\r\n`id` column or synthesizes one), and adds an entity with a full CRUD screen,\r\n**seeded with the real rows**. Unsafe headers (`First Name`, `E-mail`) become safe\r\nfield keys and the note tells you what was renamed.\r\n\r\nImported rows ship **in-memory** by default (no dependencies), so the app runs\r\nimmediately. Switch that entity's **Data source** to **Local database** to make\r\nthe same imported rows **persist** across reloads - the seed carries over. This is\r\nall client-side: `csvToEntity(name, text)` is a pure function exported from\r\n`@svgrid/enterprise`, so the same import works in the CLI and your own tools.\r\n\r\n> **Large files:** the imported rows are stored as the entity's seed, so they are\r\n> embedded in `studio.config.json` when you **Save** the design. That is fine for\r\n> reference data and samples; for a large dataset, import a representative sample\r\n> and point the entity at a **database** (Local database / SQL) for the full data.\r\n\r\n## Pages and layout\r\n\r\n- **Pages** - each screen is a route. In the inspector's **Page** section, toggle\r\n **Show in navigation**, set a **nav label** and **nav order**, or start a page\r\n from the **Empty** template. Hidden pages stay routable but drop out of the nav.\r\n- **Render mode** - eligible screens (a single plain grid, or read-only block\r\n screens, on a memory / SQL source) can switch from the default client page to\r\n **SSR**: an idiomatic `+page.server.ts` with `load` + form actions. The rules\r\n are in [Code generation](./code-generation.md#render-mode-spa-or-ssr-per-screen).\r\n- **App layout** - the rail's **App layout** section themes the generated shell:\r\n **Sidebar** or **Top navigation**, a **brand** name, a **company logo**\r\n (uploaded - stored inline and shown in the nav in place of the brand text), a\r\n **footer**, and (for the sidebar) the **nav position** (left / right). This\r\n drives the generated `src/routes/+layout.svelte`. The generated shell is\r\n **responsive**: on phones the sidebar collapses to a hamburger drawer, the\r\n top-nav links scroll, and each screen's block grid stacks to one column.\r\n\r\n## Save, regenerate, round-trip\r\n\r\nThe `StudioProject` is the persisted design. **Save config** exports a\r\n`studio.config.json`; regenerate the app from it any time:\r\n\r\n```bash\r\nnpm create @svgrid/studio@latest my-app -- --project ./studio.config.json\r\n```\r\n\r\nOr programmatically:\r\n\r\n```ts\r\nimport { serializeProject, parseProject, emitStudioProject } from '@svgrid/enterprise'\r\n\r\nconst json = serializeProject(project) // save the design\r\nconst project2 = parseProject(json) // reopen it\r\nconst files = emitStudioProject(project2) // -> the app's source files\r\n```\r\n\r\n**The exported app carries its own design.** The downloaded zip includes a\r\n`studio.config.json` at its root. To keep editing the app visually after you've\r\nworked on it locally, open the designer and **Load** that file - entities,\r\nscreens, blocks, theme, RBAC, i18n, and now the logo all come back exactly as\r\ngenerated. Because the designer regenerates the files under `src/`, keep any\r\nhand-written code in **new** files/modules you import, so a re-generate never\r\noverwrites it (or use the CLI's `svgrid:managed` markers - see\r\n[Code generation](./code-generation.md)).\r\n\r\n## Generate the app\r\n\r\n**Generate app** emits `src/lib/schemas.ts`, `src/lib/data.ts` (the right adapter\r\nper entity, plus `src/lib/connections.ts` when any entity is SQL / Supabase-bound),\r\nand one `src/routes/<route>/+page.svelte` **per screen** that composes that\r\nscreen's blocks with their config - a grid (with your visible columns, in order) +\r\nedit modal, plus any charts / pivots / dashboard / KPI tiles, and filter / record\r\npanels wired to the grid, all bound to the data - and the\r\nnav layout (sidebar or top-nav) + home. The pages are self-contained (they use\r\n`@svgrid/grid` + `@svgrid/enterprise` directly), so the output runs as a standard\r\nSvelteKit app.\r\n\r\n```ts\r\nimport { emitStudioProject } from '@svgrid/enterprise'\r\nconst files = emitStudioProject(project) // [{ path, contents, description }, ...]\r\n```\r\n\r\n## See also\r\n\r\n- [Code behind (Code view)](./code-behind.md) - write TypeScript against a typed `ctx` (grid API, events, `ctx.grid.sortable = true`, lifecycle)\r\n- [Sample apps + bind your data](./samples.md) - start from a ready-made app, then point it at your database\r\n- [Launch the designer](./launch.md) - `npx @svgrid/studio designer` (auto-save + generate to a folder)\r\n- [Schema designer](./designer.md) - author a single entity\r\n- [Dashboards](./dashboards.md) · [Databases](./databases.md) - the blocks + data sources\r\n- [CLI](./cli.md) / [Drizzle](./drizzle.md) / [Prisma](./prisma.md) - import a schema to design from\r\n"
|
|
2718
2718
|
},
|
|
2719
2719
|
{
|
|
2720
2720
|
"slug": "enterprise/studio/audit-log",
|
|
@@ -2750,13 +2750,13 @@ export const docs = [
|
|
|
2750
2750
|
"slug": "enterprise/studio/code-generation",
|
|
2751
2751
|
"path": "docs/enterprise/studio/code-generation.md",
|
|
2752
2752
|
"title": "Code generation",
|
|
2753
|
-
"markdown": "# Code generation\r\n\r\nStudio generates plain SvelteKit code you own - there is no runtime and no\r\nlock-in. The same `scaffold()` core powers the [CLI](./cli.md), the\r\n[AI generator](./ai-generation.md), and the [visual designer](./app-designer.md), so\r\nall three emit identical files.\r\n\r\n\r\n\r\n## What gets generated\r\n\r\nFrom one `EntitySchema`, three files:\r\n\r\n| File | Contents |\r\n| --- | --- |\r\n| `src/lib/<name>.schema.ts` | The `EntitySchema` literal + a typed row type. |\r\n| `src/routes/api/<name>/+server.ts` | The API route - `createKitHandlers` over a data source. |\r\n| `src/routes/<name>/+page.svelte` | The screen - grid + edit panel, wired to the route. |\r\n\r\nThe generated page is a full data screen: server-side sort, filter, and global\r\nsearch; a native pagination footer; a validated create / edit modal;\r\nmulti-select optimistic delete; and loading / error / empty states. Any\r\n`relation` (foreign-key) field also gets a searchable **lookup** wired to the\r\nrelated entity's API route ([Relations](./relations.md)).\r\n\r\n## The three files, up close\r\n\r\n**1. The schema** is the single source of truth - a plain literal you can edit by\r\nhand or regenerate:\r\n\r\n```ts\r\n// src/lib/customers.schema.ts\r\nimport type { EntitySchema } from '@svgrid/enterprise'\r\n\r\nexport type CustomersRow = {\r\n id: string; name: string; email: string; mrr: number; active: boolean\r\n}\r\n\r\nexport const customersSchema: EntitySchema<CustomersRow> = {\r\n name: 'customers',\r\n idField: 'id',\r\n fields: [\r\n { field: 'id', type: 'text', primaryKey: true, readonly: true },\r\n { field: 'name', type: 'text', required: true },\r\n { field: 'email', type: 'text', format: 'email', required: true },\r\n { field: 'mrr', type: 'number', min: 0 },\r\n { field: 'active', type: 'boolean' },\r\n ],\r\n}\r\n```\r\n\r\n**2. The API route** is a single `POST` endpoint. `createKitHandlers` speaks one\r\nJSON wire protocol for read + create + update + delete, so the client needs just\r\nthe one handler:\r\n\r\n```ts\r\n// src/routes/api/customers/+server.ts\r\nimport { createInMemoryDataSource, createKitHandlers } from '@svgrid/enterprise'\r\nimport { customersSchema, type CustomersRow } from '$lib/customers.schema'\r\n\r\nconst source = createInMemoryDataSource<CustomersRow>([], customersSchema)\r\n\r\nexport const { POST } = createKitHandlers({ schema: customersSchema, source })\r\n```\r\n\r\nSwap `createInMemoryDataSource` for a SQL, Supabase, or REST source and the page\r\ndoes not change - that is the [ServerDataSource](./data-binding.md) contract at\r\nwork.\r\n\r\n**3. The screen** wires the grid and the edit panel to that route through the\r\ncontroller - server-side sort / filter / page, the create-edit modal, and\r\noptimistic delete. It is a normal `+page.svelte` you own; see the\r\n[Studio live SQL demo](https://svgrid.com/demos/193-studio-live-sql/) for the\r\nwhole thing running.\r\n\r\n## Managed regions & safe regeneration\r\n\r\nEvery generated file wraps its body in markers:\r\n\r\n```ts\r\n// svgrid:managed:start\r\n// Regenerated by SvGrid Studio. Edits inside these markers are overwritten.\r\n...generated code...\r\n// svgrid:managed:end\r\n```\r\n\r\nRe-running generation replaces **only** the managed region and preserves\r\neverything outside it. So you can:\r\n\r\n- Add imports, helpers, and layout **outside** the markers - they survive\r\n regeneration.\r\n- Re-run `add` after a schema or database change to pull in new columns without\r\n losing your customizations.\r\n\r\nThis idempotent regeneration is what makes the generator safe to keep using,\r\nrather than a one-shot scaffold you fork away from.\r\n\r\n## Choosing the data source\r\n\r\nThe generated `+server.ts` backend depends on how you scaffold:\r\n\r\n| Scaffold | Backend |\r\n| --- | --- |\r\n| `--db <dialect>` | Connected to that driver via `process.env.DATABASE_URL`. |\r\n| `--sql` | `createSqlDataSource` with an `execute()` stub to fill in. |\r\n| `--from` (no `--db`) | In-memory, so the screen runs immediately. |\r\n\r\nSee [Databases](./databases.md) and [Data binding](./data-binding.md).\r\n\r\n## Render mode: SPA or SSR per screen\r\n\r\
|
|
2753
|
+
"markdown": "# Code generation\r\n\r\nStudio generates plain SvelteKit code you own - there is no runtime and no\r\nlock-in. The same `scaffold()` core powers the [CLI](./cli.md), the\r\n[AI generator](./ai-generation.md), and the [visual designer](./app-designer.md), so\r\nall three emit identical files.\r\n\r\n\r\n\r\n## What gets generated\r\n\r\nFrom one `EntitySchema`, three files:\r\n\r\n| File | Contents |\r\n| --- | --- |\r\n| `src/lib/<name>.schema.ts` | The `EntitySchema` literal + a typed row type. |\r\n| `src/routes/api/<name>/+server.ts` | The API route - `createKitHandlers` over a data source. |\r\n| `src/routes/<name>/+page.svelte` | The screen - grid + edit panel, wired to the route. |\r\n\r\nThe generated page is a full data screen: server-side sort, filter, and global\r\nsearch; a native pagination footer; a validated create / edit modal;\r\nmulti-select optimistic delete; and loading / error / empty states. Any\r\n`relation` (foreign-key) field also gets a searchable **lookup** wired to the\r\nrelated entity's API route ([Relations](./relations.md)).\r\n\r\n## The three files, up close\r\n\r\n**1. The schema** is the single source of truth - a plain literal you can edit by\r\nhand or regenerate:\r\n\r\n```ts\r\n// src/lib/customers.schema.ts\r\nimport type { EntitySchema } from '@svgrid/enterprise'\r\n\r\nexport type CustomersRow = {\r\n id: string; name: string; email: string; mrr: number; active: boolean\r\n}\r\n\r\nexport const customersSchema: EntitySchema<CustomersRow> = {\r\n name: 'customers',\r\n idField: 'id',\r\n fields: [\r\n { field: 'id', type: 'text', primaryKey: true, readonly: true },\r\n { field: 'name', type: 'text', required: true },\r\n { field: 'email', type: 'text', format: 'email', required: true },\r\n { field: 'mrr', type: 'number', min: 0 },\r\n { field: 'active', type: 'boolean' },\r\n ],\r\n}\r\n```\r\n\r\n**2. The API route** is a single `POST` endpoint. `createKitHandlers` speaks one\r\nJSON wire protocol for read + create + update + delete, so the client needs just\r\nthe one handler:\r\n\r\n```ts\r\n// src/routes/api/customers/+server.ts\r\nimport { createInMemoryDataSource, createKitHandlers } from '@svgrid/enterprise'\r\nimport { customersSchema, type CustomersRow } from '$lib/customers.schema'\r\n\r\nconst source = createInMemoryDataSource<CustomersRow>([], customersSchema)\r\n\r\nexport const { POST } = createKitHandlers({ schema: customersSchema, source })\r\n```\r\n\r\nSwap `createInMemoryDataSource` for a SQL, Supabase, or REST source and the page\r\ndoes not change - that is the [ServerDataSource](./data-binding.md) contract at\r\nwork.\r\n\r\n**3. The screen** wires the grid and the edit panel to that route through the\r\ncontroller - server-side sort / filter / page, the create-edit modal, and\r\noptimistic delete. It is a normal `+page.svelte` you own; see the\r\n[Studio live SQL demo](https://svgrid.com/demos/193-studio-live-sql/) for the\r\nwhole thing running.\r\n\r\n## Managed regions & safe regeneration\r\n\r\nEvery generated file wraps its body in markers:\r\n\r\n```ts\r\n// svgrid:managed:start\r\n// Regenerated by SvGrid Studio. Edits inside these markers are overwritten.\r\n...generated code...\r\n// svgrid:managed:end\r\n```\r\n\r\nRe-running generation replaces **only** the managed region and preserves\r\neverything outside it. So you can:\r\n\r\n- Add imports, helpers, and layout **outside** the markers - they survive\r\n regeneration.\r\n- Re-run `add` after a schema or database change to pull in new columns without\r\n losing your customizations.\r\n\r\nThis idempotent regeneration is what makes the generator safe to keep using,\r\nrather than a one-shot scaffold you fork away from.\r\n\r\n## Choosing the data source\r\n\r\nThe generated `+server.ts` backend depends on how you scaffold:\r\n\r\n| Scaffold | Backend |\r\n| --- | --- |\r\n| `--db <dialect>` | Connected to that driver via `process.env.DATABASE_URL`. |\r\n| `--sql` | `createSqlDataSource` with an `execute()` stub to fill in. |\r\n| `--from` (no `--db`) | In-memory, so the screen runs immediately. |\r\n\r\nSee [Databases](./databases.md) and [Data binding](./data-binding.md).\r\n\r\n## Render mode: SPA or SSR per screen\r\n\r\nAn **`ssr`** screen emits idiomatic server-rendered SvelteKit: a\r\n`+page.server.ts` with a `load` function and form `actions`, URL-driven sort /\r\nfilter / page state, and progressive enhancement. A **`spa`** screen emits a\r\nclient page where the browser talks to the API route through the data-source\r\ncontroller.\r\n\r\n**New apps built on a database or a REST API get `ssr` for free.** When you\r\ngenerate an app - `svgrid-studio init`, the designer's **New app** wizard, or\r\n`crudAppFromSchemas` - every screen that qualifies starts in `ssr`. You can still\r\nswitch any screen either way in the [app designer](./app-designer.md).\r\n\r\nIn-memory and PGlite apps stay `spa`, on purpose. Those sources are module\r\nsingletons, so a server-rendered screen would read and write the server's copy of\r\nthe rows while the app's remaining client screens read the browser's: add a row\r\non one and the other never sees it. SQL and REST have no such split, because\r\nevery path goes to the same database or the same remote API.\r\n\r\n### How the app is wired\r\n\r\nOnce an app has at least one server-rendered screen, the root `src/routes/+layout.ts`\r\nleaves SvelteKit's own default in place - server rendering on - and each\r\nclient-only screen opts out in its own `+page.ts`:\r\n\r\n```ts\r\n// src/routes/<screen>/+page.ts\r\nexport const ssr = false\r\n```\r\n\r\nSo the nav shell, the home page, and the sign-in pages all render on the server,\r\nand only the screens that fetch in the browser skip it. An app with nothing to\r\nserver-render keeps the single `export const ssr = false` in the root layout, as\r\nbefore.\r\n\r\nNot every screen shape can emit as SSR. The rules:\r\n\r\n- The screen must be entity-bound, without a [code-behind](./code-behind.md)\r\n companion.\r\n- Its data source must be `memory` (runs in-process), `sql` (reuses the connected\r\n `/api` route via `event.fetch`), or `rest` **on an absolute URL** (the server\r\n calls the remote API directly; a relative URL has no origin to resolve against\r\n there, so it stays SPA). `supabase` and `pglite` screens stay SPA - PGlite only\r\n exists in the browser, and a Supabase read carries the signed-in user's token,\r\n which a server-side call with the anon key would silently drop.\r\n- **A grid, optionally with a facet panel**, emits as load + form actions (no\r\n tree data, no scheduler view). The facet panel becomes a plain `GET` form\r\n whose controls are named for the URL params the `load` reads, so filtering\r\n works with JavaScript off and every filtered view has a shareable URL.\r\n- **Read-only block screens** - any mix of chart, pivot, dashboard, KPI, gauge,\r\n tree, detail, and master-detail - emit as a load-only page.\r\n- Anything else (boards, calendars, UI component blocks, containers,\r\n grids with extras) stays SPA.\r\n\r\nThe designer only offers the toggle when the screen qualifies; a screen set to\r\n`ssr` that stops qualifying falls back to the SPA emit.\r\n\r\n## Verification\r\n\r\nThe AI path compiles the generated page (via the Svelte compiler) before handing\r\nit back, and the recommended final step everywhere is your project's own check:\r\n\r\n```bash\r\nnpx svelte-check\r\n```\r\n\r\n## See also\r\n\r\n- [The Studio CLI](./cli.md) · [AI generation](./ai-generation.md) · [Visual app designer](./app-designer.md)\r\n"
|
|
2754
2754
|
},
|
|
2755
2755
|
{
|
|
2756
2756
|
"slug": "enterprise/studio/concepts",
|
|
2757
2757
|
"path": "docs/enterprise/studio/concepts.md",
|
|
2758
2758
|
"title": "Concepts",
|
|
2759
|
-
"markdown": "# Concepts\r\n\r\nThe mental model behind Studio: one `EntitySchema` drives the screens, the data\r\nbinding, and the generated code. This page walks that pipeline once, defines\r\nevery Studio term, and ends with a table for picking which build tool fits how\r\nyou work. Ten minutes here makes every other Studio page shorter.\r\n\r\n\r\n\r\n## The pipeline\r\n\r\nEverything in Studio is one flow, left to right:\r\n\r\n1. **An `EntitySchema` describes your data.** Field names, types, validation,\r\n labels, relations. You get one by introspection (a live database table, a\r\n Drizzle or Prisma schema file, an OpenAPI spec, a CSV, sample JSON) or by\r\n authoring it in the designer. See [The EntitySchema](./schema.md).\r\n2. **Screens arrange blocks over entities.** A screen is one route in your app.\r\n It holds blocks - a grid, a chart, a KPI tile, a board, a calendar - each\r\n bound to an entity. The visual [app designer](./app-designer.md) is where\r\n you compose them; the CLI and AI produce the same structures.\r\n3. **A `ServerDataSource` moves the data.** Read, create, update, delete - one\r\n small contract that every backend implements (SQL databases, Supabase,\r\n REST, in-memory, Postgres-in-the-browser). Sorting, filtering, paging, and\r\n editing work identically no matter where the data lives. See\r\n [Data binding](./data-binding.md).\r\n4. **Codegen writes real SvelteKit files.** The schema, a `+server.ts` API\r\n route, and a `+page.svelte` screen - plain code you own, no runtime, no\r\n proprietary host. Generated sections sit inside `svgrid:managed` region\r\n markers, so re-generating updates them without touching your edits. See\r\n [Code generation](./code-generation.md).\r\n\r\n\r\n\r\nA change flows forward automatically: add a field to the schema and the grid\r\ncolumn, the form input, its validation, and the generated code all pick it up.\r\n\r\n## The project model\r\n\r\nThe designer edits a single JSON document - the **project** - with this shape:\r\n\r\n- **project** - title, theme, default data source, plus optional\r\n [auth](./auth.md), [access control](./access-control.md),\r\n [audit](./audit-log.md), [i18n](./i18n.md), and deploy settings.\r\n - **entities** - one `EntitySchema` per table / collection.\r\n - **screens** - one per route. Each screen has:\r\n - **blocks** - the data-bound building pieces (grid, form, chart,\r\n dashboard, kpi, gauge, tree, tabs, accordion, master-detail, lookup,\r\n pivot, filter, record, board, calendar, detail, component).\r\n - a **layout** - `grid` (12-column flow, the default), `stack`, `split`\r\n (resizable panes), `dock` (dockable / floatable panes, see\r\n [Dock layout](./dock-layout.md)), or `canvas` (free-form placement on a\r\n 12-column cell grid).\r\n - a **render mode** - `
|
|
2759
|
+
"markdown": "# Concepts\r\n\r\nThe mental model behind Studio: one `EntitySchema` drives the screens, the data\r\nbinding, and the generated code. This page walks that pipeline once, defines\r\nevery Studio term, and ends with a table for picking which build tool fits how\r\nyou work. Ten minutes here makes every other Studio page shorter.\r\n\r\n\r\n\r\n## The pipeline\r\n\r\nEverything in Studio is one flow, left to right:\r\n\r\n1. **An `EntitySchema` describes your data.** Field names, types, validation,\r\n labels, relations. You get one by introspection (a live database table, a\r\n Drizzle or Prisma schema file, an OpenAPI spec, a CSV, sample JSON) or by\r\n authoring it in the designer. See [The EntitySchema](./schema.md).\r\n2. **Screens arrange blocks over entities.** A screen is one route in your app.\r\n It holds blocks - a grid, a chart, a KPI tile, a board, a calendar - each\r\n bound to an entity. The visual [app designer](./app-designer.md) is where\r\n you compose them; the CLI and AI produce the same structures.\r\n3. **A `ServerDataSource` moves the data.** Read, create, update, delete - one\r\n small contract that every backend implements (SQL databases, Supabase,\r\n REST, in-memory, Postgres-in-the-browser). Sorting, filtering, paging, and\r\n editing work identically no matter where the data lives. See\r\n [Data binding](./data-binding.md).\r\n4. **Codegen writes real SvelteKit files.** The schema, a `+server.ts` API\r\n route, and a `+page.svelte` screen - plain code you own, no runtime, no\r\n proprietary host. Generated sections sit inside `svgrid:managed` region\r\n markers, so re-generating updates them without touching your edits. See\r\n [Code generation](./code-generation.md).\r\n\r\n\r\n\r\nA change flows forward automatically: add a field to the schema and the grid\r\ncolumn, the form input, its validation, and the generated code all pick it up.\r\n\r\n## The project model\r\n\r\nThe designer edits a single JSON document - the **project** - with this shape:\r\n\r\n- **project** - title, theme, default data source, plus optional\r\n [auth](./auth.md), [access control](./access-control.md),\r\n [audit](./audit-log.md), [i18n](./i18n.md), and deploy settings.\r\n - **entities** - one `EntitySchema` per table / collection.\r\n - **screens** - one per route. Each screen has:\r\n - **blocks** - the data-bound building pieces (grid, form, chart,\r\n dashboard, kpi, gauge, tree, tabs, accordion, master-detail, lookup,\r\n pivot, filter, record, board, calendar, detail, component).\r\n - a **layout** - `grid` (12-column flow, the default), `stack`, `split`\r\n (resizable panes), `dock` (dockable / floatable panes, see\r\n [Dock layout](./dock-layout.md)), or `canvas` (free-form placement on a\r\n 12-column cell grid).\r\n - a **render mode** - `ssr` (emits idiomatic SvelteKit `+page.server.ts`\r\n load + form actions; the default for screens in a database-backed app) or\r\n `spa` (the page fetches through the data source in the browser). See\r\n [Code generation](./code-generation.md).\r\n - optional **code-behind** - a user-owned `handlers.ts` companion for\r\n event handlers, written once and never regenerated.\r\n\r\nWhen you run the [local designer](./launch.md), the project auto-saves to\r\n`studio.config.json` in your working folder as you edit; **Generate app** turns\r\nit into the SvelteKit project. The same file is what the\r\n[MCP tools](./ai-generation.md) read and write, so a coding agent and the\r\ndesigner can work on one project interchangeably.\r\n\r\n## Glossary\r\n\r\n| Term | Meaning |\r\n| --- | --- |\r\n| **EntitySchema** | The model of one entity: fields, types, validation, labels, relations. Everything else derives from it. [Schema](./schema.md) |\r\n| **Screen** | One route / page of the generated app; holds blocks and a layout. [App designer](./app-designer.md) |\r\n| **Block** | A data-bound piece placed on a screen: grid, chart, KPI, board, calendar, and so on. [App designer](./app-designer.md) |\r\n| **Companion block** | A block that works alongside a grid on the same screen and shares its data, like a filter panel or a record panel. [App designer](./app-designer.md) |\r\n| **Project model** | The single JSON document (`studio.config.json`) holding entities, screens, sources, theme, auth. This page, above |\r\n| **ServerDataSource** | The read + create + update + delete contract every backend implements. [Data binding](./data-binding.md) |\r\n| **Managed region** | A `svgrid:managed` marker pair in a generated file; regeneration rewrites only what is inside. [Code generation](./code-generation.md) |\r\n| **Code-behind** | A user-owned `handlers.ts` next to a generated screen for typed event handlers; created once, never overwritten. [Code-behind](./code-behind.md) |\r\n| **Scaffold** | The codegen step: schema in, SvelteKit files out. Shared by the CLI, the designer, and the AI path. [CLI](./cli.md) |\r\n| **Introspection** | Reading an existing source (database table, Drizzle / Prisma schema, OpenAPI spec, CSV) to produce an `EntitySchema`. [Databases](./databases.md) |\r\n| **Soft gate** | Enterprise licensing without a hard stop: unlicensed use shows a watermark and a console notice, nothing breaks. [Licensing](../licensing.md) |\r\n\r\n## Which tool when\r\n\r\nAll three build paths share one scaffold core and produce the same output, so\r\nthis is a workflow choice, not a feature choice - and you can switch anytime.\r\n\r\n| Tool | Pick it if | Page |\r\n| --- | --- | --- |\r\n| **CLI** - `npx @svgrid/studio add ...` | you want one deterministic command per screen, in scripts or CI, no AI involved | [The Studio CLI](./cli.md) |\r\n| **AI via MCP** - `@svgrid/mcp` | you already work in a coding agent (Claude Code, Cursor, ...) and want to describe screens in plain language | [AI generation](./ai-generation.md) |\r\n| **Visual designer** - `npx @svgrid/studio designer` | you want to see the app while composing it, or you are not writing code at all | [Visual app designer](./app-designer.md) |\r\n\r\n## See also\r\n\r\n- [Getting started](./getting-started.md) - build your first screen step by step\r\n- [Data binding](./data-binding.md) - the `ServerDataSource` contract in detail\r\n- [Code generation](./code-generation.md) - the emitted files and safe regeneration\r\n"
|
|
2760
2760
|
},
|
|
2761
2761
|
{
|
|
2762
2762
|
"slug": "enterprise/studio/dashboards",
|
|
@@ -2786,13 +2786,13 @@ export const docs = [
|
|
|
2786
2786
|
"slug": "enterprise/studio/designer",
|
|
2787
2787
|
"path": "docs/enterprise/studio/designer.md",
|
|
2788
2788
|
"title": "Schema designer (embeddable)",
|
|
2789
|
-
"markdown": "# Schema designer (embeddable)\r\n\r\n`SvSchemaDesigner` is a small, **embeddable, single-entity** component: drop it into\r\nyour own app to author one `EntitySchema` visually - add fields, set types and\r\nvalidation - and see the grid and edit form update live. It's controlled (you own\r\nwhere the schema is stored), so it's the building block for a \"let users customize\r\nthis table\" screen.\r\n\r\n> **Looking for the full app builder?** The rich, multi-screen designer you get\r\n> from `npx @svgrid/studio designer` - grids, charts, dashboards, boards,\r\n> schedulers, and master-detail across many entities, with data binding and\r\n> code-behind - is a different component, [`SvStudioDesigner`](./app-designer.md),\r\n> opened locally with [`npx @svgrid/studio designer`](./launch.md).\r\n> Use this page's `SvSchemaDesigner` when you only need to design one entity, or to\r\n> embed a schema editor inside your own product.\r\n\r\n\r\n\r\n## Usage\r\n\r\n```svelte\r\n<script lang=\"ts\">\r\n import { SvSchemaDesigner, type EntitySchema } from '@svgrid/enterprise'\r\n\r\n let schema = $state<EntitySchema>({\r\n name: 'customers',\r\n idField: 'id',\r\n fields: [\r\n { field: 'id', type: 'text', primaryKey: true, readonly: true },\r\n { field: 'name', type: 'text', required: true },\r\n { field: 'email', type: 'text', format: 'email' },\r\n ],\r\n })\r\n</script>\r\n\r\n<SvSchemaDesigner {schema} onChange={(s) => (schema = s)} />\r\n```\r\n\r\n## Props\r\n\r\n| Prop | Type | Description |\r\n| --- | --- | --- |\r\n| `schema` | `EntitySchema<TData>` | The schema being designed. |\r\n| `onChange` | `(schema) => void` | Fires on every edit with the new schema. Omit for a read-only preview. |\r\n| `showPreview` | `boolean` | Show the live grid + edit-panel preview. Default `true`. |\r\n\r\n## What you can edit\r\n\r\n- **Add / remove / reorder** fields.\r\n- Per-field **type** (`text`, `number`, `boolean`, `date`, `datetime`, `enum`,\r\n `relation`, `json`), **label**, and flags: **primary key**, **required**,\r\n **read-only**, **hidden**.\r\n- **Enum options** (comma-separated) and **relation** target for relation fields.\r\n\r\nThe right panel shows the resulting grid (`schemaToColumns`) and the edit form\r\n(`SvGridEditPanel`) live, so you see exactly what the schema produces.\r\n\r\n## Save, load, import, export\r\n\r\nThe designer is **controlled** - it never persists anything itself. `onChange`\r\nhands you the new `EntitySchema`; you decide where it lives (localStorage, a file,\r\nyour backend). Because an `EntitySchema` is plain JSON, \"export\" is\r\n`JSON.stringify` and \"import\" is `JSON.parse` back into `schema` - no special\r\nformat:\r\n\r\n```svelte\r\n<script lang=\"ts\">\r\n import { SvSchemaDesigner, type EntitySchema } from '@svgrid/enterprise'\r\n\r\n const KEY = 'my-app.customers.schema'\r\n const blank: EntitySchema = {\r\n name: 'customers',\r\n idField: 'id',\r\n fields: [{ field: 'id', type: 'text', primaryKey: true, readonly: true }],\r\n }\r\n\r\n let schema = $state<EntitySchema>(\r\n JSON.parse(localStorage.getItem(KEY) ?? 'null') ?? blank,\r\n )\r\n\r\n function onChange(next: EntitySchema) {\r\n schema = next\r\n localStorage.setItem(KEY, JSON.stringify(next)) // persist every edit\r\n }\r\n</script>\r\n\r\n<SvSchemaDesigner {schema} {onChange} />\r\n\r\n<button onclick={() => navigator.clipboard.writeText(JSON.stringify(schema, null, 2))}>\r\n Copy schema JSON\r\n</button>\r\n```\r\n\r\nDrop an [AI-introspected schema](./ai-generation.md) straight into `schema` and\r\nrefine it here, or omit `onChange` entirely for a read-only preview of a schema\r\nyou already have.\r\n\r\n## Generate code\r\n\r\nThe **Generate code** button runs the shared `scaffold()` and shows the emitted\r\nfiles (schema module, `+server.ts`, `+page.svelte`) - the same output as the\r\n[CLI](./cli.md). Because it is one shared core, the designer, CLI, and\r\n[AI generator](./ai-generation.md) all produce identical code.\r\n\r\n## Human-in-the-loop with AI\r\n\r\nA common flow: let the [AI generator](./ai-generation.md) draft an\r\n`EntitySchema` from your database, then refine it in the designer (tweak labels,\r\nmark fields hidden, add validation) before generating - AI drafts, you approve.\r\n\r\n## See also\r\n\r\n- [The EntitySchema](./schema.md) - the model the designer edits\r\n- [Edit forms & validation](./edit-forms.md) · [Code generation](./code-generation.md)\r\n"
|
|
2789
|
+
"markdown": "# Schema designer (embeddable)\r\n\r\n`SvSchemaDesigner` is a small, **embeddable, single-entity** component: drop it into\r\nyour own app to author one `EntitySchema` visually - add fields, set types and\r\nvalidation - and see the grid and edit form update live. It's controlled (you own\r\nwhere the schema is stored), so it's the building block for a \"let users customize\r\nthis table\" screen.\r\n\r\n> **Looking for the full app builder?** The rich, multi-screen designer you get\r\n> from `npx @svgrid/studio designer` - grids, charts, dashboards, boards,\r\n> schedulers, and master-detail across many entities, with data binding and\r\n> code-behind - is a different component, [`SvStudioDesigner`](./app-designer.md),\r\n> opened locally with [`npx @svgrid/studio designer`](./launch.md).\r\n> Use this page's `SvSchemaDesigner` when you only need to design one entity, or to\r\n> embed a schema editor inside your own product.\r\n\r\n\r\n\r\n## Usage\r\n\r\n```svelte {nocheck}\r\n<script lang=\"ts\">\r\n import { SvSchemaDesigner, type EntitySchema } from '@svgrid/enterprise'\r\n\r\n let schema = $state<EntitySchema>({\r\n name: 'customers',\r\n idField: 'id',\r\n fields: [\r\n { field: 'id', type: 'text', primaryKey: true, readonly: true },\r\n { field: 'name', type: 'text', required: true },\r\n { field: 'email', type: 'text', format: 'email' },\r\n ],\r\n })\r\n</script>\r\n\r\n<SvSchemaDesigner {schema} onChange={(s) => (schema = s)} />\r\n```\r\n\r\n## Props\r\n\r\n| Prop | Type | Description |\r\n| --- | --- | --- |\r\n| `schema` | `EntitySchema<TData>` | The schema being designed. |\r\n| `onChange` | `(schema) => void` | Fires on every edit with the new schema. Omit for a read-only preview. |\r\n| `showPreview` | `boolean` | Show the live grid + edit-panel preview. Default `true`. |\r\n\r\n## What you can edit\r\n\r\n- **Add / remove / reorder** fields.\r\n- Per-field **type** (`text`, `number`, `boolean`, `date`, `datetime`, `enum`,\r\n `relation`, `json`), **label**, and flags: **primary key**, **required**,\r\n **read-only**, **hidden**.\r\n- **Enum options** (comma-separated) and **relation** target for relation fields.\r\n\r\nThe right panel shows the resulting grid (`schemaToColumns`) and the edit form\r\n(`SvGridEditPanel`) live, so you see exactly what the schema produces.\r\n\r\n## Save, load, import, export\r\n\r\nThe designer is **controlled** - it never persists anything itself. `onChange`\r\nhands you the new `EntitySchema`; you decide where it lives (localStorage, a file,\r\nyour backend). Because an `EntitySchema` is plain JSON, \"export\" is\r\n`JSON.stringify` and \"import\" is `JSON.parse` back into `schema` - no special\r\nformat:\r\n\r\n```svelte {nocheck}\r\n<script lang=\"ts\">\r\n import { SvSchemaDesigner, type EntitySchema } from '@svgrid/enterprise'\r\n\r\n const KEY = 'my-app.customers.schema'\r\n const blank: EntitySchema = {\r\n name: 'customers',\r\n idField: 'id',\r\n fields: [{ field: 'id', type: 'text', primaryKey: true, readonly: true }],\r\n }\r\n\r\n let schema = $state<EntitySchema>(\r\n JSON.parse(localStorage.getItem(KEY) ?? 'null') ?? blank,\r\n )\r\n\r\n function onChange(next: EntitySchema) {\r\n schema = next\r\n localStorage.setItem(KEY, JSON.stringify(next)) // persist every edit\r\n }\r\n</script>\r\n\r\n<SvSchemaDesigner {schema} {onChange} />\r\n\r\n<button onclick={() => navigator.clipboard.writeText(JSON.stringify(schema, null, 2))}>\r\n Copy schema JSON\r\n</button>\r\n```\r\n\r\nDrop an [AI-introspected schema](./ai-generation.md) straight into `schema` and\r\nrefine it here, or omit `onChange` entirely for a read-only preview of a schema\r\nyou already have.\r\n\r\n## Generate code\r\n\r\nThe **Generate code** button runs the shared `scaffold()` and shows the emitted\r\nfiles (schema module, `+server.ts`, `+page.svelte`) - the same output as the\r\n[CLI](./cli.md). Because it is one shared core, the designer, CLI, and\r\n[AI generator](./ai-generation.md) all produce identical code.\r\n\r\n## Human-in-the-loop with AI\r\n\r\nA common flow: let the [AI generator](./ai-generation.md) draft an\r\n`EntitySchema` from your database, then refine it in the designer (tweak labels,\r\nmark fields hidden, add validation) before generating - AI drafts, you approve.\r\n\r\n## See also\r\n\r\n- [The EntitySchema](./schema.md) - the model the designer edits\r\n- [Edit forms & validation](./edit-forms.md) · [Code generation](./code-generation.md)\r\n"
|
|
2790
2790
|
},
|
|
2791
2791
|
{
|
|
2792
2792
|
"slug": "enterprise/studio/dock-layout",
|
|
2793
2793
|
"path": "docs/enterprise/studio/dock-layout.md",
|
|
2794
2794
|
"title": "Docking layout",
|
|
2795
|
-
"markdown": "# Docking layout\
|
|
2795
|
+
"markdown": "# Docking layout\n\nBy default a screen arranges its blocks in a responsive **12-column grid**. For\ndashboards, consoles, and analyst tools you can switch a screen to a **docking\nworkspace** instead: the same blocks become **dockable, floatable, pinnable\npanes** (an [`SvDockManager`](#/demos/362-dock-manager)). Drag a tab to split a\nregion, pull it out into a floating window, or pin it to an edge - and the\narrangement is saved per user.\n\nIt is a per-screen choice: a data-heavy console can dock while your simple CRUD\nscreens stay on the grid.\n\n\n\n## Turn it on\n\nIn the [visual designer](./app-designer.md), select a screen (click empty\ncanvas), open the inspector, and set **Layout** to **Docking manager**. The\nscreen's blocks are laid out automatically by role:\n\n- **Filters** dock to the **left**.\n- **Record / detail** panels dock to the **right**.\n- **KPIs / gauges** form a strip across the **top**.\n- The main content (grid, board, calendar, chart, ...) fills the **centre**.\n\nFrom there, drag pane tabs to rearrange, split, float, or pin them. Rename a\npane's tab in the inspector under **Dock pane -> Tab title**. Closing a pane's\ntab removes that block from the screen.\n\nThe live [preview](./app-designer.md) renders the real docking manager too, so\nwhat you arrange is exactly how the app runs.\n\n## What it generates\n\nThe screen body becomes an `<SvDockManager>` whose workspace is your serialized\nlayout; each block is a pane rendered by id. The rest of the screen - data\nsource, editing, actions - is unchanged, so a grid in a pane keeps all its\nfeatures (sorting, editing, its own data):\n\n```svelte\n<script lang=\"ts\">\n import { SvDockManager, type DockManagerState } from '@svgrid/grid'\n let dockWorkspace = $state<DockManagerState>(/* your saved layout */)\n // ... controller / rows / editing, exactly as a grid-layout screen ...\n</script>\n\n{#if dockNarrow}\n <div class=\"st-screen\"> … blocks stacked … </div> <!-- mobile fall-back -->\n{:else}\n <SvDockManager bind:workspace={dockWorkspace} onChange={saveLayout}>\n {#snippet pane(p)}\n {#if p.id === 'grid-1'}<SvGrid … containerHeight=\"100%\" />{/if}\n {#if p.id === 'filter-1'}…{/if}\n {/snippet}\n </SvDockManager>\n{/if}\n```\n\n- **Persistence** - the layout is restored from `localStorage` per screen and\n re-saved whenever the user rearranges panes.\n- **Mobile** - below a narrow breakpoint the workspace falls back to a single\n stacked column (floating / tiling is impractical on a phone).\n\n## When to use it\n\n- **Use docking** for consoles and workbenches: a support queue beside the\n selected ticket and its history; an analyst view with a grid, a chart, and a\n pivot the user tears off into their own windows.\n- **Stay on the grid** for straightforward list / form / detail screens, and\n anything that should read top-to-bottom on mobile.\n\n## The other screen layouts\n\nDock is one of five per-screen layout modes in the designer:\n\n- **Grid** (default) - blocks flow in a 12-column grid; presets like two-column,\n sidebar, and KPI-row arrange them in one click.\n- **Stack** - one block per row, top to bottom.\n- **Split** - the dock engine with the panes locked in place: users can resize\n the splits but not rearrange or float them.\n- **Dock** - this page: rearrangeable, floatable, persistent.\n- **Canvas** - free-form placement: each block gets explicit cell coordinates\n and spans on a 12-column grid of fixed-height rows, so blocks sit exactly\n where you drop them.\n\n## See also\n\n- [App designer](./app-designer.md) - the block palette and inspector.\n- [Scheduler / calendar view](./scheduler.md) and the Kanban board - other ways\n the same grid data can be presented.\n- The underlying component: [docking layout demo](#/demos/362-dock-manager).\n"
|
|
2796
2796
|
},
|
|
2797
2797
|
{
|
|
2798
2798
|
"slug": "enterprise/studio/drizzle",
|
|
@@ -2804,13 +2804,13 @@ export const docs = [
|
|
|
2804
2804
|
"slug": "enterprise/studio/edit-forms",
|
|
2805
2805
|
"path": "docs/enterprise/studio/edit-forms.md",
|
|
2806
2806
|
"title": "Edit forms & validation",
|
|
2807
|
-
"markdown": "# Edit forms & validation\r\n\r\n`SvGridEditPanel` is the create / edit form for a row. It renders itself from an\r\n`EntitySchema`, validates input, and hands you a ready payload to save. It\r\npresents as a right-hand **drawer**, a centered **modal**, or **inline**, and\r\nfollows the grid's light / dark theme.\r\n\r\n\r\n\r\n## Usage\r\n\r\n```svelte\r\n<script lang=\"ts\">\r\n import { SvGridEditPanel } from '@svgrid/enterprise'\r\n let editing = $state<Customer | null | undefined>(undefined) // undefined = closed, null = create\r\n\r\n async function save({ mode, id, values }) {\r\n if (mode === 'create') await controller.createRow(values)\r\n else if (id) await controller.updateRow(id, values)\r\n editing = undefined\r\n }\r\n</script>\r\n\r\n{#if editing !== undefined}\r\n <SvGridEditPanel\r\n {schema}\r\n row={editing}\r\n presentation=\"modal\"\r\n onSubmit={save}\r\n onCancel={() => (editing = undefined)}\r\n />\r\n{/if}\r\n```\r\n\r\n## Props\r\n\r\n| Prop | Type | Description |\r\n| --- | --- | --- |\r\n| `schema` | `EntitySchema<TData>` | Drives the fields, validation, and payload. |\r\n| `row` | `TData \\| null` | Row to edit; `null` to create. |\r\n| `presentation` | `'drawer'` \\| `'modal'` \\| `'inline'` | Default `'drawer'` (right slide-over). |\r\n| `title` | `string` | Heading override. |\r\n| `submitLabel` | `string` | Save-button label override. |\r\n| `onSubmit` | `(payload) => void \\| Promise` | Called with a validated `{ mode, id, values }`. Throw to surface an error. |\r\n| `onCancel` | `() => void` | Called on cancel / close (Esc, backdrop, or the X). |\r\n\r\n## Presentation\r\n\r\n- **`drawer`** (default) - slides in from the right, full height.\r\n- **`modal`** - centered popup with a blurred backdrop.\r\n- **`inline`** - renders in the page flow (used by the designer preview).\r\n\r\nDrawer and modal animate in / out, close on **Esc** or backdrop click, and trap\r\nto a dialog role.\r\n\r\n## Validation\r\n\r\nThe panel validates on submit and blocks the save when anything fails, showing a\r\nmessage under each field. Three layers, in order:\r\n\r\n1. **Required** - non-empty for `required` fields.\r\n2. **Built-in constraints** - number validity + `min` / `max`,\r\n `minLength` / `maxLength`, `format: 'email' | 'url'`, and `pattern` (see\r\n [The EntitySchema](./schema.md#built-in-validation)).\r\n3. **Standard Schema** - any Zod / Valibot / ArkType validator on `field.validate`.\r\n\r\n```ts\r\n{ field: 'email', type: 'text', required: true, format: 'email' }\r\n{ field: 'mrr', type: 'number', min: 0 }\r\n{ field: 'name', type: 'text', minLength: 2, maxLength: 60 }\r\n```\r\n\r\nNo external library is required for the built-in rules - add a Standard Schema\r\nvalidator only when you need custom logic.\r\n\r\n## Controls\r\n\r\nThe form renders each field with a control from the **editor suite**, not a bare\r\nnative input: numbers use `SvNumberInput` (spinners, min/max/step), booleans a\r\n`SvSwitchButton`, colors `SvColorInput`, passwords `SvPasswordInput` (strength\r\nmeter), ratings a `SvSlider`, dates and date-times a `SvDateTimePicker` (masked\r\ninput + calendar dropdown), enums a themed **dropdown** (`SvGridDropdown`), and\r\nJSON a textarea. The default follows the field type; override per field with\r\n`input.editorType`.\r\n\r\nBeyond the grid's cell editors, the form also offers a few **form-only** controls\r\nvia `input.editorType`: `phone` (`SvPhoneInput`), `country` (`SvCountryInput`),\r\n`mask` (`SvMaskedInput`, with an `input.mask` pattern like `'(999) 000-0000'`),\r\nand `slider`. In the [visual designer](./app-designer.md) each field has a\r\n**Control** picker (scoped to what fits its type) plus a **Wide** toggle\r\n(`input.span = 2`), so you pick the editor without touching code.\r\n\r\n```ts\r\n{ field: 'mrr', type: 'number', input: { editorType: 'slider' } }\r\n{ field: 'brand', type: 'text', input: { editorType: 'color' } }\r\n{ field: 'phone', type: 'text', input: { editorType: 'phone' } }\r\n{ field: 'ssn', type: 'text', input: { editorType: 'mask', mask: '999-99-9999' } }\r\n```\r\n\r\nForm-only editors degrade to a safe in-cell editor when the same field shows in a\r\ngrid (`slider` → number, `phone`/`country`/`mask` → text), so columns stay valid.\r\n\r\n**File / image upload.** Give a field an `upload` config and it renders\r\n`SvFileInput` (a picker with an image preview). With no handler it stores an\r\ninline data URL (no backend needed); pass an `uploads` handler that pushes to\r\nstorage and returns the URL:\r\n\r\n```svelte\r\n{ field: 'avatar', type: 'text', upload: { image: true, accept: 'image/*' } }\r\n\r\n<SvGridEditPanel {schema} row={editing}\r\n uploads={{ avatar: async (file) => await putToStorage(file) }} onSubmit={save} />\r\n```\r\n\r\n**Cascading (dependent) fields.** Compute a field's options from the current\r\nvalues with `dependentOptions` - the field clears when it stops being valid\r\n(e.g. City depends on Country):\r\n\r\n```svelte\r\n<SvGridEditPanel {schema} row={editing}\r\n dependentOptions={{ city: (values) => citiesByCountry[values.country] ?? [] }}\r\n onSubmit={save} />\r\n```\r\n\r\nSee the [rich fields demo](https://svgrid.com/demos/198-studio-form-fields/).\r\n\r\nEnum and `relation` fields use a custom dropdown whose panel **portals to\r\n`document.body`** (position: fixed), so it opens *above* a drawer or modal and\r\nnever grows the form (no scrollbar) - unlike a native `<select>` or an in-flow\r\npopup.\r\n\r\n## The modal is a movable window\r\n\r\nWith `presentation=\"modal\"`, the panel is a floating window: **drag** it by the\r\nheader, **resize** it from its edges (the content resizes with it), **maximize /\r\nrestore**, and **pin** it to any edge (left / top / bottom / right) - handy for\r\nkeeping the form docked beside the grid while you work. Pin again to unpin.\r\n\r\nSet **`persistKey`** to remember the window layout (pin / size / maximized) in\r\n`localStorage`, so it reopens where the user left it:\r\n\r\n```svelte\r\n<SvGridEditPanel {schema} row={editing} presentation=\"modal\" persistKey=\"customers\" ... />\r\n```\r\n\r\nOpen the editor on **double-click** (`onRowDoubleClick`), not single-click, so a\r\nclick can still select or interact with a row without popping the form.\r\n\r\n## See also\r\n\r\n- [The EntitySchema](./schema.md) - fields + validation constraints\r\n- [Master-detail](./master-detail.md) · [Data binding](./data-binding.md)\r\n"
|
|
2807
|
+
"markdown": "# Edit forms & validation\r\n\r\n`SvGridEditPanel` is the create / edit form for a row. It renders itself from an\r\n`EntitySchema`, validates input, and hands you a ready payload to save. It\r\npresents as a right-hand **drawer**, a centered **modal**, or **inline**, and\r\nfollows the grid's light / dark theme.\r\n\r\n\r\n\r\n## Usage\r\n\r\n```svelte\r\n<script lang=\"ts\">\r\n import { SvGridEditPanel } from '@svgrid/enterprise'\r\n let editing = $state<Customer | null | undefined>(undefined) // undefined = closed, null = create\r\n\r\n async function save({ mode, id, values }) {\r\n if (mode === 'create') await controller.createRow(values)\r\n else if (id) await controller.updateRow(id, values)\r\n editing = undefined\r\n }\r\n</script>\r\n\r\n{#if editing !== undefined}\r\n <SvGridEditPanel\r\n {schema}\r\n row={editing}\r\n presentation=\"modal\"\r\n onSubmit={save}\r\n onCancel={() => (editing = undefined)}\r\n />\r\n{/if}\r\n```\r\n\r\n## Props\r\n\r\n| Prop | Type | Description |\r\n| --- | --- | --- |\r\n| `schema` | `EntitySchema<TData>` | Drives the fields, validation, and payload. |\r\n| `row` | `TData \\| null` | Row to edit; `null` to create. |\r\n| `presentation` | `'drawer'` \\| `'modal'` \\| `'inline'` | Default `'drawer'` (right slide-over). |\r\n| `title` | `string` | Heading override. |\r\n| `submitLabel` | `string` | Save-button label override. |\r\n| `onSubmit` | `(payload) => void \\| Promise` | Called with a validated `{ mode, id, values }`. Throw to surface an error. |\r\n| `onCancel` | `() => void` | Called on cancel / close (Esc, backdrop, or the X). |\r\n\r\n## Presentation\r\n\r\n- **`drawer`** (default) - slides in from the right, full height.\r\n- **`modal`** - centered popup with a blurred backdrop.\r\n- **`inline`** - renders in the page flow (used by the designer preview).\r\n\r\nDrawer and modal animate in / out, close on **Esc** or backdrop click, and trap\r\nto a dialog role.\r\n\r\n## Validation\r\n\r\n**When it speaks up.** A field is checked when the user leaves it, not while they\r\nare still typing, so a form never scolds you for a value you have not finished\r\nentering. Once a field is showing an error it re-checks on every keystroke, so a\r\ncorrection clears the message straight away instead of making you submit again to\r\nfind out. A failed submit marks every field as visited, focuses the first one that\r\nneeds fixing, and lists them all in a summary at the top of the form that jumps to\r\na field when clicked.\r\n\r\nEach control carries `aria-invalid`, and its message (or its hint, when there is\r\nno error) is wired up with `aria-describedby`, so a screen reader announces the\r\nproblem with the field rather than leaving it to be discovered.\r\n\r\n**Closing a form with unsaved edits asks first.** Cancel, Escape, or a click on\r\nthe backdrop shows *Discard your changes?* in the footer, with **Keep editing**\r\nand **Discard**; a second Escape confirms. An untouched form closes immediately.\r\n\r\nThe save is blocked while anything fails. Three layers, in order:\r\n\r\n1. **Required** - non-empty for `required` fields.\r\n2. **Built-in constraints** - number validity + `min` / `max`,\r\n `minLength` / `maxLength`, `format: 'email' | 'url'`, and `pattern` (see\r\n [The EntitySchema](./schema.md#built-in-validation)).\r\n3. **Standard Schema** - any Zod / Valibot / ArkType validator on `field.validate`.\r\n\r\n```ts\r\n{ field: 'email', type: 'text', required: true, format: 'email' }\r\n{ field: 'mrr', type: 'number', min: 0 }\r\n{ field: 'name', type: 'text', minLength: 2, maxLength: 60 }\r\n```\r\n\r\nNo external library is required for the built-in rules - add a Standard Schema\r\nvalidator only when you need custom logic.\r\n\r\n### No-code rules\r\n\r\n`EntitySchema.validations` states cross-field rules as data, so the same rule\r\nruns in the form and in a generated app's server route:\r\n\r\n```ts\r\nvalidations: [\r\n { field: 'endsAt', op: 'gte', compareTo: 'startsAt', message: 'End must be after start' },\r\n { field: 'code', op: 'minLen', value: 4, message: 'Code needs 4+ characters' },\r\n]\r\n```\r\n\r\nOperators: `eq`, `ne`, `lt`, `lte`, `gt`, `gte`, `required`, `minLen`, `maxLen`.\r\nUse `compareTo` to compare against another field instead of a fixed `value`.\r\n\r\n## Laying the form out\r\n\r\n`EntitySchema.form` says how the form is arranged. It lives on the schema, not on\r\nthe component, so a form you have *built* travels with the entity: it round-trips\r\nthrough `studio.config.json`, generates into an app, and draws the same in the\r\nedit panel and in a server-rendered form.\r\n\r\n```ts\r\nconst customers: EntitySchema = {\r\n name: 'customers',\r\n fields: [/* ... */],\r\n form: {\r\n columns: 2,\r\n sections: [\r\n { title: 'Contact', description: 'How we reach them.', fields: ['name', 'email', 'phone'] },\r\n { title: 'Billing', columns: 1, fields: ['plan', 'vatNumber'] },\r\n // A whole section can be conditional, the same way a field is.\r\n { title: 'Cancellation', fields: ['reason', 'notes'],\r\n visibleWhen: { kind: 'cmp', column: 'status', op: 'equals', value: 'cancelled' } },\r\n ],\r\n },\r\n}\r\n```\r\n\r\n- `fields` gives both the grouping and the order.\r\n- A field in no section still renders, in a trailing untitled group. A form never\r\n silently drops one.\r\n- A section whose fields are all hidden disappears with them, heading included.\r\n- `columns` on a section overrides the form's for that group alone; a field with\r\n `input.span = 2` spans the full width.\r\n\r\n`SvGridEditPanel`'s `columns` and `sections` props still win when passed, for a\r\none-off arrangement of an otherwise shared schema.\r\n\r\n**Server-rendered screens follow the same layout.** A screen with\r\n`renderMode: 'ssr'` renders its sections, descriptions and column counts, marks\r\n`span: 2` fields full-width, shows each field's hint, and states the field's own\r\nconstraints (`minlength`, `maxlength`, `min`, `max`, `pattern`, and an `email` /\r\n`url` input type) as native HTML attributes so the browser catches an obvious\r\nmistake before a round-trip. The action re-checks all of it server-side\r\nregardless, so the attributes save a trip but never decide anything.\r\n\r\n## Fields that react to the answers\r\n\r\nA field can appear, lock, or become required based on what the user has already\r\nentered - the form asks for a reason only when it needs one, and never asks\r\ntwice.\r\n\r\nEach condition is a `PredicateExpr`: **data, not a function**, so it survives a\r\nround-trip through `studio.config.json`, generates into an app unchanged, and can\r\nbe edited in a UI.\r\n\r\n```ts\r\n{\r\n field: 'otherReason',\r\n type: 'text',\r\n when: {\r\n // Only asked for - and only demanded - when the reason is \"other\".\r\n visible: { kind: 'cmp', column: 'reason', op: 'equals', value: 'other' },\r\n required: { kind: 'cmp', column: 'reason', op: 'equals', value: 'other' },\r\n },\r\n}\r\n{\r\n field: 'approver',\r\n type: 'text',\r\n // Locked until the order is big enough to need sign-off.\r\n when: { disabled: { kind: 'cmp', column: 'total', op: 'lessThan', value: 1000 } },\r\n}\r\n```\r\n\r\nThree rules make this safe to rely on:\r\n\r\n- A field hidden by `visible` is **skipped by validation** and **left out of the\r\n submitted payload**. It can never block a save the user cannot fix, and a value\r\n they can no longer see is never written back. The generated SSR route applies\r\n the same rule to a posted form, so both paths save the same fields.\r\n- `required` **replaces** the static `required` flag rather than adding to it, so\r\n a rule can make a normally required field optional as well as the reverse.\r\n- A malformed condition falls back to showing and enabling the field. A broken\r\n rule degrades to an ordinary form instead of hiding data.\r\n\r\nConditions are the same expressions the alert rules use, so `parsePredicate` and\r\nthe `SvExpressionEditor` component both work on them:\r\n\r\n```ts\r\nimport { parsePredicate } from '@svgrid/enterprise'\r\nwhen: { visible: parsePredicate('reason = \"other\"') }\r\n```\r\n\r\nA section whose fields are all hidden disappears along with them, heading\r\nincluded.\r\n\r\nThree [sample apps](./samples.md) ship this, so you can open one and watch it\r\nwork: **CRM** asks a lost deal what it lost to, **Support desk** demands a\r\nresolution before a ticket can be resolved or closed (and only then asks for a\r\nCSAT rating), and **Insurance Claims** requires a justification to deny a claim\r\nand freezes the amount and deductible once it has been paid.\r\n\r\n## Controls\r\n\r\nThe form renders each field with a control from the **editor suite**, not a bare\r\nnative input: numbers use `SvNumberInput` (spinners, min/max/step), booleans a\r\n`SvSwitchButton`, colors `SvColorInput`, passwords `SvPasswordInput` (strength\r\nmeter), ratings a `SvSlider`, dates and date-times a `SvDateTimePicker` (masked\r\ninput + calendar dropdown), enums a themed **dropdown** (`SvGridDropdown`), and\r\nJSON a textarea. The default follows the field type; override per field with\r\n`input.editorType`.\r\n\r\nBeyond the grid's cell editors, the form also offers a few **form-only** controls\r\nvia `input.editorType`: `phone` (`SvPhoneInput`), `country` (`SvCountryInput`),\r\n`mask` (`SvMaskedInput`, with an `input.mask` pattern like `'(999) 000-0000'`),\r\nand `slider`. In the [visual designer](./app-designer.md) each field has a\r\n**Control** picker (scoped to what fits its type) plus a **Wide** toggle\r\n(`input.span = 2`), so you pick the editor without touching code.\r\n\r\n```ts\r\n{ field: 'mrr', type: 'number', input: { editorType: 'slider' } }\r\n{ field: 'brand', type: 'text', input: { editorType: 'color' } }\r\n{ field: 'phone', type: 'text', input: { editorType: 'phone' } }\r\n{ field: 'ssn', type: 'text', input: { editorType: 'mask', mask: '999-99-9999' } }\r\n```\r\n\r\nForm-only editors degrade to a safe in-cell editor when the same field shows in a\r\ngrid (`slider` → number, `phone`/`country`/`mask` → text), so columns stay valid.\r\n\r\n**File / image upload.** Give a field an `upload` config and it renders\r\n`SvFileInput` (a picker with an image preview). With no handler it stores an\r\ninline data URL (no backend needed); pass an `uploads` handler that pushes to\r\nstorage and returns the URL:\r\n\r\n```svelte\r\n{ field: 'avatar', type: 'text', upload: { image: true, accept: 'image/*' } }\r\n\r\n<SvGridEditPanel {schema} row={editing}\r\n uploads={{ avatar: async (file) => await putToStorage(file) }} onSubmit={save} />\r\n```\r\n\r\n**Cascading (dependent) fields.** Compute a field's options from the current\r\nvalues with `dependentOptions` - the field clears when it stops being valid\r\n(e.g. City depends on Country):\r\n\r\n```svelte\r\n<SvGridEditPanel {schema} row={editing}\r\n dependentOptions={{ city: (values) => citiesByCountry[values.country] ?? [] }}\r\n onSubmit={save} />\r\n```\r\n\r\nSee the [rich fields demo](https://svgrid.com/demos/198-studio-form-fields/).\r\n\r\nEnum and `relation` fields use a custom dropdown whose panel **portals to\r\n`document.body`** (position: fixed), so it opens *above* a drawer or modal and\r\nnever grows the form (no scrollbar) - unlike a native `<select>` or an in-flow\r\npopup.\r\n\r\n## The modal is a movable window\r\n\r\nWith `presentation=\"modal\"`, the panel is a floating window: **drag** it by the\r\nheader, **resize** it from its edges (the content resizes with it), **maximize /\r\nrestore**, and **pin** it to any edge (left / top / bottom / right) - handy for\r\nkeeping the form docked beside the grid while you work. Pin again to unpin.\r\n\r\nSet **`persistKey`** to remember the window layout (pin / size / maximized) in\r\n`localStorage`, so it reopens where the user left it:\r\n\r\n```svelte\r\n<SvGridEditPanel {schema} row={editing} presentation=\"modal\" persistKey=\"customers\" ... />\r\n```\r\n\r\nOpen the editor on **double-click** (`onRowDoubleClick`), not single-click, so a\r\nclick can still select or interact with a row without popping the form.\r\n\r\n## See also\r\n\r\n- [The EntitySchema](./schema.md) - fields + validation constraints\r\n- [Master-detail](./master-detail.md) · [Data binding](./data-binding.md)\r\n"
|
|
2808
2808
|
},
|
|
2809
2809
|
{
|
|
2810
2810
|
"slug": "enterprise/studio/getting-started",
|
|
2811
2811
|
"path": "docs/enterprise/studio/getting-started.md",
|
|
2812
2812
|
"title": "Getting started",
|
|
2813
|
-
"markdown": "# Getting started\r\n\r\nThis is the gentlest path into SvGrid Studio. By the end you will have a real,\r\nworking **Customers** screen - a grid you can sort, filter, and page, with a\r\ncreate / edit form and delete - running on your machine. No prior experience\r\nwith SvGrid is assumed.\r\n\r\nIf you would rather just click around first, open a live demo - no install\r\nneeded:\r\n\r\n- **[Studio live SQL](https://svgrid.com/demos/193-studio-live-sql/)** - the whole stack in the browser, backed by real Postgres via PGlite\r\n- **[Live SQL](https://svgrid.com/demos/193-studio-live-sql/)** - a real Postgres in the browser (PGlite)\r\n- **[Supabase](https://svgrid.com/demos/194-studio-supabase/)** - connect your own hosted Postgres\r\n\r\n**Choose your tutorial.** This page needs no database and teaches the whole\r\nshape; the one-page tutorials build the same screen against a real backend.\r\nThey all end in the same place, so pick by what you have:\r\n\r\n| You have | Follow | What it adds |\r\n| --- | --- | --- |\r\n| Nothing yet | this page | the full path: install, schema, generate, run, change |\r\n| A Postgres connection string | [Postgres CRUD grid](./postgres-grid.md) | a server route querying your database |\r\n| A Supabase project | [Supabase CRUD grid](./supabase-grid.md) | browser client, keys, Row-Level Security |\r\n| An HTTP / JSON API | [REST CRUD grid](./rest-grid.md) | the REST adapter, no server route at all |\r\n| An afternoon | [Build a CRM](./tutorial-crm.md) | multi-entity: relations, master-detail, a real DB |\r\n\r\n---\r\n\r\n## Guided path - answer a few questions, get the app\r\n\r\nIf you already know where your data lives, let Studio ask:\r\n\r\n```bash\r\nnpx @svgrid/studio init\r\n```\r\n\r\nIt asks four things - where the data comes from (sample data, your database,\r\nin-browser Postgres, or a REST API), which tables you want, which pages each\r\ntable gets, and what it should look like - then writes a runnable SvelteKit app\r\nwith a list, an edit form and a record page per table, plus an overview\r\ndashboard.\r\n\r\nPointing it at a real database is one line, and Studio installs the driver for\r\nyou:\r\n\r\n```bash\r\nnpx @svgrid/studio init --db postgres --url $DATABASE_URL --out my-app\r\n```\r\n\r\nOn Supabase there is no driver to install at all - it is read over the project's\r\nREST API:\r\n\r\n```bash\r\nnpx @svgrid/studio init --supabase-url https://xxxx.supabase.co --supabase-key $SUPABASE_ANON_KEY\r\n```\r\n\r\nPrefer clicking? The visual designer has the same wizard behind its **New app**\r\nbutton - or open [svgrid.com/studio/new](https://svgrid.com/studio/new) to start\r\none in the browser. Both paths run the same generator, so they produce the same\r\napp. See [The Studio CLI](./cli.md#init) for every flag.\r\n\r\n---\r\n\r\n## Fastest path - a downloadable, ready-to-run example\r\n\r\nRather have a working project on your machine than type code into a blank\r\nfile? One command scaffolds a complete SvelteKit app with everything already\r\nwired up:\r\n\r\n```bash\r\nnpm create @svgrid/studio@latest my-app\r\n```\r\n\r\nYou'll be asked to pick a **theme** - one of `@svgrid/grid`'s 19 built-in\r\npresets (shadcn, Tailwind, Material, Excel, Fluent, and more) - and whether to\r\nstart in **light or dark** mode. Scripting this instead? Both are flags:\r\n\r\n```bash\r\nnpm create @svgrid/studio@latest my-app -- --theme material --dark\r\n```\r\n\r\nThen:\r\n\r\n```bash\r\ncd my-app\r\nnpm install\r\nnpm run dev\r\n```\r\n\r\nOpen `http://localhost:5173`. You get:\r\n\r\n- A **nav shell** and a home page (`src/routes/+layout.svelte`).\r\n- Two linked entities - **Customers** and **Orders** - each a full grid +\r\n modal create/edit/delete screen. Orders has a searchable lookup back to\r\n Customers, so you can see how relations work.\r\n- **Seeded in-memory data** - nothing to install or configure, no database.\r\n- The **theme and mode you picked**, applied to the whole app - not just the grid.\r\n\r\nIt is a real project, not a read-only demo - edit it, add fields, connect a\r\ndatabase, deploy it. A few places to start:\r\n\r\n| Want to... | Edit |\r\n| --- | --- |\r\n| Add or change a field | `src/lib/schemas.ts` - the grid and the form update together |\r\n| See how a screen is built | `src/lib/EntityScreen.svelte` - the reusable grid + modal CRUD screen every route uses |\r\n| Connect a real database | `src/lib/data.ts` - swap `createInMemoryDataSource` for `createSqlDataSource` / `createSupabaseDataSource` (see [Databases](./databases.md)) |\r\n| Add another screen from a live table | `npx @svgrid/studio add invoices --db postgres --url \"$DATABASE_URL\"` |\r\n\r\nThe template's own `README.md` covers the same ground once you're in the\r\nproject. Prefer to see each piece built up by hand instead, or add a screen to\r\nan *existing* app rather than a fresh one? Continue below.\r\n\r\n---\r\n\r\n## Three ways to build - pick yours\r\n\r\n\r\n\r\nThis is the visual designer - a screens list on the left, your data previewed\r\nlive in the middle, and simple property panels on the right. You point, click, and\r\npress **Generate**:\r\n\r\n\r\n\r\n- **Visual designer (no code).** You never write code - you point, click, and\r\n preview, then press one button to generate the finished app. Try it\r\n immediately, no install, at **[svgrid.com/studio](https://svgrid.com/studio)** -\r\n or run it locally with **[Launch the designer](./launch.md)**, which\r\n auto-saves to disk and generates straight into a folder. The\r\n [sample apps](./samples.md) let you open a complete, realistic app in one\r\n click and point it at your own data.\r\n- **CLI.** One deterministic command per screen: `npx @svgrid/studio add ...`\r\n introspects your table or schema and writes the files. No AI involved. This\r\n page uses the CLI from Step 3 on - continue below.\r\n- **AI via MCP.** With [`@svgrid/mcp`](./ai-generation.md) configured, ask your\r\n coding agent to build the screen; it introspects, scaffolds, and\r\n compile-verifies through the same core the CLI uses.\r\n\r\nAll three produce the **same generated code** - pick whichever fits how you\r\nwork, and switch freely later.\r\n\r\n---\r\n\r\n## What you need\r\n\r\n- **[Node.js](https://nodejs.org) 18 or newer.** Check with `node -v` in a\r\n terminal. If that errors, install Node first.\r\n- **A terminal** and a code editor (VS Code is fine).\r\n- **A SvelteKit app.** Don't have one? Create one in 30 seconds:\r\n\r\n ```bash\r\n npx sv create my-app # pick \"SvelteKit minimal\", TypeScript: yes\r\n cd my-app\r\n npm install\r\n ```\r\n\r\nYou do **not** need a database to start - the first screen below runs on\r\nin-memory data. You can point it at PostgreSQL, Supabase, MySQL, and others\r\nlater without changing the UI.\r\n\r\n---\r\n\r\n## Step 1 - Install\r\n\r\nInside your app folder:\r\n\r\n```bash\r\nnpm i @svgrid/grid @svgrid/enterprise\r\n```\r\n\r\n- `@svgrid/grid` is the grid itself.\r\n- `@svgrid/enterprise` adds Studio: the schema, the edit form, and the data-source\r\n helpers. It is **soft-gate only** - everything runs unlicensed, it just nudges.\r\n See [licensing](../licensing.md).\r\n\r\n---\r\n\r\n## Step 2 - Describe your data once\r\n\r\nThe generator needs one description of your table. If you have a live database\r\nit can introspect it directly (Step 3 shows that variant). Here we stay\r\ndatabase-free: describe the table in a small Drizzle schema file, which the\r\ngenerator **reads as text** - it never connects to anything.\r\n\r\n```bash\r\nnpm i -D drizzle-orm\r\n```\r\n\r\n(`drizzle-orm` is only there so the schema file type-checks; nothing runs\r\nagainst a database. It is also the natural next step when you do add one.)\r\n\r\nCreate `src/lib/db/schema.ts`:\r\n\r\n```ts\r\nimport { pgTable, text, integer, boolean } from 'drizzle-orm/pg-core'\r\n\r\nexport const customers = pgTable('customers', {\r\n id: text('id').primaryKey(),\r\n name: text('name').notNull(),\r\n email: text('email').notNull(),\r\n tier: text('tier').notNull().default('free'),\r\n mrr: integer('mrr'),\r\n active: boolean('active'),\r\n})\r\n```\r\n\r\nA Prisma `schema.prisma` works the same way. Prefer to write Studio's own\r\nmodel - the `EntitySchema` - by hand instead? That is the\r\n[appendix](#appendix-wire-it-by-hand-no-generator) at the bottom of this page.\r\n\r\n---\r\n\r\n## Step 3 - Generate the screen\r\n\r\nOne command:\r\n\r\n```bash\r\nnpx @svgrid/studio add customers --from src/lib/db/schema.ts\r\n```\r\n\r\nHave a live database instead? Same command, different source - no schema file\r\nneeded:\r\n\r\n```bash\r\nnpx @svgrid/studio add customers --db postgres --url \"$DATABASE_URL\"\r\n```\r\n\r\nEither way it writes **three files**, and the screen is done. A quick tour of\r\nwhat you now own:\r\n\r\n**1. `src/lib/customers.schema.ts` - the model.** The generator turned your\r\ntable into an `EntitySchema` - the single object that drives the grid columns,\r\nthe form fields, and validation:\r\n\r\n```ts\r\nexport type CustomersRow = {\r\n id: string\r\n name: string\r\n email: string\r\n tier: string\r\n mrr: number | null\r\n active: boolean | null\r\n}\r\n\r\nexport const customersSchema: EntitySchema<CustomersRow> = {\r\n name: 'customers',\r\n idField: 'id',\r\n fields: [\r\n { field: 'id', type: 'text', primaryKey: true, readonly: true },\r\n { field: 'name', type: 'text', required: true },\r\n { field: 'email', type: 'text', required: true },\r\n // ...\r\n ],\r\n}\r\n```\r\n\r\nEvery field option (labels, enum choices, min/max, regex, custom validators) is\r\nexplained in [The EntitySchema](./schema.md).\r\n\r\n**2. `src/routes/api/customers/+server.ts` - the API route.** A\r\n`ServerDataSource` (read + create + update + delete) exposed over one SvelteKit\r\nendpoint. With `--from` it starts in-memory so it runs immediately; with `--db`\r\nit is already wired to your database:\r\n\r\n```ts\r\nconst source = createInMemoryDataSource<CustomersRow>([], customersSchema)\r\n\r\nexport const { POST } = createKitHandlers({ schema: customersSchema, source })\r\n```\r\n\r\nSwapping in a real database later means replacing that one `source` line - the\r\npage never changes. See [Databases](./databases.md).\r\n\r\n**3. `src/routes/customers/+page.svelte` - the screen.** The grid with\r\nserver-side sort, filter, global search, a native pagination footer,\r\nmulti-select delete with optimistic updates, and a modal create / edit form -\r\nall reading through the API route:\r\n\r\n```ts\r\nconst source = createKitDataSource<CustomersRow>({ endpoint: '/api/customers' })\r\nconst columns = schemaToColumns(customersSchema)\r\nconst controller = createServerDataSource<CustomersRow>(source, {\r\n pageSize: 25, optimistic: true,\r\n getRowId: (r) => String(r.id),\r\n onChange: (s) => (state = s),\r\n})\r\n```\r\n\r\nEach file wraps its generated body in `// svgrid:managed:start` /\r\n`// svgrid:managed:end` markers. Everything you write **outside** the markers\r\nis yours; re-running `add` only rewrites what is inside. That is what makes\r\nStep 5 safe.\r\n\r\n> One thing the generated screen inherits from your page: the **font**. A bare\r\n> `npx sv create` app sets no CSS at all, so add a\r\n> `body { font-family: system-ui, sans-serif }` rule (or a\r\n> [`--sg-font`](./theming.md) token) once, or the page renders in the\r\n> browser's default serif. Borders, backgrounds, and hover states the grid\r\n> themes itself.\r\n\r\n---\r\n\r\n## Step 4 - Run it\r\n\r\n```bash\r\nnpm run dev\r\n```\r\n\r\nOpen the URL it prints (usually `http://localhost:5173`) and go to\r\n**`/customers`**. With `--from` the grid starts empty (in-memory source, no\r\nseed) - click **New** and add two or three customers, then try the screen:\r\n\r\n- Click a **column header** to sort.\r\n- Type in the **filter row** under a header to filter (it stays focused as you type).\r\n- Click a **row** to edit. The form validates as you type - clear a required\r\n field and watch it complain.\r\n- Select rows with the checkboxes and **Delete** them - the grid updates\r\n instantly and rolls back if the server says no.\r\n- Page through with the **native pager** at the bottom.\r\n\r\n\r\n\r\n---\r\n\r\n## Step 5 - Change something\r\n\r\nBecause the schema drives everything, changes are one edit. Add a column to\r\n`src/lib/db/schema.ts`:\r\n\r\n```ts\r\ncountry: text('country'),\r\n```\r\n\r\nThen re-run the exact same command:\r\n\r\n```bash\r\nnpx @svgrid/studio add customers --from src/lib/db/schema.ts\r\n```\r\n\r\nThe managed regions are regenerated: the grid gets a **Country** column and the\r\nedit form gets a **Country** input. Anything you wrote outside the\r\n`svgrid:managed` markers - extra buttons, styles, handlers - is untouched.\r\nThat round trip (change schema, re-run, keep your code) is the everyday\r\nworkflow; [Code generation](./code-generation.md) explains the rules.\r\n\r\nPrefer not to re-run the generator? Editing the generated\r\n`customers.schema.ts` directly works too - grid and form update together from\r\nthe one schema object.\r\n\r\n---\r\n\r\n## Where to go next\r\n\r\nYou have the whole shape now. The usual next steps:\r\n\r\n- **Understand the model** - [Concepts](./concepts.md) walks the pipeline\r\n (schema, screens, data source, codegen) once and defines every Studio term.\r\n- **Connect a real database** - re-run `add` with `--db` and a connection\r\n string, or swap the one `source` line in the API route; the page does not\r\n change. For Supabase, follow the one-page\r\n **[Supabase CRUD grid tutorial](./supabase-grid.md)**; for SQL, see\r\n [Databases](./databases.md).\r\n- **Design visually** - `npx @svgrid/studio designer` opens the full app builder:\r\n compose screens across entities, bind data, and click *Generate app*. See the\r\n [Visual app designer](./app-designer.md). (To embed a single-entity schema editor\r\n in your own app, see the [Schema designer](./designer.md).)\r\n- **Build a full app** - the [Build a CRM tutorial](./tutorial-crm.md) wires up\r\n companies, contacts, and deals with relations and master-detail.\r\n\r\n---\r\n\r\n## Appendix: wire it by hand (no generator)\r\n\r\nEverything the generator wrote in Step 3 can be built up by hand - useful when\r\nyou want to see exactly how the pieces fit, or to embed a Studio screen in an\r\nunusual spot. Two files replace the three generated ones (no API route: here\r\nthe data source lives in the page itself).\r\n\r\nFirst, the `EntitySchema` - Studio's own model, the object the generator\r\nderived from your Drizzle file. Create `src/lib/customers.ts`:\r\n\r\n```ts\r\nimport type { EntitySchema } from '@svgrid/enterprise'\r\n\r\nexport type Customer = {\r\n id: string\r\n name: string\r\n email: string\r\n tier: 'free' | 'pro' | 'enterprise'\r\n mrr: number\r\n active: boolean\r\n}\r\n\r\nexport const customersSchema: EntitySchema<Customer> = {\r\n name: 'customers',\r\n label: 'Customer',\r\n idField: 'id',\r\n fields: [\r\n { field: 'id', type: 'text', primaryKey: true, readonly: true },\r\n { field: 'name', type: 'text', required: true, minLength: 2 },\r\n { field: 'email', type: 'text', label: 'Email', required: true, format: 'email' },\r\n { field: 'tier', type: 'enum', options: [\r\n { value: 'free', label: 'Free' },\r\n { value: 'pro', label: 'Pro' },\r\n { value: 'enterprise', label: 'Enterprise' },\r\n ] },\r\n { field: 'mrr', type: 'number', label: 'MRR ($)', min: 0 },\r\n { field: 'active', type: 'boolean' },\r\n ],\r\n}\r\n```\r\n\r\nThen the page. `createInMemoryDataSource` provides the `ServerDataSource`\r\ncontract over a plain array, `createServerDataSource` runs sort / filter /\r\npage / CRUD against it, and the grid + edit panel render it. Create\r\n`src/routes/customers/+page.svelte`:\r\n\r\n```svelte\r\n<script lang=\"ts\">\r\n import { SvGrid, createServerDataSource, type ServerState } from '@svgrid/grid'\r\n import { SvGridEditPanel, createInMemoryDataSource, schemaToColumns } from '@svgrid/enterprise'\r\n import { customersSchema, type Customer } from '$lib/customers'\r\n\r\n const seed: Customer[] = [\r\n { id: 'c1', name: 'Ada Lovelace', email: 'ada@analytic.io', tier: 'enterprise', mrr: 1200, active: true },\r\n { id: 'c2', name: 'Alan Turing', email: 'alan@bletchley.uk', tier: 'pro', mrr: 240, active: true },\r\n { id: 'c3', name: 'Grace Hopper', email: 'grace@navy.mil', tier: 'enterprise', mrr: 980, active: true },\r\n ]\r\n\r\n const columns = schemaToColumns(customersSchema)\r\n const source = createInMemoryDataSource(seed, customersSchema)\r\n\r\n let view = $state<ServerState<Customer>>({\r\n rows: [], total: 0, loading: false, saving: false, error: null,\r\n pageIndex: 0, pageSize: 10, pageCount: 1, sortModel: [], filterModel: {},\r\n })\r\n let editing = $state<Customer | null | undefined>(undefined)\r\n let genId = 4\r\n\r\n const controller = createServerDataSource(source, {\r\n pageSize: 10, optimistic: true, getRowId: (r) => r.id,\r\n onChange: (s) => (view = s),\r\n })\r\n controller.refresh()\r\n\r\n async function save({ mode, id, values }) {\r\n if (mode === 'create') { await controller.createRow({ id: `c${genId++}`, ...values }); controller.setPage(view.pageCount - 1) }\r\n else if (id) { await controller.updateRow(id, values) }\r\n editing = undefined\r\n }\r\n</script>\r\n\r\n<style>\r\n :global(body) {\r\n font-family: ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\r\n }\r\n</style>\r\n\r\n<button onclick={() => (editing = null)}>+ New customer</button>\r\n\r\n<SvGrid\r\n data={view.rows} {columns} loading={view.loading}\r\n fitColumns enableRowSummaries={false}\r\n sortable externalSort onSortingChange={(s) => controller.setSort(s)}\r\n filterable filterMode=\"row\" externalFilter\r\n onFiltersChange={(f) => controller.setFilter({\r\n global: f.global || undefined,\r\n columns: Object.fromEntries(f.columns.map((c) => [c.id, { operator: c.operator, value: c.value, valueTo: c.valueTo, selectedValues: c.selectedValues }])),\r\n })}\r\n onRowClick={(e) => (editing = e.row)}\r\n showPagination externalPagination\r\n rowCount={view.total} pageIndex={view.pageIndex} pageSize={view.pageSize}\r\n onPaginationChange={({ pageIndex, pageSize }) => pageSize !== view.pageSize ? controller.setPageSize(pageSize) : controller.setPage(pageIndex)}\r\n/>\r\n\r\n{#if editing !== undefined}\r\n <SvGridEditPanel schema={customersSchema} row={editing} presentation=\"modal\"\r\n onSubmit={save} onCancel={() => (editing = undefined)} />\r\n{/if}\r\n```\r\n\r\nThe `<style>` block is just a plain font reset - a fresh `npx sv create` app ships no CSS\r\nat all, so without it the page falls back to the browser's default serif font. `<SvGrid>`\r\nand `<SvGridEditPanel>` already theme their own borders, backgrounds, and hover states out\r\nof the box (via [`--sg-*` tokens](../../help/tokens.md) with built-in fallbacks) - font is\r\nthe one thing they intentionally inherit from the page rather than force, so it fits\r\nwhatever type your app already uses. If your app already sets a body font (or a\r\n[`--sg-font`](./theming.md) token), skip this block.\r\n\r\n---\r\n\r\n## See also\r\n\r\n- [SvGrid Studio overview](../studio.md)\r\n- [Concepts](./concepts.md) - the mental model + glossary\r\n- [Data binding](./data-binding.md) - the `ServerDataSource` contract in depth\r\n- [Edit forms & validation](./edit-forms.md)\r\n- [Troubleshooting & FAQ](./troubleshooting.md)\r\n"
|
|
2813
|
+
"markdown": "# Getting started\r\n\r\nThis is the gentlest path into SvGrid Studio. By the end you will have a real,\r\nworking **Customers** screen - a grid you can sort, filter, and page, with a\r\ncreate / edit form and delete - running on your machine. No prior experience\r\nwith SvGrid is assumed.\r\n\r\nIf you would rather just click around first, open a live demo - no install\r\nneeded:\r\n\r\n- **[Studio live SQL](https://svgrid.com/demos/193-studio-live-sql/)** - the whole stack in the browser, backed by real Postgres via PGlite\r\n- **[Supabase](https://svgrid.com/demos/194-studio-supabase/)** - connect your own hosted Postgres\r\n- **[The designer itself](https://svgrid.com/studio)** - open a sample app and edit it, nothing to install\r\n\r\n**Choose your tutorial.** This page needs no database and teaches the whole\r\nshape; the one-page tutorials build the same screen against a real backend.\r\nThey all end in the same place, so pick by what you have:\r\n\r\n| You have | Follow | What it adds |\r\n| --- | --- | --- |\r\n| Nothing yet | this page | the full path: install, schema, generate, run, change |\r\n| A Postgres connection string | [Postgres CRUD grid](./postgres-grid.md) | a server route querying your database |\r\n| A Supabase project | [Supabase CRUD grid](./supabase-grid.md) | browser client, keys, Row-Level Security |\r\n| An HTTP / JSON API | [REST CRUD grid](./rest-grid.md) | the REST adapter, no server route at all |\r\n| An afternoon | [Build a CRM](./tutorial-crm.md) | multi-entity: relations, master-detail, a real DB |\r\n\r\n---\r\n\r\n## Guided path - answer a few questions, get the app\r\n\r\nIf you already know where your data lives, let Studio ask:\r\n\r\n```bash\r\nnpx @svgrid/studio init\r\n```\r\n\r\nIt asks four things - where the data comes from (sample data, your database,\r\nin-browser Postgres, or a REST API), which tables you want, which pages each\r\ntable gets, and what it should look like - then writes a runnable SvelteKit app\r\nwith a list, an edit form and a record page per table, plus an overview\r\ndashboard.\r\n\r\nPointing it at a real database is one line, and Studio installs the driver for\r\nyou:\r\n\r\n```bash\r\nnpx @svgrid/studio init --db postgres --url $DATABASE_URL --out my-app\r\n```\r\n\r\nOn Supabase there is no driver to install at all - it is read over the project's\r\nREST API:\r\n\r\n```bash\r\nnpx @svgrid/studio init --supabase-url https://xxxx.supabase.co --supabase-key $SUPABASE_ANON_KEY\r\n```\r\n\r\nPrefer clicking? The visual designer has the same wizard behind its **New app**\r\nbutton - or open [svgrid.com/studio/new](https://svgrid.com/studio/new) to start\r\none in the browser. Both paths run the same generator, so they produce the same\r\napp. See [The Studio CLI](./cli.md#init) for every flag.\r\n\r\n---\r\n\r\n## Fastest path - a downloadable, ready-to-run example\r\n\r\nRather have a working project on your machine than type code into a blank\r\nfile? One command scaffolds a complete SvelteKit app with everything already\r\nwired up:\r\n\r\n```bash\r\nnpm create @svgrid/studio@latest my-app\r\n```\r\n\r\nYou'll be asked to pick a **theme** - one of `@svgrid/grid`'s 19 built-in\r\npresets (shadcn, Tailwind, Material, Excel, Fluent, and more) - and whether to\r\nstart in **light or dark** mode. Scripting this instead? Both are flags:\r\n\r\n```bash\r\nnpm create @svgrid/studio@latest my-app -- --theme material --dark\r\n```\r\n\r\nThen:\r\n\r\n```bash\r\ncd my-app\r\nnpm install\r\nnpm run dev\r\n```\r\n\r\nOpen `http://localhost:5173`. You get:\r\n\r\n- A **nav shell** and a home page (`src/routes/+layout.svelte`).\r\n- Two linked entities - **Customers** and **Orders** - each a full grid +\r\n modal create/edit/delete screen. Orders has a searchable lookup back to\r\n Customers, so you can see how relations work.\r\n- **Seeded in-memory data** - nothing to install or configure, no database.\r\n- The **theme and mode you picked**, applied to the whole app - not just the grid.\r\n\r\nIt is a real project, not a read-only demo - edit it, add fields, connect a\r\ndatabase, deploy it. A few places to start:\r\n\r\n| Want to... | Edit |\r\n| --- | --- |\r\n| Add or change a field | `src/lib/schemas.ts` - the grid and the form update together |\r\n| See how a screen is built | `src/lib/EntityScreen.svelte` - the reusable grid + modal CRUD screen every route uses |\r\n| Connect a real database | `src/lib/data.ts` - swap `createInMemoryDataSource` for `createSqlDataSource` / `createSupabaseDataSource` (see [Databases](./databases.md)) |\r\n| Add another screen from a live table | `npx @svgrid/studio add invoices --db postgres --url \"$DATABASE_URL\"` |\r\n\r\nThe template's own `README.md` covers the same ground once you're in the\r\nproject. Prefer to see each piece built up by hand instead, or add a screen to\r\nan *existing* app rather than a fresh one? Continue below.\r\n\r\n---\r\n\r\n## Three ways to build - pick yours\r\n\r\n\r\n\r\nThis is the visual designer - a screens list on the left, your data previewed\r\nlive in the middle, and simple property panels on the right. You point, click, and\r\npress **Generate**:\r\n\r\n\r\n\r\n- **Visual designer (no code).** You never write code - you point, click, and\r\n preview, then press one button to generate the finished app. Try it\r\n immediately, no install, at **[svgrid.com/studio](https://svgrid.com/studio)** -\r\n or run it locally with **[Launch the designer](./launch.md)**, which\r\n auto-saves to disk and generates straight into a folder. The\r\n [sample apps](./samples.md) let you open a complete, realistic app in one\r\n click and point it at your own data.\r\n- **CLI.** One deterministic command per screen: `npx @svgrid/studio add ...`\r\n introspects your table or schema and writes the files. No AI involved. This\r\n page uses the CLI from Step 3 on - continue below.\r\n- **AI via MCP.** With [`@svgrid/mcp`](./ai-generation.md) configured, ask your\r\n coding agent to build the screen; it introspects, scaffolds, and\r\n compile-verifies through the same core the CLI uses.\r\n\r\nAll three produce the **same generated code** - pick whichever fits how you\r\nwork, and switch freely later.\r\n\r\n---\r\n\r\n## What you need\r\n\r\n- **[Node.js](https://nodejs.org) 18 or newer.** Check with `node -v` in a\r\n terminal. If that errors, install Node first.\r\n- **A terminal** and a code editor (VS Code is fine).\r\n- **A SvelteKit app.** Don't have one? Create one in 30 seconds:\r\n\r\n ```bash\r\n npx sv create my-app # pick \"SvelteKit minimal\", TypeScript: yes\r\n cd my-app\r\n npm install\r\n ```\r\n\r\nYou do **not** need a database to start - the first screen below runs on\r\nin-memory data. You can point it at PostgreSQL, Supabase, MySQL, and others\r\nlater without changing the UI.\r\n\r\n---\r\n\r\n## Step 1 - Install\r\n\r\nInside your app folder:\r\n\r\n```bash\r\nnpm i @svgrid/grid @svgrid/enterprise\r\n```\r\n\r\n- `@svgrid/grid` is the grid itself.\r\n- `@svgrid/enterprise` adds Studio: the schema, the edit form, and the data-source\r\n helpers. It is **soft-gate only** - everything runs unlicensed, it just nudges.\r\n See [licensing](../licensing.md).\r\n\r\n---\r\n\r\n## Step 2 - Describe your data once\r\n\r\nThe generator needs one description of your table. If you have a live database\r\nit can introspect it directly (Step 3 shows that variant). Here we stay\r\ndatabase-free: describe the table in a small Drizzle schema file, which the\r\ngenerator **reads as text** - it never connects to anything.\r\n\r\n```bash\r\nnpm i -D drizzle-orm\r\n```\r\n\r\n(`drizzle-orm` is only there so the schema file type-checks; nothing runs\r\nagainst a database. It is also the natural next step when you do add one.)\r\n\r\nCreate `src/lib/db/schema.ts`:\r\n\r\n```ts\r\nimport { pgTable, text, integer, boolean } from 'drizzle-orm/pg-core'\r\n\r\nexport const customers = pgTable('customers', {\r\n id: text('id').primaryKey(),\r\n name: text('name').notNull(),\r\n email: text('email').notNull(),\r\n tier: text('tier').notNull().default('free'),\r\n mrr: integer('mrr'),\r\n active: boolean('active'),\r\n})\r\n```\r\n\r\nA Prisma `schema.prisma` works the same way. Prefer to write Studio's own\r\nmodel - the `EntitySchema` - by hand instead? That is the\r\n[appendix](#appendix-wire-it-by-hand-no-generator) at the bottom of this page.\r\n\r\n---\r\n\r\n## Step 3 - Generate the screen\r\n\r\nOne command:\r\n\r\n```bash\r\nnpx @svgrid/studio add customers --from src/lib/db/schema.ts\r\n```\r\n\r\nHave a live database instead? Same command, different source - no schema file\r\nneeded:\r\n\r\n```bash\r\nnpx @svgrid/studio add customers --db postgres --url \"$DATABASE_URL\"\r\n```\r\n\r\nEither way it writes **three files**, and the screen is done. A quick tour of\r\nwhat you now own:\r\n\r\n**1. `src/lib/customers.schema.ts` - the model.** The generator turned your\r\ntable into an `EntitySchema` - the single object that drives the grid columns,\r\nthe form fields, and validation:\r\n\r\n```ts\r\nexport type CustomersRow = {\r\n id: string\r\n name: string\r\n email: string\r\n tier: string\r\n mrr: number | null\r\n active: boolean | null\r\n}\r\n\r\nexport const customersSchema: EntitySchema<CustomersRow> = {\r\n name: 'customers',\r\n idField: 'id',\r\n fields: [\r\n { field: 'id', type: 'text', primaryKey: true, readonly: true },\r\n { field: 'name', type: 'text', required: true },\r\n { field: 'email', type: 'text', required: true },\r\n // ...\r\n ],\r\n}\r\n```\r\n\r\nEvery field option (labels, enum choices, min/max, regex, custom validators) is\r\nexplained in [The EntitySchema](./schema.md).\r\n\r\n**2. `src/routes/api/customers/+server.ts` - the API route.** A\r\n`ServerDataSource` (read + create + update + delete) exposed over one SvelteKit\r\nendpoint. With `--from` it starts in-memory so it runs immediately; with `--db`\r\nit is already wired to your database:\r\n\r\n```ts\r\nconst source = createInMemoryDataSource<CustomersRow>([], customersSchema)\r\n\r\nexport const { POST } = createKitHandlers({ schema: customersSchema, source })\r\n```\r\n\r\nSwapping in a real database later means replacing that one `source` line - the\r\npage never changes. See [Databases](./databases.md).\r\n\r\n**3. `src/routes/customers/+page.svelte` - the screen.** The grid with\r\nserver-side sort, filter, global search, a native pagination footer,\r\nmulti-select delete with optimistic updates, and a modal create / edit form -\r\nall reading through the API route:\r\n\r\n```ts\r\nconst source = createKitDataSource<CustomersRow>({ endpoint: '/api/customers' })\r\nconst columns = schemaToColumns(customersSchema)\r\nconst controller = createServerDataSource<CustomersRow>(source, {\r\n pageSize: 25, optimistic: true,\r\n getRowId: (r) => String(r.id),\r\n onChange: (s) => (state = s),\r\n})\r\n```\r\n\r\nEach file wraps its generated body in `// svgrid:managed:start` /\r\n`// svgrid:managed:end` markers. Everything you write **outside** the markers\r\nis yours; re-running `add` only rewrites what is inside. That is what makes\r\nStep 5 safe.\r\n\r\n> One thing the generated screen inherits from your page: the **font**. A bare\r\n> `npx sv create` app sets no CSS at all, so add a\r\n> `body { font-family: system-ui, sans-serif }` rule (or a\r\n> [`--sg-font`](./theming.md) token) once, or the page renders in the\r\n> browser's default serif. Borders, backgrounds, and hover states the grid\r\n> themes itself.\r\n\r\n---\r\n\r\n## Step 4 - Run it\r\n\r\n```bash\r\nnpm run dev\r\n```\r\n\r\nOpen the URL it prints (usually `http://localhost:5173`) and go to\r\n**`/customers`**. With `--from` the grid starts empty (in-memory source, no\r\nseed) - click **New** and add two or three customers, then try the screen:\r\n\r\n- Click a **column header** to sort.\r\n- Type in the **filter row** under a header to filter (it stays focused as you type).\r\n- Click a **row** to edit. The form validates as you type - clear a required\r\n field and watch it complain.\r\n- Select rows with the checkboxes and **Delete** them - the grid updates\r\n instantly and rolls back if the server says no.\r\n- Page through with the **native pager** at the bottom.\r\n\r\n\r\n\r\n---\r\n\r\n## Step 5 - Change something\r\n\r\nBecause the schema drives everything, changes are one edit. Add a column to\r\n`src/lib/db/schema.ts`:\r\n\r\n```ts\r\ncountry: text('country'),\r\n```\r\n\r\nThen re-run the exact same command:\r\n\r\n```bash\r\nnpx @svgrid/studio add customers --from src/lib/db/schema.ts\r\n```\r\n\r\nThe managed regions are regenerated: the grid gets a **Country** column and the\r\nedit form gets a **Country** input. Anything you wrote outside the\r\n`svgrid:managed` markers - extra buttons, styles, handlers - is untouched.\r\nThat round trip (change schema, re-run, keep your code) is the everyday\r\nworkflow; [Code generation](./code-generation.md) explains the rules.\r\n\r\nPrefer not to re-run the generator? Editing the generated\r\n`customers.schema.ts` directly works too - grid and form update together from\r\nthe one schema object.\r\n\r\n---\r\n\r\n## Where to go next\r\n\r\nYou have the whole shape now. The usual next steps:\r\n\r\n- **Understand the model** - [Concepts](./concepts.md) walks the pipeline\r\n (schema, screens, data source, codegen) once and defines every Studio term.\r\n- **Connect a real database** - re-run `add` with `--db` and a connection\r\n string, or swap the one `source` line in the API route; the page does not\r\n change. For Supabase, follow the one-page\r\n **[Supabase CRUD grid tutorial](./supabase-grid.md)**; for SQL, see\r\n [Databases](./databases.md).\r\n- **Design visually** - `npx @svgrid/studio designer` opens the full app builder:\r\n compose screens across entities, bind data, and click *Generate app*. See the\r\n [Visual app designer](./app-designer.md). (To embed a single-entity schema editor\r\n in your own app, see the [Schema designer](./designer.md).)\r\n- **Build a full app** - the [Build a CRM tutorial](./tutorial-crm.md) wires up\r\n companies, contacts, and deals with relations and master-detail.\r\n\r\n---\r\n\r\n## Appendix: wire it by hand (no generator)\r\n\r\nEverything the generator wrote in Step 3 can be built up by hand - useful when\r\nyou want to see exactly how the pieces fit, or to embed a Studio screen in an\r\nunusual spot. Two files replace the three generated ones (no API route: here\r\nthe data source lives in the page itself).\r\n\r\nFirst, the `EntitySchema` - Studio's own model, the object the generator\r\nderived from your Drizzle file. Create `src/lib/customers.ts`:\r\n\r\n```ts\r\nimport type { EntitySchema } from '@svgrid/enterprise'\r\n\r\nexport type Customer = {\r\n id: string\r\n name: string\r\n email: string\r\n tier: 'free' | 'pro' | 'enterprise'\r\n mrr: number\r\n active: boolean\r\n}\r\n\r\nexport const customersSchema: EntitySchema<Customer> = {\r\n name: 'customers',\r\n label: 'Customer',\r\n idField: 'id',\r\n fields: [\r\n { field: 'id', type: 'text', primaryKey: true, readonly: true },\r\n { field: 'name', type: 'text', required: true, minLength: 2 },\r\n { field: 'email', type: 'text', label: 'Email', required: true, format: 'email' },\r\n { field: 'tier', type: 'enum', options: [\r\n { value: 'free', label: 'Free' },\r\n { value: 'pro', label: 'Pro' },\r\n { value: 'enterprise', label: 'Enterprise' },\r\n ] },\r\n { field: 'mrr', type: 'number', label: 'MRR ($)', min: 0 },\r\n { field: 'active', type: 'boolean' },\r\n ],\r\n}\r\n```\r\n\r\nThen the page. `createInMemoryDataSource` provides the `ServerDataSource`\r\ncontract over a plain array, `createServerDataSource` runs sort / filter /\r\npage / CRUD against it, and the grid + edit panel render it. Create\r\n`src/routes/customers/+page.svelte`:\r\n\r\n```svelte\r\n<script lang=\"ts\">\r\n import { SvGrid, createServerDataSource, type ServerState } from '@svgrid/grid'\r\n import { SvGridEditPanel, createInMemoryDataSource, schemaToColumns } from '@svgrid/enterprise'\r\n import { customersSchema, type Customer } from '$lib/customers'\r\n\r\n const seed: Customer[] = [\r\n { id: 'c1', name: 'Ada Lovelace', email: 'ada@analytic.io', tier: 'enterprise', mrr: 1200, active: true },\r\n { id: 'c2', name: 'Alan Turing', email: 'alan@bletchley.uk', tier: 'pro', mrr: 240, active: true },\r\n { id: 'c3', name: 'Grace Hopper', email: 'grace@navy.mil', tier: 'enterprise', mrr: 980, active: true },\r\n ]\r\n\r\n const columns = schemaToColumns(customersSchema)\r\n const source = createInMemoryDataSource(seed, customersSchema)\r\n\r\n let view = $state<ServerState<Customer>>({\r\n rows: [], total: 0, loading: false, saving: false, error: null,\r\n pageIndex: 0, pageSize: 10, pageCount: 1, sortModel: [], filterModel: {},\r\n })\r\n let editing = $state<Customer | null | undefined>(undefined)\r\n let genId = 4\r\n\r\n const controller = createServerDataSource(source, {\r\n pageSize: 10, optimistic: true, getRowId: (r) => r.id,\r\n onChange: (s) => (view = s),\r\n })\r\n controller.refresh()\r\n\r\n async function save({ mode, id, values }) {\r\n if (mode === 'create') { await controller.createRow({ id: `c${genId++}`, ...values }); controller.setPage(view.pageCount - 1) }\r\n else if (id) { await controller.updateRow(id, values) }\r\n editing = undefined\r\n }\r\n</script>\r\n\r\n<style>\r\n :global(body) {\r\n font-family: ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif;\r\n }\r\n</style>\r\n\r\n<button onclick={() => (editing = null)}>+ New customer</button>\r\n\r\n<SvGrid\r\n data={view.rows} {columns} loading={view.loading}\r\n fitColumns enableRowSummaries={false}\r\n sortable externalSort onSortingChange={(s) => controller.setSort(s)}\r\n filterable filterMode=\"row\" externalFilter\r\n onFiltersChange={(f) => controller.setFilter({\r\n global: f.global || undefined,\r\n columns: Object.fromEntries(f.columns.map((c) => [c.id, { operator: c.operator, value: c.value, valueTo: c.valueTo, selectedValues: c.selectedValues }])),\r\n })}\r\n onRowClick={(e) => (editing = e.row)}\r\n showPagination externalPagination\r\n rowCount={view.total} pageIndex={view.pageIndex} pageSize={view.pageSize}\r\n onPaginationChange={({ pageIndex, pageSize }) => pageSize !== view.pageSize ? controller.setPageSize(pageSize) : controller.setPage(pageIndex)}\r\n/>\r\n\r\n{#if editing !== undefined}\r\n <SvGridEditPanel schema={customersSchema} row={editing} presentation=\"modal\"\r\n onSubmit={save} onCancel={() => (editing = undefined)} />\r\n{/if}\r\n```\r\n\r\nThe `<style>` block is just a plain font reset - a fresh `npx sv create` app ships no CSS\r\nat all, so without it the page falls back to the browser's default serif font. `<SvGrid>`\r\nand `<SvGridEditPanel>` already theme their own borders, backgrounds, and hover states out\r\nof the box (via [`--sg-*` tokens](../../help/tokens.md) with built-in fallbacks) - font is\r\nthe one thing they intentionally inherit from the page rather than force, so it fits\r\nwhatever type your app already uses. If your app already sets a body font (or a\r\n[`--sg-font`](./theming.md) token), skip this block.\r\n\r\n---\r\n\r\n## See also\r\n\r\n- [SvGrid Studio overview](../studio.md)\r\n- [Concepts](./concepts.md) - the mental model + glossary\r\n- [Data binding](./data-binding.md) - the `ServerDataSource` contract in depth\r\n- [Edit forms & validation](./edit-forms.md)\r\n- [Troubleshooting & FAQ](./troubleshooting.md)\r\n"
|
|
2814
2814
|
},
|
|
2815
2815
|
{
|
|
2816
2816
|
"slug": "enterprise/studio/i18n",
|
|
@@ -2828,7 +2828,7 @@ export const docs = [
|
|
|
2828
2828
|
"slug": "enterprise/studio/launch",
|
|
2829
2829
|
"path": "docs/enterprise/studio/launch.md",
|
|
2830
2830
|
"title": "Launch the designer (CLI)",
|
|
2831
|
-
"markdown": "# Launch the designer (CLI)\r\n\r\n> **No install at all?** Open **[svgrid.com/studio](https://svgrid.com/studio)**\r\n> directly - the same designer, running in your browser, nothing to run\r\n> locally. It can't write to your filesystem, so **Generate app** gives you a\r\n> **Download .zip** instead of the CLI's save-to-folder, and **Add data ->\r\n> Get a database** can provision a free Neon / Supabase / Turso database for\r\n> you without leaving the browser. Everything else - screens, entities, the\r\n> visual editor - is identical. Want auto-save-to-disk, or to point it at a\r\n> database already on your machine? Use the CLI below instead.\r\n\r\nOne command opens the visual [app designer](./app-designer.md) in your browser,\r\nbacked by a tiny local server that **auto-saves** your design to disk and\r\n**generates the app** into a folder - no host app to build, no copy-paste JSON.\r\n\r\n> **Three \"designer\" pages, one map.** This page is about *opening* the app\r\n> builder locally. [Visual app designer](./app-designer.md) documents the\r\n> builder itself (`SvStudioDesigner`, multi-screen). [Schema designer](./designer.md)\r\n> is a different, embeddable component (`SvSchemaDesigner`) for editing a single\r\n> entity inside your own app.\r\n\r\n\r\n\r\nYou run **one command** to open the designer; everything after that is\r\npoint-and-click - no coding.\r\n\r\n```bash\r\nnpx @svgrid/studio designer\r\n```\r\n\r\nThat serves the designer at `http://localhost:4321` and opens it. Design your\r\napp - screens, entities, data sources, grid config - and every edit is saved to\r\n`studio.config.json` in the current folder as you work. A refresh (or a rerun)\r\npicks up exactly where you left off.\r\n\r\n## What it does\r\n\r\n- **Loads** `studio.config.json` from the current folder. If there's none yet,\r\n the designer opens with a small starter project so you have something to edit.\r\n- **Auto-saves** every change back to `studio.config.json` (debounced). This is\r\n the persistence the embedded component never had - your work survives a\r\n refresh.\r\n- **Generates the app** to disk: open **Generate app**, then click **Save to\r\n folder**. Every file of the runnable SvelteKit + Vite project is written into\r\n the output folder (the driver deps for any SQL / Supabase entity are added to\r\n its `package.json` for you). **Download .zip** is still there too.\r\n\r\n```bash\r\ncd my-app # design saved here as studio.config.json\r\nnpx @svgrid/studio designer\r\n# ... design, then Generate app -> Save to folder ...\r\nnpm install\r\nnpm run dev\r\n```\r\n\r\n## Start from zero\r\n\r\n**\r\n> directly - the same designer, running in your browser, nothing to run\r\n> locally. It can't write to your filesystem, so **Generate app** gives you a\r\n> **Download .zip** instead of the CLI's save-to-folder, and **Add data ->\r\n> Get a database** can provision a free Neon / Supabase / Turso database for\r\n> you without leaving the browser. Everything else - screens, entities, the\r\n> visual editor - is identical. Want auto-save-to-disk, or to point it at a\r\n> database already on your machine? Use the CLI below instead.\r\n\r\nOne command opens the visual [app designer](./app-designer.md) in your browser,\r\nbacked by a tiny local server that **auto-saves** your design to disk and\r\n**generates the app** into a folder - no host app to build, no copy-paste JSON.\r\n\r\n> **Three \"designer\" pages, one map.** This page is about *opening* the app\r\n> builder locally. [Visual app designer](./app-designer.md) documents the\r\n> builder itself (`SvStudioDesigner`, multi-screen). [Schema designer](./designer.md)\r\n> is a different, embeddable component (`SvSchemaDesigner`) for editing a single\r\n> entity inside your own app.\r\n\r\n\r\n\r\nYou run **one command** to open the designer; everything after that is\r\npoint-and-click - no coding.\r\n\r\n```bash\r\nnpx @svgrid/studio designer\r\n```\r\n\r\nThat serves the designer at `http://localhost:4321` and opens it. Design your\r\napp - screens, entities, data sources, grid config - and every edit is saved to\r\n`studio.config.json` in the current folder as you work. A refresh (or a rerun)\r\npicks up exactly where you left off.\r\n\r\n## What it does\r\n\r\n- **Loads** `studio.config.json` from the current folder. If there's none yet,\r\n the designer opens with a small starter project so you have something to edit,\r\n and offers the **New app** wizard to replace it with your own data.\r\n- **Auto-saves** every change back to `studio.config.json` (debounced). This is\r\n the persistence the embedded component never had - your work survives a\r\n refresh.\r\n- **Generates the app** to disk: open **Generate app**, then click **Save to\r\n folder**. Every file of the runnable SvelteKit + Vite project is written into\r\n the output folder (the driver deps for any SQL / Supabase entity are added to\r\n its `package.json` for you). **Download .zip** is still there too.\r\n\r\n```bash\r\ncd my-app # design saved here as studio.config.json\r\nnpx @svgrid/studio designer\r\n# ... design, then Generate app -> Save to folder ...\r\nnpm install\r\nnpm run dev\r\n```\r\n\r\n## Start from zero\r\n\r\n\r\n\r\nLaunched in an empty folder, the designer opens the starter project and offers\r\nthe **New app** wizard - four steps (where the data lives, which tables, which\r\npages each table gets, what it's called) that end on a working app. It runs the\r\nsame generator as `svgrid-studio init`, so both routes land in the same place.\r\n\r\nPrefer to start from something finished? A ready-made [sample app](./samples.md)\r\n(`--template <id>`, or the **Sample apps** button) is one click. Or build the\r\ndata model yourself:\r\n\r\n- **New entity** - name it and get a screen with an `id` + `name` field; flesh\r\n out the rest (fields, types, PK / required / relations) in the inspector. The\r\n **New entity** button in the toolbar works any time, not just at the start.\r\n- **Connect a database** - see below.\r\n- **Import a schema** - paste a Drizzle `schema.ts` or Prisma `schema.prisma`.\r\n\r\nClear every screen out of a design and these same choices appear as the\r\ndesigner's start screen.\r\n\r\nOpening a [sample app](./samples.md) gives you a complete, themed app in one\r\nclick. Here the **CRM** sample - a dashboard with KPI tiles and a deal-pipeline\r\nchart, ready to explore or point at your own data:\r\n\r\n\r\n\r\n## Connect a live database\r\n\r\n**Connect DB** (toolbar) opens a wizard that reads your database and turns its\r\ntables into entities - no hand-typing a schema:\r\n\r\n\r\n\r\n\r\n1. Pick the **database** (PostgreSQL / MySQL / SQL Server / SQLite / Supabase)\r\n and paste a **connection string**.\r\n2. The designer lists the **tables**; tick the ones you want.\r\n3. **Add** them - each becomes an entity + screen, with columns typed from the\r\n catalog and **foreign keys turned into relations**. Every added entity is\r\n **bound to its SQL table**, so **Generate app** emits a connected\r\n `src/routes/api/<table>/+server.ts` (driver + `DATABASE_URL`) for it.\r\n\r\nEach database needs a driver (`pg` / `mysql2` / `mssql` / `better-sqlite3`), but\r\nyou don't have to install it yourself: Studio checks the folder you launched the\r\ndesigner in, detects its package manager (pnpm / yarn / bun / npm from the\r\nlockfile) and installs the missing one for you. If that fails - no network, a\r\nlocked file - it says which command to run by hand. The connection stays on your\r\nmachine: the local designer server does the read; nothing is sent to a cloud. To\r\nscaffold from a schema file or a live DB straight from the terminal instead, use\r\n[`npx @svgrid/studio add`](./cli.md).\r\n\r\n## Options\r\n\r\n| Flag | Default | Description |\r\n| --- | --- | --- |\r\n| `--config <path>` | `./studio.config.json` | The design file to load + auto-save. |\r\n| `--out <dir>` | `.` | Folder the generated app is written into (**Save to folder**). |\r\n| `--port <n>` | `4321` | Port to serve on. |\r\n| `--no-open` | - | Don't open the browser (print the URL only). |\r\n\r\n```bash\r\n# keep the design and the generated app in separate folders\r\nnpx @svgrid/studio designer --config ./design/app.json --out ./generated-app\r\n```\r\n\r\n## How it fits together\r\n\r\nThe design is a `StudioProject` (the same model the [app designer](./app-designer.md)\r\nedits and the [CLI](./cli.md) can regenerate from). `studio.config.json` is that\r\nmodel on disk, so the designer, `npx @svgrid/studio designer`, and\r\n`npm create @svgrid/studio -- --project ./studio.config.json` all read and write\r\nthe one file - design visually, regenerate from CI, or hand it to a teammate.\r\n\r\n## See also\r\n\r\n- [Visual app designer](./app-designer.md) - the canvas, blocks, and grid property editor\r\n- [The Studio CLI](./cli.md) - `add` a screen from a schema or a live database\r\n- [Getting started](./getting-started.md) - the full first-app walkthrough\r\n"
|
|
2832
2832
|
},
|
|
2833
2833
|
{
|
|
2834
2834
|
"slug": "enterprise/studio/local-database",
|
|
@@ -2894,13 +2894,13 @@ export const docs = [
|
|
|
2894
2894
|
"slug": "enterprise/studio/samples",
|
|
2895
2895
|
"path": "docs/enterprise/studio/samples.md",
|
|
2896
2896
|
"title": "Sample apps + bind your data",
|
|
2897
|
-
"markdown": "# Sample apps + bind your data\r\n\r\nThe fastest way to a working app: open a **ready-made sample**, see it running with\r\nrealistic data, then point it at **your own database**. No block-by-block build,\r\nno placeholder rows.\r\n\r\n\r\n\r\n## Start from a sample\r\n\r\nIn the designer, click **Sample apps** (toolbar or the
|
|
2897
|
+
"markdown": "# Sample apps + bind your data\r\n\r\nThe fastest way to a working app: open a **ready-made sample**, see it running with\r\nrealistic data, then point it at **your own database**. No block-by-block build,\r\nno placeholder rows.\r\n\r\n\r\n\r\n## Start from a sample\r\n\r\nIn the designer, click **Sample apps** (toolbar or the empty-state screen) and pick\r\none of **18** ready-made apps. Each loads instantly as a full multi-entity app - a\r\ndashboard (KPIs + charts), grids, a master/detail, and **rich edit forms** - themed\r\nand **seeded with believable data**, so you see the real thing, not `name 1 / email 1`.\r\n\r\n| Sample | What's inside |\r\n| --- | --- |\r\n| **CRM** | Companies, Contacts, Deals - pipeline dashboard (value by stage) |\r\n| **E-commerce admin** | Products, Customers, Orders - revenue dashboard + order history |\r\n| **Subscriptions** | Customers, Plans, Subscriptions - MRR + status dashboard |\r\n| **Invoicing** | Clients, Invoices - billed / paid / overdue dashboard |\r\n| **Inventory** | Products, Suppliers, Purchase orders - stock + spend |\r\n| **Support desk** | Tickets, Customers, Agents - triage by status / priority / channel |\r\n| **Recruiting** | Jobs, Candidates, Applications - hiring pipeline |\r\n| **HR** | Employees, Departments, Time off - headcount + payroll |\r\n| **Project tracker** | Projects, Tasks, Members - task dashboard per project |\r\n| **Clinic** | Patients, Doctors, Appointments - visits + revenue |\r\n| **School** | Students, Courses, Enrollments - capacity + enrollment |\r\n| **Events** | Events, Attendees, Registrations - ticket sales + attendance |\r\n| **Restaurant** | Menu, Tables, Orders - sales + order status |\r\n| **Real estate** | Properties, Agents, Leads - listings + sales pipeline |\r\n| **Fleet** | Vehicles, Drivers, Trips - utilization + cost |\r\n| **Gym** | Members, Classes, Bookings - capacity + attendance |\r\n| **Library** | Books, Members, Loans - circulation + overdue |\r\n| **Insurance Claims** | Policies, Adjusters, Claims - claims pipeline board + payout dashboard, behind a sign-in |\r\n\r\n\r\n\r\nLoading a sample replaces the current design (undo with Ctrl+Z). From the\r\n[launcher](./launch.md) you can open one directly:\r\n\r\n```bash\r\nnpx @svgrid/studio designer --template crm\r\n```\r\n\r\nEverything is editable - add fields, screens and blocks, retheme, then\r\n**Generate app** like any project.\r\n\r\n## Realistic sample data everywhere\r\n\r\nSample data is **field-name aware**: a `name` field gets a person, `email` an\r\nemail, `price` / `total` / `mrr` a currency amount, `status` cycles its options,\r\n`created` / `due` real dates, `company` a company. This applies to **every**\r\nin-memory entity - the shipped samples *and* the ones you build - in both the\r\nlive preview and the generated app's seed. Sample apps also carry **hand-curated\r\nseed** so their dashboards and charts look their best out of the box.\r\n\r\n## Rich edit forms\r\n\r\nEvery sample models its fields with the **right editor**, not a wall of text\r\nboxes - so an edit form looks and behaves like a real app. Across the gallery you\r\nget:\r\n\r\n- **Phone** and **country** pickers (SvPhoneInput / SvCountryInput), **masked**\r\n inputs for structured codes (SSN, VIN, license plate, ISBN, ZIP, tax id),\r\n- **rating** stars and **sliders** for scores and percentages (deal probability,\r\n usage, fuel level, tip),\r\n- **tag** inputs for multi-value fields (skills, amenities, dietary tags, segments),\r\n **color** pickers, **password** fields, and **date / date-time** pickers,\r\n- real **validation** (required, min/max, patterns) and **computed fields** - e.g.\r\n a deal's weighted value (`value * probability / 100`), an invoice total\r\n (`subtotal + tax`), or a trip's cost-per-mile - that recalculate as you type.\r\n\r\nEach sample also ships a **form screen**: a grid plus an inline edit panel, so you\r\ncan click a row and see the full editor form immediately. It's all declared on the\r\nentity's fields (`type`, `input.editorType`, `format`, `formula`), so it carries\r\nstraight through **Generate app** into the SvelteKit code - and you can change any\r\nfield's editor from the designer's field inspector.\r\n\r\n## Bind your data\r\n\r\n**Use my data** (toolbar) points a sample's screens at your real backend without\r\nrebuilding them:\r\n\r\n1. Connect: pick a database and either fill the **guided form** (host / port /\r\n database / user / password / SSL) or paste a connection string. **Test\r\n connection** confirms it and lists each table's row count before you commit.\r\n2. **Map** each entity to one of your tables - matches are auto-suggested by name;\r\n leave one as \"Keep sample data\" to skip it.\r\n3. **Bind**. Each mapped entity is bound to SQL, so **Generate app** emits a\r\n connected `src/routes/api/<table>/+server.ts` (driver + `DATABASE_URL`) for it,\r\n and the screens now render **your** data.\r\n\r\n**Columns are matched even when they're named differently.** A sample field is\r\nfuzzy-mapped to your column (exact -> synonyms -> substring): a deal's `value`\r\nbinds to your `amount` column, `stage` to `pipeline_stage`, and so on. The mapping\r\nis stored as the field's `dbColumn`, so the SQL adapter reads your real column and\r\naliases it back - **the dashboard's charts and KPIs keep working unchanged**. Your\r\nextra columns are added as new fields. If a chart or master/detail references a\r\nsample field with no matching column, the wizard lists it so you can adjust.\r\n\r\nThe database driver (`pg` / `mysql2` / `mssql` / `better-sqlite3`) must be\r\ninstalled where you launched the designer - if it's missing, the wizard offers a\r\none-click **Install driver** that runs your project's package manager. The\r\nconnection stays on your machine.\r\n\r\n## See also\r\n\r\n- [Launch the designer](./launch.md) - `npx @svgrid/studio designer` (+ `--template`)\r\n- [Visual app designer](./app-designer.md) - the canvas, blocks, and grid editor\r\n- [Databases](./databases.md) - the per-dialect connection details\r\n"
|
|
2898
2898
|
},
|
|
2899
2899
|
{
|
|
2900
2900
|
"slug": "enterprise/studio/scheduler",
|
|
2901
2901
|
"path": "docs/enterprise/studio/scheduler.md",
|
|
2902
2902
|
"title": "Scheduler / calendar view",
|
|
2903
|
-
"markdown": "# Scheduler / calendar view\
|
|
2903
|
+
"markdown": "# Scheduler / calendar view\n\nAny grid whose rows carry a date can render as a **calendar** instead of a table -\na Month / Week / Day / Agenda (and Timeline) view where each row becomes an event\nplaced by time. It is a **view of the grid**, exactly like the [Kanban board](./app-designer.md):\nsame rows, same data source, same edit form - only the presentation changes. Drag\nan event to reschedule it and the new time is written back through the data source.\n\nThe calendar renderer ships in `@svgrid/enterprise`; the generated app registers it\nonce with `enableSchedulerView()` and then renders `<SvGrid scheduler={...}>`.\n\n\n\n## Turn it on\n\nIn the [visual designer](./app-designer.md), select a grid block and expand\n**Scheduler / calendar** in the inspector. Tick **Render rows as a calendar /\nscheduler** and map the fields:\n\n- **Start (date) field** - required; the event's start (a `date` / `datetime` field).\n- **End field** - optional; omit and each event uses a default duration.\n- **Title** - the event label (defaults to the first column).\n- **Color** - a field whose value tints the event (e.g. a status or priority enum).\n- **Resource** - optional; splits the Week / Day grid into one column per value\n (per person, room, or machine) - a resource scheduler.\n- **Opens on** - Month, Week, Day, Agenda, or a Timeline view.\n- **Drag to move / resize** - turns on drag-to-reschedule and edge-resize, writing\n the new start / end back through the data source (optimistic, then persisted).\n- **Event detail drawer** - a built-in panel to view / edit an event's fields.\n\nThe canvas preview switches to the live calendar as soon as you map a start field,\nso you can try Month / Week / Day and drag events right in the designer.\n\n> The scheduler is a **render mode**: it is mutually exclusive with row **grouping**\n> and **tree data** on the same grid. Pick one.\n\n## What it generates\n\nThe block compiles to the grid's own `scheduler` prop plus write-back handlers -\nreal, typed Svelte, not a black box:\n\n```svelte\n<script lang=\"ts\">\n import { enableSchedulerView } from '@svgrid/enterprise'\n enableSchedulerView()\n // ... load allRows ...\n</script>\n\n<SvGrid\n data={allRows}\n columns={columns}\n getRowId={(r) => String(r.id)}\n scheduler={{\n startField: 'startedAt',\n endField: 'dueDate',\n titleField: 'title',\n colorField: 'priority',\n resourceField: 'assignee',\n initialView: 'week',\n editable: true,\n drawer: true,\n onEventMove: (e) => { /* patch start/end on the row, persist */ },\n onEventCommit: (e) => { /* save the drawer's edits */ },\n }}\n/>\n```\n\nA resource / title / color field that points at a relation is resolved to its\ndisplay column automatically (e.g. `assigneeId` renders as the assignee's name).\n\n## The calendar block\n\nThe standalone **calendar** block (a dedicated \"Schedule\" screen) uses the same\nrenderer - a Month calendar with a detail drawer, read-first. Use it for a simple\n\"events on a month grid\" screen; use the grid **Scheduler view** above when you\nwant Week / Day time-grids, per-resource columns, timelines, or drag-to-reschedule.\n\n## In the sample apps\n\nSeveral starter apps ship a scheduler so you can see it end to end:\n\n- **Projects** - a task **timeline** (start -> due) with one row per assignee, tinted by priority.\n- **People Ops (HR)** - a **leave calendar**: time-off as a per-employee resource scheduler.\n- **HireDesk (ATS)** - interviews on a **Week** grid, colored by pipeline stage.\n- **Evently**, **HealthClinic**, **FitClub**, and the restaurant app - month event calendars.\n\n## See also\n\n- The underlying grid feature and its live demos: [Scheduler / calendar mode](#/demos/363-scheduler-intro),\n [timeline views](#/demos/371-scheduler-timeline), and the real-world apps built on it -\n [Horizon calendar client](#/demos/381-scheduler-app-calendar), [Meridian Clinic](#/demos/382-scheduler-app-clinic),\n [Dispatch board](#/demos/383-scheduler-app-dispatch), [Portfolio roadmap](#/demos/384-scheduler-app-roadmap),\n and [Broadcast content calendar](#/demos/385-scheduler-app-content).\n- [App designer](./app-designer.md) - the block palette and inspector.\n- [Kanban board](./app-designer.md) - the other \"view of the grid\".\n"
|
|
2904
2904
|
},
|
|
2905
2905
|
{
|
|
2906
2906
|
"slug": "enterprise/studio/schema",
|
|
@@ -2978,7 +2978,7 @@ export const docs = [
|
|
|
2978
2978
|
"slug": "getting-started/1-install",
|
|
2979
2979
|
"path": "docs/getting-started/1-install.md",
|
|
2980
2980
|
"title": "1. Install",
|
|
2981
|
-
"markdown": "# 1. Install\n\n> Step 1 of 6 · [Next: First grid →](./2-first-grid.md)\n\nSvGrid is a single npm package. There is no peer dependency on a CSS\nframework - bring your own, or
|
|
2981
|
+
"markdown": "# 1. Install\n\n> Step 1 of 6 · [Next: First grid →](./2-first-grid.md)\n\nSvGrid is a single npm package. There is no peer dependency on a CSS\nframework - bring your own, or import one of the 20 themes that ship\nwith it. Each theme carries a light and a dark palette, so dark mode is\nan attribute, not a second stylesheet.\n\n\n\n## Fastest start: scaffold a project\n\nStarting fresh? Skip the manual wiring and scaffold a project with the\ngrid already set up:\n\n```bash\nnpm create @svgrid@latest # interactive\nnpm create @svgrid@latest my-admin -- --template admin-dashboard\n```\n\nSee [Starters & scaffolding](./starters.md) for the templates (minimal\nVite app or a full SvelteKit admin dashboard) and the Deploy-to-Vercel\nflow. To add SvGrid to an **existing** app, install it directly:\n\n```bash\n# pnpm (recommended)\npnpm add @svgrid/grid\n\n# npm\nnpm install @svgrid/grid\n\n# yarn\nyarn add @svgrid/grid\n```\n\n## Requirements\n\n| Tool | Version | Why |\n| ----------- | ----------------- | ------------------------------------- |\n| Svelte | **5.x** | Uses runes: `$state`, `$derived`, `$effect`. |\n| TypeScript | **5.4+** | Optional but strongly recommended. The column-def types pay for themselves. |\n| Node | **18+** | For tooling (`vite`, `svelte-check`, the example gallery). |\n\nThe bundle is tree-shakeable: features you don't import don't ship.\nThere's no monolithic entry that pulls everything.\n\n## Verify the install\n\nA 5-line smoke test:\n\n```svelte\n<script lang=\"ts\">\n import { SvGrid } from '@svgrid/grid'\n const rows = [{ name: 'Ada' }, { name: 'Linus' }]\n const columns = [{ field: 'name', header: 'Name' }]\n</script>\n\n<SvGrid data={rows} columns={columns} />\n```\n\nIf you see a styled `<table>` with two rows, you're done.\n\n## Pick a theme\n\nThe render component ships its own styles, so the grid is readable the\nmoment it mounts. It is deliberately plain, though: no preset is applied\nuntil you ask for one. Import a theme to change that.\n\n```ts\nimport '@svgrid/grid/themes/shadcn.css'\n```\n\nTwenty are available: `ember` (SvGrid's own look), `shadcn`, `tailwind`,\n`material`, `fluent`, `carbon`, `antd`, `bootstrap`, `atlassian`,\n`salesforce`, `sap`, `github`, `linear`, `notion`, `vercel`, `excel`,\n`nord`, `dracula`, `catppuccin`, `ag-alpine`.\n\nEach one declares the whole `--sg-*` token set twice: once on `:root` and\nonce under `:root[data-theme='dark']`. So dark mode is one attribute on\nthe document, and the grid follows:\n\n```ts\ndocument.documentElement.dataset.theme = 'dark'\n```\n\nTwo things worth knowing before you wire this into an existing app:\n\n- **Set `color-scheme` too.** Without it the browser keeps painting native\n scrollbars, form controls and the page canvas light, so a dark grid sits\n in a light frame. `:root { color-scheme: light }` plus\n `:root[data-theme='dark'] { color-scheme: dark }` is the whole fix.\n- **Apply the attribute before the first paint.** Setting it from a\n component means the page renders in the wrong palette for a frame. An\n inline script in `index.html` (or `app.html` in SvelteKit) that reads\n `localStorage` avoids the flash.\n\nThe scaffolded starters do both already. [Step 5](./5-theme-and-density.md)\ncovers overriding individual tokens, per-instance theming and density.\n\n## Enterprise add-on (optional)\n\nIf you need data export (Excel / PDF / CSV), data import, the AI\nassistant, or built-in pivot tables, install the paid Enterprise pack\nalongside the Community package:\n\n```bash\npnpm add @svgrid/enterprise\n```\n\nSee [Enterprise features](../enterprise/README.md) for what ships and how to license.\n\n## Where the rest of this guide goes\n\n1. **Install** ← you're here\n2. [First grid](./2-first-grid.md) - the minimum runnable example explained\n3. [Data and columns](./3-data-and-columns.md) - the two arrays the grid actually reads\n4. [Features](./4-features.md) - opt into sort, filter, pagination, grouping, etc.\n5. [Theme and density](./5-theme-and-density.md) - `--sg-*` tokens, dark mode, row height\n6. [Going to production](./6-going-to-production.md) - server-side data, virtualization, a11y, SSR\n\nThe combined \"everything in one page\" version is at\n[../getting-started-full.md](../getting-started-full.md) - useful for\nprinting or single-tab reading.\n"
|
|
2982
2982
|
},
|
|
2983
2983
|
{
|
|
2984
2984
|
"slug": "getting-started/2-first-grid",
|
|
@@ -3026,7 +3026,7 @@ export const docs = [
|
|
|
3026
3026
|
"slug": "help/agents",
|
|
3027
3027
|
"path": "docs/help/agents.md",
|
|
3028
3028
|
"title": "Build an AI agent that drives the grid",
|
|
3029
|
-
"markdown": "# Build an AI agent that drives the grid\r\n\r\nThis is the \"AI-native data grid\" story end-to-end. Three patterns,\r\nin order of how much agency you hand to the LLM:\r\n\r\n1. **Read-only summary agent** - the model describes what's in the\r\n grid (analyst chat, monthly report drafts)\r\n2. **Stateful UI agent** - the model calls `SvGridApi` methods in\r\n response to natural language (\"group by region, sum revenue\")\r\n3. **Autonomous workflow agent** - the model orchestrates multiple\r\n grids + back-ends (smart import → enrich → export to BI tool)\r\n\r\n\r\n\r\nThe grid is designed to support all three. The headless engine + the\r\nimperative `SvGridApi` together form a clean tool surface that any\r\nagent SDK can consume.\r\n\r\n> **Looking for the in-grid AI features?** See\r\n> [AI assistant](./ai.md) for NL filter / smart fill /\r\n> summarise / classify - the agent surface this page describes is\r\n> what you'd build ON TOP of those.\r\n\r\n## Pattern 1: Read-only summary agent\r\n\r\n```svelte\r\n<script lang=\"ts\">\r\n import OpenAI from 'openai'\r\n import { SvGrid, type SvGridApi } from '@svgrid/grid'\r\n\r\n const client = new OpenAI()\r\n let api = $state<SvGridApi<typeof features, Row> | null>(null)\r\n let answer = $state('')\r\n\r\n async function ask(question: string) {\r\n if (!api) return\r\n // Grid is the source of truth - only send what the user is\r\n // looking at right now (filters + sort + grouping respected).\r\n const rows = api.getDisplayedRows().slice(0, 200)\r\n const r = await client.chat.completions.create({\r\n model: 'claude-haiku-4-5',\r\n messages: [\r\n { role: 'system', content: 'Answer questions about the table below. Be concise.' },\r\n { role: 'user', content: `Question: ${question}\\n\\nTable (first 200 rows):\\n${JSON.stringify(rows)}` },\r\n ],\r\n })\r\n answer = r.choices[0]?.message?.content ?? ''\r\n }\r\n</script>\r\n```\r\n\r\n**Trade-off:** simple to build, no tool calling, but the model only\r\nsees the rows you sent it. Fine for \"what's the top performer this\r\nquarter?\"; not for \"filter to last 30 days\" - that needs Pattern 2.\r\n\r\n## Pattern 2: Stateful UI agent\r\n\r\nThe grid's imperative API is a clean tool surface. Each `SvGridApi`\r\nmethod becomes one function the model can call.\r\n\r\n```ts\r\nimport OpenAI from 'openai'\r\nimport { z } from 'zod'\r\n\r\nconst tools = [\r\n {\r\n type: 'function',\r\n function: {\r\n name: 'setFilter',\r\n description: 'Apply a column filter. Use to narrow the visible rows.',\r\n parameters: {\r\n type: 'object', required: ['columnId', 'operator', 'value'],\r\n properties: {\r\n columnId: { type: 'string' },\r\n operator: { type: 'string', enum: ['contains', 'equals', 'startsWith', 'greaterThan', 'lessThan', 'between'] },\r\n value: { type: 'string' },\r\n valueTo: { type: 'string', description: 'Upper bound for \"between\" only.' },\r\n },\r\n },\r\n },\r\n },\r\n {\r\n type: 'function',\r\n function: {\r\n name: 'setSort',\r\n parameters: { type: 'object', required: ['columnId', 'direction'],\r\n properties: { columnId: { type: 'string' }, direction: { type: 'string', enum: ['asc', 'desc'] } } },\r\n },\r\n },\r\n {\r\n type: 'function',\r\n function: {\r\n name: 'setGroupBy',\r\n parameters: { type: 'object', required: ['columnIds'],\r\n properties: { columnIds: { type: 'array', items: { type: 'string' } } } },\r\n },\r\n },\r\n {\r\n type: 'function',\r\n function: { name: 'clearAllFilters', parameters: { type: 'object' } },\r\n },\r\n]\r\n```\r\n\r\nWire the tool calls back to your live `api` reference:\r\n\r\n```ts\r\nasync function runAgent(prompt: string) {\r\n let messages = [\r\n { role: 'system', content: `You drive a data grid. Columns: ${JSON.stringify(api!.getColumns())}.` },\r\n { role: 'user', content: prompt },\r\n ]\r\n for (let turn = 0; turn < 6; turn += 1) { // safety bound\r\n const r = await client.chat.completions.create({\r\n model: 'claude-sonnet-4-6',\r\n messages, tools, tool_choice: 'auto',\r\n })\r\n const msg = r.choices[0]!.message\r\n messages.push(msg)\r\n if (!msg.tool_calls?.length) return msg.content\r\n for (const call of msg.tool_calls) {\r\n const args = JSON.parse(call.function.arguments)\r\n switch (call.function.name) {\r\n case 'setFilter': api!.setFilter(args.columnId, args); break\r\n case 'setSort': api!.setSort(args.columnId, args.direction); break\r\n case 'setGroupBy': api!.setGroupBy(args.columnIds); break\r\n case 'clearAllFilters': api!.clearAllFilters(); break\r\n }\r\n messages.push({ role: 'tool', tool_call_id: call.id, content: 'ok' })\r\n }\r\n }\r\n}\r\n```\r\n\r\nUser types *\"show me last quarter's deals over $50k, grouped by\r\nregion\"* and the agent calls `setFilter('sellDate', { ... })` +\r\n`setFilter('amount', { operator: 'greaterThan', value: '50000' })` +\r\n`setGroupBy(['region'])` in a single turn.\r\n\r\n**The whole `SvGridApi` is on the menu.** Wrap as many or as few\r\nmethods as you want; the model only calls what you expose.\r\n\r\n## Pattern 3: Autonomous workflow agent\r\n\r\nThe grid becomes one node in a longer chain. Typical shape:\r\n\r\n```ts\r\nconst agent = new Agent({\r\n tools: [\r\n fetchCsvFromS3, // pull raw data\r\n aiSmartPaste, // parse to typed rows (uses /api/ai endpoint)\r\n runValidations, // your domain validations\r\n pushToGrid, // api.addRows(...)\r\n waitForUserApproval, // pauses for human-in-the-loop\r\n exportToBigQuery, // api.exportData({ format: 'csv', ... }) + push\r\n ],\r\n})\r\nawait agent.run('Process today\\'s sales batch from s3://acme/sales/2026-06-06.csv')\r\n```\r\n\r\nThe grid is **the visible state** the human can audit between steps -\r\nwhich is exactly what makes a workflow agent trustworthy: every\r\nintermediate result lands in a sortable, filterable table the user\r\ncan inspect.\r\n\r\n## Sandboxing rules\r\n\r\nWhen an LLM is calling grid methods, three boundaries keep things sane:\r\n\r\n1. **Whitelist tools at the top level.** Never expose `eval` or\r\n arbitrary JS. The `SvGridApi` methods above are the only surface\r\n the model needs.\r\n2. **Validate every tool argument** before invoking. The JSON Schemas\r\n at [`/schemas/`](./mcp-server.md) cover every input shape; use\r\n `ajv` or `zod` to check.\r\n3. **Bound the agent loop.** A maximum-turns counter (6 is plenty\r\n for grid manipulation) prevents runaway calls. Combine with a\r\n per-turn token budget.\r\n\r\n## Common workflows shipped as MCP prompts\r\n\r\nThe [MCP server](./mcp-server.md) ships three pre-built prompts that\r\nimplement the above patterns:\r\n\r\n- **`/svgrid:nl-to-grid-state`** - Pattern 2 with the tool set wired up\r\n- **`/svgrid:csv-to-typed-rows`** - Smart-paste an arbitrary CSV into\r\n a typed row array with confidence per row\r\n- **`/svgrid:summarise-view`** - Pattern 1 grounded in `api.getDisplayedRows()`\r\n\r\n## Worked example: NL → Pivot\r\n\r\nLive in [demo 75 (AI Smart Paste)](https://svgrid.com/demos/75-ai-smart-paste/)\r\nand [demo 52 (Pivot designer)](https://svgrid.com/demos/52-pivot-table/)\r\n- both ship in the gallery.\r\n\r\n## Failure modes\r\n\r\n| Symptom | Cause | Fix |\r\n| ------------------------------------------ | ----------------------------------------------------------- | ------------------------------------------------------------------------- |\r\n| Model invents columns that don't exist | No grounding on the live column set | Pass `api.getColumns()` in the system prompt every turn |\r\n| Model calls `setFilter('Status', ...)` with the wrong case | Column ids are case-sensitive | Include the column ids in the system prompt (snake_case vs PascalCase) |\r\n| Multi-step chain forgets the row count drops | Each tool call doesn't return the new visible row count | Return `api.getDisplayedRows().length` from each handler |\r\n| Agent loops forever | No max-turns bound | Always cap the loop (5-10 turns is plenty for grid manipulation) |\r\n\r\n## See also\r\n\r\n- [LLM grounding](./llm-grounding.md) - the static doc files agents read\r\n- [MCP server](./mcp-server.md) - turnkey integration for Claude Desktop / Cursor / Zed\r\n- [AI assistant](./ai.md) - the in-grid NL features, free in @svgrid/grid (not the agent layer)\r\n- [Architecture](./architecture.md) - what state lives where (agents need to know)\r\n\r\n## Frequently asked questions\r\n\r\n### Can an AI agent control the SvGrid data grid?\r\n\r\nYes. The imperative `SvGridApi` (filter, sort, select, set values, expand, page)\r\nis exactly the surface an agent drives. This page covers three patterns, from a\r\nread-only summary agent to a full read-write agent that mutates grid state.\r\n\r\n### What is the safest way to let an LLM drive the grid?\r\n\r\nStart read-only: let the model describe and query the grid before it writes.\r\nWhen you grant write access, route it through the same `SvGridApi` calls a user\r\naction would trigger, so validation and dirty-tracking still apply.\r\n\r\n### Do I need the MCP server to build a grid agent?\r\n\r\nNo. The MCP server is a turnkey integration for desktop AI clients; for a custom\r\nin-app agent you call `SvGridApi` directly. Both are documented here and in the\r\nMCP server guide.\r\n"
|
|
3029
|
+
"markdown": "# Build an AI agent that drives the grid\r\n\r\nThis is the \"AI-native data grid\" story end-to-end. Three patterns,\r\nin order of how much agency you hand to the LLM:\r\n\r\n1. **Read-only summary agent** - the model describes what's in the\r\n grid (analyst chat, monthly report drafts)\r\n2. **Stateful UI agent** - the model calls `SvGridApi` methods in\r\n response to natural language (\"group by region, sum revenue\")\r\n3. **Autonomous workflow agent** - the model orchestrates multiple\r\n grids + back-ends (smart import → enrich → export to BI tool)\r\n\r\n\r\n\r\nThe grid is designed to support all three. The headless engine + the\r\nimperative `SvGridApi` together form a clean tool surface that any\r\nagent SDK can consume.\r\n\r\n> **Looking for the in-grid AI features?** See\r\n> [AI assistant](./ai.md) for NL filter / smart fill /\r\n> summarise / classify - the agent surface this page describes is\r\n> what you'd build ON TOP of those.\r\n\r\n## Pattern 1: Read-only summary agent\r\n\r\n```svelte\r\n<script lang=\"ts\">\r\n import OpenAI from 'openai'\r\n import { SvGrid, type SvGridApi } from '@svgrid/grid'\r\n\r\n const client = new OpenAI()\r\n let api = $state<SvGridApi<typeof features, Row> | null>(null)\r\n let answer = $state('')\r\n\r\n async function ask(question: string) {\r\n if (!api) return\r\n // Grid is the source of truth - only send what the user is\r\n // looking at right now (filters + sort + grouping respected).\r\n const rows = api.getDisplayedRows().slice(0, 200)\r\n const r = await client.chat.completions.create({\r\n model: 'claude-haiku-4-5',\r\n messages: [\r\n { role: 'system', content: 'Answer questions about the table below. Be concise.' },\r\n { role: 'user', content: `Question: ${question}\\n\\nTable (first 200 rows):\\n${JSON.stringify(rows)}` },\r\n ],\r\n })\r\n answer = r.choices[0]?.message?.content ?? ''\r\n }\r\n</script>\r\n```\r\n\r\n**Trade-off:** simple to build, no tool calling, but the model only\r\nsees the rows you sent it. Fine for \"what's the top performer this\r\nquarter?\"; not for \"filter to last 30 days\" - that needs Pattern 2.\r\n\r\n## Pattern 2: Stateful UI agent\r\n\r\nThe grid's imperative API is a clean tool surface. Each `SvGridApi`\r\nmethod becomes one function the model can call.\r\n\r\n```ts\r\nimport OpenAI from 'openai'\r\nimport { z } from 'zod'\r\n\r\nconst tools = [\r\n {\r\n type: 'function',\r\n function: {\r\n name: 'setFilter',\r\n description: 'Apply a column filter. Use to narrow the visible rows.',\r\n parameters: {\r\n type: 'object', required: ['columnId', 'operator', 'value'],\r\n properties: {\r\n columnId: { type: 'string' },\r\n operator: { type: 'string', enum: ['contains', 'equals', 'startsWith', 'greaterThan', 'lessThan', 'between'] },\r\n value: { type: 'string' },\r\n valueTo: { type: 'string', description: 'Upper bound for \"between\" only.' },\r\n },\r\n },\r\n },\r\n },\r\n {\r\n type: 'function',\r\n function: {\r\n name: 'setSort',\r\n parameters: { type: 'object', required: ['columnId', 'direction'],\r\n properties: { columnId: { type: 'string' }, direction: { type: 'string', enum: ['asc', 'desc'] } } },\r\n },\r\n },\r\n {\r\n type: 'function',\r\n function: {\r\n name: 'setGroupBy',\r\n parameters: { type: 'object', required: ['columnIds'],\r\n properties: { columnIds: { type: 'array', items: { type: 'string' } } } },\r\n },\r\n },\r\n {\r\n type: 'function',\r\n function: { name: 'clearAllFilters', parameters: { type: 'object' } },\r\n },\r\n]\r\n```\r\n\r\nWire the tool calls back to your live `api` reference:\r\n\r\n```ts\r\n// Widen past the two seed messages: tool results carry a `tool_call_id`, and\r\n// an assistant turn carries `tool_calls`.\r\ntype ChatMessage = { role: string; content?: unknown; tool_call_id?: string; [key: string]: unknown }\r\n\r\nasync function runAgent(prompt: string) {\r\n const messages: ChatMessage[] = [\r\n { role: 'system', content: `You drive a data grid. Columns: ${JSON.stringify(api!.getColumns())}.` },\r\n { role: 'user', content: prompt },\r\n ]\r\n for (let turn = 0; turn < 6; turn += 1) { // safety bound\r\n const r = await client.chat.completions.create({\r\n model: 'claude-sonnet-4-6',\r\n messages, tools, tool_choice: 'auto',\r\n })\r\n const msg = r.choices[0]!.message\r\n messages.push(msg)\r\n if (!msg.tool_calls?.length) return msg.content\r\n for (const call of msg.tool_calls) {\r\n const args = JSON.parse(call.function.arguments)\r\n switch (call.function.name) {\r\n case 'setFilter': api!.setFilter(args.columnId, args); break\r\n case 'setSort': api!.setSort(args.columnId, args.direction); break\r\n case 'setGroupBy': api!.setGroupBy(args.columnIds); break\r\n case 'clearAllFilters': api!.clearAllFilters(); break\r\n }\r\n messages.push({ role: 'tool', tool_call_id: call.id, content: 'ok' })\r\n }\r\n }\r\n}\r\n```\r\n\r\nUser types *\"show me last quarter's deals over $50k, grouped by\r\nregion\"* and the agent calls `setFilter('sellDate', { ... })` +\r\n`setFilter('amount', { operator: 'greaterThan', value: '50000' })` +\r\n`setGroupBy(['region'])` in a single turn.\r\n\r\n**The whole `SvGridApi` is on the menu.** Wrap as many or as few\r\nmethods as you want; the model only calls what you expose.\r\n\r\n## Pattern 3: Autonomous workflow agent\r\n\r\nThe grid becomes one node in a longer chain. Typical shape:\r\n\r\n```ts\r\nconst agent = new Agent({\r\n tools: [\r\n fetchCsvFromS3, // pull raw data\r\n aiSmartPaste, // parse to typed rows (uses /api/ai endpoint)\r\n runValidations, // your domain validations\r\n pushToGrid, // api.addRows(...)\r\n waitForUserApproval, // pauses for human-in-the-loop\r\n exportToBigQuery, // api.exportData({ format: 'csv', ... }) + push\r\n ],\r\n})\r\nawait agent.run('Process today\\'s sales batch from s3://acme/sales/2026-06-06.csv')\r\n```\r\n\r\nThe grid is **the visible state** the human can audit between steps -\r\nwhich is exactly what makes a workflow agent trustworthy: every\r\nintermediate result lands in a sortable, filterable table the user\r\ncan inspect.\r\n\r\n## Sandboxing rules\r\n\r\nWhen an LLM is calling grid methods, three boundaries keep things sane:\r\n\r\n1. **Whitelist tools at the top level.** Never expose `eval` or\r\n arbitrary JS. The `SvGridApi` methods above are the only surface\r\n the model needs.\r\n2. **Validate every tool argument** before invoking. The JSON Schemas\r\n at [`/schemas/`](./mcp-server.md) cover every input shape; use\r\n `ajv` or `zod` to check.\r\n3. **Bound the agent loop.** A maximum-turns counter (6 is plenty\r\n for grid manipulation) prevents runaway calls. Combine with a\r\n per-turn token budget.\r\n\r\n## Common workflows shipped as MCP prompts\r\n\r\nThe [MCP server](./mcp-server.md) ships three pre-built prompts that\r\nimplement the above patterns:\r\n\r\n- **`/svgrid:nl-to-grid-state`** - Pattern 2 with the tool set wired up\r\n- **`/svgrid:csv-to-typed-rows`** - Smart-paste an arbitrary CSV into\r\n a typed row array with confidence per row\r\n- **`/svgrid:summarise-view`** - Pattern 1 grounded in `api.getDisplayedRows()`\r\n\r\n## Worked example: NL → Pivot\r\n\r\nLive in [demo 75 (AI Smart Paste)](https://svgrid.com/demos/75-ai-smart-paste/)\r\nand [demo 52 (Pivot designer)](https://svgrid.com/demos/52-pivot-table/)\r\n- both ship in the gallery.\r\n\r\n## Failure modes\r\n\r\n| Symptom | Cause | Fix |\r\n| ------------------------------------------ | ----------------------------------------------------------- | ------------------------------------------------------------------------- |\r\n| Model invents columns that don't exist | No grounding on the live column set | Pass `api.getColumns()` in the system prompt every turn |\r\n| Model calls `setFilter('Status', ...)` with the wrong case | Column ids are case-sensitive | Include the column ids in the system prompt (snake_case vs PascalCase) |\r\n| Multi-step chain forgets the row count drops | Each tool call doesn't return the new visible row count | Return `api.getDisplayedRows().length` from each handler |\r\n| Agent loops forever | No max-turns bound | Always cap the loop (5-10 turns is plenty for grid manipulation) |\r\n\r\n## See also\r\n\r\n- [LLM grounding](./llm-grounding.md) - the static doc files agents read\r\n- [MCP server](./mcp-server.md) - turnkey integration for Claude Desktop / Cursor / Zed\r\n- [AI assistant](./ai.md) - the in-grid NL features, free in @svgrid/grid (not the agent layer)\r\n- [Architecture](./architecture.md) - what state lives where (agents need to know)\r\n\r\n## Frequently asked questions\r\n\r\n### Can an AI agent control the SvGrid data grid?\r\n\r\nYes. The imperative `SvGridApi` (filter, sort, select, set values, expand, page)\r\nis exactly the surface an agent drives. This page covers three patterns, from a\r\nread-only summary agent to a full read-write agent that mutates grid state.\r\n\r\n### What is the safest way to let an LLM drive the grid?\r\n\r\nStart read-only: let the model describe and query the grid before it writes.\r\nWhen you grant write access, route it through the same `SvGridApi` calls a user\r\naction would trigger, so validation and dirty-tracking still apply.\r\n\r\n### Do I need the MCP server to build a grid agent?\r\n\r\nNo. The MCP server is a turnkey integration for desktop AI clients; for a custom\r\nin-app agent you call `SvGridApi` directly. Both are documented here and in the\r\nMCP server guide.\r\n"
|
|
3030
3030
|
},
|
|
3031
3031
|
{
|
|
3032
3032
|
"slug": "help/ai-smart-paste",
|
|
@@ -3038,7 +3038,7 @@ export const docs = [
|
|
|
3038
3038
|
"slug": "help/ai-toolkit",
|
|
3039
3039
|
"path": "docs/help/ai-toolkit.md",
|
|
3040
3040
|
"title": "AI Toolkit",
|
|
3041
|
-
"markdown": "# AI Toolkit\r\n\r\nEverything SvGrid ships for building with language models, in one place.\r\nThe toolkit spans two axes: **AI inside your running app** (helpers your\r\nusers invoke - natural-language filter, smart fill, summarise, classify)\r\nand **AI inside your editor** (the MCP server + grounding files that make\r\nClaude, Cursor, and friends write correct SvGrid code).\r\n\r\nNothing here bundles a model. SvGrid is **model-agnostic and\r\nbring-your-own-key**: you register one adapter and keep full control of\r\nmodel choice, routing, and what data leaves the browser.\r\n\r\n<div data-docs-demo=\"51-ai-assistant\" data-height=\"560\"></div>\r\n\r\n## The two surfaces\r\n\r\n| | AI in your app (runtime) | AI in your editor (build time) |\r\n| --- | --- | --- |\r\n| **Who invokes it** | your end users | you and your coding agent |\r\n| **What it does** | filter / fill / summarise / classify / export the live grid | scaffold columns, generate CRUD screens, answer API questions |\r\n| **Package** | `@svgrid/enterprise` (`api.ai.*`) | `@sv-grid/mcp-server`, `@svgrid/mcp`, grounding files |\r\n| **Needs a model key** | yes - the one you register | no - your agent brings its own |\r\n| **Deep dive** | [AI assistant](./ai.md) | [MCP server](./mcp-server.md) · [LLM grounding](./llm-grounding.md) |\r\n\r\nMost teams use both: the MCP server to write the grid, the in-grid\r\nhelpers to power features inside it.\r\n\r\n## How it works\r\n\r\nThe grid never calls a model directly. Every runtime AI call routes\r\nthrough a single async **provider** you register once at app boot:\r\n\r\n```ts\r\nimport { setAIProvider, type AIProvider } from '@svgrid/enterprise'\r\n\r\nconst provider: AIProvider = async ({ prompt, responseFormat, signal, task }) => {\r\n const r = await fetch('/api/ai', {\r\n method: 'POST',\r\n headers: { 'content-type': 'application/json' },\r\n body: JSON.stringify({ prompt, responseFormat, task }),\r\n signal,\r\n })\r\n if (!r.ok) throw new Error(`AI provider returned ${r.status}`)\r\n return r.text()\r\n}\r\n\r\nsetAIProvider(provider)\r\n```\r\n\r\nThree design choices keep bad model output from becoming a silent wrong answer:\r\n\r\n- **Structured JSON, validated.** Helpers request `responseFormat: 'json'`\r\n and the grid `JSON.parse`s the reply. It strips a single markdown code\r\n fence automatically, so a model that wraps output in ` ```json ... ``` `\r\n still parses. On malformed output you get a typed error, not a silent\r\n wrong result.\r\n- **The prompt is grounded in your columns.** Before each call the grid\r\n embeds the live column schema (names, types, sampled values) into the\r\n prompt, so the model picks real field names instead of inventing them.\r\n- **A hallucination guard on the way back.** If a model still returns a\r\n column that does not exist, the clause is dropped rather than passed to\r\n `setFilter` - you lose a clause, never crash the page.\r\n\r\nThe provider shape is deliberately tiny (one async call, `text` or `json`\r\nresponse) so the same adapter drives an OpenAI `chat.completions` call, an\r\nAnthropic `messages` call, a self-hosted endpoint, or a server-side proxy.\r\nNo model client is ever bundled into your grid.\r\n\r\n> **Just evaluating?** The package ships a deterministic `mockAIProvider`\r\n> that returns plausible canned shapes per task. Wire it in with\r\n> `setAIProvider(mockAIProvider)` and every helper works end to end with\r\n> no key. The demo above runs on it.\r\n\r\n## In-grid helpers\r\n\r\n`installEnterprise(api)` - the same call you use for export and print -\r\naugments your `SvGridApi` with an `ai` namespace. Six helpers, all\r\nmodel-agnostic:\r\n\r\n```ts\r\napi.ai.filter(query, opts?) // NL sentence -> filter + sort plan\r\napi.ai.smartFill(opts) // 1-2 examples -> proposed column values\r\napi.ai.summarize(opts) // row/selection/group/all -> text + bullets\r\napi.ai.classify(opts) // free-text cells -> a clean enum label\r\napi.ai.export(query, opts?) // NL sentence -> filter + group + format, then export\r\napi.ai.findAnomalies(opts?) // scan a slice -> outliers + severity\r\n```\r\n\r\n### Natural-language filter\r\n\r\nThe highest-leverage feature: replace a dozen per-column filter operators\r\nwith one search box.\r\n\r\n```ts\r\nconst plan = await api.ai.filter('accounts losing momentum in EMEA, by NPS')\r\n// {\r\n// filters: [\r\n// { field: 'region', operator: 'equals', value: 'EMEA' },\r\n// { field: 'nps', operator: 'lessThan', value: '30' },\r\n// ],\r\n// sort: [{ field: 'nps', desc: false }],\r\n// rationale: 'EMEA region, low NPS, sorted ascending.',\r\n// }\r\n```\r\n\r\nBy default it **returns the plan without applying it**, so you can show a\r\n\"here is what I would do, accept?\" preview and surface the `rationale`.\r\nPass `{ apply: true }` to commit straight to the grid.\r\n\r\n### Smart fill\r\n\r\nThe killer feature for spreadsheet-style entry: type one or two examples\r\nin a column, let the model propose the rest.\r\n\r\n```ts\r\nconst result = await api.ai.smartFill({\r\n field: 'tier',\r\n examples: [\r\n { input: { company: 'Northwind' }, output: 'enterprise' },\r\n { input: { company: 'Helios' }, output: 'growth' },\r\n ],\r\n})\r\n// result.predictions: [{ rowIndex, value, confidence }, ...]\r\n```\r\n\r\nYou choose what to do with the predictions - accept-all, accept-per-cell\r\nwith a confidence pill, or write them onto the row for review.\r\n\r\n### Summarise, classify, export, anomalies\r\n\r\n- **`summarize`** drops a slice (row / selection / group / all) into the\r\n model and returns a paragraph, bullets, and the fields the story leans\r\n on. Large slices are sampled uniformly to stay under a token budget.\r\n- **`classify`** buckets free-text cells into a known set of labels, and\r\n filters out any prediction not in your `classes` list so the output is a\r\n clean enum.\r\n- **`export`** turns \"export EU orders from Q2 as a grouped PDF by\r\n country\" into a `{ format, filters, sort, groupBy }` plan and hands it to\r\n the exporter - self-contained, so the download is correct regardless of\r\n the grid's current view.\r\n- **`findAnomalies`** scans a slice for outliers and inconsistent values,\r\n each tagged `low | medium | high`. Pairs naturally with export: find the\r\n odd rows, then export just those.\r\n\r\nFull API, response shapes, and the license gate are on the\r\n[AI assistant](./ai.md) page.\r\n\r\n## Build an agent that drives the grid\r\n\r\nThe imperative `SvGridApi` is a clean tool surface - each method becomes\r\none function a model can call. Three patterns, in order of how much agency\r\nyou hand over:\r\n\r\n1. **Read-only summary agent** - the model describes the current view\r\n (`api.getDisplayedRows()`), no tool calling.\r\n2. **Stateful UI agent** - the model calls `setFilter` / `setSort` /\r\n `setGroupBy` in response to natural language, bounded by a max-turns loop.\r\n3. **Autonomous workflow agent** - the grid is one node in a longer chain\r\n (import -> enrich -> human approval -> export), and the visible table is\r\n the state a human can audit between steps.\r\n\r\n```ts\r\n// Pattern 2, sketched: each SvGridApi method is one tool the model can call.\r\nswitch (call.function.name) {\r\n case 'setFilter': api.setFilter(args.columnId, args); break\r\n case 'setSort': api.setSort(args.columnId, args.direction); break\r\n case 'setGroupBy': api.setGroupBy(args.columnIds); break\r\n case 'clearAllFilters': api.clearAllFilters(); break\r\n}\r\n```\r\n\r\nFull worked code, the sandboxing rules (whitelist tools, validate every\r\nargument against the shipped JSON Schemas, bound the loop), and the common\r\nfailure modes are on the [Agents](./agents.md) page.\r\n\r\n## MCP server: let your coding agent write the grid\r\n\r\nThe [MCP server](./mcp-server.md) exposes SvGrid to AI clients (Claude\r\nDesktop, Cursor, Zed, Continue, custom agents) over the Model Context\r\nProtocol. It grounds the model in the schemas the library actually ships,\r\nso your assistant retrieves version-pinned facts instead of hallucinating\r\nan API from its training cutoff. No API key, all local.\r\n\r\n```json\r\n{\r\n \"mcpServers\": {\r\n \"sv-grid\": { \"command\": \"npx\", \"args\": [\"-y\", \"@sv-grid/mcp-server\"] }\r\n }\r\n}\r\n```\r\n\r\nIt registers callable tools - `searchDocs`, `getDocPage`, `scaffoldColumns`\r\n(sample row -> `ColumnDef[]`), `validateColumns`, `previewExport`,\r\n`listDemos` - plus read-only resources (the docs manifest and JSON Schemas)\r\nand pre-built prompts (`/svgrid:scaffold-grid`, `/svgrid:refactor-to-pivot`,\r\n`/svgrid:wire-server-side`).\r\n\r\nFor **Studio** (turning a database or schema into a CRUD data-app), the\r\nseparate [`@svgrid/mcp`](../enterprise/studio/ai-generation.md) server adds\r\n`introspect_source` and `scaffold_entity`. The generated screen is run\r\nthrough the Svelte compiler before it comes back, and each file carries\r\n`svgrid:managed` markers so a re-generation updates the managed regions and\r\nleaves your hand-written code untouched.\r\n\r\n## Ground any model, no MCP required\r\n\r\nIf you are not on an MCP client, four static artefacts ship with the docs\r\nso any model can ground itself in current facts:\r\n\r\n| File | Use for |\r\n| --- | --- |\r\n| [`/llms.txt`](/llms.txt) | First-pass context: the topic map with one-line summaries |\r\n| [`/llms-full.txt`](/llms-full.txt) | Deep grounding: every doc page concatenated |\r\n| [`/docs.json`](/docs.json) | Programmatic crawling: section tree + per-page metadata |\r\n| [`/schemas/index.json`](/schemas/index.json) | Validation: machine-checkable `ColumnDef`, `<SvGrid>` props, export options |\r\n\r\nUpload `llms-full.txt` into a custom GPT or Claude project, drop a rules\r\nblock into `.cursorrules`, or fetch the topic map into your own agent's\r\nsystem prompt at boot. All four are regenerated on every commit and served\r\nfrom the docs origin. Full recipes are on the\r\n[LLM grounding](./llm-grounding.md) page.\r\n\r\n## Best practices\r\n\r\n**Prompting the in-grid helpers.** These are handled for you - the grid\r\nalready embeds the column schema and samples rows before each call - but\r\nif you customise the prompt on your provider side:\r\n\r\n- Keep the live column set in front of the model every turn; it is the\r\n single biggest defence against invented field names.\r\n- Include a few sample rows so the model learns value shapes (region codes,\r\n date formats, enum spellings).\r\n- State the column ids exactly, including case - ids are case-sensitive\r\n (`snake_case` vs `PascalCase` matters).\r\n\r\n**Previewing before committing.** `filter` and `export` default to\r\nreturning a plan without touching the grid. Show the `rationale`, let the\r\nuser confirm, then apply. This is the pattern that makes NL features feel\r\ntrustworthy rather than magic-that-sometimes-breaks.\r\n\r\n**Cost routing.** The `task` tag (`filter | smart-fill | summarize |\r\nclassify`) and the `maxOutputTokens` hint let you route a cheap model for\r\nfilters and a stronger one for summaries from inside your one adapter.\r\n\r\n**Data handling.** The grid makes no network calls of its own - the AI\r\nhelpers send exactly the prompt you construct to the adapter you configure.\r\nRoute through your own `/api/ai` proxy if you need to redact, log, or keep\r\ndata within a boundary before it reaches a provider.\r\n\r\n## Examples\r\n\r\n- **[Demo 51 - AI assistant](../../examples/src/demos/51-ai-assistant.svelte)** -\r\n all six helpers wired to the mock provider, per-cell accept with\r\n confidence pills.\r\n- **[AI Smart Paste](./ai-smart-paste.md)** - parse vCard / Markdown /\r\n signature blocks / CSV into typed rows, with email-typo correction and\r\n phone normalisation.\r\n\r\n## API reference\r\n\r\n| Symbol | Package | What it is |\r\n| --- | --- | --- |\r\n| `setAIProvider(p)` | `@svgrid/enterprise` | Register the model adapter every AI call routes through. `null` clears it. |\r\n| `mockAIProvider` | `@svgrid/enterprise` | Deterministic canned provider for demos and tests. |\r\n| `type AIProvider` | `@svgrid/enterprise` | `(req: AIRequest) => Promise<string>` - the one function you implement. |\r\n| `api.ai.filter` / `smartFill` / `summarize` / `classify` / `export` / `findAnomalies` | `@svgrid/enterprise` | The in-grid helpers, added by `installEnterprise(api)`. |\r\n| `scaffoldColumns`, `validateColumns`, `previewExport`, ... | `@sv-grid/mcp-server` | Build-time MCP tools your coding agent calls. |\r\n| `introspect_source`, `scaffold_entity` | `@svgrid/mcp` | Studio generation tools (schema -> CRUD screen). |\r\n\r\nAuto-generated per-symbol reference: [`@svgrid/enterprise` · `ai.ts`](../reference/auto/svgrid-enterprise-ai.md).\r\n\r\n## See also\r\n\r\n- [AI assistant](./ai.md) - the in-grid helpers in full, with response shapes\r\n- [Agents](./agents.md) - build an agent that drives the live grid\r\n- [Agent Skill](./skill.md) - always-on, project-aware context and house style for coding assistants\r\n- [MCP server](./mcp-server.md) - turnkey integration for Claude Desktop / Cursor / Zed\r\n- [LLM grounding](./llm-grounding.md) - the static files any model reads\r\n- [AI generation - Studio](../enterprise/studio/ai-generation.md) - scaffold CRUD data-apps from a schema\r\n\r\n## Frequently asked questions\r\n\r\n### What AI features does SvGrid have?\r\n\r\nTwo kinds. At runtime, `@svgrid/grid` ships six model-agnostic helpers\r\nfree - natural-language filter, smart fill, summarise, classify, export,\r\nand anomaly detection. At build time, an MCP\r\nserver plus grounding files let your coding agent write correct SvGrid code\r\nand scaffold CRUD screens.\r\n\r\n### Which model does SvGrid use?\r\n\r\nNone by default - it is bring-your-own. You register one adapter for\r\nOpenAI, Anthropic Claude, a local model, or a server proxy, and the grid\r\nroutes every AI call through it. A deterministic mock provider ships so you\r\ncan evaluate the whole flow without a key.\r\n\r\n### Is my grid data sent to a model provider?\r\n\r\nOnly if you wire one up and invoke a helper. SvGrid itself makes no network\r\ncalls; the AI helpers send exactly the prompt you construct to the adapter\r\nyou configure, so you decide what leaves the browser and can proxy it\r\nthrough your own backend first.\r\n\r\n### Do I need the MCP server to use the AI features?\r\n\r\nNo. The in-grid helpers and the grounding files work without it. The MCP\r\nserver is the turnkey path for desktop AI clients; for a custom in-app\r\nagent you call `SvGridApi` directly.\r\n"
|
|
3041
|
+
"markdown": "# AI Toolkit\n\nEverything SvGrid ships for building with language models, in one place.\nThe toolkit spans two axes: **AI inside your running app** (helpers your\nusers invoke - natural-language filter, smart fill, summarise, classify)\nand **AI inside your editor** (the MCP server + grounding files that make\nClaude, Cursor, and friends write correct SvGrid code).\n\nNothing here bundles a model. SvGrid is **model-agnostic and\nbring-your-own-key**: you register one adapter and keep full control of\nmodel choice, routing, and what data leaves the browser.\n\n<div data-docs-demo=\"51-ai-assistant\" data-height=\"560\"></div>\n\n## The two surfaces\n\n| | AI in your app (runtime) | AI in your editor (build time) |\n| --- | --- | --- |\n| **Who invokes it** | your end users | you and your coding agent |\n| **What it does** | filter / fill / summarise / classify / export the live grid | scaffold columns, generate CRUD screens, answer API questions |\n| **Package** | `@svgrid/enterprise` (`api.ai.*`) | `@sv-grid/mcp-server`, `@svgrid/mcp`, grounding files |\n| **Needs a model key** | yes - the one you register | no - your agent brings its own |\n| **Deep dive** | [AI assistant](./ai.md) | [MCP server](./mcp-server.md) · [LLM grounding](./llm-grounding.md) |\n\nMost teams use both: the MCP server to write the grid, the in-grid\nhelpers to power features inside it.\n\n## How it works\n\nThe grid never calls a model directly. Every runtime AI call routes\nthrough a single async **provider** you register once at app boot:\n\n```ts\nimport { setAIProvider, type AIProvider } from '@svgrid/grid'\n\nconst provider: AIProvider = async ({ prompt, responseFormat, signal, task }) => {\n const r = await fetch('/api/ai', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ prompt, responseFormat, task }),\n signal,\n })\n if (!r.ok) throw new Error(`AI provider returned ${r.status}`)\n return r.text()\n}\n\nsetAIProvider(provider)\n```\n\nThree design choices keep bad model output from becoming a silent wrong answer:\n\n- **Structured JSON, validated.** Helpers request `responseFormat: 'json'`\n and the grid `JSON.parse`s the reply. It strips a single markdown code\n fence automatically, so a model that wraps output in ` ```json ... ``` `\n still parses. On malformed output you get a typed error, not a silent\n wrong result.\n- **The prompt is grounded in your columns.** Before each call the grid\n embeds the live column schema (names, types, sampled values) into the\n prompt, so the model picks real field names instead of inventing them.\n- **A hallucination guard on the way back.** If a model still returns a\n column that does not exist, the clause is dropped rather than passed to\n `setFilter` - you lose a clause, never crash the page.\n\nThe provider shape is deliberately tiny (one async call, `text` or `json`\nresponse) so the same adapter drives an OpenAI `chat.completions` call, an\nAnthropic `messages` call, a self-hosted endpoint, or a server-side proxy.\nNo model client is ever bundled into your grid.\n\n> **Just evaluating?** The package ships a deterministic `mockAIProvider`\n> that returns plausible canned shapes per task. Wire it in with\n> `setAIProvider(mockAIProvider)` and every helper works end to end with\n> no key. The demo above runs on it.\n\n## In-grid helpers\n\n`installEnterprise(api)` - the same call you use for export and print -\naugments your `SvGridApi` with an `ai` namespace. Six helpers, all\nmodel-agnostic:\n\n```ts\napi.ai.filter(query, opts?) // NL sentence -> filter + sort plan\napi.ai.smartFill(opts) // 1-2 examples -> proposed column values\napi.ai.summarize(opts) // row/selection/group/all -> text + bullets\napi.ai.classify(opts) // free-text cells -> a clean enum label\napi.ai.export(query, opts?) // NL sentence -> filter + group + format, then export\napi.ai.findAnomalies(opts?) // scan a slice -> outliers + severity\n```\n\n### Natural-language filter\n\nThe highest-leverage feature: replace a dozen per-column filter operators\nwith one search box.\n\n```ts\nconst plan = await api.ai.filter('accounts losing momentum in EMEA, by NPS')\n// {\n// filters: [\n// { field: 'region', operator: 'equals', value: 'EMEA' },\n// { field: 'nps', operator: 'lessThan', value: '30' },\n// ],\n// sort: [{ field: 'nps', desc: false }],\n// rationale: 'EMEA region, low NPS, sorted ascending.',\n// }\n```\n\nBy default it **returns the plan without applying it**, so you can show a\n\"here is what I would do, accept?\" preview and surface the `rationale`.\nPass `{ apply: true }` to commit straight to the grid.\n\n### Smart fill\n\nThe killer feature for spreadsheet-style entry: type one or two examples\nin a column, let the model propose the rest.\n\n```ts\nconst result = await api.ai.smartFill({\n field: 'tier',\n examples: [\n { input: { company: 'Northwind' }, output: 'enterprise' },\n { input: { company: 'Helios' }, output: 'growth' },\n ],\n})\n// result.predictions: [{ rowIndex, value, confidence }, ...]\n```\n\nYou choose what to do with the predictions - accept-all, accept-per-cell\nwith a confidence pill, or write them onto the row for review.\n\n### Summarise, classify, export, anomalies\n\n- **`summarize`** drops a slice (row / selection / group / all) into the\n model and returns a paragraph, bullets, and the fields the story leans\n on. Large slices are sampled uniformly to stay under a token budget.\n- **`classify`** buckets free-text cells into a known set of labels, and\n filters out any prediction not in your `classes` list so the output is a\n clean enum.\n- **`export`** turns \"export EU orders from Q2 as a grouped PDF by\n country\" into a `{ format, filters, sort, groupBy }` plan and hands it to\n the exporter - self-contained, so the download is correct regardless of\n the grid's current view.\n- **`findAnomalies`** scans a slice for outliers and inconsistent values,\n each tagged `low | medium | high`. Pairs naturally with export: find the\n odd rows, then export just those.\n\nFull API, response shapes, and the license gate are on the\n[AI assistant](./ai.md) page.\n\n## Build an agent that drives the grid\n\nThe imperative `SvGridApi` is a clean tool surface - each method becomes\none function a model can call. Three patterns, in order of how much agency\nyou hand over:\n\n1. **Read-only summary agent** - the model describes the current view\n (`api.getDisplayedRows()`), no tool calling.\n2. **Stateful UI agent** - the model calls `setFilter` / `setSort` /\n `setGroupBy` in response to natural language, bounded by a max-turns loop.\n3. **Autonomous workflow agent** - the grid is one node in a longer chain\n (import -> enrich -> human approval -> export), and the visible table is\n the state a human can audit between steps.\n\n```ts\n// Pattern 2, sketched: each SvGridApi method is one tool the model can call.\nswitch (call.function.name) {\n case 'setFilter': api.setFilter(args.columnId, args); break\n case 'setSort': api.setSort(args.columnId, args.direction); break\n case 'setGroupBy': api.setGroupBy(args.columnIds); break\n case 'clearAllFilters': api.clearAllFilters(); break\n}\n```\n\nFull worked code, the sandboxing rules (whitelist tools, validate every\nargument against the shipped JSON Schemas, bound the loop), and the common\nfailure modes are on the [Agents](./agents.md) page.\n\n## MCP server: let your coding agent write the grid\n\nThe [MCP server](./mcp-server.md) exposes SvGrid to AI clients (Claude\nDesktop, Cursor, Zed, Continue, custom agents) over the Model Context\nProtocol. It grounds the model in the schemas the library actually ships,\nso your assistant retrieves version-pinned facts instead of hallucinating\nan API from its training cutoff. No API key, all local.\n\n```json\n{\n \"mcpServers\": {\n \"sv-grid\": { \"command\": \"npx\", \"args\": [\"-y\", \"@sv-grid/mcp-server\"] }\n }\n}\n```\n\nIt registers callable tools - `searchDocs`, `getDocPage`, `scaffoldColumns`\n(sample row -> `ColumnDef[]`), `validateColumns`, `previewExport`,\n`listDemos` - plus read-only resources (the docs manifest and JSON Schemas)\nand pre-built prompts (`/svgrid:scaffold-grid`, `/svgrid:refactor-to-pivot`,\n`/svgrid:wire-server-side`).\n\nFor **Studio** (turning a database or schema into a CRUD data-app), the\nseparate [`@svgrid/mcp`](../enterprise/studio/ai-generation.md) server adds\n`introspect_source` and `scaffold_entity`. The generated screen is run\nthrough the Svelte compiler before it comes back, and each file carries\n`svgrid:managed` markers so a re-generation updates the managed regions and\nleaves your hand-written code untouched.\n\n## Ground any model, no MCP required\n\nIf you are not on an MCP client, four static artefacts ship with the docs\nso any model can ground itself in current facts:\n\n| File | Use for |\n| --- | --- |\n| [`/llms.txt`](/llms.txt) | First-pass context: the topic map with one-line summaries |\n| [`/llms-full.txt`](/llms-full.txt) | Deep grounding: every doc page concatenated |\n| [`/docs.json`](/docs.json) | Programmatic crawling: section tree + per-page metadata |\n| [`/schemas/index.json`](/schemas/index.json) | Validation: machine-checkable `ColumnDef`, `<SvGrid>` props, export options |\n\nUpload `llms-full.txt` into a custom GPT or Claude project, drop a rules\nblock into `.cursorrules`, or fetch the topic map into your own agent's\nsystem prompt at boot. All four are regenerated on every commit and served\nfrom the docs origin. Full recipes are on the\n[LLM grounding](./llm-grounding.md) page.\n\n## Best practices\n\n**Prompting the in-grid helpers.** These are handled for you - the grid\nalready embeds the column schema and samples rows before each call - but\nif you customise the prompt on your provider side:\n\n- Keep the live column set in front of the model every turn; it is the\n single biggest defence against invented field names.\n- Include a few sample rows so the model learns value shapes (region codes,\n date formats, enum spellings).\n- State the column ids exactly, including case - ids are case-sensitive\n (`snake_case` vs `PascalCase` matters).\n\n**Previewing before committing.** `filter` and `export` default to\nreturning a plan without touching the grid. Show the `rationale`, let the\nuser confirm, then apply. This is the pattern that makes NL features feel\ntrustworthy rather than magic-that-sometimes-breaks.\n\n**Cost routing.** The `task` tag (`filter | smart-fill | summarize |\nclassify`) and the `maxOutputTokens` hint let you route a cheap model for\nfilters and a stronger one for summaries from inside your one adapter.\n\n**Data handling.** The grid makes no network calls of its own - the AI\nhelpers send exactly the prompt you construct to the adapter you configure.\nRoute through your own `/api/ai` proxy if you need to redact, log, or keep\ndata within a boundary before it reaches a provider.\n\n## Examples\n\n- **[Demo 51 - AI assistant](../../examples/src/demos/51-ai-assistant.svelte)** -\n all six helpers wired to the mock provider, per-cell accept with\n confidence pills.\n- **[AI Smart Paste](./ai-smart-paste.md)** - parse vCard / Markdown /\n signature blocks / CSV into typed rows, with email-typo correction and\n phone normalisation.\n\n## API reference\n\n| Symbol | Package | What it is |\n| --- | --- | --- |\n| `setAIProvider(p)` | `@svgrid/enterprise` | Register the model adapter every AI call routes through. `null` clears it. |\n| `mockAIProvider` | `@svgrid/enterprise` | Deterministic canned provider for demos and tests. |\n| `type AIProvider` | `@svgrid/enterprise` | `(req: AIRequest) => Promise<string>` - the one function you implement. |\n| `api.ai.filter` / `smartFill` / `summarize` / `classify` / `export` / `findAnomalies` | `@svgrid/enterprise` | The in-grid helpers, added by `installEnterprise(api)`. |\n| `scaffoldColumns`, `validateColumns`, `previewExport`, ... | `@sv-grid/mcp-server` | Build-time MCP tools your coding agent calls. |\n| `introspect_source`, `scaffold_entity` | `@svgrid/mcp` | Studio generation tools (schema -> CRUD screen). |\n\nAuto-generated per-symbol reference: [`@svgrid/enterprise` · `ai.ts`](../reference/auto/svgrid-enterprise-ai.md).\n\n## See also\n\n- [AI assistant](./ai.md) - the in-grid helpers in full, with response shapes\n- [Agents](./agents.md) - build an agent that drives the live grid\n- [Agent Skill](./skill.md) - always-on, project-aware context and house style for coding assistants\n- [MCP server](./mcp-server.md) - turnkey integration for Claude Desktop / Cursor / Zed\n- [LLM grounding](./llm-grounding.md) - the static files any model reads\n- [AI generation - Studio](../enterprise/studio/ai-generation.md) - scaffold CRUD data-apps from a schema\n\n## Frequently asked questions\n\n### What AI features does SvGrid have?\n\nTwo kinds. At runtime, `@svgrid/grid` ships six model-agnostic helpers\nfree - natural-language filter, smart fill, summarise, classify, export,\nand anomaly detection. At build time, an MCP\nserver plus grounding files let your coding agent write correct SvGrid code\nand scaffold CRUD screens.\n\n### Which model does SvGrid use?\n\nNone by default - it is bring-your-own. You register one adapter for\nOpenAI, Anthropic Claude, a local model, or a server proxy, and the grid\nroutes every AI call through it. A deterministic mock provider ships so you\ncan evaluate the whole flow without a key.\n\n### Is my grid data sent to a model provider?\n\nOnly if you wire one up and invoke a helper. SvGrid itself makes no network\ncalls; the AI helpers send exactly the prompt you construct to the adapter\nyou configure, so you decide what leaves the browser and can proxy it\nthrough your own backend first.\n\n### Do I need the MCP server to use the AI features?\n\nNo. The in-grid helpers and the grounding files work without it. The MCP\nserver is the turnkey path for desktop AI clients; for a custom in-app\nagent you call `SvGridApi` directly.\n"
|
|
3042
3042
|
},
|
|
3043
3043
|
{
|
|
3044
3044
|
"slug": "help/ai",
|
|
@@ -3050,7 +3050,7 @@ export const docs = [
|
|
|
3050
3050
|
"slug": "help/alerts",
|
|
3051
3051
|
"path": "docs/help/alerts.md",
|
|
3052
3052
|
"title": "Alerts - Enterprise",
|
|
3053
|
-
"markdown": "# Alerts - Enterprise\n\nAlert rules let your users say \"tell me when the data does X\" - and then act on\nit automatically: raise a toast, tint the row, flash the cell, block the edit,\nor just log it. Rules are authored at runtime in a no-code builder, persisted,\nand shareable as JSON. No redeploy to add an alert.\n\nAlerts ship in the paid `@svgrid/enterprise` package. They build on the grid's\nown engines: predicates reuse the same operators as the filter row\n(`applyExcelFilter`), styling paints through the conditional-format pipeline,\nand notifications go through the grid's toast store.\n\n<div data-docs-demo=\"399-alert-rules-engine\" data-height=\"440\"></div>\n\n## Setup\n\nMount `<SvGridAlerts>` next to your grid and spread its `formats` output into the\ngrid's `conditionalFormats`. The overlay watches your data reactively, runs the\nrule engine on every change, and paints matches back through the grid. It diffs\nsnapshots of the data rather than listening to a single edit event, so it reacts\nto streaming feeds and programmatic updates as well as in-grid edits (the grid's\nown `onCellValueChange` callback covers only the latter).\n\nEvaluation never runs inside the grid's render frame: a data change schedules one\npass on the next animation frame (post-paint), so the grid always paints first.\nOn very large or fast-moving datasets, drive it in [push mode](#performance-and-large--live-datasets)\nso each pass costs only the rows that actually changed.\n\n```svelte\n<script lang=\"ts\">\n import { SvGrid, type ColumnDef } from '@svgrid/grid'\n import type { ConditionalFormat } from '@svgrid/grid/format'\n import { SvGridAlerts, enableAlerts, setLicenseKey, type ExprColumn } from '@svgrid/enterprise'\n\n setLicenseKey('YOUR-KEY')\n enableAlerts()\n\n let rows = $state.raw(data)\n let alertFormats = $state<ConditionalFormat<Row>[]>([])\n\n // Columns the rule/expression editors offer.\n const exprColumns: ExprColumn[] = [\n { id: 'price', name: 'Price', type: 'number' },\n { id: 'region', name: 'Region', type: 'text' },\n ]\n</script>\n\n<SvGridAlerts\n data={rows}\n columns={exprColumns}\n getRowId={(r) => r.id}\n storageKey=\"app:alerts\"\n bind:formats={alertFormats} />\n\n<SvGrid data={rows} {columns} getRowId={(r) => r.id}\n conditionalFormats={alertFormats} />\n```\n\n`<SvGridAlerts>` renders a small control group: a bell (opens the fired-alert\nlog) and an **Alerts** button (opens the rule manager). Set `controls={false}`\nto hide them and drive the panels yourself with `bind:panelOpen` /\n`bind:managerOpen`.\n\n## The rule model\n\nAn `AlertRule` has four moving parts:\n\n| Part | What it is |\n| --- | --- |\n| **predicate** | A boolean [expression](./expressions-query.md) - when it holds, the rule matches. |\n| **trigger** | *When* the rule is checked (see below). |\n| **scope** | `row`, `cell`, or `aggregate`. |\n| **actions** | What happens when it fires. |\n\n### Triggers\n\n- **`dataChange`** - fires on the true edge: when a row *newly* satisfies the\n predicate after an edit or feed update. It will not re-fire while the row keeps\n matching; it re-arms once the row stops matching.\n- **`relativeChange`** - fires when a value *moves*: an absolute delta, a percent\n change, or crossing a threshold. Uses the previous snapshot of the row.\n- **`validation`** - evaluated on edit; with a `preventEdit` action it can veto\n the change.\n- **`scheduled`** - re-checked on a cron schedule (reuses the enterprise\n scheduler), surfacing rows that currently match.\n\n### Actions\n\n| Kind | Effect |\n| --- | --- |\n| `toast` | A toast in the rule's severity colour. |\n| `highlight` | Tints matching rows/cells (persists while they match). |\n| `badge` | Colours the targeted cells. |\n| `cellFlash` | A brief flash on the cell that fired. |\n| `preventEdit` | Vetoes the edit (validation trigger). |\n| `log` | Records the firing in the alert log only. |\n\nEvery firing is recorded in the log regardless of action, so the bell badge and\nthe [alerts panel](#the-fired-alert-log) always reflect activity.\n\nMessages are templates: `{value}`, `{column}`, `{rule}`, `{severity}`, and\n`{field}` / `{row.field}` for any row field.\n\n```ts\nconst rule: AlertRule = {\n id: 'price-spike',\n name: 'Price over 700',\n enabled: true,\n severity: 'warning',\n scope: 'row',\n predicate: { kind: 'cmp', column: 'price', op: 'greaterThan', value: 700 },\n trigger: { type: 'dataChange' },\n actions: [\n { kind: 'toast', message: '{name} crossed 700 -> {value}' },\n { kind: 'highlight', style: { background: '#fef3c7', color: '#92400e' } },\n ],\n createdAt: Date.now(),\n}\n```\n\nSeed rules with the `rules` prop (used only when storage is empty), or let users\nbuild them in the manager.\n\n## Persistence and sharing\n\nPass `storageKey` to persist rules in `localStorage`; omit it for in-memory\nrules. The manager's **Export** / **Import** buttons round-trip the whole rule\nset as JSON, so a team lead can hand a set of alerts to colleagues. Under the\nhood this is the same pluggable-storage shape as\n[saved views](../recipes/saved-views.md):\n\n```ts\nimport { createAlertRules, localStorageAlertRules } from '@svgrid/enterprise'\n\nconst rules = createAlertRules(localStorageAlertRules('app:alerts'))\nrules.save(rule)\nconst json = rules.export() // share\nrules.import(json) // load\n```\n\n## Headless usage\n\nNot on Svelte, or want to run the engine yourself? `attachAlertEngine` observes\na data source and routes fired events to their side effects, returning the\nconditional formats + flash targets to apply:\n\n```ts\nimport { attachAlertEngine, localStorageAlertRules } from '@svgrid/enterprise'\n\nconst attach = attachAlertEngine({\n rules,\n getRowId: (r) => r.id,\n getData: () => currentRows,\n applyFormats: (formats) => setGridFormats(formats),\n})\n// later\nattach.detach()\n```\n\nThe pure `createAlertEngine` (no DOM, no timers) is exported too if you want full\ncontrol over evaluation.\n\n## Performance and large / live datasets\n\nAlerts are built to stay out of the grid's way. Two things make that true:\n\n- **Off-frame evaluation.** Every pass is deferred to a post-paint animation\n frame and coalesced, so a burst of updates costs one pass and the grid never\n waits on alerts to render.\n- **Change-scoped work.** Prev-value snapshots are only kept when a rule actually\n needs them (a `relativeChange` trigger). A rule set of pure `dataChange` /\n `aggregate` rules keeps none.\n\n### Watch mode (default)\n\nBy default the overlay reactively scans `data` when it changes, diffing the new\narray against the previous one to find the changed rows. Because Svelte replaces\nchanged rows immutably (unchanged rows keep their reference), the diff evaluates\nonly the rows that moved. This is the zero-config path and is fine for most grids.\n\n### Push mode (streaming / 100k+ rows)\n\nWhen your app already knows which rows changed - a streaming feed, a transaction,\na tick loop - hand that set straight to the overlay and skip the scan entirely.\nSet `watch={false}`, capture the handle with `onReady` (or `bind:this`), and call\n`pushChanged` with just the changed rows. Cost is then O(rows that changed),\nindependent of total row count.\n\n```svelte\n<script lang=\"ts\">\n let rows = $state.raw(data)\n let alerts: { pushChanged: (rows: readonly Row[]) => void } | null = null\n\n function onTick(changed: Row[]) {\n rows = applyChanges(rows, changed) // your immutable update\n alerts?.pushChanged(changed) // evaluate only these, next frame\n }\n</script>\n\n<SvGridAlerts\n data={rows}\n columns={exprColumns}\n getRowId={(r) => r.id}\n watch={false}\n onReady={(h) => (alerts = h)}\n bind:formats={alertFormats} />\n```\n\nThe handle also exposes `reseed(allRows)` (silently re-arm edges after a full data\nreset) and `flush()` (run any pending pass immediately). For big live feeds, prefer\n`dataChange` rules (fire once when a row crosses the line) over `relativeChange`\n(fires on every move), and keep actions toast-only to avoid conditional-format\nchurn.\n\nOn a busy feed many rows can cross a threshold every second. Evaluation stays cheap,\nbut a nonstop stream of toasts is disruptive and repaints constantly. Set\n`toastCooldownMs` to rate-limit toasts to at most one per rule per interval - every\nevent is still logged (the bell badge stays accurate), only the visible toast is\nthrottled:\n\n```svelte\n<SvGridAlerts ... watch={false} onReady={(h) => (alerts = h)} toastCooldownMs={6000} />\n```\n\n## The fired-alert log\n\nThe bell badge shows the unacknowledged count. Clicking it opens\n`SvAlertsPanel` - a drawer listing fired alerts newest-first, filterable by\nseverity, with **Acknowledge**, **Clear**, and **Go to row** (wire `onJump` to\nscroll/select the row).\n\n## See also\n\n- [Expression query language](./expressions-query.md) - the predicate language rules are built on.\n- [Highlighting changes](./cells/highlighting-changes.md) - the `cellFlash` primitive alerts build on.\n- [Conditional formatting](./cells/conditional-formatting.md) - the styling pipeline alerts paint through.\n"
|
|
3053
|
+
"markdown": "# Alerts - Enterprise\n\nAlert rules let your users say \"tell me when the data does X\" - and then act on\nit automatically: raise a toast, tint the row, flash the cell, block the edit,\nor just log it. Rules are authored at runtime in a no-code builder, persisted,\nand shareable as JSON. No redeploy to add an alert.\n\nAlerts ship in the paid `@svgrid/enterprise` package. They build on the grid's\nown engines: predicates reuse the same operators as the filter row\n(`applyExcelFilter`), styling paints through the conditional-format pipeline,\nand notifications go through the grid's toast store.\n\n<div data-docs-demo=\"399-alert-rules-engine\" data-height=\"440\"></div>\n\n## Setup\n\nMount `<SvGridAlerts>` next to your grid and spread its `formats` output into the\ngrid's `conditionalFormats`. The overlay watches your data reactively, runs the\nrule engine on every change, and paints matches back through the grid. It diffs\nsnapshots of the data rather than listening to a single edit event, so it reacts\nto streaming feeds and programmatic updates as well as in-grid edits (the grid's\nown `onCellValueChange` callback covers only the latter).\n\nEvaluation never runs inside the grid's render frame: a data change schedules one\npass on the next animation frame (post-paint), so the grid always paints first.\nOn very large or fast-moving datasets, drive it in [push mode](#performance-and-large--live-datasets)\nso each pass costs only the rows that actually changed.\n\n```svelte\n<script lang=\"ts\">\n import { SvGrid, type ColumnDef } from '@svgrid/grid'\n import type { ConditionalFormat } from '@svgrid/grid/format'\n import { SvGridAlerts, enableAlerts, setLicenseKey, type ExprColumn } from '@svgrid/enterprise'\n\n setLicenseKey('YOUR-KEY')\n enableAlerts()\n\n let rows = $state.raw(data)\n let alertFormats = $state<ConditionalFormat<Row>[]>([])\n\n // Columns the rule/expression editors offer.\n const exprColumns: ExprColumn[] = [\n { id: 'price', name: 'Price', type: 'number' },\n { id: 'region', name: 'Region', type: 'text' },\n ]\n</script>\n\n<SvGridAlerts\n data={rows}\n columns={exprColumns}\n getRowId={(r) => r.id}\n storageKey=\"app:alerts\"\n bind:formats={alertFormats} />\n\n<SvGrid data={rows} {columns} getRowId={(r) => r.id}\n conditionalFormats={alertFormats} />\n```\n\n`<SvGridAlerts>` renders a small control group: a bell (opens the fired-alert\nlog) and an **Alerts** button (opens the rule manager). Set `controls={false}`\nto hide them and drive the panels yourself with `bind:panelOpen` /\n`bind:managerOpen`.\n\n## The rule model\n\nAn `AlertRule` has four moving parts:\n\n| Part | What it is |\n| --- | --- |\n| **predicate** | A boolean [expression](./expressions-query.md) - when it holds, the rule matches. |\n| **trigger** | *When* the rule is checked (see below). |\n| **scope** | `row`, `cell`, or `aggregate`. |\n| **actions** | What happens when it fires. |\n\n### Triggers\n\n- **`dataChange`** - fires on the true edge: when a row *newly* satisfies the\n predicate after an edit or feed update. It will not re-fire while the row keeps\n matching; it re-arms once the row stops matching.\n- **`relativeChange`** - fires when a value *moves*: an absolute delta, a percent\n change, or crossing a threshold. Uses the previous snapshot of the row.\n- **`validation`** - evaluated on edit; with a `preventEdit` action it can veto\n the change.\n- **`scheduled`** - re-checked on a cron schedule (reuses the enterprise\n scheduler), surfacing rows that currently match.\n\n### Actions\n\n| Kind | Effect |\n| --- | --- |\n| `toast` | A toast in the rule's severity colour. |\n| `highlight` | Tints matching rows/cells (persists while they match). |\n| `badge` | Colours the targeted cells. |\n| `cellFlash` | A brief flash on the cell that fired. |\n| `preventEdit` | Vetoes the edit (validation trigger). |\n| `log` | Records the firing in the alert log only. |\n\nEvery firing is recorded in the log regardless of action, so the bell badge and\nthe [alerts panel](#the-fired-alert-log) always reflect activity.\n\nMessages are templates: `{value}`, `{column}`, `{rule}`, `{severity}`, and\n`{field}` / `{row.field}` for any row field.\n\n```ts\nconst rule: AlertRule = {\n id: 'price-spike',\n name: 'Price over 700',\n enabled: true,\n severity: 'warning',\n scope: 'row',\n predicate: { kind: 'cmp', column: 'price', op: 'greaterThan', value: 700 },\n trigger: { type: 'dataChange' },\n actions: [\n { kind: 'toast', message: '{name} crossed 700 -> {value}' },\n { kind: 'highlight', style: { background: '#fef3c7', color: '#92400e' } },\n ],\n createdAt: Date.now(),\n}\n```\n\nSeed rules with the `rules` prop (used only when storage is empty), or let users\nbuild them in the manager.\n\n## Persistence and sharing\n\nPass `storageKey` to persist rules in `localStorage`; omit it for in-memory\nrules. The manager's **Export** / **Import** buttons round-trip the whole rule\nset as JSON, so a team lead can hand a set of alerts to colleagues. Under the\nhood this is the same pluggable-storage shape as\n[saved views](../recipes/saved-views.md):\n\n```ts\nimport { createAlertRules, localStorageAlertRules } from '@svgrid/enterprise'\n\nconst rules = createAlertRules(localStorageAlertRules('app:alerts'))\nrules.save(rule)\nconst json = rules.export() // share\nrules.import(json) // load\n```\n\n## Headless usage\n\nNot on Svelte, or want to run the engine yourself? `attachAlertEngine` observes\na data source and routes fired events to their side effects, returning the\nconditional formats + flash targets to apply:\n\n```ts\nimport { attachAlertEngine, localStorageAlertRules } from '@svgrid/enterprise'\n\n// Pass your row type: without it TData defaults to Record<string, unknown>\n// and `r.id` comes back as `unknown`.\nconst attach = attachAlertEngine<Row>({\n rules,\n getRowId: (r) => r.id,\n getData: () => currentRows,\n applyFormats: (formats) => setGridFormats(formats),\n})\n// later\nattach.detach()\n```\n\nThe pure `createAlertEngine` (no DOM, no timers) is exported too if you want full\ncontrol over evaluation.\n\n## Performance and large / live datasets\n\nAlerts are built to stay out of the grid's way. Two things make that true:\n\n- **Off-frame evaluation.** Every pass is deferred to a post-paint animation\n frame and coalesced, so a burst of updates costs one pass and the grid never\n waits on alerts to render.\n- **Change-scoped work.** Prev-value snapshots are only kept when a rule actually\n needs them (a `relativeChange` trigger). A rule set of pure `dataChange` /\n `aggregate` rules keeps none.\n\n### Watch mode (default)\n\nBy default the overlay reactively scans `data` when it changes, diffing the new\narray against the previous one to find the changed rows. Because Svelte replaces\nchanged rows immutably (unchanged rows keep their reference), the diff evaluates\nonly the rows that moved. This is the zero-config path and is fine for most grids.\n\n### Push mode (streaming / 100k+ rows)\n\nWhen your app already knows which rows changed - a streaming feed, a transaction,\na tick loop - hand that set straight to the overlay and skip the scan entirely.\nSet `watch={false}`, capture the handle with `onReady` (or `bind:this`), and call\n`pushChanged` with just the changed rows. Cost is then O(rows that changed),\nindependent of total row count.\n\n```svelte\n<script lang=\"ts\">\n let rows = $state.raw(data)\n let alerts: { pushChanged: (rows: readonly Row[]) => void } | null = null\n\n function onTick(changed: Row[]) {\n rows = applyChanges(rows, changed) // your immutable update\n alerts?.pushChanged(changed) // evaluate only these, next frame\n }\n</script>\n\n<SvGridAlerts\n data={rows}\n columns={exprColumns}\n getRowId={(r) => r.id}\n watch={false}\n onReady={(h) => (alerts = h)}\n bind:formats={alertFormats} />\n```\n\nThe handle also exposes `reseed(allRows)` (silently re-arm edges after a full data\nreset) and `flush()` (run any pending pass immediately). For big live feeds, prefer\n`dataChange` rules (fire once when a row crosses the line) over `relativeChange`\n(fires on every move), and keep actions toast-only to avoid conditional-format\nchurn.\n\nOn a busy feed many rows can cross a threshold every second. Evaluation stays cheap,\nbut a nonstop stream of toasts is disruptive and repaints constantly. Set\n`toastCooldownMs` to rate-limit toasts to at most one per rule per interval - every\nevent is still logged (the bell badge stays accurate), only the visible toast is\nthrottled:\n\n```svelte\n<SvGridAlerts ... watch={false} onReady={(h) => (alerts = h)} toastCooldownMs={6000} />\n```\n\n## The fired-alert log\n\nThe bell badge shows the unacknowledged count. Clicking it opens\n`SvAlertsPanel` - a drawer listing fired alerts newest-first, filterable by\nseverity, with **Acknowledge**, **Clear**, and **Go to row** (wire `onJump` to\nscroll/select the row).\n\n## See also\n\n- [Expression query language](./expressions-query.md) - the predicate language rules are built on.\n- [Highlighting changes](./cells/highlighting-changes.md) - the `cellFlash` primitive alerts build on.\n- [Conditional formatting](./cells/conditional-formatting.md) - the styling pipeline alerts paint through.\n"
|
|
3054
3054
|
},
|
|
3055
3055
|
{
|
|
3056
3056
|
"slug": "help/api-stability",
|
|
@@ -3062,13 +3062,13 @@ export const docs = [
|
|
|
3062
3062
|
"slug": "help/architecture",
|
|
3063
3063
|
"path": "docs/help/architecture.md",
|
|
3064
3064
|
"title": "Architecture overview",
|
|
3065
|
-
"markdown": "# Architecture overview\r\n\r\nA one-page mental model that should let you reason about every other\r\ntopic in the docs. SvGrid is a strict three-layer system - if you know\r\nwhich layer a piece of code lives in, you know what it can and cannot\r\ndo.\r\n\r\n\r\n\r\n## The three layers\r\n\r\n```\r\n┌─────────────────────────────────────────────────────────────┐\r\n│ Layer 3 │ <SvGrid> render component (Svelte 5) │\r\n│ │ - DOM, scroll, virtualization, editor popovers │\r\n│ │ - keyboard handlers, pointer events │\r\n│ │ - sticks the headless engine to a viewport │\r\n├───────────┼─────────────────────────────────────────────────┤\r\n│ Layer 2 │ Headless engine (createSvGrid) │\r\n│ │ - column model + row model pipeline │\r\n│ │ - sort, filter, group, paginate, expand │\r\n│ │ - aggregators, accessors, comparators │\r\n│ │ - 100% pure functions, no DOM │\r\n├───────────┼─────────────────────────────────────────────────┤\r\n│ Layer 1 │ Your data + your column definitions │\r\n│ │ - the only thing YOU author │\r\n│ │ - plain TypeScript: arrays, objects, types │\r\n└───────────┴─────────────────────────────────────────────────┘\r\n```\r\n\r\nLayer 1 is yours. Layer 2 is `@svgrid/grid` minus the renderer.\r\nLayer 3 is the `<SvGrid>` component everyone uses by default.\r\n\r\n**You can use Layer 2 without Layer 3.** That's the headless promise:\r\nif you want to render the grid yourself - in Tailwind cards, a print\r\nPDF template, a custom virtualization layer - import `createSvGrid`\r\nand read the state directly.\r\n\r\n## Data flow on every render\r\n\r\n```\r\nraw data ──► engine pipeline ──► visible rows ──► renderer\r\n (you) (Layer 2) (Layer 2 out) (Layer 3)\r\n │\r\n ▼\r\n ┌────────────────────────────┐\r\n │ 1. coreRowModel │ shape data into Row objects\r\n │ 2. filteredRowModel │ apply column + global filters\r\n │ 3. sortedRowModel │ apply sort spec\r\n │ 4. groupedRowModel │ apply groupBy + aggregators\r\n │ 5. expandedRowModel │ flatten expanded groups\r\n │ 6. paginatedRowModel │ slice the visible page\r\n └────────────────────────────┘\r\n```\r\n\r\nEvery \"feature\" you register in `tableFeatures({ ... })` plugs one or\r\nmore row models into this pipeline. Disable a feature and that stage\r\nno-ops. The pipeline runs once per state change, **not per scroll\r\nframe** - virtualization is purely a presentational concern.\r\n\r\n## The two APIs you'll use\r\n\r\n### Declarative (the `<SvGrid>` props)\r\n\r\n99% of consumers stop here. You author `data`, `columns`, and `features`,\r\nthen handle events from props:\r\n\r\n```svelte\r\n<SvGrid\r\n data={rows}\r\n columns={columns}\r\n features={features}\r\n onCellValueChange={handleChange}\r\n onActiveCellChange={handleFocus}\r\n/>\r\n```\r\n\r\n### Imperative (`SvGridApi`)\r\n\r\nFor toolbars, ribbons, keyboard shortcuts that need to drive the grid,\r\nask for the API via `onApiReady`:\r\n\r\n```svelte\r\n<SvGrid\r\n ...\r\n onApiReady={(api) => {\r\n api.setSort('name', 'asc')\r\n api.setFilter('region', { operator: 'equals', value: 'EMEA' })\r\n }}\r\n/>\r\n```\r\n\r\nSee the [API reference](./api-reference.md) for the full surface.\r\n\r\n## State ownership\r\n\r\nTwo questions decide where each piece of state lives:\r\n\r\n| Question | Lives in |\r\n| ------------------------------------------ | ------------------------- |\r\n| Does it change row content or cell values? | **Your component** (`$state`) - hand the new array down to `data`. |\r\n| Is it a column/grid setting (sort, filter, group, page)? | **The engine** owns it. Use `api.setSort(...)` etc., or pass an initial state. |\r\n| Is it a visual concern (column widths, hover)? | **The renderer** owns it. The grid manages this internally. |\r\n\r\nThis split is deliberate: the engine is dataless, so it can't \"lose\"\r\nyour rows. Your component is rendererless, so it can't accidentally\r\nmutate DOM nodes during a sort.\r\n\r\n## Where each topic page sits\r\n\r\n| Topic | Layer | Why |\r\n| ------------------------------------ | ------ | ------------------------------------------------------------------------- |\r\n| [Column definitions](./columns/column-definitions.md) | 1 | Pure types you author. |\r\n| [Row data](./rows/row-data.md) | 1 | Your input. |\r\n| [Row sorting](./rows/row-sorting.md) | 2 | Engine row-model. |\r\n| [Filtering overview](./filtering/overview.md) | 2 + 3 | Engine for the pipeline; renderer for the popovers + filter row. |\r\n| [Row pagination](./rows/row-pagination.md) | 2 | Engine slice. |\r\n| [Editing](./editing/overview.md) | 3 | DOM editors live in the renderer. |\r\n| [Tree rows](./rows/tree-rows.md) | 1 + 3 | You derive `visibleRows`; the renderer indents + draws chevrons. |\r\n| [Pivot tables](./pivot.md) | 1 + 2 | You build the pivot engine; the renderer uses standard nested headers. |\r\n| [AI assistant](./ai.md) | 2 | Pure helpers; the renderer never sees them. |\r\n| [Export / import](./export.md), [import](./import.md) | 2 + 3 | Helpers + browser-side file IO. |\r\n\r\n## Why this matters for shipping\r\n\r\n- **You can test Layer 2 without a DOM.** Every engine helper is a\r\n pure function. Vitest in node, no jsdom required. See\r\n [Testing your grid](./testing.md).\r\n- **You can swap Layer 3.** If your design system has its own table\r\n primitive, drop `<SvGrid>` and read from `createSvGrid()` directly.\r\n- **Layer 2 is the public API surface.** Imports, exports, and types\r\n are versioned per the [API stability](./api-stability.md) policy.\r\n The renderer's CSS classes are NOT - override them at your peril.\r\n\r\n## Where the layers physically live\r\n\r\n| Layer | Source path | Build output |\r\n| ----- | ------------------------------------------------- | ------------------------------------ |\r\n| 1 | Your app | n/a |\r\n| 2 | `packages/grid/src/core.ts` + row-models | `dist/index.js` (~2 kB gzip) |\r\n| 3 | `packages/grid/src/SvGrid.svelte` | bundled with the engine (~
|
|
3065
|
+
"markdown": "# Architecture overview\r\n\r\nA one-page mental model that should let you reason about every other\r\ntopic in the docs. SvGrid is a strict three-layer system - if you know\r\nwhich layer a piece of code lives in, you know what it can and cannot\r\ndo.\r\n\r\n\r\n\r\n## The three layers\r\n\r\n```\r\n┌─────────────────────────────────────────────────────────────┐\r\n│ Layer 3 │ <SvGrid> render component (Svelte 5) │\r\n│ │ - DOM, scroll, virtualization, editor popovers │\r\n│ │ - keyboard handlers, pointer events │\r\n│ │ - sticks the headless engine to a viewport │\r\n├───────────┼─────────────────────────────────────────────────┤\r\n│ Layer 2 │ Headless engine (createSvGrid) │\r\n│ │ - column model + row model pipeline │\r\n│ │ - sort, filter, group, paginate, expand │\r\n│ │ - aggregators, accessors, comparators │\r\n│ │ - 100% pure functions, no DOM │\r\n├───────────┼─────────────────────────────────────────────────┤\r\n│ Layer 1 │ Your data + your column definitions │\r\n│ │ - the only thing YOU author │\r\n│ │ - plain TypeScript: arrays, objects, types │\r\n└───────────┴─────────────────────────────────────────────────┘\r\n```\r\n\r\nLayer 1 is yours. Layer 2 is `@svgrid/grid` minus the renderer.\r\nLayer 3 is the `<SvGrid>` component everyone uses by default.\r\n\r\n**You can use Layer 2 without Layer 3.** That's the headless promise:\r\nif you want to render the grid yourself - in Tailwind cards, a print\r\nPDF template, a custom virtualization layer - import `createSvGrid`\r\nand read the state directly.\r\n\r\n## Data flow on every render\r\n\r\n```\r\nraw data ──► engine pipeline ──► visible rows ──► renderer\r\n (you) (Layer 2) (Layer 2 out) (Layer 3)\r\n │\r\n ▼\r\n ┌────────────────────────────┐\r\n │ 1. coreRowModel │ shape data into Row objects\r\n │ 2. filteredRowModel │ apply column + global filters\r\n │ 3. sortedRowModel │ apply sort spec\r\n │ 4. groupedRowModel │ apply groupBy + aggregators\r\n │ 5. expandedRowModel │ flatten expanded groups\r\n │ 6. paginatedRowModel │ slice the visible page\r\n └────────────────────────────┘\r\n```\r\n\r\nEvery \"feature\" you register in `tableFeatures({ ... })` plugs one or\r\nmore row models into this pipeline. Disable a feature and that stage\r\nno-ops. The pipeline runs once per state change, **not per scroll\r\nframe** - virtualization is purely a presentational concern.\r\n\r\n## The two APIs you'll use\r\n\r\n### Declarative (the `<SvGrid>` props)\r\n\r\n99% of consumers stop here. You author `data`, `columns`, and `features`,\r\nthen handle events from props:\r\n\r\n```svelte\r\n<SvGrid\r\n data={rows}\r\n columns={columns}\r\n features={features}\r\n onCellValueChange={handleChange}\r\n onActiveCellChange={handleFocus}\r\n/>\r\n```\r\n\r\n### Imperative (`SvGridApi`)\r\n\r\nFor toolbars, ribbons, keyboard shortcuts that need to drive the grid,\r\nask for the API via `onApiReady`:\r\n\r\n```svelte\r\n<SvGrid\r\n ...\r\n onApiReady={(api) => {\r\n api.setSort('name', 'asc')\r\n api.setFilter('region', { operator: 'equals', value: 'EMEA' })\r\n }}\r\n/>\r\n```\r\n\r\nSee the [API reference](./api-reference.md) for the full surface.\r\n\r\n## State ownership\r\n\r\nTwo questions decide where each piece of state lives:\r\n\r\n| Question | Lives in |\r\n| ------------------------------------------ | ------------------------- |\r\n| Does it change row content or cell values? | **Your component** (`$state`) - hand the new array down to `data`. |\r\n| Is it a column/grid setting (sort, filter, group, page)? | **The engine** owns it. Use `api.setSort(...)` etc., or pass an initial state. |\r\n| Is it a visual concern (column widths, hover)? | **The renderer** owns it. The grid manages this internally. |\r\n\r\nThis split is deliberate: the engine is dataless, so it can't \"lose\"\r\nyour rows. Your component is rendererless, so it can't accidentally\r\nmutate DOM nodes during a sort.\r\n\r\n## Where each topic page sits\r\n\r\n| Topic | Layer | Why |\r\n| ------------------------------------ | ------ | ------------------------------------------------------------------------- |\r\n| [Column definitions](./columns/column-definitions.md) | 1 | Pure types you author. |\r\n| [Row data](./rows/row-data.md) | 1 | Your input. |\r\n| [Row sorting](./rows/row-sorting.md) | 2 | Engine row-model. |\r\n| [Filtering overview](./filtering/overview.md) | 2 + 3 | Engine for the pipeline; renderer for the popovers + filter row. |\r\n| [Row pagination](./rows/row-pagination.md) | 2 | Engine slice. |\r\n| [Editing](./editing/overview.md) | 3 | DOM editors live in the renderer. |\r\n| [Tree rows](./rows/tree-rows.md) | 1 + 3 | You derive `visibleRows`; the renderer indents + draws chevrons. |\r\n| [Pivot tables](./pivot.md) | 1 + 2 | You build the pivot engine; the renderer uses standard nested headers. |\r\n| [AI assistant](./ai.md) | 2 | Pure helpers; the renderer never sees them. |\r\n| [Export / import](./export.md), [import](./import.md) | 2 + 3 | Helpers + browser-side file IO. |\r\n\r\n## Why this matters for shipping\r\n\r\n- **You can test Layer 2 without a DOM.** Every engine helper is a\r\n pure function. Vitest in node, no jsdom required. See\r\n [Testing your grid](./testing.md).\r\n- **You can swap Layer 3.** If your design system has its own table\r\n primitive, drop `<SvGrid>` and read from `createSvGrid()` directly.\r\n- **Layer 2 is the public API surface.** Imports, exports, and types\r\n are versioned per the [API stability](./api-stability.md) policy.\r\n The renderer's CSS classes are NOT - override them at your peril.\r\n\r\n## Where the layers physically live\r\n\r\n| Layer | Source path | Build output |\r\n| ----- | ------------------------------------------------- | ------------------------------------ |\r\n| 1 | Your app | n/a |\r\n| 2 | `packages/grid/src/core.ts` + row-models | `dist/index.js` (~2 kB gzip) |\r\n| 3 | `packages/grid/src/SvGrid.svelte` | bundled with the engine (~78 kB gzip + 9 kB CSS) |\r\n| | `packages/enterprise/src/{export,print,import,ai}.ts` | `@svgrid/enterprise/dist/*` (lazy-loaded peers) |\r\n\r\n## See also\r\n\r\n- [Why headless?](../why-headless.md) - the design rationale for the\r\n Layer 2 / Layer 3 split.\r\n- [API reference](./api-reference.md) - every export with its layer\r\n noted.\r\n- [Performance benchmarks](./benchmarks.md) - numbers from each layer\r\n in isolation.\r\n\r\n## Frequently asked questions\r\n\r\n### How is SvGrid architected?\r\n\r\nAs three strict layers: a headless core engine (state + row-model pipeline), a\r\nSvelte render component (`<SvGrid>`) that draws the DOM, and your application\r\ncode. Knowing which layer a piece of code lives in tells you what it can and\r\ncannot do.\r\n\r\n### What does \"headless\" mean for SvGrid?\r\n\r\nThe core engine computes sorting, filtering, grouping, and selection state\r\nwithout rendering anything. You can drive your own markup with it, or drop in the\r\nbatteries-included `<SvGrid>` component that renders on top of the same engine.\r\n\r\n### Can I use the engine without the SvGrid component?\r\n\r\nYes. Use `createSvGrid` and the row-model factories directly to build a custom\r\nrendering layer. The render component is optional sugar over the same public\r\nengine API.\r\n"
|
|
3066
3066
|
},
|
|
3067
3067
|
{
|
|
3068
3068
|
"slug": "help/benchmarks",
|
|
3069
3069
|
"path": "docs/help/benchmarks.md",
|
|
3070
3070
|
"title": "Performance benchmarks",
|
|
3071
|
-
"markdown": "# Performance benchmarks\r\n\r\nHeadline numbers from the regression suite. Every figure below is from\r\nthe same hardware + browser configuration, re-measured on each release.\r\n\r\nReproduce them yourself rather than taking these on trust: the\r\n[benchmark harness](../recipes/benchmark-harness.md) is a copy-paste\r\n`<SvGrid>` probe that measures time-to-first-paint across any (rows x\r\ncolumns) matrix, and the bundle-size figures come from `pnpm size`\r\n(`packages/grid/scripts/measure-size.mjs`). Numbers from your own\r\nmachine and data shape are the ones worth planning against.\r\n\r\nLive load - 100k rows x 100 columns with row + column virtualization:\r\n\r\n<div data-docs-demo=\"06-large-dataset\" data-height=\"500\"></div>\r\n\r\n## Test rig\r\n\r\n| Component | Spec |\r\n| --------- | ----------------------------------------- |\r\n| CPU | Apple M2 (8-core), 10W TDP |\r\n| RAM | 16 GB LPDDR5 |\r\n| Browser | Chrome 131 (release channel) |\r\n| Display | 1 page worth of cells visible at any time |\r\n| Throttle | None - the regression run isn't throttled, but we also publish a separate \"4x slow-down\" line below for each scenario |\r\n\r\nNumbers are the median of 5 runs after a warm-up pass. We track the\r\n**95th percentile of frame time** during scroll rather than mean FPS -\r\nthe former catches jank that mean averages smooths over.\r\n\r\n## Bundle size\r\n\r\nProduction build, gzipped. The first two rows come from\r\n`node packages/grid/scripts/measure-size.mjs` (Svelte excluded as a peer);\r\nsee the [bundle size reference](../reference/bundle-size.md).\r\n\r\n| Surface | gzip | Notes |\r\n| ----------------------------------- | ------ | -------------------------------------- |\r\n| `@svgrid/grid` (full `<SvGrid>`) | 80 kB | One import covers the entire renderer; + 9 kB CSS |\r\n| Headless engine (`createGrid`) | 2 kB | If you bring your own renderer |\r\n| Lazy chunks (charts, date editors, menus, export) | 64 kB | Loaded on demand, not in the initial bundle |\r\n| `@svgrid/enterprise` core | 8 kB | Export + print + import shells |\r\n| `@svgrid/enterprise` import module only | 6 kB | Imported via `'@svgrid/enterprise/import'` |\r\n| Peer: `jszip` | 35 kB | Loaded on first `xlsx` export *or* import |\r\n| Peer: `pdfmake` + vfs | ~280 kB| Loaded on first `pdf` export only |\r\n\r\nThe AI helpers are no longer in this table: they moved into the free\r\n`@svgrid/grid` and tree-shake out unless you import them.\r\n\r\nTree-shaking is friendly: importing `{ SvGrid, tableFeatures }`\r\nwithout `rowSortingFeature` doesn't pull the sort module.\r\n\r\n## First paint\r\n\r\n100k synthetic rows, 9 columns, default density, no virtualization\r\noverride. Measured from `mount()` to the first row painting:\r\n\r\n| Scenario | Time (ms) |\r\n| --------------------------------- | --------- |\r\n| 10 rows × 9 cols | 4 |\r\n| 1,000 rows × 9 cols | 14 |\r\n| 10,000 rows × 9 cols | 38 |\r\n| 100,000 rows × 9 cols (virtualized) | 82 |\r\n| 100,000 rows × 100 cols (row + col virtualization) | 110 |\r\n\r\nThe slope is sub-linear because virtualization caps the rendered cell\r\ncount regardless of dataset size.\r\n\r\n## Scroll performance\r\n\r\nSustained vertical scroll, 60 px/frame, measured as the 95th\r\npercentile frame time:\r\n\r\n| Scenario | p95 frame | Equivalent FPS |\r\n| ------------------------------------- | --------- | -------------- |\r\n| 100k rows × 9 cols | 8 ms | ~120 fps |\r\n| 100k rows × 100 cols (col virt) | 11 ms | ~90 fps |\r\n| 100k rows × 9 cols, custom cell snippets w/ sparklines | 14 ms | ~70 fps |\r\n| 100k rows × 9 cols, **4x CPU throttle** | 22 ms | ~45 fps |\r\n\r\nHorizontal scroll on a 100-column grid stays under 12 ms p95 because\r\nthe column virtualizer is identical machinery.\r\n\r\n## Sort, filter, group\r\n\r\nIn-memory operations on 100k rows:\r\n\r\n| Operation | Time (ms) |\r\n| ------------------------------------- | --------- |\r\n| Sort 100k rows by one column | 18 |\r\n| Sort 100k rows by 3 columns (multi-sort) | 28 |\r\n| Filter 100k rows (one operator) | 9 |\r\n| Filter 100k rows (5 operators ANDed) | 17 |\r\n| Group 100k rows by 2 columns + 3 aggregators | 36 |\r\n| Pivot 100k facts → 4 row dims × 2 col dims × 3 measures (see demo 52) | 62 |\r\n\r\nThe sort path uses a stable comparator built per-column to keep\r\nallocations down; the filter pipeline short-circuits on the first\r\nfailing predicate.\r\n\r\n## Memory\r\n\r\nHeap snapshot at idle, 100k rows × 9 columns loaded, after a full\r\nscroll pass:\r\n\r\n- ~22 MB heap (the Row objects + the visible-cell pool).\r\n- Virtualization keeps the rendered DOM under ~600 `<td>` nodes\r\n regardless of dataset size.\r\n- No retained references when the grid unmounts - the cleanup path is\r\n exercised by the unmount test in `svgrid.behavior.test.ts`.\r\n\r\n## Server-side / chunked loading\r\n\r\nDemo [33. Server-side infinite scroll](https://svgrid.com/demos/33-server-infinite/) covers the chunked-load path. Numbers from that demo:\r\n\r\n| Scenario | Result |\r\n| ------------------------------------------------ | --------------------------------------- |\r\n| Initial paint, sparse 100k-row dataset | 110 ms to first chunk visible |\r\n| Scroll 50,000 rows in 1.5 s (fast wheel-flick) | 16 chunk requests cancelled mid-flight |\r\n| Sort 100k server-side rows | round-trip dominated by the mock latency (50-140 ms) |\r\n\r\n## AI helpers\r\n\r\nEnd-to-end timings against the bundled `mockAIProvider`:\r\n\r\n| Helper | Median time (ms) |\r\n| --------------- | ---------------- |\r\n| `aiFilter` | 350-750 (mock latency dominated) |\r\n| `aiSmartFill` (50 rows) | 400-900 |\r\n| `aiSummarize` | 350-750 |\r\n| `aiClassify` (20 rows) | 400-750 |\r\n\r\nAgainst a real model the latency is provider-side. The grid's own\r\nprompt-build + result-parse work stays under ~6 ms even for 1000-row\r\nclassify jobs.\r\n\r\n## Import / export\r\n\r\n| Operation | Time |\r\n| ------------------------------------ | ------ |\r\n| Parse CSV, 10k rows × 9 cols | 28 ms |\r\n| Parse xlsx, 10k rows × 9 cols | 140 ms (jszip unzip-dominated) |\r\n| Export CSV, 10k rows × 9 cols | 18 ms |\r\n| Export xlsx, 10k rows × 9 cols | 220 ms |\r\n| Export PDF, 1k rows × 9 cols (pdfmake) | 700 ms |\r\n\r\n## Reproducing locally\r\n\r\n```bash\r\ngit clone https://github.com/sv-grid/sv-grid\r\ncd sv-grid\r\npnpm install\r\npnpm bench # runs the suite, prints the same table\r\npnpm bench --json > my-results.json # for trend tracking\r\n```\r\n\r\nThe bench script also produces a comparison table against the previous\r\nrun if you pass `--baseline=path/to/prev.json`. Regressions over 10%\r\nfail CI on the main branch.\r\n\r\n## What we *don't* claim\r\n\r\n- \"Smoothest grid on the market\" - that depends entirely on what your\r\n cells render. A sparkline + currency formatter in every cell costs\r\n more than a number, and we don't pretend otherwise.\r\n- \"Zero allocations during scroll\" - the virtualizer recycles DOM\r\n nodes but cell snippets still allocate. The numbers above include\r\n real-world snippets (status pills, mini-bars).\r\n- Single-thread performance > 1M rows. For >1M, do the heavy lifting\r\n on the server and feed chunks through the [server-side infinite\r\n scroll pattern](https://svgrid.com/demos/33-server-infinite/).\r\n\r\n## See also\r\n\r\n- [Browser support](./browser-support.md) - the matrix the benchmarks\r\n ran against.\r\n- [Testing and quality](./testing-and-quality.md) - the coverage\r\n thresholds that gate every release.\r\n\r\n## Frequently asked questions\r\n\r\n### How fast is SvGrid?\r\n\r\nIt virtualizes both rows and columns, so only the visible window is in the DOM -\r\na 100,000-row × 100-column grid scrolls smoothly. The numbers on this page come\r\nfrom a checked-in regression suite re-measured on every release, not marketing\r\nestimates.\r\n\r\n### How many rows can SvGrid handle?\r\n\r\nClient-side, 100k+ rows scroll smoothly thanks to virtualization. For millions\r\nof rows, page or chunk from the server (see Server-side data). The DOM only ever\r\nholds the visible window regardless of total row count.\r\n\r\n### How fast is SvGrid, and how big is it?\r\n\r\nIt ships a much smaller bundle (~
|
|
3071
|
+
"markdown": "# Performance benchmarks\r\n\r\nHeadline numbers from the regression suite. Every figure below is from\r\nthe same hardware + browser configuration, re-measured on each release.\r\n\r\nReproduce them yourself rather than taking these on trust: the\r\n[benchmark harness](../recipes/benchmark-harness.md) is a copy-paste\r\n`<SvGrid>` probe that measures time-to-first-paint across any (rows x\r\ncolumns) matrix, and the bundle-size figures come from `pnpm size`\r\n(`packages/grid/scripts/measure-size.mjs`). Numbers from your own\r\nmachine and data shape are the ones worth planning against.\r\n\r\nLive load - 100k rows x 100 columns with row + column virtualization:\r\n\r\n<div data-docs-demo=\"06-large-dataset\" data-height=\"500\"></div>\r\n\r\n## Test rig\r\n\r\n| Component | Spec |\r\n| --------- | ----------------------------------------- |\r\n| CPU | Apple M2 (8-core), 10W TDP |\r\n| RAM | 16 GB LPDDR5 |\r\n| Browser | Chrome 131 (release channel) |\r\n| Display | 1 page worth of cells visible at any time |\r\n| Throttle | None - the regression run isn't throttled, but we also publish a separate \"4x slow-down\" line below for each scenario |\r\n\r\nNumbers are the median of 5 runs after a warm-up pass. We track the\r\n**95th percentile of frame time** during scroll rather than mean FPS -\r\nthe former catches jank that mean averages smooths over.\r\n\r\n## Bundle size\r\n\r\nProduction build, gzipped. The first two rows come from\r\n`node packages/grid/scripts/measure-size.mjs` (Svelte excluded as a peer);\r\nsee the [bundle size reference](../reference/bundle-size.md).\r\n\r\n| Surface | gzip | Notes |\r\n| ----------------------------------- | ------ | -------------------------------------- |\r\n| `@svgrid/grid` (full `<SvGrid>`) | 80 kB | One import covers the entire renderer; + 9 kB CSS |\r\n| Headless engine (`createGrid`) | 2 kB | If you bring your own renderer |\r\n| Lazy chunks (charts, date editors, menus, export) | 64 kB | Loaded on demand, not in the initial bundle |\r\n| `@svgrid/enterprise` core | 8 kB | Export + print + import shells |\r\n| `@svgrid/enterprise` import module only | 6 kB | Imported via `'@svgrid/enterprise/import'` |\r\n| Peer: `jszip` | 35 kB | Loaded on first `xlsx` export *or* import |\r\n| Peer: `pdfmake` + vfs | ~280 kB| Loaded on first `pdf` export only |\r\n\r\nThe AI helpers are no longer in this table: they moved into the free\r\n`@svgrid/grid` and tree-shake out unless you import them.\r\n\r\nTree-shaking is friendly: importing `{ SvGrid, tableFeatures }`\r\nwithout `rowSortingFeature` doesn't pull the sort module.\r\n\r\n## First paint\r\n\r\n100k synthetic rows, 9 columns, default density, no virtualization\r\noverride. Measured from `mount()` to the first row painting:\r\n\r\n| Scenario | Time (ms) |\r\n| --------------------------------- | --------- |\r\n| 10 rows × 9 cols | 4 |\r\n| 1,000 rows × 9 cols | 14 |\r\n| 10,000 rows × 9 cols | 38 |\r\n| 100,000 rows × 9 cols (virtualized) | 82 |\r\n| 100,000 rows × 100 cols (row + col virtualization) | 110 |\r\n\r\nThe slope is sub-linear because virtualization caps the rendered cell\r\ncount regardless of dataset size.\r\n\r\n## Scroll performance\r\n\r\nSustained vertical scroll, 60 px/frame, measured as the 95th\r\npercentile frame time:\r\n\r\n| Scenario | p95 frame | Equivalent FPS |\r\n| ------------------------------------- | --------- | -------------- |\r\n| 100k rows × 9 cols | 8 ms | ~120 fps |\r\n| 100k rows × 100 cols (col virt) | 11 ms | ~90 fps |\r\n| 100k rows × 9 cols, custom cell snippets w/ sparklines | 14 ms | ~70 fps |\r\n| 100k rows × 9 cols, **4x CPU throttle** | 22 ms | ~45 fps |\r\n\r\nHorizontal scroll on a 100-column grid stays under 12 ms p95 because\r\nthe column virtualizer is identical machinery.\r\n\r\n## Sort, filter, group\r\n\r\nIn-memory operations on 100k rows:\r\n\r\n| Operation | Time (ms) |\r\n| ------------------------------------- | --------- |\r\n| Sort 100k rows by one column | 18 |\r\n| Sort 100k rows by 3 columns (multi-sort) | 28 |\r\n| Filter 100k rows (one operator) | 9 |\r\n| Filter 100k rows (5 operators ANDed) | 17 |\r\n| Group 100k rows by 2 columns + 3 aggregators | 36 |\r\n| Pivot 100k facts → 4 row dims × 2 col dims × 3 measures (see demo 52) | 62 |\r\n\r\nThe sort path uses a stable comparator built per-column to keep\r\nallocations down; the filter pipeline short-circuits on the first\r\nfailing predicate.\r\n\r\n## Memory\r\n\r\nHeap snapshot at idle, 100k rows × 9 columns loaded, after a full\r\nscroll pass:\r\n\r\n- ~22 MB heap (the Row objects + the visible-cell pool).\r\n- Virtualization keeps the rendered DOM under ~600 `<td>` nodes\r\n regardless of dataset size.\r\n- No retained references when the grid unmounts - the cleanup path is\r\n exercised by the unmount test in `svgrid.behavior.test.ts`.\r\n\r\n## Server-side / chunked loading\r\n\r\nDemo [33. Server-side infinite scroll](https://svgrid.com/demos/33-server-infinite/) covers the chunked-load path. Numbers from that demo:\r\n\r\n| Scenario | Result |\r\n| ------------------------------------------------ | --------------------------------------- |\r\n| Initial paint, sparse 100k-row dataset | 110 ms to first chunk visible |\r\n| Scroll 50,000 rows in 1.5 s (fast wheel-flick) | 16 chunk requests cancelled mid-flight |\r\n| Sort 100k server-side rows | round-trip dominated by the mock latency (50-140 ms) |\r\n\r\n## AI helpers\r\n\r\nEnd-to-end timings against the bundled `mockAIProvider`:\r\n\r\n| Helper | Median time (ms) |\r\n| --------------- | ---------------- |\r\n| `aiFilter` | 350-750 (mock latency dominated) |\r\n| `aiSmartFill` (50 rows) | 400-900 |\r\n| `aiSummarize` | 350-750 |\r\n| `aiClassify` (20 rows) | 400-750 |\r\n\r\nAgainst a real model the latency is provider-side. The grid's own\r\nprompt-build + result-parse work stays under ~6 ms even for 1000-row\r\nclassify jobs.\r\n\r\n## Import / export\r\n\r\n| Operation | Time |\r\n| ------------------------------------ | ------ |\r\n| Parse CSV, 10k rows × 9 cols | 28 ms |\r\n| Parse xlsx, 10k rows × 9 cols | 140 ms (jszip unzip-dominated) |\r\n| Export CSV, 10k rows × 9 cols | 18 ms |\r\n| Export xlsx, 10k rows × 9 cols | 220 ms |\r\n| Export PDF, 1k rows × 9 cols (pdfmake) | 700 ms |\r\n\r\n## Reproducing locally\r\n\r\n```bash\r\ngit clone https://github.com/sv-grid/sv-grid\r\ncd sv-grid\r\npnpm install\r\npnpm bench # runs the suite, prints the same table\r\npnpm bench --json > my-results.json # for trend tracking\r\n```\r\n\r\nThe bench script also produces a comparison table against the previous\r\nrun if you pass `--baseline=path/to/prev.json`. Regressions over 10%\r\nfail CI on the main branch.\r\n\r\n## What we *don't* claim\r\n\r\n- \"Smoothest grid on the market\" - that depends entirely on what your\r\n cells render. A sparkline + currency formatter in every cell costs\r\n more than a number, and we don't pretend otherwise.\r\n- \"Zero allocations during scroll\" - the virtualizer recycles DOM\r\n nodes but cell snippets still allocate. The numbers above include\r\n real-world snippets (status pills, mini-bars).\r\n- Single-thread performance > 1M rows. For >1M, do the heavy lifting\r\n on the server and feed chunks through the [server-side infinite\r\n scroll pattern](https://svgrid.com/demos/33-server-infinite/).\r\n\r\n## See also\r\n\r\n- [Browser support](./browser-support.md) - the matrix the benchmarks\r\n ran against.\r\n- [Testing and quality](./testing-and-quality.md) - the coverage\r\n thresholds that gate every release.\r\n\r\n## Frequently asked questions\r\n\r\n### How fast is SvGrid?\r\n\r\nIt virtualizes both rows and columns, so only the visible window is in the DOM -\r\na 100,000-row × 100-column grid scrolls smoothly. The numbers on this page come\r\nfrom a checked-in regression suite re-measured on every release, not marketing\r\nestimates.\r\n\r\n### How many rows can SvGrid handle?\r\n\r\nClient-side, 100k+ rows scroll smoothly thanks to virtualization. For millions\r\nof rows, page or chunk from the server (see Server-side data). The DOM only ever\r\nholds the visible window regardless of total row count.\r\n\r\n### How fast is SvGrid, and how big is it?\r\n\r\nIt ships a much smaller bundle (~78 KB gzipped for the full render component,\r\nor ~2 KB for the headless core) and virtualizes by default. Raw scroll\r\nperformance is comparable for typical workloads; the bigger practical win is\r\nbundle size and a Svelte-native runtime with no framework bridge.\r\n"
|
|
3072
3072
|
},
|
|
3073
3073
|
{
|
|
3074
3074
|
"slug": "help/browser-support",
|
|
@@ -3218,7 +3218,7 @@ export const docs = [
|
|
|
3218
3218
|
"slug": "help/columns/column-spanning",
|
|
3219
3219
|
"path": "docs/help/columns/column-spanning.md",
|
|
3220
3220
|
"title": "Column & row spanning (merged cells)",
|
|
3221
|
-
"markdown": "# Column & row spanning (merged cells)\r\n\r\nSpanning lets a single body cell cover **multiple columns** and/or **rows** -\r\nmerged report headers, grouped labels, financial statements. SvGrid does this\r\nwith a real `colspan` / `rowspan` merge engine; there are two ways to drive it.\r\n\n<div data-docs-demo=\"170-cell-merging\" data-height=\"480\"></div>\n\r\n## 1. Explicit merges (spreadsheet-style)\r\n\r\nDeclare exact merges as `MergeSpec[]` and apply them with the\r\n`spreadsheetLayout` action. The origin cell `(rowIndex, columnId)` spans\r\n`colspan` columns right and `rowspan` rows down; covered cells are hidden.\r\n\r\n```svelte\r\n<script lang=\"ts\">\r\n import { spreadsheetLayout, type MergeSpec } from '@svgrid/grid'\r\n\r\n const merges: MergeSpec[] = [\r\n { rowIndex: 0, columnId: 'A', colspan: 6 }, // title bar\r\n { rowIndex: 14, columnId: 'A', colspan: 3 }, // \"Total\" label\r\n ]\r\n</script>\r\n\r\n<div use:spreadsheetLayout={{ merges, columnOrder: columns.map((c) => c.id) }}>\r\n <SvGrid {data} {columns} />\r\n</div>\r\n```\r\n\r\nSee [demos/170-cell-merging.svelte](../../../examples/src/demos/170-cell-merging.svelte).\r\n\r\n## 2. Declarative `colSpan` / `rowSpan` (value-driven)\r\n\r\nFor data-driven spanning - \"merge each run of equal values\", \"this cell spans 2\r\ncolumns when X\" - put `colSpan` / `rowSpan` callbacks on the column and turn\r\nthem into merges with `spansToMerges`. This runs on the **same** merge engine\r\nas option 1 (no separate code path).\r\n\r\n```svelte\r\n<script lang=\"ts\">\r\n import { spreadsheetLayout, spansToMerges, type
|
|
3221
|
+
"markdown": "# Column & row spanning (merged cells)\r\n\r\nSpanning lets a single body cell cover **multiple columns** and/or **rows** -\r\nmerged report headers, grouped labels, financial statements. SvGrid does this\r\nwith a real `colspan` / `rowspan` merge engine; there are two ways to drive it.\r\n\r\n<div data-docs-demo=\"170-cell-merging\" data-height=\"480\"></div>\r\n\r\n## 1. Explicit merges (spreadsheet-style)\r\n\r\nDeclare exact merges as `MergeSpec[]` and apply them with the\r\n`spreadsheetLayout` action. The origin cell `(rowIndex, columnId)` spans\r\n`colspan` columns right and `rowspan` rows down; covered cells are hidden.\r\n\r\n```svelte\r\n<script lang=\"ts\">\r\n import { spreadsheetLayout, type MergeSpec } from '@svgrid/grid'\r\n\r\n const merges: MergeSpec[] = [\r\n { rowIndex: 0, columnId: 'A', colspan: 6 }, // title bar\r\n { rowIndex: 14, columnId: 'A', colspan: 3 }, // \"Total\" label\r\n ]\r\n</script>\r\n\r\n<div use:spreadsheetLayout={{ merges, columnOrder: columns.map((c) => c.id) }}>\r\n <SvGrid {data} {columns} />\r\n</div>\r\n```\r\n\r\nSee [demos/170-cell-merging.svelte](../../../examples/src/demos/170-cell-merging.svelte).\r\n\r\n## 2. Declarative `colSpan` / `rowSpan` (value-driven)\r\n\r\nFor data-driven spanning - \"merge each run of equal values\", \"this cell spans 2\r\ncolumns when X\" - put `colSpan` / `rowSpan` callbacks on the column and turn\r\nthem into merges with `spansToMerges`. This runs on the **same** merge engine\r\nas option 1 (no separate code path).\r\n\r\n```svelte\r\n<script lang=\"ts\">\r\n // `SpanColumn` rather than `ColumnDef`: `spansToMerges` needs `id` to be\r\n // present, and on a ColumnDef it is optional.\r\n import { spreadsheetLayout, spansToMerges, type SpanColumn } from '@svgrid/grid'\r\n\r\n const columns: SpanColumn<Row>[] = [\r\n { id: 'region', field: 'region',\r\n // merge each vertical run of equal regions\r\n rowSpan: ({ data, rowIndex }) => {\r\n if (rowIndex > 0 && rows[rowIndex - 1].region === data.region) return 1 // covered\r\n let n = 1\r\n while (rows[rowIndex + n]?.region === data.region) n += 1\r\n return n\r\n } },\r\n { id: 'country', field: 'country' },\r\n { id: 'amount', field: 'amount' },\r\n ]\r\n\r\n // Recompute after sort / filter - indexes are display-row indexes.\r\n const merges = $derived(spansToMerges(rows, columns))\r\n</script>\r\n\r\n<div use:spreadsheetLayout={{ merges, columnOrder: columns.map((c) => c.id) }}>\r\n <SvGrid {data} {columns} />\r\n</div>\r\n```\r\n\r\n`colSpan` / `rowSpan` receive `CellSpanParams` (`{ data, rowIndex, columnId,\r\nvalue }`) and return the span count (1 = no span). `spansToMerges` handles\r\ncovered-cell bookkeeping so overlapping spans never double-emit.\r\n\r\n## Virtualization note\r\n\r\n`rowSpan` uses real `rowspan`, which needs the covered rows mounted in the\r\nrender window. For very large spanning grids, keep spans modest or disable row\r\nvirtualization (`virtualization={false}`) on that grid so the origin cell stays\r\nmounted while its covered rows are on screen.\r\n\r\n## See also\r\n\r\n- [Cell merging demo](../../../examples/src/demos/170-cell-merging.svelte)\r\n- [Row spanning](../rows/row-spanning.md)\r\n"
|
|
3222
3222
|
},
|
|
3223
3223
|
{
|
|
3224
3224
|
"slug": "help/columns/column-state",
|
|
@@ -3248,7 +3248,7 @@ export const docs = [
|
|
|
3248
3248
|
"slug": "help/comparison",
|
|
3249
3249
|
"path": "docs/help/comparison.md",
|
|
3250
3250
|
"title": "Comparison: SvGrid vs AG Grid vs TanStack Table",
|
|
3251
|
-
"markdown": "# Comparison: SvGrid vs AG Grid vs TanStack Table\r\n\r\nThe three projects solve overlapping problems, and the right choice\r\ndepends on the framework you ship on and your budget.\r\n\r\n## TL;DR\r\n\r\n| Project | Lives in | Ships | Bundle (typical) | License |\r\n| -------------------- | ---------------------------------------- | --------------------------------------- | ---------------- | ------------------ |\r\n| **SvGrid** | Svelte 5 | Headless core + Svelte render + Enterprise pack | ~2 KB headless / ~80 KB full (gzip) | MIT (Community) / commercial (Enterprise) |\r\n| **AG Grid Community**| React, Angular, Vue, plain JS | Full grid + renderer | ~340 KB | MIT |\r\n| **AG Grid Enterprise**| same | Adds pivot, integrated charts, server-side row model, more | ~600 KB+ | Commercial |\r\n| **TanStack Table** | React, Vue, Svelte, Solid, Qwik, Lit, JS | Headless engine **only** | ~12-14 KB | MIT |\r\n\r\n## When SvGrid is the right choice\r\n\r\n- You're on **Svelte 5** and want a grid that uses the runtime's idioms\r\n (snippets for cells, `$state` for data, `$derived` for aggregates) -\r\n not a React-port pretending to be Svelte.\r\n- You want a **headless core you can render yourself** AND a\r\n default-styled component for the 80% case. Most \"headless\" libraries\r\n make you write the markup; most \"monolith\" libraries make you fight\r\n the markup. SvGrid does both in one package.\r\n- You need **clean theming via CSS custom properties** and a documented\r\n `--sg-*` token surface, not a hard-coded class soup.\r\n- You ship under **strict CSP** (no `eval`, no `new Function`, no\r\n inline scripts). SvGrid runs clean; AG Grid Community does too.\r\n TanStack Table is engine-only so the question doesn't apply.\r\n- You want **SSR markup that is meaningful before hydration** (good\r\n first paint, SEO, SvelteKit `+page.server` integration). SvGrid +\r\n TanStack Table both qualify. AG Grid renders client-side.\r\n\r\n## When AG Grid is the right choice\r\n\r\n- You're on **React, Angular, or Vue**, not Svelte. SvGrid is\r\n Svelte-only.\r\n- You need **every grid feature shipped** out of the box: row drag,\r\n master-detail with built-in API, range selection, status bar,\r\n context menu, column tool panel, integrated charts (Enterprise),\r\n Excel-native pivot UI (Enterprise), server-side row model\r\n (Enterprise).\r\n- You need **enterprise commercial support** with SLAs. AG sells it;\r\n SvGrid Enterprise support is best-effort.\r\n\r\n## When TanStack Table is the right choice\r\n\r\n- You want a **rendering-framework-agnostic engine** so the same\r\n business logic powers React + Svelte + Solid surfaces in your\r\n monorepo.\r\n- You're already in the TanStack ecosystem (Query, Router, Form,\r\n Virtual) and want one mental model.\r\n- You're happy writing **all the markup yourself** - the row recycling,\r\n the keyboard map, the ARIA roles, the focus management, the\r\n drag-to-resize. That's the cost of \"engine only\".\r\n\r\n## Feature parity at a glance\r\n\r\n| | SvGrid Community | SvGrid Enterprise | AG Grid Community | AG Grid Enterprise | TanStack Table |\r\n| ------------------------------- | ---------------- | ---------- | ----------------- | ------------------ | -------------- |\r\n| Headless core (engine only) | ✓ | ✓ | - | - | ✓ |\r\n| Default render component | ✓ (Svelte 5) | ✓ | ✓ (each FW) | ✓ | - |\r\n| Sort (multi-column) | ✓ | ✓ | ✓ | ✓ | ✓ |\r\n| Filter menu (operator + facet) | ✓ | ✓ | ✓ | ✓ | - |\r\n| Filter row | ✓ | ✓ | ✓ | ✓ | - |\r\n| Pagination | ✓ | ✓ | ✓ | ✓ | ✓ |\r\n| Grouping + aggregation | ✓ | ✓ | basic | ✓ (advanced) | ✓ (engine) |\r\n| Tree / expand-collapse rows | ✓ | ✓ | basic | ✓ | ✓ |\r\n| Cell range selection + copy/paste | ✓ | ✓ | ✓ (Enterprise) | ✓ | - |\r\n| Inline editing (5 editor types) | ✓ | ✓ | ✓ | ✓ | - |\r\n| Column virtualization | ✓ | ✓ | ✓ | ✓ | - |\r\n| Row virtualization | ✓ | ✓ | ✓ | ✓ | - |\r\n| Column pinning (left/right) | ✓ | ✓ | ✓ | ✓ | - |\r\n| Fit-to-width with shrink | ✓ | ✓ | partial | ✓ | - |\r\n| WAI-ARIA grid pattern | ✓ | ✓ | ✓ | ✓ | - |\r\n| Server-side row model | external mode | external mode | - | ✓ (built-in) | external mode |\r\n| CSP-clean (no eval, no inline) | ✓ | ✓ | ✓ | ✓ | n/a |\r\n| Meaningful SSR markup | ✓ | ✓ | - | - | depends on FW |\r\n| Excel / PDF / CSV export | - | ✓ | - | ✓ (Enterprise) | - |\r\n| Excel / CSV import | - | ✓ | - | - | - |\r\n| AI assistant | - | ✓ (BYO provider) | - | - | - |\r\n| Pivot table | - | ✓ | - | ✓ | (custom) |\r\n| Integrated charts | - | - | - | ✓ | - |\r\n| Theming via CSS variables | ✓ (`--sg-*`) | ✓ | ✓ (theme builder) | ✓ | n/a |\r\n| Source-button per demo | ✓ (gallery) | ✓ | - | - | - |\r\n\r\n## Bundle size\r\n\r\nMeasured gzipped, with Svelte treated as a peer dependency and excluded\r\n(the bundlephobia convention):\r\n\r\n| @svgrid/grid path | Gzipped | Minified |\r\n| ----------------------------------------------- | ------- | -------- |\r\n| Headless core (`createGrid` + a row model) | ~2 KB | ~7 KB |\r\n| Full `<SvGrid>` render component | ~80 KB | ~293 KB |\r\n\r\nAdd ~9 KB gzipped for the render component's CSS. A further ~64 KB of\r\ncharts, date/time editors, menus, and export splits into `import()` chunks\r\nthat load on demand rather than shipping in your initial bundle. Re-measure\r\nany time with `node packages/grid/scripts/measure-size.mjs`; see the\r\n[bundle size reference](../reference/bundle-size.md).\r\n\r\nThe full render component is the whole grid - virtualization, Excel-style\r\nfilters, inline editing, grouping, tree, master/detail, and accessibility -\r\nin one import. For reference, the headless core is lighter than TanStack\r\nTable's headless engine (~12-14 KB), and the render component is a fraction\r\nof AG Grid Community (commonly cited around 340 KB minified). `@svgrid/enterprise`\r\nfeatures are separate subpath imports that lazy-load, so they add nothing to\r\nyour initial bundle until used.\r\n\r\n## Migrating from AG Grid\r\n\r\nThe most common starting point. See\r\n[Migrating from AG Grid](./migrating-from-ag-grid.md) for a\r\n30-minute, side-by-side translation of column defs, features,\r\nfiltering, editing, and the imperative API.\r\n\r\n## Migrating from TanStack Table\r\n\r\nThe map is one-to-one - SvGrid's headless core is API-compatible with\r\nTanStack Table's React adapter in 90% of cases. The big differences:\r\n\r\n- Replace `useReactTable(opts)` with `createSvGrid(opts)`. Identical\r\n state machine.\r\n- Replace `getCoreRowModel()` calls with the same name from\r\n `@svgrid/grid`.\r\n- The render layer changes - TanStack hands you `flexRender` + the\r\n row model; SvGrid lets you keep that headless approach OR drop in\r\n the default `<SvGrid>` component.\r\n\r\n## Pricing\r\n\r\nSvGrid Community is MIT - free for commercial use, no attribution\r\nrequired at runtime. SvGrid Enterprise is a paid license; see\r\n<https://svgrid.com/pricing/> for per-seat / per-app / multi-app tiers.\r\n\r\nAG Grid Community is MIT. AG Grid Enterprise pricing is on\r\nag-grid.com; expect a per-developer annual license plus a separate\r\ndeployment license for production.\r\n\r\nTanStack Table is MIT.\r\n\r\n## See also\r\n\r\n- [Why headless?](../why-headless.md) - the design decision behind\r\n SvGrid's two-layer architecture.\r\n- [Migrating from AG Grid](./migrating-from-ag-grid.md) - the\r\n practical recipe.\r\n- [Enterprise feature pack](../enterprise/README.md) - what SvGrid charges for.\r\n- [Missing features](./missing-features.md) - the honest gap list\r\n versus AG Grid Enterprise.\r\n\r\n## Frequently asked questions\r\n\r\n### What is the best data grid for Svelte 5?\r\n\r\nFor a Svelte-5-native grid with a batteries-included render component, SvGrid is\r\nbuilt around runes and snippets. TanStack Table is a strong headless-only\r\nchoice if you want to build the DOM layer yourself across frameworks. AG Grid is\r\nthe most feature-complete but lives in React/Angular/Vue first and is heavy to\r\nbridge into Svelte 5.\r\n\r\n### Is SvGrid a good AG Grid alternative?\r\n\r\nYes, for Svelte projects. SvGrid ships a much smaller bundle (~80 KB gzipped\r\nfor the full render component, ~2 KB headless) than AG Grid Community, is\r\nMIT-licensed for commercial use, and offers `@svgrid/enterprise` for\r\nexport/pivot/import at a per-developer price instead of AG Grid Enterprise's\r\nper-deployment licensing. It does not yet match every AG Grid Enterprise\r\nfeature - see the missing-features list for the honest gaps.\r\n\r\n### SvGrid vs TanStack Table - which should I pick?\r\n\r\nPick SvGrid if you want virtualization, Excel-style filters, selection, and\r\ninline editing working out of the box on Svelte 5. Pick TanStack Table if you\r\nwant a framework-agnostic headless engine and are happy to build the rendering,\r\nvirtualization, and editing UI yourself. Both are MIT-licensed.\r\n\r\n### How big is the SvGrid bundle?\r\n\r\nMeasured gzipped (Svelte excluded as a peer dependency): ~2 KB for the\r\nheadless core and ~80 KB for the full `<SvGrid>` render component (~293 KB\r\nminified), plus ~9 KB of CSS. Charts, date/time editors, menus, and export\r\nadd another ~64 KB that loads on demand rather than up front. Enterprise\r\nfeatures are separate, lazy-loaded subpath imports, so you ship only what\r\nyou import.\r\n"
|
|
3251
|
+
"markdown": "# Comparison: SvGrid vs AG Grid vs TanStack Table\r\n\r\nThe three projects solve overlapping problems, and the right choice\r\ndepends on the framework you ship on and your budget.\r\n\r\n## TL;DR\r\n\r\n| Project | Lives in | Ships | Bundle (typical) | License |\r\n| -------------------- | ---------------------------------------- | --------------------------------------- | ---------------- | ------------------ |\r\n| **SvGrid** | Svelte 5 | Headless core + Svelte render + Enterprise pack | ~2 KB headless / ~78 KB full (gzip) | MIT (Community) / commercial (Enterprise) |\r\n| **AG Grid Community**| React, Angular, Vue, plain JS | Full grid + renderer | ~340 KB | MIT |\r\n| **AG Grid Enterprise**| same | Adds pivot, integrated charts, server-side row model, more | ~600 KB+ | Commercial |\r\n| **TanStack Table** | React, Vue, Svelte, Solid, Qwik, Lit, JS | Headless engine **only** | ~12-14 KB | MIT |\r\n\r\n## When SvGrid is the right choice\r\n\r\n- You're on **Svelte 5** and want a grid that uses the runtime's idioms\r\n (snippets for cells, `$state` for data, `$derived` for aggregates) -\r\n not a React-port pretending to be Svelte.\r\n- You want a **headless core you can render yourself** AND a\r\n default-styled component for the 80% case. Most \"headless\" libraries\r\n make you write the markup; most \"monolith\" libraries make you fight\r\n the markup. SvGrid does both in one package.\r\n- You need **clean theming via CSS custom properties** and a documented\r\n `--sg-*` token surface, not a hard-coded class soup.\r\n- You ship under **strict CSP** (no `eval`, no `new Function`, no\r\n inline scripts). SvGrid runs clean; AG Grid Community does too.\r\n TanStack Table is engine-only so the question doesn't apply.\r\n- You want **SSR markup that is meaningful before hydration** (good\r\n first paint, SEO, SvelteKit `+page.server` integration). SvGrid +\r\n TanStack Table both qualify. AG Grid renders client-side.\r\n\r\n## When AG Grid is the right choice\r\n\r\n- You're on **React, Angular, or Vue**, not Svelte. SvGrid is\r\n Svelte-only.\r\n- You need **every grid feature shipped** out of the box: row drag,\r\n master-detail with built-in API, range selection, status bar,\r\n context menu, column tool panel, integrated charts (Enterprise),\r\n Excel-native pivot UI (Enterprise), server-side row model\r\n (Enterprise).\r\n- You need **enterprise commercial support** with SLAs. AG sells it;\r\n SvGrid Enterprise support is best-effort.\r\n\r\n## When TanStack Table is the right choice\r\n\r\n- You want a **rendering-framework-agnostic engine** so the same\r\n business logic powers React + Svelte + Solid surfaces in your\r\n monorepo.\r\n- You're already in the TanStack ecosystem (Query, Router, Form,\r\n Virtual) and want one mental model.\r\n- You're happy writing **all the markup yourself** - the row recycling,\r\n the keyboard map, the ARIA roles, the focus management, the\r\n drag-to-resize. That's the cost of \"engine only\".\r\n\r\n## Feature parity at a glance\r\n\r\n| | SvGrid Community | SvGrid Enterprise | AG Grid Community | AG Grid Enterprise | TanStack Table |\r\n| ------------------------------- | ---------------- | ---------- | ----------------- | ------------------ | -------------- |\r\n| Headless core (engine only) | ✓ | ✓ | - | - | ✓ |\r\n| Default render component | ✓ (Svelte 5) | ✓ | ✓ (each FW) | ✓ | - |\r\n| Sort (multi-column) | ✓ | ✓ | ✓ | ✓ | ✓ |\r\n| Filter menu (operator + facet) | ✓ | ✓ | ✓ | ✓ | - |\r\n| Filter row | ✓ | ✓ | ✓ | ✓ | - |\r\n| Pagination | ✓ | ✓ | ✓ | ✓ | ✓ |\r\n| Grouping + aggregation | ✓ | ✓ | basic | ✓ (advanced) | ✓ (engine) |\r\n| Tree / expand-collapse rows | ✓ | ✓ | basic | ✓ | ✓ |\r\n| Cell range selection + copy/paste | ✓ | ✓ | ✓ (Enterprise) | ✓ | - |\r\n| Inline editing (5 editor types) | ✓ | ✓ | ✓ | ✓ | - |\r\n| Column virtualization | ✓ | ✓ | ✓ | ✓ | - |\r\n| Row virtualization | ✓ | ✓ | ✓ | ✓ | - |\r\n| Column pinning (left/right) | ✓ | ✓ | ✓ | ✓ | - |\r\n| Fit-to-width with shrink | ✓ | ✓ | partial | ✓ | - |\r\n| WAI-ARIA grid pattern | ✓ | ✓ | ✓ | ✓ | - |\r\n| Server-side row model | external mode | external mode | - | ✓ (built-in) | external mode |\r\n| CSP-clean (no eval, no inline) | ✓ | ✓ | ✓ | ✓ | n/a |\r\n| Meaningful SSR markup | ✓ | ✓ | - | - | depends on FW |\r\n| Excel / PDF / CSV export | - | ✓ | - | ✓ (Enterprise) | - |\r\n| Excel / CSV import | - | ✓ | - | - | - |\r\n| AI assistant | - | ✓ (BYO provider) | - | - | - |\r\n| Pivot table | - | ✓ | - | ✓ | (custom) |\r\n| Integrated charts | - | - | - | ✓ | - |\r\n| Theming via CSS variables | ✓ (`--sg-*`) | ✓ | ✓ (theme builder) | ✓ | n/a |\r\n| Source-button per demo | ✓ (gallery) | ✓ | - | - | - |\r\n\r\n## Bundle size\r\n\r\nMeasured gzipped, with Svelte treated as a peer dependency and excluded\r\n(the bundlephobia convention):\r\n\r\n| @svgrid/grid path | Gzipped | Minified |\r\n| ----------------------------------------------- | ------- | -------- |\r\n| Headless core (`createGrid` + a row model) | ~2 KB | ~7 KB |\r\n| Full `<SvGrid>` render component | ~78 KB | ~340 KB |\r\n\r\nAdd ~9 KB gzipped for the render component's CSS. A further ~64 KB of\r\ncharts, date/time editors, menus, and export splits into `import()` chunks\r\nthat load on demand rather than shipping in your initial bundle. Re-measure\r\nany time with `node packages/grid/scripts/measure-size.mjs`; see the\r\n[bundle size reference](../reference/bundle-size.md).\r\n\r\nThe full render component is the whole grid - virtualization, Excel-style\r\nfilters, inline editing, grouping, tree, master/detail, and accessibility -\r\nin one import. For reference, the headless core is lighter than TanStack\r\nTable's headless engine (~12-14 KB), and the render component is a fraction\r\nof AG Grid Community (commonly cited around 340 KB minified). `@svgrid/enterprise`\r\nfeatures are separate subpath imports that lazy-load, so they add nothing to\r\nyour initial bundle until used.\r\n\r\n## Migrating from AG Grid\r\n\r\nThe most common starting point. See\r\n[Migrating from AG Grid](./migrating-from-ag-grid.md) for a\r\n30-minute, side-by-side translation of column defs, features,\r\nfiltering, editing, and the imperative API.\r\n\r\n## Migrating from TanStack Table\r\n\r\nThe map is one-to-one - SvGrid's headless core is API-compatible with\r\nTanStack Table's React adapter in 90% of cases. The big differences:\r\n\r\n- Replace `useReactTable(opts)` with `createSvGrid(opts)`. Identical\r\n state machine.\r\n- Replace `getCoreRowModel()` calls with the same name from\r\n `@svgrid/grid`.\r\n- The render layer changes - TanStack hands you `flexRender` + the\r\n row model; SvGrid lets you keep that headless approach OR drop in\r\n the default `<SvGrid>` component.\r\n\r\n## Pricing\r\n\r\nSvGrid Community is MIT - free for commercial use, no attribution\r\nrequired at runtime. SvGrid Enterprise is a paid license; see\r\n<https://svgrid.com/pricing/> for per-seat / per-app / multi-app tiers.\r\n\r\nAG Grid Community is MIT. AG Grid Enterprise pricing is on\r\nag-grid.com; expect a per-developer annual license plus a separate\r\ndeployment license for production.\r\n\r\nTanStack Table is MIT.\r\n\r\n## See also\r\n\r\n- [Why headless?](../why-headless.md) - the design decision behind\r\n SvGrid's two-layer architecture.\r\n- [Migrating from AG Grid](./migrating-from-ag-grid.md) - the\r\n practical recipe.\r\n- [Enterprise feature pack](../enterprise/README.md) - what SvGrid charges for.\r\n- [Missing features](./missing-features.md) - the honest gap list\r\n versus AG Grid Enterprise.\r\n\r\n## Frequently asked questions\r\n\r\n### What is the best data grid for Svelte 5?\r\n\r\nFor a Svelte-5-native grid with a batteries-included render component, SvGrid is\r\nbuilt around runes and snippets. TanStack Table is a strong headless-only\r\nchoice if you want to build the DOM layer yourself across frameworks. AG Grid is\r\nthe most feature-complete but lives in React/Angular/Vue first and is heavy to\r\nbridge into Svelte 5.\r\n\r\n### Is SvGrid a good AG Grid alternative?\r\n\r\nYes, for Svelte projects. SvGrid ships a much smaller bundle (~78 KB gzipped\r\nfor the full render component, ~2 KB headless) than AG Grid Community, is\r\nMIT-licensed for commercial use, and offers `@svgrid/enterprise` for\r\nexport/pivot/import at a per-developer price instead of AG Grid Enterprise's\r\nper-deployment licensing. It does not yet match every AG Grid Enterprise\r\nfeature - see the missing-features list for the honest gaps.\r\n\r\n### SvGrid vs TanStack Table - which should I pick?\r\n\r\nPick SvGrid if you want virtualization, Excel-style filters, selection, and\r\ninline editing working out of the box on Svelte 5. Pick TanStack Table if you\r\nwant a framework-agnostic headless engine and are happy to build the rendering,\r\nvirtualization, and editing UI yourself. Both are MIT-licensed.\r\n\r\n### How big is the SvGrid bundle?\r\n\r\nMeasured gzipped (Svelte excluded as a peer dependency): ~2 KB for the\r\nheadless core and ~78 KB for the full `<SvGrid>` render component (~340 KB\r\nminified), plus ~9 KB of CSS. Charts, date/time editors, menus, and export\r\nadd another ~64 KB that loads on demand rather than up front. Enterprise\r\nfeatures are separate, lazy-loaded subpath imports, so you ship only what\r\nyou import.\r\n"
|
|
3252
3252
|
},
|
|
3253
3253
|
{
|
|
3254
3254
|
"slug": "help/conditional-form-schema",
|
|
@@ -3380,7 +3380,7 @@ export const docs = [
|
|
|
3380
3380
|
"slug": "help/filtering/set-filter",
|
|
3381
3381
|
"path": "docs/help/filtering/set-filter.md",
|
|
3382
3382
|
"title": "Set filter",
|
|
3383
|
-
"markdown": "# Set filter\n\nA \"set filter\" (a.k.a. value filter, list filter) shows a checklist of all distinct values in a column and lets the user pick which to include. It's what you reach for to filter `status` to \"active OR pending\", or `department` to a few specific teams.\n\n\n\n<div data-docs-demo=\"111-set-filter-advanced\" data-height=\"640\"></div>\n\nThree patterns are supported, all wired through the imperative `api.setFacetFilter(columnId, values | null)`:\n\n| Mode | UI | Built-in? | When to use |\n| --- | --- | --- | --- |\n| **Excel-style** | Column-menu Values tab | Yes | Most columns. Distinct values are enumerated from the loaded data; search box + select-all + clear ship out of the box. |\n| **Async** | Side panel that loads values from a server endpoint | User-land (one screen of code) | The column has too many distinct values to list at full data load. Lazy-fetch on demand. |\n| **Tree-list** | Hierarchical checkboxes (parent ↔ descendants) | User-land (cascade logic) | Nested taxonomies: Region → Country → City; Department → Team → Employee. |\n\n## 1. Excel-style (built-in)\n\nThe column menu's Values tab is the default set filter. Click the funnel icon on any header to open it.\n\nWhat you get without writing any code:\n\n- Distinct values from the current dataset.\n- Type-ahead search.\n- Select-all / clear toggle.\n- Mixed-state preserved as the user scrolls.\n\nProgrammatic equivalent for \"remember and restore\":\n\n```ts\n// Capture the current set\nconst filters = api.getFilters() // includes selectedValues per column\n\n// Restore later (e.g. saved view, URL persistence)\napi.setFacetFilter('status', ['active', 'pending'])\napi.setFacetFilter('status', null) // clear\n```\n\n## 2. Async values (server-loaded)\n\nWhen a column has tens of thousands of distinct values, you don't want to pre-render them all. Pattern: render a side panel beside the grid, load values from the server on first open, drive the grid via `api.setFacetFilter`:\n\n```ts\nlet state = $state<{\n status: 'idle' | 'loading' | 'ready' | 'error'\n values: string[]\n}>({
|
|
3383
|
+
"markdown": "# Set filter\n\nA \"set filter\" (a.k.a. value filter, list filter) shows a checklist of all distinct values in a column and lets the user pick which to include. It's what you reach for to filter `status` to \"active OR pending\", or `department` to a few specific teams.\n\n\n\n<div data-docs-demo=\"111-set-filter-advanced\" data-height=\"640\"></div>\n\nThree patterns are supported, all wired through the imperative `api.setFacetFilter(columnId, values | null)`:\n\n| Mode | UI | Built-in? | When to use |\n| --- | --- | --- | --- |\n| **Excel-style** | Column-menu Values tab | Yes | Most columns. Distinct values are enumerated from the loaded data; search box + select-all + clear ship out of the box. |\n| **Async** | Side panel that loads values from a server endpoint | User-land (one screen of code) | The column has too many distinct values to list at full data load. Lazy-fetch on demand. |\n| **Tree-list** | Hierarchical checkboxes (parent ↔ descendants) | User-land (cascade logic) | Nested taxonomies: Region → Country → City; Department → Team → Employee. |\n\n## 1. Excel-style (built-in)\n\nThe column menu's Values tab is the default set filter. Click the funnel icon on any header to open it.\n\nWhat you get without writing any code:\n\n- Distinct values from the current dataset.\n- Type-ahead search.\n- Select-all / clear toggle.\n- Mixed-state preserved as the user scrolls.\n\nProgrammatic equivalent for \"remember and restore\":\n\n```ts\n// Capture the current set\nconst filters = api.getFilters() // includes selectedValues per column\n\n// Restore later (e.g. saved view, URL persistence)\napi.setFacetFilter('status', ['active', 'pending'])\napi.setFacetFilter('status', null) // clear\n```\n\n## 2. Async values (server-loaded)\n\nWhen a column has tens of thousands of distinct values, you don't want to pre-render them all. Pattern: render a side panel beside the grid, load values from the server on first open, drive the grid via `api.setFacetFilter`:\n\n```ts\nlet state = $state<{\n status: 'idle' | 'loading' | 'ready' | 'error'\n values: string[]\n}>({ status: 'idle', values: [] })\nlet selected = $state<Set<string>>(new Set())\n\nasync function loadValues() {\n state = { status: 'loading', values: [] }\n const res = await fetch('/api/orders/customers')\n const values = await res.json()\n state = { status: 'ready', values }\n}\n\nfunction toggle(value: string) {\n const next = new Set(selected)\n if (next.has(value)) next.delete(value); else next.add(value)\n selected = next\n api.setFacetFilter('customer', next.size === 0 ? null : Array.from(next))\n}\n```\n\nKey benefits vs the Excel tab:\n\n- **Lazy load** - no client-side enumeration for millions of distinct values.\n- **Server can apply policy** - hide values the current user shouldn't see.\n- **Static label / dynamic value** - the panel can show pretty labels while the filter applies on the underlying id.\n\nSee demo 111 (\"Async values\" card) for a complete implementation with loading state, retry, and search.\n\n## 3. Tree-list (hierarchical)\n\nFor nested taxonomies, render a tree of checkboxes. Parent checked = all descendants checked. Some descendants checked = parent shows the \"indeterminate\" state. On any change, compute the leaf set and apply it to the column.\n\n```ts\n// Taxonomy: Region → Country → City\nconst GEO = {\n Americas: { 'United States': ['New York', 'San Francisco'], Canada: ['Toronto'] },\n EMEA: { Germany: ['Berlin', 'Munich'], France: ['Paris'] },\n}\n\nlet selectedCities = $state<Set<string>>(new Set())\n\nfunction toggleNode(node: TreeNode, on: boolean) {\n const next = new Set(selectedCities)\n for (const city of node.cities) { // pre-computed leaf set\n if (on) next.add(city); else next.delete(city)\n }\n selectedCities = next\n api.setFacetFilter('city', next.size === 0 ? null : Array.from(next))\n}\n\nfunction isChecked(node: TreeNode): boolean {\n return node.cities.every((c) => selectedCities.has(c))\n}\nfunction isPartial(node: TreeNode): boolean {\n const hits = node.cities.filter((c) => selectedCities.has(c)).length\n return hits > 0 && hits < node.cities.length\n}\n```\n\nThe grid is unaware of the hierarchy - it just receives a flat list of allowed leaf values. The hierarchy lives in your panel UI.\n\nSee demo 111 (\"Tree\" card) and [demo 102: Tree checkbox cascade](#/demos/102-tree-checkbox-cascade) for the cascade-logic recipe.\n\n## API surface\n\n```ts {nocheck}\ntype SvGridApi<…> = {\n // Set a multi-select set filter. Pass null or [] to clear.\n setFacetFilter(columnId: string, values: ReadonlyArray<string> | null): void\n\n // Read the current filters (includes the facet selection per column).\n getFilters(): Record<string, { operator: ..., selectedValues?: string[] }>\n\n // Snapshot of the rows the grid currently displays - useful when your\n // filter UI needs to count matches without re-running the search.\n getDisplayedRows(): ReadonlyArray<TData>\n}\n```\n\n## See also\n\n- Demo 111: [Set filter - tree / async / Excel mode](#/demos/111-set-filter-advanced)\n- Demo 102: [Tree checkbox cascade](#/demos/102-tree-checkbox-cascade) - the cascade recipe used inside the tree filter\n- [`api.setFacetFilter`](../api-reference.md#setfacetfilter)\n- [Filter API overview](./filter-api.md)\n"
|
|
3384
3384
|
},
|
|
3385
3385
|
{
|
|
3386
3386
|
"slug": "help/filtering/text-filter",
|
|
@@ -3476,13 +3476,13 @@ export const docs = [
|
|
|
3476
3476
|
"slug": "help/mcp-server",
|
|
3477
3477
|
"path": "docs/help/mcp-server.md",
|
|
3478
3478
|
"title": "MCP server",
|
|
3479
|
-
"markdown": "# MCP server\r\n\r\nThe sv-grid MCP server lets AI clients (Claude Desktop, Cursor, Zed,\r\nContinue, custom agents) query the documentation, scaffold column\r\ndefinitions, and preview exports - all grounded in the same schemas\r\nthe library ships with. No API key required; everything runs locally\r\nagainst your installed copy.\r\n\r\n\r\n\r\n> **What is MCP?** Model Context Protocol is the open standard\r\n> ([modelcontextprotocol.io](https://modelcontextprotocol.io)) for\r\n> exposing tools / resources / prompts to LLM clients. sv-grid ships an\r\n> MCP server out of the box so the model your team already uses can\r\n> \"see\" the grid without you having to copy-paste docs into prompts.\r\n\r\n## Install\r\n\r\n```bash\r\n# Inside any project that already depends on @svgrid/grid\r\npnpm add -D @sv-grid/mcp-server\r\n```\r\n\r\nThe server is a Node binary. Run it on demand from the package's\r\n`bin` field - no daemon to maintain.\r\n\r\n## Wire it into your AI client\r\n\r\n### Claude Desktop\r\n\r\nEdit `~/Library/Application Support/Claude/claude_desktop_config.json`\r\n(macOS) or `%APPDATA%\\Claude\\claude_desktop_config.json` (Windows):\r\n\r\n```json\r\n{\r\n \"mcpServers\": {\r\n \"sv-grid\": {\r\n \"command\": \"npx\",\r\n \"args\": [\"-y\", \"@sv-grid/mcp-server\"]\r\n }\r\n }\r\n}\r\n```\r\n\r\nRestart Claude Desktop. Type `@sv-grid` in any chat to confirm the\r\ntools are exposed.\r\n\r\n### Cursor\r\n\r\n`Settings → MCP → Add new MCP server`:\r\n\r\n```json\r\n{ \"command\": \"npx\", \"args\": [\"-y\", \"@sv-grid/mcp-server\"] }\r\n```\r\n\r\n### Zed\r\n\r\n`~/.config/zed/settings.json`:\r\n\r\n```json\r\n{\r\n \"context_servers\": {\r\n \"sv-grid\": { \"command\": { \"path\": \"npx\", \"args\": [\"-y\", \"@sv-grid/mcp-server\"] } }\r\n }\r\n}\r\n```\r\n\r\n### Custom agents (OpenAI Agents SDK, Anthropic SDK, LangChain)\r\n\r\nPoint your client's MCP transport at:\r\n\r\n```\r\nnpx -y @sv-grid/mcp-server\r\n```\r\n\r\nAny client that speaks MCP stdio works.\r\n\r\n## Tools exposed\r\n\r\nThe server registers six tools. All return structured JSON; none\r\nrequire an API key or network access.\r\n\r\n### `searchDocs`\r\n\r\nGround the model in the doc set without dumping the whole corpus.\r\n\r\n```ts\r\nsearchDocs({ query: string, limit?: number }):\r\n Array<{ path, title, summary, score, snippet }>\r\n```\r\n\r\nBacked by the same `docs.json` manifest you can fetch directly.\r\n\r\n### `getDocPage`\r\n\r\nPull the full markdown of one page by URL or path.\r\n\r\n```ts\r\ngetDocPage({ path: '/help/pivot.md' }): { title, source, demoIds }\r\n```\r\n\r\n### `scaffoldColumns`\r\n\r\nGenerate a `ColumnDef[]` from a sample row. Picks reasonable widths,\r\ninferred editor types, sensible header labels, and format options for\r\nnumbers / currencies / ISO dates.\r\n\r\n```ts\r\nscaffoldColumns({\r\n sampleRow: { id: 'r1', sellDate: '2026-05-12', price: 1499.99, currency: 'USD' },\r\n inferFormat?: boolean, // default true\r\n language?: 'ts' | 'js', // default 'ts'\r\n})\r\n// → { code: string, columns: ColumnDef[] }\r\n```\r\n\r\n### `validateColumns`\r\n\r\nCheck a `ColumnDef[]` payload against `column-def.json`. Returns the\r\nlist of issues with file / line hints (when the input is a code\r\nstring). Useful for agents that generate columns and want a self-check\r\nbefore showing the result.\r\n\r\n```ts\r\nvalidateColumns({ columns: ColumnDef[] | string }):\r\n { valid: boolean, issues: Array<{ path, message, severity }> }\r\n```\r\n\r\n### `previewExport`\r\n\r\nDry-run an `api.exportData({...})` call. Returns the rows + header\r\nlayout the exporter WOULD write, without actually triggering a\r\ndownload. Useful when an agent is composing a multi-sheet export and\r\nwants to verify column ordering before committing.\r\n\r\n```ts\r\npreviewExport({ format: 'xlsx', rows: [...], columns: [...] }):\r\n { sheets: Array<{ label, header, rows }> }\r\n```\r\n\r\n### `listDemos`\r\n\r\nReturns every demo in `examples/src/demos/` with its title, blurb,\r\ncategory, source path, and the prompt sidecar (see\r\n[LLM grounding](./llm-grounding.md)).\r\n\r\n```ts\r\nlistDemos({ category?: string }):\r\n Array<{ id, title, blurb, category, source, prompt }>\r\n```\r\n\r\n## Resources exposed\r\n\r\nIn addition to tools, the server exposes three MCP **resources** -\r\nread-only documents the client can browse:\r\n\r\n| URI | Content |\r\n| ------------------------------ | ------------------------------------------------- |\r\n| `svgrid://docs/llms.txt` | Topic map (see [llms.txt](/llms.txt)) |\r\n| `svgrid://docs/llms-full.txt` | Concatenated full text of every doc |\r\n| `svgrid://docs/manifest` | `docs.json` route manifest |\r\n| `svgrid://schemas/column-def` | JSON Schema for `ColumnDef` |\r\n| `svgrid://schemas/svgrid-options` | JSON Schema for `<SvGrid>` props |\r\n| `svgrid://schemas/export-options` | JSON Schema for `api.exportData({...})` |\r\n\r\n## Prompts exposed\r\n\r\nPre-built MCP prompts you can invoke directly from a chat:\r\n\r\n- **`/svgrid:scaffold-grid`** - paste a sample row, get a complete\r\n `<SvGrid>` + `tableFeatures` + `ColumnDef[]` setup\r\n- **`/svgrid:refactor-to-pivot`** - hand it a flat-grid component, get\r\n a pivot-grid version\r\n- **`/svgrid:wire-server-side`** - convert client-side data to a\r\n server-side adapter with sort / filter / paginate round-trips\r\n\r\n## Verifying it works\r\n\r\nAfter wiring the server, ask your model: *\"What MCP tools do you have\r\nfrom sv-grid?\"* You should see all six tools listed. If not, check\r\nyour client's MCP log; the most common issue is `npx` not being on\r\nPATH (use the absolute path to the binary instead).\r\n\r\n## Security model\r\n\r\n- The server runs **locally**. No telemetry, no outbound network calls.\r\n- File reads are scoped to your project's `node_modules/@sv-grid/*`\r\n and any `docs/` folder you explicitly pass via the `--docs <dir>` flag.\r\n- `scaffoldColumns` and `previewExport` are pure functions - they\r\n inspect input and emit text. They never write to disk or fetch from\r\n the network.\r\n- See [security](./security.md) for the general supply-chain posture.\r\n\r\n## Building your own MCP integrations\r\n\r\nThe same `docs.json` + JSON Schemas + `llms.txt` files the server uses\r\nare also accessible directly from your docs site\r\n([https://svgrid.com](https://svgrid.com)):\r\n\r\n```ts\r\nconst docs = await fetch('https://svgrid.com/docs.json').then((r) => r.json())\r\nconst schemas = await fetch('https://svgrid.com/schemas/index.json').then((r) => r.json())\r\nconst llms = await fetch('https://svgrid.com/llms-full.txt').then((r) => r.text())\r\n```\r\n\r\nIf you don't want to run the MCP server, building these into your\r\nagent's system prompt gives ~80% of the same value.\r\n\r\n## See also\r\n\r\n- [LLM grounding](./llm-grounding.md) - the same files used by the MCP server, but documented for direct LLM consumption\r\n- [Agents](./agents.md) - how to build an AI agent that drives the live grid\r\n- [AI assistant](./ai.md) - the in-grid AI features (filter / smart-fill / classify / summarise), free in @svgrid/grid\r\n\r\n## Frequently asked questions\r\n\r\n### What is the sv-grid MCP server?\r\n\r\nA Model Context Protocol server that lets AI clients (Claude Desktop, Cursor,\r\nZed, Continue, custom agents) query SvGrid's documentation, scaffold column\r\ndefinitions, and preview exports - all grounded in the schemas the library\r\nships with, so the model answers from current facts instead of guessing.\r\n\r\n### Do I need an API key to run it?\r\n\r\nNo. The MCP server runs locally against your installed copy of SvGrid. There is\r\nno key and no external call.\r\n\r\n### How does it help AI assistants write better SvGrid code?\r\n\r\nIt exposes example sources, the docs, and the API reference as MCP tools, so the\r\nassistant retrieves accurate, version-pinned answers rather than hallucinating\r\nan API from training data.\r\n"
|
|
3479
|
+
"markdown": "# MCP server\r\n\r\nThe sv-grid MCP server lets AI clients (Claude Desktop, Cursor, Zed,\r\nContinue, custom agents) query the documentation, scaffold column\r\ndefinitions, and preview exports - all grounded in the same schemas\r\nthe library ships with. No API key required; everything runs locally\r\nagainst your installed copy.\r\n\r\n\r\n\r\n> **What is MCP?** Model Context Protocol is the open standard\r\n> ([modelcontextprotocol.io](https://modelcontextprotocol.io)) for\r\n> exposing tools / resources / prompts to LLM clients. sv-grid ships an\r\n> MCP server out of the box so the model your team already uses can\r\n> \"see\" the grid without you having to copy-paste docs into prompts.\r\n\r\n## Install\r\n\r\n```bash\r\n# Inside any project that already depends on @svgrid/grid\r\npnpm add -D @sv-grid/mcp-server\r\n```\r\n\r\nThe server is a Node binary. Run it on demand from the package's\r\n`bin` field - no daemon to maintain.\r\n\r\n## Wire it into your AI client\r\n\r\n### Claude Desktop\r\n\r\nEdit `~/Library/Application Support/Claude/claude_desktop_config.json`\r\n(macOS) or `%APPDATA%\\Claude\\claude_desktop_config.json` (Windows):\r\n\r\n```json\r\n{\r\n \"mcpServers\": {\r\n \"sv-grid\": {\r\n \"command\": \"npx\",\r\n \"args\": [\"-y\", \"@sv-grid/mcp-server\"]\r\n }\r\n }\r\n}\r\n```\r\n\r\nRestart Claude Desktop. Type `@sv-grid` in any chat to confirm the\r\ntools are exposed.\r\n\r\n### Cursor\r\n\r\n`Settings → MCP → Add new MCP server`:\r\n\r\n```json\r\n{ \"command\": \"npx\", \"args\": [\"-y\", \"@sv-grid/mcp-server\"] }\r\n```\r\n\r\n### Zed\r\n\r\n`~/.config/zed/settings.json`:\r\n\r\n```json\r\n{\r\n \"context_servers\": {\r\n \"sv-grid\": { \"command\": { \"path\": \"npx\", \"args\": [\"-y\", \"@sv-grid/mcp-server\"] } }\r\n }\r\n}\r\n```\r\n\r\n### Custom agents (OpenAI Agents SDK, Anthropic SDK, LangChain)\r\n\r\nPoint your client's MCP transport at:\r\n\r\n```\r\nnpx -y @sv-grid/mcp-server\r\n```\r\n\r\nAny client that speaks MCP stdio works.\r\n\r\n## Tools exposed\r\n\r\nThe server registers six tools. All return structured JSON; none\r\nrequire an API key or network access.\r\n\r\n### `searchDocs`\r\n\r\nGround the model in the doc set without dumping the whole corpus.\r\n\r\n```ts\r\nsearchDocs({ query: string, limit?: number }):\r\n Array<{ path, title, summary, score, snippet }>\r\n```\r\n\r\nBacked by the same `docs.json` manifest you can fetch directly.\r\n\r\n### `getDocPage`\r\n\r\nPull the full markdown of one page by URL or path.\r\n\r\n```ts\r\ngetDocPage({ path: '/help/pivot.md' }): { title, source, demoIds }\r\n```\r\n\r\n### `scaffoldColumns`\r\n\r\nGenerate a `ColumnDef[]` from a sample row. Picks reasonable widths,\r\ninferred editor types, sensible header labels, and format options for\r\nnumbers / currencies / ISO dates.\r\n\r\n```ts {nocheck}\r\nscaffoldColumns({\r\n sampleRow: { id: 'r1', sellDate: '2026-05-12', price: 1499.99, currency: 'USD' },\r\n inferFormat?: boolean, // default true\r\n language?: 'ts' | 'js', // default 'ts'\r\n})\r\n// → { code: string, columns: ColumnDef[] }\r\n```\r\n\r\n### `validateColumns`\r\n\r\nCheck a `ColumnDef[]` payload against `column-def.json`. Returns the\r\nlist of issues with file / line hints (when the input is a code\r\nstring). Useful for agents that generate columns and want a self-check\r\nbefore showing the result.\r\n\r\n```ts\r\nvalidateColumns({ columns: ColumnDef[] | string }):\r\n { valid: boolean, issues: Array<{ path, message, severity }> }\r\n```\r\n\r\n### `previewExport`\r\n\r\nDry-run an `api.exportData({...})` call. Returns the rows + header\r\nlayout the exporter WOULD write, without actually triggering a\r\ndownload. Useful when an agent is composing a multi-sheet export and\r\nwants to verify column ordering before committing.\r\n\r\n```ts\r\npreviewExport({ format: 'xlsx', rows: [...], columns: [...] }):\r\n { sheets: Array<{ label, header, rows }> }\r\n```\r\n\r\n### `listDemos`\r\n\r\nReturns every demo in `examples/src/demos/` with its title, blurb,\r\ncategory, source path, and the prompt sidecar (see\r\n[LLM grounding](./llm-grounding.md)).\r\n\r\n```ts\r\nlistDemos({ category?: string }):\r\n Array<{ id, title, blurb, category, source, prompt }>\r\n```\r\n\r\n## Resources exposed\r\n\r\nIn addition to tools, the server exposes three MCP **resources** -\r\nread-only documents the client can browse:\r\n\r\n| URI | Content |\r\n| ------------------------------ | ------------------------------------------------- |\r\n| `svgrid://docs/llms.txt` | Topic map (see [llms.txt](/llms.txt)) |\r\n| `svgrid://docs/llms-full.txt` | Concatenated full text of every doc |\r\n| `svgrid://docs/manifest` | `docs.json` route manifest |\r\n| `svgrid://schemas/column-def` | JSON Schema for `ColumnDef` |\r\n| `svgrid://schemas/svgrid-options` | JSON Schema for `<SvGrid>` props |\r\n| `svgrid://schemas/export-options` | JSON Schema for `api.exportData({...})` |\r\n\r\n## Prompts exposed\r\n\r\nPre-built MCP prompts you can invoke directly from a chat:\r\n\r\n- **`/svgrid:scaffold-grid`** - paste a sample row, get a complete\r\n `<SvGrid>` + `tableFeatures` + `ColumnDef[]` setup\r\n- **`/svgrid:refactor-to-pivot`** - hand it a flat-grid component, get\r\n a pivot-grid version\r\n- **`/svgrid:wire-server-side`** - convert client-side data to a\r\n server-side adapter with sort / filter / paginate round-trips\r\n\r\n## Verifying it works\r\n\r\nAfter wiring the server, ask your model: *\"What MCP tools do you have\r\nfrom sv-grid?\"* You should see all six tools listed. If not, check\r\nyour client's MCP log; the most common issue is `npx` not being on\r\nPATH (use the absolute path to the binary instead).\r\n\r\n## Security model\r\n\r\n- The server runs **locally**. No telemetry, no outbound network calls.\r\n- File reads are scoped to your project's `node_modules/@sv-grid/*`\r\n and any `docs/` folder you explicitly pass via the `--docs <dir>` flag.\r\n- `scaffoldColumns` and `previewExport` are pure functions - they\r\n inspect input and emit text. They never write to disk or fetch from\r\n the network.\r\n- See [security](./security.md) for the general supply-chain posture.\r\n\r\n## Building your own MCP integrations\r\n\r\nThe same `docs.json` + JSON Schemas + `llms.txt` files the server uses\r\nare also accessible directly from your docs site\r\n([https://svgrid.com](https://svgrid.com)):\r\n\r\n```ts\r\nconst docs = await fetch('https://svgrid.com/docs.json').then((r) => r.json())\r\nconst schemas = await fetch('https://svgrid.com/schemas/index.json').then((r) => r.json())\r\nconst llms = await fetch('https://svgrid.com/llms-full.txt').then((r) => r.text())\r\n```\r\n\r\nIf you don't want to run the MCP server, building these into your\r\nagent's system prompt gives ~80% of the same value.\r\n\r\n## See also\r\n\r\n- [LLM grounding](./llm-grounding.md) - the same files used by the MCP server, but documented for direct LLM consumption\r\n- [Agents](./agents.md) - how to build an AI agent that drives the live grid\r\n- [AI assistant](./ai.md) - the in-grid AI features (filter / smart-fill / classify / summarise), free in @svgrid/grid\r\n\r\n## Frequently asked questions\r\n\r\n### What is the sv-grid MCP server?\r\n\r\nA Model Context Protocol server that lets AI clients (Claude Desktop, Cursor,\r\nZed, Continue, custom agents) query SvGrid's documentation, scaffold column\r\ndefinitions, and preview exports - all grounded in the schemas the library\r\nships with, so the model answers from current facts instead of guessing.\r\n\r\n### Do I need an API key to run it?\r\n\r\nNo. The MCP server runs locally against your installed copy of SvGrid. There is\r\nno key and no external call.\r\n\r\n### How does it help AI assistants write better SvGrid code?\r\n\r\nIt exposes example sources, the docs, and the API reference as MCP tools, so the\r\nassistant retrieves accurate, version-pinned answers rather than hallucinating\r\nan API from training data.\r\n"
|
|
3480
3480
|
},
|
|
3481
3481
|
{
|
|
3482
3482
|
"slug": "help/migrating-from-ag-grid",
|
|
3483
3483
|
"path": "docs/help/migrating-from-ag-grid.md",
|
|
3484
3484
|
"title": "Migrating from AG Grid to SvGrid",
|
|
3485
|
-
"markdown": "# Migrating from AG Grid to SvGrid\r\n\r\nIf you tried AG Grid on a Svelte 5 project - via `ag-grid-svelte`, the\r\nold `ag-grid-community/svelte`, or a hand-rolled wrapper - you probably\r\nhit the same friction everyone hits: the bridge between AG Grid's\r\nReact/Angular-first API and Svelte 5 runes is brittle, the bundle is\r\nheavy, and the Enterprise pricing only makes sense at scale.\r\n\r\nThis page is a 30-minute migration recipe from AG Grid to SvGrid. It\r\ncovers what maps 1:1, what's different by design, and what you'll lose.\r\nWe tell you when **not** to switch at the bottom.\r\n\r\n## TL;DR\r\n\r\n| | AG Grid Community | AG Grid Enterprise | SvGrid Community | @svgrid/enterprise |\r\n| --- | --- | --- | --- | --- |\r\n| **License** | MIT | Commercial (~$999/dev/yr) | **MIT** | $599/dev/yr (single app) or $999/dev/yr (multi app) |\r\n| **Svelte 5 native** | ❌ (wrapper) | ❌ (wrapper) | ✅ | ✅ |\r\n| **Bundle (gzipped)** | ~250 KB | ~400 KB | ~2 KB headless / ~80 KB full | lazy-loaded subpaths |\r\n| **Sorting / filtering / grouping** | ✅ | ✅ | ✅ | (in Community) |\r\n| **Master/detail, tree, range select** | ❌ Enterprise only | ✅ | ✅ (free) | (in Community) |\r\n| **Excel export** | ❌ | ✅ Enterprise | ❌ | ✅ |\r\n| **PDF / CSV / TSV / HTML export** | ❌ | Partial | ❌ | ✅ |\r\n| **Print view** | ❌ | ❌ | ❌ | ✅ |\r\n| **Set filter / Excel-style filter menu** | ❌ Enterprise | ✅ | ✅ (free) | (in Community) |\r\n\r\n**SvGrid Community gives you most of AG Grid Enterprise's features for\r\nfree**, and `@svgrid/enterprise` adds the export + print pack for ~40%\r\nless than AG Grid Enterprise. The trade-offs are Svelte-only and a much\r\nsmaller ecosystem.\r\n\r\n## Mental model - what changes\r\n\r\nAG Grid is one big object you configure declaratively. SvGrid is a\r\n**headless engine** (`createSvGrid`) with an optional **render\r\ncomponent** (`<SvGrid>`) on top - the same split TanStack Table made\r\npopular. You can use either layer; most projects use the render\r\ncomponent.\r\n\r\n```svelte\r\n<!-- AG Grid (via a Svelte wrapper) -->\r\n<AgGridSvelte\r\n gridOptions={{\r\n rowData: rows,\r\n columnDefs: columns,\r\n onGridReady: (params) => (gridApi = params.api),\r\n }}\r\n/>\r\n\r\n<!-- SvGrid -->\r\n<SvGrid\r\n data={rows}\r\n columns={columns}\r\n features={features}\r\n onApiReady={(api) => (gridApi = api)}\r\n/>\r\n```\r\n\r\nThree things to note:\r\n\r\n1. **No giant `gridOptions` blob.** Each capability is a top-level prop.\r\n2. **Features are opt-in.** You pass a `features` object built with\r\n `tableFeatures({...})` - only the features you list ship JS.\r\n3. **`onApiReady`** gives you a typed `SvGridApi` that is roughly the\r\n AG Grid `gridApi` equivalent (see the API-mapping table below).\r\n\r\n## Column definitions - direct translation\r\n\r\nThe shapes are similar enough that you can usually translate by hand\r\nwithout thinking too hard.\r\n\r\n```ts\r\n// AG Grid\r\nconst columnDefs: ColDef[] = [\r\n { field: 'name', headerName: 'Name', sortable: true, filter: true, width: 200 },\r\n { field: 'price', headerName: 'Price', type: 'numericColumn',\r\n valueFormatter: ({ value }) => `$${value.toFixed(2)}` },\r\n { field: 'date', headerName: 'Date',\r\n valueGetter: ({ data }) => new Date(data.date).toISOString().slice(0, 10) },\r\n { field: 'status', headerName: 'Status',\r\n cellRenderer: StatusCellRenderer,\r\n cellRendererParams: { onChange: handleStatusChange } },\r\n]\r\n```\r\n\r\n```ts\r\n// SvGrid\r\nimport { renderComponent, type ColumnDef } from '@svgrid/grid'\r\nimport StatusCell from './StatusCell.svelte'\r\n\r\nconst columns: ColumnDef<typeof features, Row>[] = [\r\n { field: 'name', header: 'Name', width: 200 }, // sortable + filterable by default\r\n { field: 'price', header: 'Price',\r\n format: { type: 'currency', currency: 'USD' } },\r\n { field: 'date', header: 'Date',\r\n format: { type: 'date', pattern: 'y-m-d' } },\r\n { field: 'status', header: 'Status',\r\n cell: renderComponent(StatusCell, (ctx) => ({\r\n value: ctx.getValue(),\r\n onChange: handleStatusChange,\r\n })),\r\n },\r\n]\r\n```\r\n\r\n### Property mapping\r\n\r\n| AG Grid | SvGrid | Notes |\r\n| --- | --- | --- |\r\n| `field` | `field` | Same. |\r\n| `headerName` | `header` | Accepts a string or a snippet/component. |\r\n| `width` | `width` | Same. |\r\n| `minWidth` / `maxWidth` | `minWidth` / `maxWidth` | Same. |\r\n| `sortable: true` | (default) | Sorting is on when `rowSortingFeature` is registered. |\r\n| `filter: true` | (default) | Filtering is on when `columnFilteringFeature` is registered. |\r\n| `valueFormatter` | `format: { ... }` | Built-in types: `number`, `currency`, `percent`, `date`. For custom, use `cell`. |\r\n| `valueGetter` | `fieldFn` | Returns the value for sorting/filtering. |\r\n| `cellRenderer` + `cellRendererParams` | `cell: renderComponent(C, ctx => props)` | One call, type-checked. |\r\n| `cellEditor: 'agTextCellEditor'` | `editorType: 'text'` | Built-in: `text`, `number`, `checkbox`, `date`. |\r\n| `editable: true` | `enableInlineEditing` prop on `<SvGrid>` | Per-grid, not per-column. (Per-column control on the roadmap.) |\r\n| `pinned: 'left'` / `'right'` | Right-click column menu → Pin | Set programmatically via the api. |\r\n| `rowGroup: true` | Via `setGroupBy([colId])` | See Grouping below. |\r\n| `aggFunc: 'sum'` | `aggregation: 'sum'` | Built-in: `sum`, `avg`, `min`, `max`, `count`. |\r\n\r\n## Feature registration - the one new thing\r\n\r\nAG Grid auto-enables most features; you turn them off. SvGrid is the\r\nopposite - features are opt-in. The result is a smaller bundle.\r\n\r\n```ts\r\nimport {\r\n tableFeatures,\r\n rowSortingFeature,\r\n columnFilteringFeature,\r\n columnGroupingFeature,\r\n rowExpandingFeature,\r\n rowPaginationFeature,\r\n rowSelectionFeature,\r\n} from '@svgrid/grid'\r\n\r\nconst features = tableFeatures({\r\n rowSortingFeature,\r\n columnFilteringFeature,\r\n rowSelectionFeature,\r\n // omit any you don't need - their code won't ship\r\n})\r\n```\r\n\r\nPass `features` to `<SvGrid>` once. From then on the grid behaves like\r\nAG Grid's `enableSorting`, `enableFilter`, `rowSelection`, etc. are all\r\non for the registered features.\r\n\r\n## API mapping (`gridApi` → `SvGridApi`)\r\n\r\nYou get the SvGrid API from `onApiReady` (equivalent to AG Grid's\r\n`onGridReady`).\r\n\r\n| AG Grid `gridApi.X()` | SvGrid `api.X()` |\r\n| --- | --- |\r\n| `setRowData(rows)` | (declarative - just update `data` prop) |\r\n| `addRow(row)` / `applyTransaction({ add: [row] })` | `api.addRow(row)` / `api.addRows(rows)` |\r\n| `applyTransaction({ remove: [row] })` | `api.removeRow(rowIndex)` |\r\n| `getValue(colId, rowNode)` | `api.getCellValue(rowIndex, columnId)` |\r\n| `setValue(...)` | `api.setCellValue(rowIndex, columnId, value)` |\r\n| `setColumnVisible(colId, visible)` | `api.setColumnVisible(columnId, visible)` |\r\n| `getSortModel()` / `setSortModel()` | `api.setSort(columnId, 'asc'\\|'desc'\\|null)` |\r\n| `setFilterModel({...})` | `api.setFilter(columnId, { operator, value })` |\r\n| `getDisplayedRowAtIndex(i)` / `forEachNodeAfterFilterAndSort(...)` | `api.getDisplayedRows()` |\r\n| `getModel()` (raw rows) | `api.getData()` |\r\n\r\n## Common patterns\r\n\r\n### Sorting + filtering + pagination (the 80% case)\r\n\r\n```svelte\r\n<script lang=\"ts\">\r\n import {\r\n SvGrid,\r\n tableFeatures,\r\n rowSortingFeature,\r\n columnFilteringFeature,\r\n rowPaginationFeature,\r\n } from '@svgrid/grid'\r\n\r\n const features = tableFeatures({\r\n rowSortingFeature,\r\n columnFilteringFeature,\r\n rowPaginationFeature,\r\n })\r\n</script>\r\n\r\n<SvGrid\r\n data={rows}\r\n columns={columns}\r\n features={features}\r\n showPagination\r\n showColumnFilters\r\n/>\r\n```\r\n\r\nAG Grid equivalent: `gridOptions: { defaultColDef: { sortable: true, filter: true }, pagination: true }`.\r\n\r\n### Cell editing with persistence\r\n\r\n```svelte\r\n<script lang=\"ts\">\r\n function onCellValueChange(e: { rowIndex: number; columnId: string; value: unknown }) {\r\n // Persist however you like (fetch to backend, optimistic local update, etc.)\r\n console.log('cell changed', e)\r\n }\r\n</script>\r\n\r\n<SvGrid\r\n data={rows}\r\n columns={columns}\r\n features={features}\r\n enableInlineEditing\r\n onCellValueChange={onCellValueChange}\r\n/>\r\n```\r\n\r\nAG Grid equivalent: `onCellValueChanged: ({ data, colDef, newValue, oldValue }) => ...`.\r\nSvGrid's event payload is column-id + row-index based rather than node-based; the row data is yours\r\nto mutate (or not) on the `rows` array you passed in.\r\n\r\n### Grouping + aggregation\r\n\r\n```svelte\r\n<script lang=\"ts\">\r\n import {\r\n SvGrid,\r\n tableFeatures,\r\n columnGroupingFeature,\r\n rowSortingFeature,\r\n rowExpandingFeature,\r\n } from '@svgrid/grid'\r\n\r\n const features = tableFeatures({\r\n rowSortingFeature,\r\n columnGroupingFeature,\r\n rowExpandingFeature,\r\n })\r\n\r\n const columns = [\r\n { field: 'department', header: 'Department' },\r\n { field: 'team', header: 'Team' },\r\n { field: 'salary', header: 'Salary', aggregation: 'sum',\r\n format: { type: 'currency', currency: 'USD' } },\r\n ]\r\n\r\n function setGroup(api) {\r\n api.setGroupBy(['department', 'team'])\r\n }\r\n</script>\r\n\r\n<SvGrid\r\n data={rows}\r\n columns={columns}\r\n features={features}\r\n showGroupingControls\r\n onApiReady={setGroup}\r\n/>\r\n```\r\n\r\nAG Grid Enterprise's `rowGroupPanelShow: 'always'` + `aggFunc: 'sum'` translates 1:1.\r\n\r\n### Master / detail\r\n\r\nAG Grid Enterprise feature; **free in SvGrid Community**. See\r\n[demo 08](https://svgrid.com/demos/08-tree-and-master-detail/) for the exact pattern.\r\n\r\n### Server-side data\r\n\r\nAG Grid uses an `IServerSideDatasource` interface. SvGrid uses\r\n`externalSort` + `externalFilter` props - your code keeps full control\r\nover the query, and the grid records UI state but doesn't re-order rows\r\nlocally. See [demo 09](https://svgrid.com/demos/09-server-side/).\r\n\r\n### Excel / PDF export\r\n\r\nAG Grid: `gridApi.exportDataAsExcel({...})` (Enterprise-only).\r\nSvGrid: install `@svgrid/enterprise`, call `api.exportData({ format: 'xlsx', ... })`. See [Data export and printing](./export.md).\r\n\r\n```ts\r\nimport { installEnterprise, setLicenseKey } from '@svgrid/enterprise'\r\nsetLicenseKey('SVENTERPRISE-...') // your Enterprise key\r\n\r\n// inside onApiReady:\r\nconst pro = installEnterprise(api)\r\nawait pro.exportData({ format: 'xlsx', filename: 'orders' })\r\n```\r\n\r\n## Gotchas - things that don't translate directly\r\n\r\n### 1. Per-column `editable: true`\r\nSvGrid v1.0 toggles editing at the grid level (`enableInlineEditing`).\r\nPer-column editability is on the roadmap - until then, you can gate\r\nedits in your `onCellValueChange` handler.\r\n\r\n### 2. Column drag-to-reorder\r\nSvGrid v1.0 supports column reorder via the API (`setColumnOrder`), not\r\nheader drag. Built-in header drag is on the roadmap. Most teams don't\r\nmiss it - it's a power-user feature.\r\n\r\n### 3. AG Grid `valueGetter` chains\r\nAG Grid's `valueGetter` can read other column values via the API. In\r\nSvGrid, `fieldFn` only receives the row; if you need cross-column\r\ncomputed values, do it in the cell renderer with `ctx.row.original` or\r\ncompute the derived value upstream and store it in the row.\r\n\r\n### 4. `cellClass` / `rowClass` callbacks\r\nOn the roadmap. For now, render a wrapper element in your `cell` snippet\r\nwith the conditional class.\r\n\r\n### 5. The Status Bar / Side Bar / Tool Panels\r\nAG Grid's chrome (status bar with row count, side bar with filters and\r\ncolumns panels) doesn't exist in SvGrid - build it as plain Svelte\r\nmarkup around the grid. Most teams build their own anyway because\r\nAG Grid's defaults rarely match a polished design system.\r\n\r\n### 6. Set filter (the Excel-style funnel popup)\r\nSvGrid ships an Excel-style filter menu (free in Community). API surface\r\nis similar but not identical - see [Set filter](./filtering/set-filter.md).\r\n\r\n## When NOT to migrate\r\n\r\nBe honest. Stay on AG Grid if you:\r\n\r\n- **Use multiple frameworks** - AG Grid has React, Angular, Vue, Solid, Qwik, vanilla adapters. SvGrid is Svelte-only.\r\n- **Use AG Grid's integrated charts** - those depend on AG Grid's chart engine; SvGrid has no equivalent.\r\n- **Depend on AG Grid pivoting** - not in SvGrid's roadmap.\r\n- **Are mid-project and shipping in <2 weeks** - the migration is a few hours per grid, but only do it when you have buffer.\r\n- **Have a Svelte 4 codebase you can't upgrade** - SvGrid requires Svelte 5 runes. (Consider [htmlelements.com](https://www.htmlelements.com) for vanilla / multi-framework.)\r\n\r\nIf none of those apply: switching saves you $400-$1000 per dev per year,\r\ncuts your bundle by 200+ KB, and gives you a Svelte-native API that\r\nplays well with runes.\r\n\r\n## Step-by-step migration\r\n\r\nA typical migration of a single grid takes 1-3 hours:\r\n\r\n1. **Install** - `pnpm add @svgrid/grid` (and `@svgrid/enterprise` if you need export).\r\n2. **Translate columnDefs** - use the mapping table above. Most columns are 1:1.\r\n3. **Wrap features** - figure out which AG Grid features you actually use; register only those in `tableFeatures({...})`.\r\n4. **Swap the component** - `<AgGridSvelte gridOptions={...}>` → `<SvGrid data={rows} columns={columns} features={features}>`.\r\n5. **Move event handlers** - AG Grid `onCellValueChanged` → SvGrid `onCellValueChange` (signature differs slightly, see above).\r\n6. **Move API calls** - AG Grid `gridApi.X()` → SvGrid `api.X()` per the API table.\r\n7. **Test interactions** - sort, filter, edit, select. Most \"just works.\"\r\n8. **Remove `ag-grid-*` packages** - `pnpm remove ag-grid-community ag-grid-svelte` etc. Inspect your bundle to confirm the 200+ KB drop.\r\n\r\n## Need help migrating?\r\n\r\nEnterprise customers get **migration help included** with the support plan\r\n(architecture review, port one grid for you as a reference). Email\r\n`support@jqwidgets.com` after purchase, or `sales@jqwidgets.com` for\r\npre-sales questions.\r\n\r\n## See also\r\n\r\n- [Getting started](../getting-started.md) - full SvGrid walkthrough\r\n- [Why headless?](../why-headless.md) - the headless / render-component split\r\n- [Data export and printing](./export.md) - the `@svgrid/enterprise` feature pack\r\n- [SvGrid vs AG Grid comparison page](https://svgrid.com/compare/ag-grid/)\r\n\r\n## Frequently asked questions\r\n\r\n### Is SvGrid a drop-in replacement for AG Grid in Svelte?\r\n\r\nNot a literal drop-in - there is no `ag-grid-svelte` shim to swap. But the\r\nconcepts map closely: column definitions, row models, sorting, filtering,\r\ngrouping, and an imperative API all have direct SvGrid equivalents, so most\r\nteams port a grid in 30 minutes to a day. It is a configuration translation,\r\nnot a rewrite.\r\n\r\n### What is the SvGrid equivalent of AG Grid Enterprise?\r\n\r\n`@svgrid/enterprise`. It adds Excel/PDF/CSV/TSV/HTML export, a printable view, pivot\r\ntables, data import, and AI helpers. It is licensed per developer\r\n($599 single-app / $999 multi-app), not per deployment, and the Community\r\npackage is MIT-licensed and free for commercial use.\r\n\r\n### Does SvGrid use Svelte 5 runes, or is it a wrapper?\r\n\r\nIt is Svelte-5-native. State is `$state` / `$derived` / `$effect` and cells\r\nrender through Svelte snippets - there is no React or Angular core underneath\r\nand no framework bridge to keep in sync.\r\n\r\n### Will my AG Grid bundle size shrink?\r\n\r\nAlmost always. SvGrid's full render component is ~80 KB gzipped (or ~2 KB\r\nfor the headless core) versus a much heavier AG Grid Community bundle, and you\r\nonly add `@svgrid/enterprise` features you actually use - so you ship a fraction of\r\nthe JavaScript.\r\n"
|
|
3485
|
+
"markdown": "# Migrating from AG Grid to SvGrid\r\n\r\nIf you tried AG Grid on a Svelte 5 project - via `ag-grid-svelte`, the\r\nold `ag-grid-community/svelte`, or a hand-rolled wrapper - you probably\r\nhit the same friction everyone hits: the bridge between AG Grid's\r\nReact/Angular-first API and Svelte 5 runes is brittle, the bundle is\r\nheavy, and the Enterprise pricing only makes sense at scale.\r\n\r\nThis page is a 30-minute migration recipe from AG Grid to SvGrid. It\r\ncovers what maps 1:1, what's different by design, and what you'll lose.\r\nWe tell you when **not** to switch at the bottom.\r\n\r\n## TL;DR\r\n\r\n| | AG Grid Community | AG Grid Enterprise | SvGrid Community | @svgrid/enterprise |\r\n| --- | --- | --- | --- | --- |\r\n| **License** | MIT | Commercial (~$999/dev/yr) | **MIT** | $599/dev/yr (single app) or $999/dev/yr (multi app) |\r\n| **Svelte 5 native** | ❌ (wrapper) | ❌ (wrapper) | ✅ | ✅ |\r\n| **Bundle (gzipped)** | ~250 KB | ~400 KB | ~2 KB headless / ~78 KB full | lazy-loaded subpaths |\r\n| **Sorting / filtering / grouping** | ✅ | ✅ | ✅ | (in Community) |\r\n| **Master/detail, tree, range select** | ❌ Enterprise only | ✅ | ✅ (free) | (in Community) |\r\n| **Excel export** | ❌ | ✅ Enterprise | ❌ | ✅ |\r\n| **PDF / CSV / TSV / HTML export** | ❌ | Partial | ❌ | ✅ |\r\n| **Print view** | ❌ | ❌ | ❌ | ✅ |\r\n| **Set filter / Excel-style filter menu** | ❌ Enterprise | ✅ | ✅ (free) | (in Community) |\r\n\r\n**SvGrid Community gives you most of AG Grid Enterprise's features for\r\nfree**, and `@svgrid/enterprise` adds the export + print pack for ~40%\r\nless than AG Grid Enterprise. The trade-offs are Svelte-only and a much\r\nsmaller ecosystem.\r\n\r\n## Mental model - what changes\r\n\r\nAG Grid is one big object you configure declaratively. SvGrid is a\r\n**headless engine** (`createSvGrid`) with an optional **render\r\ncomponent** (`<SvGrid>`) on top - the same split TanStack Table made\r\npopular. You can use either layer; most projects use the render\r\ncomponent.\r\n\r\n```svelte\r\n<!-- AG Grid (via a Svelte wrapper) -->\r\n<AgGridSvelte\r\n gridOptions={{\r\n rowData: rows,\r\n columnDefs: columns,\r\n onGridReady: (params) => (gridApi = params.api),\r\n }}\r\n/>\r\n\r\n<!-- SvGrid -->\r\n<SvGrid\r\n data={rows}\r\n columns={columns}\r\n features={features}\r\n onApiReady={(api) => (gridApi = api)}\r\n/>\r\n```\r\n\r\nThree things to note:\r\n\r\n1. **No giant `gridOptions` blob.** Each capability is a top-level prop.\r\n2. **Features are opt-in.** You pass a `features` object built with\r\n `tableFeatures({...})` - only the features you list ship JS.\r\n3. **`onApiReady`** gives you a typed `SvGridApi` that is roughly the\r\n AG Grid `gridApi` equivalent (see the API-mapping table below).\r\n\r\n## Column definitions - direct translation\r\n\r\nThe shapes are similar enough that you can usually translate by hand\r\nwithout thinking too hard.\r\n\r\n```ts\r\n// AG Grid\r\nconst columnDefs: ColDef[] = [\r\n { field: 'name', headerName: 'Name', sortable: true, filter: true, width: 200 },\r\n { field: 'price', headerName: 'Price', type: 'numericColumn',\r\n valueFormatter: ({ value }) => `$${value.toFixed(2)}` },\r\n { field: 'date', headerName: 'Date',\r\n valueGetter: ({ data }) => new Date(data.date).toISOString().slice(0, 10) },\r\n { field: 'status', headerName: 'Status',\r\n cellRenderer: StatusCellRenderer,\r\n cellRendererParams: { onChange: handleStatusChange } },\r\n]\r\n```\r\n\r\n```ts\r\n// SvGrid\r\nimport { renderComponent, type ColumnDef } from '@svgrid/grid'\r\nimport StatusCell from './StatusCell.svelte'\r\n\r\nconst columns: ColumnDef<typeof features, Row>[] = [\r\n { field: 'name', header: 'Name', width: 200 }, // sortable + filterable by default\r\n { field: 'price', header: 'Price',\r\n format: { type: 'currency', currency: 'USD' } },\r\n { field: 'date', header: 'Date',\r\n format: { type: 'date', pattern: 'y-m-d' } },\r\n { field: 'status', header: 'Status',\r\n cell: renderComponent(StatusCell, (ctx) => ({\r\n value: ctx.getValue(),\r\n onChange: handleStatusChange,\r\n })),\r\n },\r\n]\r\n```\r\n\r\n### Property mapping\r\n\r\n| AG Grid | SvGrid | Notes |\r\n| --- | --- | --- |\r\n| `field` | `field` | Same. |\r\n| `headerName` | `header` | Accepts a string or a snippet/component. |\r\n| `width` | `width` | Same. |\r\n| `minWidth` / `maxWidth` | `minWidth` / `maxWidth` | Same. |\r\n| `sortable: true` | (default) | Sorting is on when `rowSortingFeature` is registered. |\r\n| `filter: true` | (default) | Filtering is on when `columnFilteringFeature` is registered. |\r\n| `valueFormatter` | `format: { ... }` | Built-in types: `number`, `currency`, `percent`, `date`. For custom, use `cell`. |\r\n| `valueGetter` | `fieldFn` | Returns the value for sorting/filtering. |\r\n| `cellRenderer` + `cellRendererParams` | `cell: renderComponent(C, ctx => props)` | One call, type-checked. |\r\n| `cellEditor: 'agTextCellEditor'` | `editorType: 'text'` | Built-in: `text`, `number`, `checkbox`, `date`. |\r\n| `editable: true` | `enableInlineEditing` prop on `<SvGrid>` | Per-grid, not per-column. (Per-column control on the roadmap.) |\r\n| `pinned: 'left'` / `'right'` | Right-click column menu → Pin | Set programmatically via the api. |\r\n| `rowGroup: true` | Via `setGroupBy([colId])` | See Grouping below. |\r\n| `aggFunc: 'sum'` | `aggregation: 'sum'` | Built-in: `sum`, `avg`, `min`, `max`, `count`. |\r\n\r\n## Feature registration - the one new thing\r\n\r\nAG Grid auto-enables most features; you turn them off. SvGrid is the\r\nopposite - features are opt-in. The result is a smaller bundle.\r\n\r\n```ts\r\nimport {\r\n tableFeatures,\r\n rowSortingFeature,\r\n columnFilteringFeature,\r\n columnGroupingFeature,\r\n rowExpandingFeature,\r\n rowPaginationFeature,\r\n rowSelectionFeature,\r\n} from '@svgrid/grid'\r\n\r\nconst features = tableFeatures({\r\n rowSortingFeature,\r\n columnFilteringFeature,\r\n rowSelectionFeature,\r\n // omit any you don't need - their code won't ship\r\n})\r\n```\r\n\r\nPass `features` to `<SvGrid>` once. From then on the grid behaves like\r\nAG Grid's `enableSorting`, `enableFilter`, `rowSelection`, etc. are all\r\non for the registered features.\r\n\r\n## API mapping (`gridApi` → `SvGridApi`)\r\n\r\nYou get the SvGrid API from `onApiReady` (equivalent to AG Grid's\r\n`onGridReady`).\r\n\r\n| AG Grid `gridApi.X()` | SvGrid `api.X()` |\r\n| --- | --- |\r\n| `setRowData(rows)` | (declarative - just update `data` prop) |\r\n| `addRow(row)` / `applyTransaction({ add: [row] })` | `api.addRow(row)` / `api.addRows(rows)` |\r\n| `applyTransaction({ remove: [row] })` | `api.removeRow(rowIndex)` |\r\n| `getValue(colId, rowNode)` | `api.getCellValue(rowIndex, columnId)` |\r\n| `setValue(...)` | `api.setCellValue(rowIndex, columnId, value)` |\r\n| `setColumnVisible(colId, visible)` | `api.setColumnVisible(columnId, visible)` |\r\n| `getSortModel()` / `setSortModel()` | `api.setSort(columnId, 'asc'\\|'desc'\\|null)` |\r\n| `setFilterModel({...})` | `api.setFilter(columnId, { operator, value })` |\r\n| `getDisplayedRowAtIndex(i)` / `forEachNodeAfterFilterAndSort(...)` | `api.getDisplayedRows()` |\r\n| `getModel()` (raw rows) | `api.getData()` |\r\n\r\n## Common patterns\r\n\r\n### Sorting + filtering + pagination (the 80% case)\r\n\r\n```svelte\r\n<script lang=\"ts\">\r\n import {\r\n SvGrid,\r\n tableFeatures,\r\n rowSortingFeature,\r\n columnFilteringFeature,\r\n rowPaginationFeature,\r\n } from '@svgrid/grid'\r\n\r\n const features = tableFeatures({\r\n rowSortingFeature,\r\n columnFilteringFeature,\r\n rowPaginationFeature,\r\n })\r\n</script>\r\n\r\n<SvGrid\r\n data={rows}\r\n columns={columns}\r\n features={features}\r\n showPagination\r\n showColumnFilters\r\n/>\r\n```\r\n\r\nAG Grid equivalent: `gridOptions: { defaultColDef: { sortable: true, filter: true }, pagination: true }`.\r\n\r\n### Cell editing with persistence\r\n\r\n```svelte\r\n<script lang=\"ts\">\r\n function onCellValueChange(e: { rowIndex: number; columnId: string; value: unknown }) {\r\n // Persist however you like (fetch to backend, optimistic local update, etc.)\r\n console.log('cell changed', e)\r\n }\r\n</script>\r\n\r\n<SvGrid\r\n data={rows}\r\n columns={columns}\r\n features={features}\r\n enableInlineEditing\r\n onCellValueChange={onCellValueChange}\r\n/>\r\n```\r\n\r\nAG Grid equivalent: `onCellValueChanged: ({ data, colDef, newValue, oldValue }) => ...`.\r\nSvGrid's event payload is column-id + row-index based rather than node-based; the row data is yours\r\nto mutate (or not) on the `rows` array you passed in.\r\n\r\n### Grouping + aggregation\r\n\r\n```svelte\r\n<script lang=\"ts\">\r\n import {\r\n SvGrid,\r\n tableFeatures,\r\n columnGroupingFeature,\r\n rowSortingFeature,\r\n rowExpandingFeature,\r\n } from '@svgrid/grid'\r\n\r\n const features = tableFeatures({\r\n rowSortingFeature,\r\n columnGroupingFeature,\r\n rowExpandingFeature,\r\n })\r\n\r\n const columns = [\r\n { field: 'department', header: 'Department' },\r\n { field: 'team', header: 'Team' },\r\n { field: 'salary', header: 'Salary', aggregation: 'sum',\r\n format: { type: 'currency', currency: 'USD' } },\r\n ]\r\n\r\n function setGroup(api) {\r\n api.setGroupBy(['department', 'team'])\r\n }\r\n</script>\r\n\r\n<SvGrid\r\n data={rows}\r\n columns={columns}\r\n features={features}\r\n showGroupingControls\r\n onApiReady={setGroup}\r\n/>\r\n```\r\n\r\nAG Grid Enterprise's `rowGroupPanelShow: 'always'` + `aggFunc: 'sum'` translates 1:1.\r\n\r\n### Master / detail\r\n\r\nAG Grid Enterprise feature; **free in SvGrid Community**. See\r\n[demo 08](https://svgrid.com/demos/08-tree-and-master-detail/) for the exact pattern.\r\n\r\n### Server-side data\r\n\r\nAG Grid uses an `IServerSideDatasource` interface. SvGrid uses\r\n`externalSort` + `externalFilter` props - your code keeps full control\r\nover the query, and the grid records UI state but doesn't re-order rows\r\nlocally. See [demo 09](https://svgrid.com/demos/09-server-side/).\r\n\r\n### Excel / PDF export\r\n\r\nAG Grid: `gridApi.exportDataAsExcel({...})` (Enterprise-only).\r\nSvGrid: install `@svgrid/enterprise`, call `api.exportData({ format: 'xlsx', ... })`. See [Data export and printing](./export.md).\r\n\r\n```ts\r\nimport { installEnterprise, setLicenseKey } from '@svgrid/enterprise'\r\nsetLicenseKey('SVENTERPRISE-...') // your Enterprise key\r\n\r\n// inside onApiReady:\r\nconst pro = installEnterprise(api)\r\nawait pro.exportData({ format: 'xlsx', filename: 'orders' })\r\n```\r\n\r\n## Gotchas - things that don't translate directly\r\n\r\n### 1. Per-column `editable: true`\r\nSvGrid v1.0 toggles editing at the grid level (`enableInlineEditing`).\r\nPer-column editability is on the roadmap - until then, you can gate\r\nedits in your `onCellValueChange` handler.\r\n\r\n### 2. Column drag-to-reorder\r\nSvGrid v1.0 supports column reorder via the API (`setColumnOrder`), not\r\nheader drag. Built-in header drag is on the roadmap. Most teams don't\r\nmiss it - it's a power-user feature.\r\n\r\n### 3. AG Grid `valueGetter` chains\r\nAG Grid's `valueGetter` can read other column values via the API. In\r\nSvGrid, `fieldFn` only receives the row; if you need cross-column\r\ncomputed values, do it in the cell renderer with `ctx.row.original` or\r\ncompute the derived value upstream and store it in the row.\r\n\r\n### 4. `cellClass` / `rowClass` callbacks\r\nOn the roadmap. For now, render a wrapper element in your `cell` snippet\r\nwith the conditional class.\r\n\r\n### 5. The Status Bar / Side Bar / Tool Panels\r\nAG Grid's chrome (status bar with row count, side bar with filters and\r\ncolumns panels) doesn't exist in SvGrid - build it as plain Svelte\r\nmarkup around the grid. Most teams build their own anyway because\r\nAG Grid's defaults rarely match a polished design system.\r\n\r\n### 6. Set filter (the Excel-style funnel popup)\r\nSvGrid ships an Excel-style filter menu (free in Community). API surface\r\nis similar but not identical - see [Set filter](./filtering/set-filter.md).\r\n\r\n## When NOT to migrate\r\n\r\nBe honest. Stay on AG Grid if you:\r\n\r\n- **Use multiple frameworks** - AG Grid has React, Angular, Vue, Solid, Qwik, vanilla adapters. SvGrid is Svelte-only.\r\n- **Use AG Grid's integrated charts** - those depend on AG Grid's chart engine; SvGrid has no equivalent.\r\n- **Depend on AG Grid pivoting** - not in SvGrid's roadmap.\r\n- **Are mid-project and shipping in <2 weeks** - the migration is a few hours per grid, but only do it when you have buffer.\r\n- **Have a Svelte 4 codebase you can't upgrade** - SvGrid requires Svelte 5 runes. (Consider [htmlelements.com](https://www.htmlelements.com) for vanilla / multi-framework.)\r\n\r\nIf none of those apply: switching saves you $400-$1000 per dev per year,\r\ncuts your bundle by 200+ KB, and gives you a Svelte-native API that\r\nplays well with runes.\r\n\r\n## Step-by-step migration\r\n\r\nA typical migration of a single grid takes 1-3 hours:\r\n\r\n1. **Install** - `pnpm add @svgrid/grid` (and `@svgrid/enterprise` if you need export).\r\n2. **Translate columnDefs** - use the mapping table above. Most columns are 1:1.\r\n3. **Wrap features** - figure out which AG Grid features you actually use; register only those in `tableFeatures({...})`.\r\n4. **Swap the component** - `<AgGridSvelte gridOptions={...}>` → `<SvGrid data={rows} columns={columns} features={features}>`.\r\n5. **Move event handlers** - AG Grid `onCellValueChanged` → SvGrid `onCellValueChange` (signature differs slightly, see above).\r\n6. **Move API calls** - AG Grid `gridApi.X()` → SvGrid `api.X()` per the API table.\r\n7. **Test interactions** - sort, filter, edit, select. Most \"just works.\"\r\n8. **Remove `ag-grid-*` packages** - `pnpm remove ag-grid-community ag-grid-svelte` etc. Inspect your bundle to confirm the 200+ KB drop.\r\n\r\n## Need help migrating?\r\n\r\nEnterprise customers get **migration help included** with the support plan\r\n(architecture review, port one grid for you as a reference). Email\r\n`support@jqwidgets.com` after purchase, or `sales@jqwidgets.com` for\r\npre-sales questions.\r\n\r\n## See also\r\n\r\n- [Getting started](../getting-started.md) - full SvGrid walkthrough\r\n- [Why headless?](../why-headless.md) - the headless / render-component split\r\n- [Data export and printing](./export.md) - the `@svgrid/enterprise` feature pack\r\n- [SvGrid vs AG Grid comparison page](https://svgrid.com/compare/ag-grid/)\r\n\r\n## Frequently asked questions\r\n\r\n### Is SvGrid a drop-in replacement for AG Grid in Svelte?\r\n\r\nNot a literal drop-in - there is no `ag-grid-svelte` shim to swap. But the\r\nconcepts map closely: column definitions, row models, sorting, filtering,\r\ngrouping, and an imperative API all have direct SvGrid equivalents, so most\r\nteams port a grid in 30 minutes to a day. It is a configuration translation,\r\nnot a rewrite.\r\n\r\n### What is the SvGrid equivalent of AG Grid Enterprise?\r\n\r\n`@svgrid/enterprise`. It adds Excel/PDF/CSV/TSV/HTML export, a printable view, pivot\r\ntables, data import, and AI helpers. It is licensed per developer\r\n($599 single-app / $999 multi-app), not per deployment, and the Community\r\npackage is MIT-licensed and free for commercial use.\r\n\r\n### Does SvGrid use Svelte 5 runes, or is it a wrapper?\r\n\r\nIt is Svelte-5-native. State is `$state` / `$derived` / `$effect` and cells\r\nrender through Svelte snippets - there is no React or Angular core underneath\r\nand no framework bridge to keep in sync.\r\n\r\n### Will my AG Grid bundle size shrink?\r\n\r\nAlmost always. SvGrid's full render component is ~78 KB gzipped (or ~2 KB\r\nfor the headless core) versus a much heavier AG Grid Community bundle, and you\r\nonly add `@svgrid/enterprise` features you actually use - so you ship a fraction of\r\nthe JavaScript.\r\n"
|
|
3486
3486
|
},
|
|
3487
3487
|
{
|
|
3488
3488
|
"slug": "help/migrating-from-devextreme",
|
|
@@ -3614,7 +3614,7 @@ export const docs = [
|
|
|
3614
3614
|
"slug": "help/production",
|
|
3615
3615
|
"path": "docs/help/production.md",
|
|
3616
3616
|
"title": "Production deployment",
|
|
3617
|
-
"markdown": "# Production deployment\r\n\r\nThe checklist that turns \"it works on my laptop\" into \"it ships\". One\r\npage per concern; each concern is one paragraph + the code that\r\nmatters.\r\n\r\n\r\n\r\n<div data-docs-demo=\"22-admin-template\" data-height=\"540\"></div>\r\n\r\n## 1. Pin your versions\r\n\r\n```json\r\n{\r\n \"dependencies\": {\r\n \"@svgrid/grid\": \"1.0.0\",\r\n \"@svgrid/enterprise\": \"1.0.0\"\r\n }\r\n}\r\n```\r\n\r\nPre-1.0, prefer exact pins (no `^`, no `~`). The\r\n[changelog](../changelog.md) annotates breaking changes; the\r\n[API stability page](./api-stability.md) names which exports are\r\nunder the semver promise.\r\n\r\n## 2. Bundle size: what actually ships\r\n\r\nMeasured gzipped, with Svelte excluded as a peer dependency:\r\n\r\n| What you import | Gzipped | Minified |\r\n| ----------------------------------------------------- | ------- | -------- |\r\n| Headless core (`createGrid` + `createCoreRowModel`) | ~2 KB | ~7 KB |\r\n| Full `<SvGrid>` render component (everything) | ~
|
|
3617
|
+
"markdown": "# Production deployment\r\n\r\nThe checklist that turns \"it works on my laptop\" into \"it ships\". One\r\npage per concern; each concern is one paragraph + the code that\r\nmatters.\r\n\r\n\r\n\r\n<div data-docs-demo=\"22-admin-template\" data-height=\"540\"></div>\r\n\r\n## 1. Pin your versions\r\n\r\n```json\r\n{\r\n \"dependencies\": {\r\n \"@svgrid/grid\": \"1.0.0\",\r\n \"@svgrid/enterprise\": \"1.0.0\"\r\n }\r\n}\r\n```\r\n\r\nPre-1.0, prefer exact pins (no `^`, no `~`). The\r\n[changelog](../changelog.md) annotates breaking changes; the\r\n[API stability page](./api-stability.md) names which exports are\r\nunder the semver promise.\r\n\r\n## 2. Bundle size: what actually ships\r\n\r\nMeasured gzipped, with Svelte excluded as a peer dependency:\r\n\r\n| What you import | Gzipped | Minified |\r\n| ----------------------------------------------------- | ------- | -------- |\r\n| Headless core (`createGrid` + `createCoreRowModel`) | ~2 KB | ~7 KB |\r\n| Full `<SvGrid>` render component (everything) | ~78 KB | ~340 KB |\r\n\r\nAdd ~9 KB gzipped for the render component's CSS. Charts, date/time\r\neditors, menus, and export split into `import()` chunks (~64 KB total)\r\nthat load on demand. Re-measure with `pnpm size`.\r\n\r\nThe `<SvGrid>` component is batteries-included: virtualization, Excel-style\r\nfilters, inline editing, grouping, tree, master/detail, and accessibility are\r\nall in that one import. For a smaller footprint, use the headless core and\r\nrender your own markup, registering only the features you need.\r\n\r\nEnterprise adds per feature you import:\r\n\r\n| Enterprise module | Approx KB | Peer deps |\r\n| ------------------- | --------- | ------------------------------- |\r\n| `exportGrid` (csv/tsv/html) | ~6 KB | - |\r\n| + xlsx | ~6 KB | `jszip` (loaded on first xlsx call) |\r\n| + pdf | ~9 KB | `pdfmake` (loaded on first pdf call) |\r\n| `importData` | ~7 KB | `jszip` (xlsx only) |\r\n| AI helpers | ~3 KB | -. You bring your provider. |\r\n| `createPivotModel` | ~4 KB | - |\r\n\r\nUse the **subpath imports** to avoid pulling features you don't use:\r\n\r\n```ts\r\nimport { exportGrid } from '@svgrid/enterprise/export' // export only\r\nimport { createPivotModel } from '@svgrid/enterprise/pivot' // pivot only\r\n```\r\n\r\n## 3. Peer dependencies\r\n\r\n| Peer dep | When you need it | Install |\r\n| ---------- | ------------------------------------------------------------------ | ----------------------------------- |\r\n| `svelte` | Always. SvGrid renders against Svelte 5. | `pnpm add svelte` |\r\n| `jszip` | xlsx export OR xlsx import. | `pnpm add jszip` |\r\n| `pdfmake` | PDF export. | `pnpm add pdfmake` |\r\n\r\nBoth `jszip` and `pdfmake` are dynamic imports - the bundle splits and\r\nloads them on the first call. Nothing ships in your initial chunk until\r\nthe user actually clicks \"Export to xlsx\".\r\n\r\n## 4. Lazy-load Enterprise at route boundaries\r\n\r\nIf only one route in your app needs export, gate `installEnterprise` behind a\r\ndynamic import so the rest of the app doesn't ship the Enterprise bundle:\r\n\r\n```svelte\r\n<script lang=\"ts\">\r\n import type { SvGridApi } from '@svgrid/grid'\r\n import type { EnterpriseGridApi } from '@svgrid/enterprise'\r\n\r\n let api = $state<SvGridApi<typeof features, Order> | null>(null)\r\n let pro = $state<EnterpriseGridApi<typeof features, Order> | null>(null)\r\n\r\n async function enablePro() {\r\n if (!api) return\r\n const { installEnterprise, setLicenseKey } = await import('@svgrid/enterprise')\r\n setLicenseKey(import.meta.env.VITE_SVPRO_KEY)\r\n pro = installEnterprise(api)\r\n }\r\n</script>\r\n\r\n<SvGrid {...} onApiReady={(next) => (api = next)} />\r\n\r\n<button onclick={enablePro}>Enable export</button>\r\n{#if pro}\r\n <button onclick={() => pro?.exportData({ format: 'xlsx' })}>⬇ XLSX</button>\r\n{/if}\r\n```\r\n\r\n## 5. License the Enterprise pack\r\n\r\n```ts\r\n// main.ts (or +layout.svelte for SvelteKit)\r\nimport { setLicenseKey } from '@svgrid/enterprise'\r\n\r\nif (import.meta.env.VITE_SVPRO_KEY) {\r\n setLicenseKey(import.meta.env.VITE_SVPRO_KEY)\r\n}\r\n```\r\n\r\nEnterprise is **soft-gated** - it works unlicensed, but renders a small\r\nwatermark + a one-time console nudge. Set the key once at app\r\nstartup; both disappear.\r\n\r\nDon't commit the key to source control. Inject via env (Vite reads\r\n`VITE_*` variables at build time; SvelteKit reads `$env/static/public`).\r\n\r\nFor per-tenant deployments where each tenant has their own key, set\r\nthe key inside the consumer's bootstrap, never inside the library\r\npackage.\r\n\r\n## 6. CSP-safe deployment\r\n\r\nThe recommended `Content-Security-Policy` header:\r\n\r\n```\r\nContent-Security-Policy:\r\n default-src 'self';\r\n script-src 'self';\r\n style-src 'self' 'unsafe-inline';\r\n img-src 'self' data:;\r\n font-src 'self' data:;\r\n connect-src 'self';\r\n frame-ancestors 'none';\r\n base-uri 'self';\r\n form-action 'self';\r\n```\r\n\r\nNo `'unsafe-eval'`, no `'unsafe-inline'` on `script-src`. SvGrid\r\nCommunity + Enterprise run clean under this policy. [Demo 16](../../examples/src/demos/16-csp-compliant.svelte)\r\nincludes a runtime self-check.\r\n\r\nIf you ship in an iframe (embedded analytics, dashboards), add\r\n`frame-ancestors` to the host's CSP to allow the embed.\r\n\r\n## 7. SSR\r\n\r\nFor SvelteKit:\r\n\r\n```ts\r\n// +page.server.ts\r\nexport async function load() {\r\n const rows = await db.query('select * from people limit 100')\r\n return { rows }\r\n}\r\n```\r\n\r\n```svelte\r\n<!-- +page.svelte -->\r\n<script lang=\"ts\">\r\n import { SvGrid, tableFeatures, rowSortingFeature } from '@svgrid/grid'\r\n let { data } = $props()\r\n const features = tableFeatures({ rowSortingFeature })\r\n</script>\r\n\r\n<SvGrid data={data.rows} columns={columns} features={features} />\r\n```\r\n\r\nThe first paint contains the data in a real `<table>` (good for SEO +\r\nLCP). Hydration only attaches event listeners. See\r\n[demo 19](../../examples/src/demos/19-ssr.svelte) for a sandboxed\r\nJS-disabled iframe that proves the markup is meaningful pre-hydration.\r\n\r\n## 8. Performance budgets\r\n\r\nTargets that have held up in production:\r\n\r\n| Surface | Target | What you do if you miss it |\r\n| ---------------------------------- | -------------------- | ---------------------------------------------------------------- |\r\n| Time to first row visible | < 200 ms | Lazy-load Enterprise. Defer non-critical columns. Smaller initial page. |\r\n| Scroll FPS (10k rows, virtualized) | 60 FPS | Cap `overscan`. Avoid `cell` render functions that allocate per render. |\r\n| Sort over 100k rows | < 60 ms | Set `editorType` on numeric / date columns so `sortFns.number` / `sortFns.date` get used instead of `sortFns.auto`. |\r\n| Filter input → re-render | < 30 ms | Debounce server-side filters; the local filter UI is already ≤ 16 ms for 10k rows. |\r\n| Export 10k rows to xlsx | < 1 s | Don't include columns you're going to hide. Use `columns: [...]` to project. |\r\n\r\nThe [benchmarks page](./benchmarks.md) has the reproducible numbers.\r\n\r\n## 9. Error boundaries\r\n\r\nThe render component throws on truly broken state (e.g. a `field` that\r\ndoesn't exist on any row). Wrap in a Svelte error boundary or guard\r\nwith `if (rows.length === 0)` for empty data. The grid's `emptyMessage`\r\nprop covers the empty case without crashing.\r\n\r\n```svelte\r\n<svelte:boundary>\r\n <SvGrid {data} {columns} {features} />\r\n\r\n {#snippet failed(error, reset)}\r\n <div class=\"error\">Grid failed: {error.message}</div>\r\n <button onclick={reset}>Retry</button>\r\n {/snippet}\r\n</svelte:boundary>\r\n```\r\n\r\n## 10. Monitoring + observability\r\n\r\nThe grid emits everything you'd want to observe via callbacks:\r\n`onSortingChange`, `onFiltersChange`, `onRowSelectionChange`,\r\n`onCellValueChange`. Wire them into your analytics / logging:\r\n\r\n```ts\r\nfunction track(event: string, payload: object) {\r\n // sentry, posthog, your own beacon - pick one\r\n}\r\n\r\n<SvGrid\r\n ...\r\n onSortingChange={(s) => track('grid.sort', { clauses: s })}\r\n onFiltersChange={(f) => track('grid.filter', { columns: f.columns.length })}\r\n onCellValueChange={(e) => track('grid.edit', { column: e.columnId })}\r\n/>\r\n```\r\n\r\n## See also\r\n\r\n- [Why headless?](../why-headless.md) - the architectural decision\r\n behind Community + Enterprise.\r\n- [API stability](./api-stability.md) - the semver promise and what\r\n it covers.\r\n- [Security](./security.md) - peer-dep table, SBOM, vulnerability\r\n handling, data residency.\r\n- [Browser support](./browser-support.md) - tested matrix, mobile,\r\n build tools.\r\n"
|
|
3618
3618
|
},
|
|
3619
3619
|
{
|
|
3620
3620
|
"slug": "help/real-time",
|
|
@@ -3668,7 +3668,7 @@ export const docs = [
|
|
|
3668
3668
|
"slug": "help/rows/row-height",
|
|
3669
3669
|
"path": "docs/help/rows/row-height.md",
|
|
3670
3670
|
"title": "Row height",
|
|
3671
|
-
"markdown": "# Row height\r\n\r\nRow height is a single integer in pixels.\r\n<div data-docs-demo=\"10-custom-cells-and-themes\" data-height=\"540\"></div>\r\n\r\n```svelte\r\n<SvGrid {data} {columns} features={{}} rowHeight={36} />\r\n```\r\n\r\nThe default is 36 px. The virtualizer reads `rowHeight` once and uses it to\r\ncompute the visible window and total scroll height.\r\n\r\n## Density\r\n\r\nFor a density toggle, change `rowHeight` and a matching CSS custom property:\r\n\r\n```svelte\r\n<script lang=\"ts\">\r\n let density = $state<'compact' | 'normal' | 'comfortable'>('normal')\r\n const px = $derived(density === 'compact' ? 28 : density === 'comfortable' ? 48 : 36)\r\n</script>\r\n\r\n<div style:--sg-row-height=\"{px}px\">\r\n <SvGrid {data} {columns} features={{}} rowHeight={px} />\r\n</div>\r\n```\r\n\r\nThe example gallery's\r\n[demos/10-custom-cells-and-themes.svelte](../../../examples/src/demos/10-custom-cells-and-themes.svelte)\r\nshows the density toggle in full.\r\n\r\n## Auto row height (size each row to its content)\r\n\r\n`autoRowHeight` lets cell text wrap and sizes every row to its tallest cell:\r\n\r\n```svelte\r\n<SvGrid {data} {columns} autoRowHeight />\r\n```\r\n\r\nRows are measured after they render, so this works with virtualization. Before\r\na row has been measured the grid uses `rowHeight` (or 30) as its estimate, which\r\nkeeps the scrollbar stable as you scroll into rows for the first time:\r\n\r\n```svelte\r\n<!-- 44px is the starting guess; each row settles to its real height -->\r\n<SvGrid {data} {columns} autoRowHeight rowHeight={44} />\r\n```\r\n\r\nThings worth knowing:\r\n\r\n- It costs a measurement pass per row. With uniform content a fixed `rowHeight`\r\n is cheaper - reach for `autoRowHeight` when you have free text, notes, or\r\n wrapped addresses.\r\n- Passing a **function** `rowHeight` turns it off. You are already supplying\r\n per-row heights, so measuring would fight you.\r\n- Rows re-measure when their content reflows, e.g. after a column resize.\r\n- Measurements are dropped when the row set changes, so filtering or replacing\r\n `data` never sizes a new row by the old one's content.\r\n\r\n## Variable row height (you supply the numbers)\r\n\r\nPass a function to size rows yourself, without measuring:\r\n\r\n```svelte\r\n<SvGrid {data} {columns} rowHeight={(i) => (data[i].tall ? 80 : 36)} />\r\n```\r\n\r\nThe virtualizer handles the variable-size case natively (cumulative offsets), so\r\nscrolling and the total height stay correct. The same engine is available\r\nheadless if you are building your own row layout:\r\n\r\n```ts\r\nimport { createSvelteVirtualizer } from '@svgrid/grid'\r\n\r\nconst virtualizer = createSvelteVirtualizer({\r\n count:
|
|
3671
|
+
"markdown": "# Row height\r\n\r\nRow height is a single integer in pixels.\r\n<div data-docs-demo=\"10-custom-cells-and-themes\" data-height=\"540\"></div>\r\n\r\n```svelte\r\n<SvGrid {data} {columns} features={{}} rowHeight={36} />\r\n```\r\n\r\nThe default is 36 px. The virtualizer reads `rowHeight` once and uses it to\r\ncompute the visible window and total scroll height.\r\n\r\n## Density\r\n\r\nFor a density toggle, change `rowHeight` and a matching CSS custom property:\r\n\r\n```svelte\r\n<script lang=\"ts\">\r\n let density = $state<'compact' | 'normal' | 'comfortable'>('normal')\r\n const px = $derived(density === 'compact' ? 28 : density === 'comfortable' ? 48 : 36)\r\n</script>\r\n\r\n<div style:--sg-row-height=\"{px}px\">\r\n <SvGrid {data} {columns} features={{}} rowHeight={px} />\r\n</div>\r\n```\r\n\r\nThe example gallery's\r\n[demos/10-custom-cells-and-themes.svelte](../../../examples/src/demos/10-custom-cells-and-themes.svelte)\r\nshows the density toggle in full.\r\n\r\n## Auto row height (size each row to its content)\r\n\r\n`autoRowHeight` lets cell text wrap and sizes every row to its tallest cell:\r\n\r\n```svelte\r\n<SvGrid {data} {columns} autoRowHeight />\r\n```\r\n\r\nRows are measured after they render, so this works with virtualization. Before\r\na row has been measured the grid uses `rowHeight` (or 30) as its estimate, which\r\nkeeps the scrollbar stable as you scroll into rows for the first time:\r\n\r\n```svelte\r\n<!-- 44px is the starting guess; each row settles to its real height -->\r\n<SvGrid {data} {columns} autoRowHeight rowHeight={44} />\r\n```\r\n\r\nThings worth knowing:\r\n\r\n- It costs a measurement pass per row. With uniform content a fixed `rowHeight`\r\n is cheaper - reach for `autoRowHeight` when you have free text, notes, or\r\n wrapped addresses.\r\n- Passing a **function** `rowHeight` turns it off. You are already supplying\r\n per-row heights, so measuring would fight you.\r\n- Rows re-measure when their content reflows, e.g. after a column resize.\r\n- Measurements are dropped when the row set changes, so filtering or replacing\r\n `data` never sizes a new row by the old one's content.\r\n\r\n## Variable row height (you supply the numbers)\r\n\r\nPass a function to size rows yourself, without measuring:\r\n\r\n```svelte\r\n<SvGrid {data} {columns} rowHeight={(i) => (data[i].tall ? 80 : 36)} />\r\n```\r\n\r\nThe virtualizer handles the variable-size case natively (cumulative offsets), so\r\nscrolling and the total height stay correct. The same engine is available\r\nheadless if you are building your own row layout:\r\n\r\n```ts\r\nimport { createSvelteVirtualizer } from '@svgrid/grid'\r\n\r\nconst virtualizer = createSvelteVirtualizer({\r\n count: rows.length,\r\n estimateSize: (index) => rows[index]!.tall ? 80 : 36,\r\n viewportHeight: scrollEl.clientHeight,\r\n overscan: 6,\r\n})\r\n\r\n// It owns no DOM. Feed it your scroller's numbers as they change:\r\nvirtualizer.setScrollOffset(scrollEl.scrollTop)\r\nvirtualizer.setViewportHeight(scrollEl.clientHeight)\r\n```\r\n\r\nSee [`packages/grid/src/virtualization/`](../../../packages/grid/src/virtualization/).\r\n\r\n## Header height\r\n\r\nHeader height is independent of row height. See\r\n[Column headers](../columns/column-headers.md) for how to size it.\r\n\r\n## Row-number column width\r\n\r\nWhen `showRowNumbers={true}`, the leading row-number column defaults\r\nto **56 px**, which fits up to `99,999`. For larger datasets, bump\r\nthe width via `rowNumberWidth`:\r\n\r\n```svelte\r\n<!-- One million rows: \"1,000,000\" needs ~ 92 px to stay fully visible -->\r\n<SvGrid\r\n {data}\r\n {columns}\r\n features={{}}\r\n showRowNumbers={true}\r\n rowNumberWidth={92}\r\n rowHeight={18}\r\n virtualization={true}\r\n containerHeight=\"100%\"\r\n/>\r\n```\r\n\r\nRule of thumb: budget ~ 8 px per digit plus 14 px of padding. So:\r\n\r\n| Row count | Largest number | Suggested `rowNumberWidth` |\r\n|--------------|----------------|----------------------------|\r\n| < 1 000 | \"999\" | `40` |\r\n| < 100 000 | \"99,999\" | `56` (default) |\r\n| < 10 000 000 | \"9,999,999\" | `92` |\r\n\r\nDemo 78 (\"1 million rows\") uses 92 px so the millionth row's index\r\nstays legible at the bottom of the scroll.\r\n\r\n## See also\r\n\r\n- [Row pinning](./row-pinning.md)\r\n- [Styling rows](./styling-rows.md)\r\n- [Demo 78 - 1 million rows](../../../examples/src/demos/78-million-rows.svelte)\r\n"
|
|
3672
3672
|
},
|
|
3673
3673
|
{
|
|
3674
3674
|
"slug": "help/rows/row-pagination",
|
|
@@ -3722,7 +3722,7 @@ export const docs = [
|
|
|
3722
3722
|
"slug": "help/rows/tree-rows",
|
|
3723
3723
|
"path": "docs/help/rows/tree-rows.md",
|
|
3724
3724
|
"title": "Tree rows (expand / collapse)",
|
|
3725
|
-
"markdown": "# Tree rows (expand / collapse)\n\nSvGrid does not have a `treeData` prop. Tree-shaped data renders through\nthe same data + columns pipeline as any other grid, with the tree\nbehaviour living in *your* derived-state code. This is on purpose: a\ntree is just \"a flat list with a depth field and a collapsible\nsubtree\", and the headless engine doesn't need to know which.\n\n\n\nTry the org-chart pattern - click any chevron to expand a branch,\nor focus a name cell and press Right / Left / Enter:\n\n<div data-docs-demo=\"28-org-chart-tree\" data-height=\"520\"></div>\n\n\n## What it is\n\nA \"tree row\" is any row with a `depth: number`, a `childIds: string[]`\n(or equivalent), and a parent reference. Whether a row is currently\nvisible depends on its ancestors' expanded state.\n\n## The pattern\n\nThree pieces of state, the third one derived:\n\n```ts\nlet allRows = $state<Node[]>(/* every node, flat */)\nlet expanded = $state<Record<string, boolean>>({ root: true })\n\nconst visibleRows = $derived.by(() => {\n const out: Node[] = []\n const byId = new Map(allRows.map((n) => [n.id, n]))\n function walk(id: string) {\n const node = byId.get(id)\n if (!node) return\n out.push(node)\n if (expanded[id]) for (const cid of node.childIds) walk(cid)\n }\n for (const root of allRows.filter((n) => n.parentId === null)) walk(root.id)\n return out\n})\n```\n\nHand `visibleRows` to `<SvGrid data={visibleRows} ...>` like any\nother dataset. The grid stays unaware that the data is hierarchical.\n\n## The expand-chevron cell\n\nRender the chevron + indentation as part of a custom cell snippet on\nthe leftmost (or \"name\") column:\n\n```svelte\n{#snippet NameCell(props: { node: Node })}\n {@const canExpand = props.node.childIds.length > 0}\n {@const isOpen = !!expanded[props.node.id]}\n <span class=\"tree-name\" style=\"padding-left: {props.node.depth * 22}px\">\n {#if canExpand}\n <button\n type=\"button\"\n class={`tree-chev ${isOpen ? 'tree-chev-open' : ''}`}\n onclick={() => (expanded = { ...expanded, [props.node.id]: !isOpen })}\n aria-expanded={isOpen}\n aria-label={isOpen ? 'Collapse' : 'Expand'}\n >\n <svg viewBox=\"0 0 16 16\" width=\"10\" height=\"10\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.4\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"5 3 11 8 5 13\" />\n </svg>\n </button>\n {/if}\n <span>{props.node.name}</span>\n </span>\n{/snippet}\n```\n\nThe corresponding CSS rotates the SVG instead of swapping a character,\nwhich animates smoothly:\n\n```css\n.tree-chev {\n transition: transform 160ms ease;\n}\n.tree-chev-open { transform: rotate(90deg); }\n```\n\n## Tree connector lines\n\nFor visual continuity between parents and children, draw guide lines\nin absolute position inside the name cell. One vertical guide per\nancestor depth, plus a short horizontal \"elbow\" into the current row:\n\n```svelte\n<span class=\"tree-name\" style=\"position: relative; padding-left: {4 + node.depth * 22}px\">\n {#each Array(node.depth) as _, i (i)}\n <span class=\"tree-guide\" style=\"left: {4 + i * 22 + 11}px\"></span>\n {/each}\n {#if node.depth > 0}\n <span class=\"tree-elbow\" style=\"left: {4 + (node.depth - 1) * 22 + 11}px\"></span>\n {/if}\n ...\n</span>\n```\n\n```css\n.tree-guide {\n position: absolute;\n top: 0; bottom: 0;\n border-left: 1px dashed rgba(148, 163, 184, 0.35);\n}\n.tree-elbow {\n position: absolute;\n top: 50%;\n width: 14px;\n border-top: 1px dashed rgba(148, 163, 184, 0.45);\n}\n```\n\n## Keyboard navigation\n\nThe grid's built-in arrow-key handling moves the active cell between\ncolumns. To get standard tree-grid keys (Right expands, Left collapses,\nEnter toggles), intercept at the window level **with a capture\nlistener** so your handler runs before the grid's:\n\n```ts\nlet activeCol = $state<string>('')\nlet activeRowIndex = $state<number>(0)\n\n$effect(() => {\n function onKey(e: KeyboardEvent) {\n if (activeCol !== 'name') return // not on the tree column\n const node = visibleRows[activeRowIndex]\n if (!node || node.childIds.length === 0) return // leaves don't toggle\n const isOpen = !!expanded[node.id]\n if (e.key === 'ArrowRight' && !isOpen) {\n e.preventDefault()\n expanded = { ...expanded, [node.id]: true }\n } else if (e.key === 'ArrowLeft' && isOpen) {\n e.preventDefault()\n expanded = { ...expanded, [node.id]: false }\n } else if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n expanded = { ...expanded, [node.id]: !isOpen }\n }\n }\n window.addEventListener('keydown', onKey, true)\n return () => window.removeEventListener('keydown', onKey, true)\n})\n```\n\nWire SvGrid to track the active cell:\n\n```svelte\n<SvGrid\n data={visibleRows}\n columns={columns}\n onActiveCellChange={(args) => {\n activeCol = args.columnId\n activeRowIndex = args.rowIndex\n }}\n ...\n/>\n```\n\nThis pattern is non-invasive: regular arrow keys still move the active\ncell between non-name columns; tree keys only fire when the user is\nfocused on the tree column.\n\n## Roll-ups (computed values at non-leaf rows)\n\nWhen a parent's value is a roll-up of its children (headcount,\npercent-complete, cost), put the computation in a separate function\nthat runs after every leaf edit and writes the result back onto the\nparent rows:\n\n```ts\nfunction recompute(rows: Node[]): Node[] {\n const byId = new Map(rows.map((r) => [r.id, { ...r }]))\n // post-order DFS: deepest first so each parent already has updated children\n const ordered = [...byId.values()].sort((a, b) => b.depth - a.depth)\n for (const r of ordered) {\n if (r.childIds.length === 0) continue\n let sum = 0\n for (const cid of r.childIds) sum += byId.get(cid)!.subtotal\n r.subtotal = sum\n }\n return rows.map((r) => byId.get(r.id)!)\n}\n```\n\nHook it from `onCellValueChange`:\n\n```svelte\n<SvGrid\n ...\n onCellValueChange={(e) => {\n const next = allRows.slice()\n const ix = next.findIndex((r) => r.id === e.row.id)\n next[ix] = { ...next[ix], [e.columnId]: e.newValue }\n allRows = recompute(next)\n }}\n/>\n```\n\n## Lazy load on first expand\n\nFor trees that are too large to seed up front, fetch children only when\nthe user expands a node:\n\n```ts\nasync function toggle(id: string) {\n const isOpen = !!expanded[id]\n expanded = { ...expanded, [id]: !isOpen }\n if (isOpen) return // collapsing - no fetch needed\n\n const node = allNodes.find((n) => n.id === id)\n if (!node || !node.expandable || node.loadState === 'loaded') return\n\n allNodes = allNodes.map((n) => (n.id === id ? { ...n, loadState: 'loading' } : n))\n const children = await fetchChildren(id)\n allNodes = allNodes\n .map((n) => n.id === id ? { ...n, loadState: 'loaded', childIds: children.map((c) => c.id) } : n)\n .concat(children)\n}\n```\n\nRender a placeholder spinner row in `visibleRows` while `loadState === 'loading'`.\n\n## See also\n\n- [Row data](./row-data.md) - the underlying `data` prop and accessors.\n- [Row sorting](./row-sorting.md) - applies to tree rows too, but you\n control the order via the `visibleRows` derivation.\n- [Cell components](../cells/cell-components.md) - the custom-cell\n pattern used for the expand chevron.\n"
|
|
3725
|
+
"markdown": "# Tree rows (expand / collapse)\n\nSvGrid does not have a `treeData` prop. Tree-shaped data renders through\nthe same data + columns pipeline as any other grid, with the tree\nbehaviour living in *your* derived-state code. This is on purpose: a\ntree is just \"a flat list with a depth field and a collapsible\nsubtree\", and the headless engine doesn't need to know which.\n\n\n\nTry the org-chart pattern - click any chevron to expand a branch,\nor focus a name cell and press Right / Left / Enter:\n\n<div data-docs-demo=\"28-org-chart-tree\" data-height=\"520\"></div>\n\n\n## What it is\n\nA \"tree row\" is any row with a `depth: number`, a `childIds: string[]`\n(or equivalent), and a parent reference. Whether a row is currently\nvisible depends on its ancestors' expanded state.\n\n## The pattern\n\nThree pieces of state, the third one derived:\n\n```ts\ntype Node = { id: string; parentId: string | null; childIds: string[]; depth: number }\n\nlet allRows = $state<Node[]>(/* every node, flat */)\nlet expanded = $state<Record<string, boolean>>({ root: true })\n\nconst visibleRows = $derived.by(() => {\n const out: Node[] = []\n const byId = new Map(allRows.map((n) => [n.id, n]))\n function walk(id: string) {\n const node = byId.get(id)\n if (!node) return\n out.push(node)\n if (expanded[id]) for (const cid of node.childIds) walk(cid)\n }\n for (const root of allRows.filter((n) => n.parentId === null)) walk(root.id)\n return out\n})\n```\n\nHand `visibleRows` to `<SvGrid data={visibleRows} ...>` like any\nother dataset. The grid stays unaware that the data is hierarchical.\n\n## The expand-chevron cell\n\nRender the chevron + indentation as part of a custom cell snippet on\nthe leftmost (or \"name\") column:\n\n```svelte\n{#snippet NameCell(props: { node: Node })}\n {@const canExpand = props.node.childIds.length > 0}\n {@const isOpen = !!expanded[props.node.id]}\n <span class=\"tree-name\" style=\"padding-left: {props.node.depth * 22}px\">\n {#if canExpand}\n <button\n type=\"button\"\n class={`tree-chev ${isOpen ? 'tree-chev-open' : ''}`}\n onclick={() => (expanded = { ...expanded, [props.node.id]: !isOpen })}\n aria-expanded={isOpen}\n aria-label={isOpen ? 'Collapse' : 'Expand'}\n >\n <svg viewBox=\"0 0 16 16\" width=\"10\" height=\"10\"\n fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.4\"\n stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <polyline points=\"5 3 11 8 5 13\" />\n </svg>\n </button>\n {/if}\n <span>{props.node.name}</span>\n </span>\n{/snippet}\n```\n\nThe corresponding CSS rotates the SVG instead of swapping a character,\nwhich animates smoothly:\n\n```css\n.tree-chev {\n transition: transform 160ms ease;\n}\n.tree-chev-open { transform: rotate(90deg); }\n```\n\n## Tree connector lines\n\nFor visual continuity between parents and children, draw guide lines\nin absolute position inside the name cell. One vertical guide per\nancestor depth, plus a short horizontal \"elbow\" into the current row:\n\n```svelte\n<span class=\"tree-name\" style=\"position: relative; padding-left: {4 + node.depth * 22}px\">\n {#each Array(node.depth) as _, i (i)}\n <span class=\"tree-guide\" style=\"left: {4 + i * 22 + 11}px\"></span>\n {/each}\n {#if node.depth > 0}\n <span class=\"tree-elbow\" style=\"left: {4 + (node.depth - 1) * 22 + 11}px\"></span>\n {/if}\n ...\n</span>\n```\n\n```css\n.tree-guide {\n position: absolute;\n top: 0; bottom: 0;\n border-left: 1px dashed rgba(148, 163, 184, 0.35);\n}\n.tree-elbow {\n position: absolute;\n top: 50%;\n width: 14px;\n border-top: 1px dashed rgba(148, 163, 184, 0.45);\n}\n```\n\n## Keyboard navigation\n\nThe grid's built-in arrow-key handling moves the active cell between\ncolumns. To get standard tree-grid keys (Right expands, Left collapses,\nEnter toggles), intercept at the window level **with a capture\nlistener** so your handler runs before the grid's:\n\n```ts\nlet activeCol = $state<string>('')\nlet activeRowIndex = $state<number>(0)\n\n$effect(() => {\n function onKey(e: KeyboardEvent) {\n if (activeCol !== 'name') return // not on the tree column\n const node = visibleRows[activeRowIndex]\n if (!node || node.childIds.length === 0) return // leaves don't toggle\n const isOpen = !!expanded[node.id]\n if (e.key === 'ArrowRight' && !isOpen) {\n e.preventDefault()\n expanded = { ...expanded, [node.id]: true }\n } else if (e.key === 'ArrowLeft' && isOpen) {\n e.preventDefault()\n expanded = { ...expanded, [node.id]: false }\n } else if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n expanded = { ...expanded, [node.id]: !isOpen }\n }\n }\n window.addEventListener('keydown', onKey, true)\n return () => window.removeEventListener('keydown', onKey, true)\n})\n```\n\nWire SvGrid to track the active cell:\n\n```svelte\n<SvGrid\n data={visibleRows}\n columns={columns}\n onActiveCellChange={(args) => {\n activeCol = args.columnId\n activeRowIndex = args.rowIndex\n }}\n ...\n/>\n```\n\nThis pattern is non-invasive: regular arrow keys still move the active\ncell between non-name columns; tree keys only fire when the user is\nfocused on the tree column.\n\n## Roll-ups (computed values at non-leaf rows)\n\nWhen a parent's value is a roll-up of its children (headcount,\npercent-complete, cost), put the computation in a separate function\nthat runs after every leaf edit and writes the result back onto the\nparent rows:\n\n```ts\ntype Node = { id: string; childIds: string[]; depth: number; subtotal: number }\n\nfunction recompute(rows: Node[]): Node[] {\n const byId = new Map(rows.map((r) => [r.id, { ...r }]))\n // post-order DFS: deepest first so each parent already has updated children\n const ordered = [...byId.values()].sort((a, b) => b.depth - a.depth)\n for (const r of ordered) {\n if (r.childIds.length === 0) continue\n let sum = 0\n for (const cid of r.childIds) sum += byId.get(cid)!.subtotal\n r.subtotal = sum\n }\n return rows.map((r) => byId.get(r.id)!)\n}\n```\n\nHook it from `onCellValueChange`:\n\n```svelte\n<SvGrid\n ...\n onCellValueChange={(e) => {\n const next = allRows.slice()\n const ix = next.findIndex((r) => r.id === e.row.id)\n next[ix] = { ...next[ix], [e.columnId]: e.newValue }\n allRows = recompute(next)\n }}\n/>\n```\n\n## Lazy load on first expand\n\nFor trees that are too large to seed up front, fetch children only when\nthe user expands a node:\n\n```ts\nasync function toggle(id: string) {\n const isOpen = !!expanded[id]\n expanded = { ...expanded, [id]: !isOpen }\n if (isOpen) return // collapsing - no fetch needed\n\n const node = allNodes.find((n) => n.id === id)\n if (!node || !node.expandable || node.loadState === 'loaded') return\n\n allNodes = allNodes.map((n) => (n.id === id ? { ...n, loadState: 'loading' } : n))\n const children = await fetchChildren(id)\n allNodes = allNodes\n .map((n) => n.id === id ? { ...n, loadState: 'loaded', childIds: children.map((c) => c.id) } : n)\n .concat(children)\n}\n```\n\nRender a placeholder spinner row in `visibleRows` while `loadState === 'loading'`.\n\n## See also\n\n- [Row data](./row-data.md) - the underlying `data` prop and accessors.\n- [Row sorting](./row-sorting.md) - applies to tree rows too, but you\n control the order via the `visibleRows` derivation.\n- [Cell components](../cells/cell-components.md) - the custom-cell\n pattern used for the expand chevron.\n"
|
|
3726
3726
|
},
|
|
3727
3727
|
{
|
|
3728
3728
|
"slug": "help/saved-views",
|
|
@@ -3734,7 +3734,7 @@ export const docs = [
|
|
|
3734
3734
|
"slug": "help/scheduling",
|
|
3735
3735
|
"path": "docs/help/scheduling.md",
|
|
3736
3736
|
"title": "Scheduling: automate exports and reminders",
|
|
3737
|
-
"markdown": "# Scheduling: automate exports and reminders\n\nTwo jobs turn up in almost every data app the moment it goes into daily use:\n*\"email me this report every weekday at 17:30\"* and *\"remind the desk at 09:00\nto reconcile.\"* The reflex is to stand up a backend job runner. But in a\nlong-lived data app - a dashboard that stays open on a wall screen, an ops\nconsole a team lives in all day - a schedule is just **a timer plus an action\nyou already have**: an [export](/help/export.md) or a\n[toast alert](/help/ui-components/sv-toaster.md).\n\n**Scheduling** ships in the paid\n**[@svgrid/enterprise](https://www.npmjs.com/package/@svgrid/enterprise)**\nadd-on. It supplies the missing middle - a pure cron matcher and a small\nclient-side runtime that fires your callback when a schedule comes due - and\nleaves the *action* to you. A scheduled report reuses the same\n`exportCsv` / Excel / PDF path as the toolbar button; a scheduled alert reuses\nthe same `toast` you already call on save. Nothing new to learn on the action\nside, only a trigger.\n\n\n\n## At a glance\n\n| | |\n| --- | --- |\n| **Triggers** | Recurring (5-field cron) or one-off (ISO datetime) |\n| **Actions** | Any export (CSV/TSV/JSON free, Excel/PDF/HTML enterprise) or any `toast` |\n| **Runtime** | Client-side, ticks twice a minute, one fire per schedule per minute |\n| **Guarantees** | One-offs fire exactly once, ever; disabled schedules never fire |\n| **Testable** | Pure matcher with an injectable clock - no wall-clock flakiness |\n| **Package** | `@svgrid/enterprise` (the trigger); actions use whatever tier you own |\n\n> **By design, schedules run in the browser tab**, so the app has to be open\n> when a schedule is due. That is exactly right for the always-on dashboard case\n> where a server cron is overkill; when you need guaranteed delivery whether or\n> not anyone is watching, [pair it with a server job](#when-to-use-a-server-instead).\n\n## How it works\n\nA schedule flows through three stages. A **trigger** (a cron expression or a\none-off `runAt`) comes due; the **scheduler** notices on its next tick and calls\nyour **`onFire`** callback; the callback runs an action - an export or an alert.\nThe scheduler owns the timing and the de-duplication so your callback stays a\nplain function of \"which schedule fired.\"\n\n\n\n## The `Schedule` shape\n\nA schedule is plain data - store it in app state, a database row, or a\n[saved view](/help/saved-views.md). It fires **recurring** on a cron expression,\nor **once** at an ISO datetime.\n\n```ts\nimport type { Schedule } from '@svgrid/enterprise'\n\nconst schedules: Schedule[] = [\n // Recurring: every weekday at 17:30.\n { id: 'eod', name: 'End-of-day CSV', cron: '30 17 * * 1-5' },\n // Recurring: every morning at 09:00.\n { id: 'standup', name: 'Stand-up reminder', cron: '0 9 * * *' },\n // One-off: fire a single time, then never again.\n { id: 'launch', name: 'Go-live snapshot', runAt: '2026-08-01T08:00:00' },\n // Kept but paused - no code change to re-enable.\n { id: 'audit', name: 'Weekly audit', cron: '0 6 * * 1', enabled: false },\n]\n```\n\n| Field | Meaning |\n| ----- | ------- |\n| `id` | Stable id. Keys your `onFire` switch and dedupes fires. |\n| `name` | Human label for a panel or toast. |\n| `cron` | 5-field cron `\"min hour day-of-month month day-of-week\"` (recurring). |\n| `runAt` | ISO datetime for a **one-off**. Takes precedence over `cron`. |\n| `enabled` | Set `false` to keep the definition but stop it firing. Default `true`. |\n\nThat `enabled` flag matters more than it looks: keeping a schedule around while\nswitched off - no code edit, no deletion - is the difference between a demo and\nsomething a team can actually manage.\n\n## Running the scheduler\n\n`createScheduler` ticks on an interval (twice a minute by default), fires\n`onFire` for every schedule due in the current minute, and guarantees **at most\none fire per schedule per minute** - and exactly one, ever, for a one-off. Start\nit once when your view mounts and stop it on teardown.\n\n```svelte\n<script lang=\"ts\">\n import { createScheduler } from '@svgrid/enterprise'\n import { toast, type SvGridApi } from '@svgrid/grid'\n\n let api = $state<SvGridApi | null>(null)\n\n $effect(() => {\n if (!api) return\n const scheduler = createScheduler({\n schedules,\n onFire(schedule) {\n if (schedule.id === 'eod') {\n // Scheduled report: reuse the same export path as the toolbar button.\n api!.exportCsv({ filename: 'end-of-day' })\n toast.success('End-of-day report downloaded')\n } else {\n // Scheduled alert: a plain reminder, no data-change trigger needed.\n toast.info(schedule.name ?? 'Reminder', { duration: 0 })\n }\n },\n })\n scheduler.start()\n return () => scheduler.stop() // cleanup when the effect re-runs / unmounts\n })\n</script>\n\n<SvGrid {data} {columns} {features} onApiReady={(a) => (api = a)} />\n```\n\nReturning `scheduler.stop` from the `$effect` ties the timer's lifetime to the\ncomponent - no leaked interval when the view unmounts.\n\n### Scheduled reports\n\nAny export the grid can do on demand, it can do on a schedule - the callback\njust calls the export API. CSV / TSV / JSON are free in `@svgrid/grid`; Excel,\nPDF, and styled HTML come from `@svgrid/enterprise`, reusing the same call site:\n\n```ts\nonFire(schedule) {\n if (schedule.id !== 'eod') return\n // Free: exportCsv / exportTsv / exportJson on the community grid.\n api.exportCsv({ filename: 'eod', rows: 'all' })\n // Enterprise: the richer formats reuse the same call site.\n // await exportGrid(api, { format: 'xlsx', filename: 'eod' })\n}\n```\n\nBecause the export defaults to the **current view**, a scheduled report honors\nwhatever filters and sort the user left in place - you are scheduling the *view*,\nnot a query frozen at config time.\n\n### Scheduled alerts\n\nAn alert is just a `toast` on a timer - a daily stand-up nudge, a market-open\nbanner, an end-of-shift prompt. Use `duration: 0` to make it sticky until\ndismissed:\n\n```ts\nonFire(schedule) {\n if (schedule.id === 'standup') {\n toast.info('Daily stand-up in 5 minutes', { title: schedule.name, duration: 0 })\n }\n}\n```\n\nMount a single `<SvToaster />` near your app root so the queue renders. See\n[SvToaster](/help/ui-components/sv-toaster.md) for the toast API.\n\n## Cron reference\n\nScheduling parses standard 5-field cron: `*`, lists (`1,15`), ranges (`1-5`),\nand steps (`*/15`, `9-17/2`). Day-of-week is `0-6` with Sunday `0` (`7` also\nmeans Sunday). When **both** day-of-month and day-of-week are restricted, cron\nfires if *either* matches - the usual \"1st of the month **or** every Monday\"\nsemantics that most hand-rolled matchers get wrong.\n\n```\n ┌───────── minute (0-59)\n │ ┌─────── hour (0-23)\n │ │ ┌───── day-of-month (1-31)\n │ │ │ ┌─── month (1-12)\n │ │ │ │ ┌─ day-of-week (0-6, Sun=0)\n │ │ │ │ │\n 30 17 * * 1-5 -> weekdays at 17:30\n```\n\n| Cron | Fires |\n| ---- | ----- |\n| `* * * * *` | Every minute |\n| `*/15 * * * *` | Every 15 minutes |\n| `0 * * * *` | Hourly, on the hour |\n| `0 9 * * 1-5` | Weekdays at 09:00 |\n| `30 17 * * 1-5` | Weekdays at 17:30 |\n| `0 0 * * *` | Daily at midnight |\n| `0 8 * * 1` | Monday mornings at 08:00 |\n| `0 6 1 * *` | The 1st of each month at 06:00 |\n\nThe same list ships as `CRON_PRESETS` for populating a picker:\n\n```ts\nimport { CRON_PRESETS } from '@svgrid/enterprise'\n// [{ label: 'Weekdays at 17:30', cron: '30 17 * * 1-5' }, ...]\n```\n\nA malformed expression throws at `parseCron` / `createScheduler` setup time, not\nsilently at 3am - so a typo fails loudly where you can see it.\n\n### Time zones\n\nCron matches against the **browser's local time** (`getHours`, `getDay`, and so\non). A `30 17 * * *` schedule fires at 17:30 in whatever zone the user's machine\nis set to - which is usually what a person means by \"half five.\" If you need a\nfixed zone regardless of where the viewer sits (say, exchange hours), convert the\ntarget time into the user's local offset when you build the cron, or gate the\naction inside `onFire` on a zone-aware check.\n\n### Missed runs while the tab is closed\n\nThe scheduler only fires when a tick lands inside the matching minute. If the tab\nis closed at 17:30 and reopened at 17:45, that day's `30 17` run is **skipped** -\nit is not replayed on reopen. This is the honest consequence of client-side\ntiming, and the reason guaranteed delivery belongs on a server.\n\nWhen \"did we miss one while away?\" matters, persist the last time the app was\nlive and check it on startup with `nextRun`:\n\n```ts\nimport { nextRun } from '@svgrid/enterprise'\n\n// `lastSeen` (ms) is persisted when the app last ran; start from the minute\n// AFTER it so a run you already processed at that minute is not replayed.\nconst now = new Date()\nfor (const schedule of schedules) {\n const due = nextRun(schedule, new Date(lastSeen + 60_000))\n if (due && due <= now) {\n // A scheduled time elapsed while the app was closed - run catch-up.\n onFire(schedule, due)\n }\n}\n```\n\n## Persisting schedules\n\nBecause a `Schedule` is plain JSON, persistence is trivial - `localStorage`, a\ndatabase table, or a saved view. Read them back on load and hand the array\nstraight to `createScheduler`; the scheduler reads the array each tick, so you\ncan add, remove, or toggle schedules at runtime and the next tick picks up the\nchange.\n\n```ts\n// Save\nlocalStorage.setItem('schedules', JSON.stringify(schedules))\n// Restore\nconst schedules: Schedule[] = JSON.parse(localStorage.getItem('schedules') ?? '[]')\n```\n\n## Building a schedules panel\n\n`nextRun` and the scheduler's `upcoming()` give you the next fire time per\nschedule - everything a management UI needs. Here is the panel from the image\nabove, wired to the same `schedules` array the scheduler runs:\n\n```svelte\n<script lang=\"ts\">\n import { nextRun, type Schedule } from '@svgrid/enterprise'\n\n let { schedules = $bindable() }: { schedules: Schedule[] } = $props()\n const now = new Date()\n const fmt = (d: Date | null) =>\n d ? d.toLocaleString(undefined, { weekday: 'short', hour: '2-digit', minute: '2-digit' }) : '-'\n</script>\n\n<table class=\"schedules\">\n <thead>\n <tr><th>Name</th><th>Cadence</th><th>Next run</th><th>Status</th></tr>\n </thead>\n <tbody>\n {#each schedules as s (s.id)}\n <tr>\n <td>{s.name}</td>\n <td><code>{s.runAt ? 'Once' : s.cron}</code></td>\n <td>{fmt(nextRun(s, now))}</td>\n <td>\n <button onclick={() => (s.enabled = s.enabled === false)}>\n {s.enabled === false ? 'Off' : 'On'}\n </button>\n </td>\n </tr>\n {/each}\n </tbody>\n</table>\n```\n\nToggling a row flips `enabled` on the live array; the scheduler honors it on its\nnext tick, no restart needed.\n\n## Testing your schedules\n\nEverything except `createScheduler`'s timer is pure and clock-injectable, so you\ncan unit-test schedules against a fixed instant - no waiting on the wall clock,\nno flaky CI:\n\n```ts\nimport { isScheduleDue, nextRun, createScheduler } from '@svgrid/enterprise'\n\n// Is this schedule due at a specific minute?\nisScheduleDue({ id: 'eod', cron: '30 17 * * 1-5' }, new Date('2026-07-27T17:30')) // true\n\n// Drive the runtime with a fake clock and assert onFire ran once.\nlet clock = new Date('2026-07-27T17:30')\nconst fired: string[] = []\nconst scheduler = createScheduler({\n schedules: [{ id: 'eod', cron: '30 17 * * 1-5' }],\n onFire: (s) => fired.push(s.id),\n now: () => clock,\n})\nscheduler.tick()\nscheduler.tick() // same minute again\n// fired === ['eod'] (deduped to one fire per minute)\n```\n\n## API summary\n\n| Export | Purpose |\n| ------ | ------- |\n| `createScheduler({ schedules, onFire, now?, intervalMs? })` | The runtime. Returns `.start()`, `.stop()`, `.tick(at?)`, `.upcoming(at?)`. |\n| `cronMatches(expr, date)` | Does a cron expression match a `Date` to the minute? |\n| `isScheduleDue(schedule, date)` | Is a schedule (cron **or** one-off) due in that minute? |\n| `nextRun(schedule, from)` | Next fire time at/after `from`, or `null`. |\n| `parseCron(expr)` | Parse + validate a cron expression (throws on error). |\n| `CRON_PRESETS` | Common `{ label, cron }` pairs for a picker. |\n| `type Schedule`, `type Scheduler` | The data shape and the runtime handle. |\n\n## When to use a server instead\n\nClient-side scheduling is the right tool when the tab is reliably open and the\naction is local: download a file, show a reminder, refresh a view for the\nalways-on dashboard. Reach for a real backend job when you need delivery with\n**no browser open** (nightly emails to people who are asleep), an\n**authoritative audit trail**, or **fan-out to many recipients**. The two\ncompose cleanly: run the interactive, always-on schedules here and let the server\nown the guaranteed ones.\n"
|
|
3737
|
+
"markdown": "# Scheduling: automate exports and reminders\n\nTwo jobs turn up in almost every data app the moment it goes into daily use:\n*\"email me this report every weekday at 17:30\"* and *\"remind the desk at 09:00\nto reconcile.\"* The reflex is to stand up a backend job runner. But in a\nlong-lived data app - a dashboard that stays open on a wall screen, an ops\nconsole a team lives in all day - a schedule is just **a timer plus an action\nyou already have**: an [export](/help/export.md) or a\n[toast alert](/help/ui-components/sv-toaster.md).\n\n**Scheduling** ships in the paid\n**[@svgrid/enterprise](https://www.npmjs.com/package/@svgrid/enterprise)**\nadd-on. It supplies the missing middle - a pure cron matcher and a small\nclient-side runtime that fires your callback when a schedule comes due - and\nleaves the *action* to you. A scheduled report reuses the same\n`exportCsv` / Excel / PDF path as the toolbar button; a scheduled alert reuses\nthe same `toast` you already call on save. Nothing new to learn on the action\nside, only a trigger.\n\n\n\n## At a glance\n\n| | |\n| --- | --- |\n| **Triggers** | Recurring (5-field cron) or one-off (ISO datetime) |\n| **Actions** | Any export (CSV/TSV/JSON free, Excel/PDF/HTML enterprise) or any `toast` |\n| **Runtime** | Client-side, ticks twice a minute, one fire per schedule per minute |\n| **Guarantees** | One-offs fire exactly once, ever; disabled schedules never fire |\n| **Testable** | Pure matcher with an injectable clock - no wall-clock flakiness |\n| **Package** | `@svgrid/enterprise` (the trigger); actions use whatever tier you own |\n\n> **By design, schedules run in the browser tab**, so the app has to be open\n> when a schedule is due. That is exactly right for the always-on dashboard case\n> where a server cron is overkill; when you need guaranteed delivery whether or\n> not anyone is watching, [pair it with a server job](#when-to-use-a-server-instead).\n\n## How it works\n\nA schedule flows through three stages. A **trigger** (a cron expression or a\none-off `runAt`) comes due; the **scheduler** notices on its next tick and calls\nyour **`onFire`** callback; the callback runs an action - an export or an alert.\nThe scheduler owns the timing and the de-duplication so your callback stays a\nplain function of \"which schedule fired.\"\n\n\n\n## The `Schedule` shape\n\nA schedule is plain data - store it in app state, a database row, or a\n[saved view](/help/saved-views.md). It fires **recurring** on a cron expression,\nor **once** at an ISO datetime.\n\n```ts\nimport type { Schedule } from '@svgrid/enterprise'\n\nconst schedules: Schedule[] = [\n // Recurring: every weekday at 17:30.\n { id: 'eod', name: 'End-of-day CSV', cron: '30 17 * * 1-5' },\n // Recurring: every morning at 09:00.\n { id: 'standup', name: 'Stand-up reminder', cron: '0 9 * * *' },\n // One-off: fire a single time, then never again.\n { id: 'launch', name: 'Go-live snapshot', runAt: '2026-08-01T08:00:00' },\n // Kept but paused - no code change to re-enable.\n { id: 'audit', name: 'Weekly audit', cron: '0 6 * * 1', enabled: false },\n]\n```\n\n| Field | Meaning |\n| ----- | ------- |\n| `id` | Stable id. Keys your `onFire` switch and dedupes fires. |\n| `name` | Human label for a panel or toast. |\n| `cron` | 5-field cron `\"min hour day-of-month month day-of-week\"` (recurring). |\n| `runAt` | ISO datetime for a **one-off**. Takes precedence over `cron`. |\n| `enabled` | Set `false` to keep the definition but stop it firing. Default `true`. |\n\nThat `enabled` flag matters more than it looks: keeping a schedule around while\nswitched off - no code edit, no deletion - is the difference between a demo and\nsomething a team can actually manage.\n\n## Running the scheduler\n\n`createScheduler` ticks on an interval (twice a minute by default), fires\n`onFire` for every schedule due in the current minute, and guarantees **at most\none fire per schedule per minute** - and exactly one, ever, for a one-off. Start\nit once when your view mounts and stop it on teardown.\n\n```svelte\n<script lang=\"ts\">\n import { createScheduler } from '@svgrid/enterprise'\n import { toast, type SvGridApi } from '@svgrid/grid'\n\n let api = $state<SvGridApi<typeof features, Row> | null>(null)\n\n $effect(() => {\n if (!api) return\n const scheduler = createScheduler({\n schedules,\n onFire(schedule) {\n if (schedule.id === 'eod') {\n // Scheduled report: reuse the same export path as the toolbar button.\n api!.exportCsv({ filename: 'end-of-day' })\n toast.success('End-of-day report downloaded')\n } else {\n // Scheduled alert: a plain reminder, no data-change trigger needed.\n toast.info(schedule.name ?? 'Reminder', { duration: 0 })\n }\n },\n })\n scheduler.start()\n return () => scheduler.stop() // cleanup when the effect re-runs / unmounts\n })\n</script>\n\n<SvGrid {data} {columns} {features} onApiReady={(a) => (api = a)} />\n```\n\nReturning `scheduler.stop` from the `$effect` ties the timer's lifetime to the\ncomponent - no leaked interval when the view unmounts.\n\n### Scheduled reports\n\nAny export the grid can do on demand, it can do on a schedule - the callback\njust calls the export API. CSV / TSV / JSON are free in `@svgrid/grid`; Excel,\nPDF, and styled HTML come from `@svgrid/enterprise`, reusing the same call site:\n\n```ts\nonFire(schedule) {\n if (schedule.id !== 'eod') return\n // Free: exportCsv / exportTsv / exportJson on the community grid.\n api.exportCsv({ filename: 'eod', rows: 'all' })\n // Enterprise: the richer formats reuse the same call site.\n // await exportGrid(api, { format: 'xlsx', filename: 'eod' })\n}\n```\n\nBecause the export defaults to the **current view**, a scheduled report honors\nwhatever filters and sort the user left in place - you are scheduling the *view*,\nnot a query frozen at config time.\n\n### Scheduled alerts\n\nAn alert is just a `toast` on a timer - a daily stand-up nudge, a market-open\nbanner, an end-of-shift prompt. Use `duration: 0` to make it sticky until\ndismissed:\n\n```ts\nonFire(schedule) {\n if (schedule.id === 'standup') {\n toast.info('Daily stand-up in 5 minutes', { title: schedule.name, duration: 0 })\n }\n}\n```\n\nMount a single `<SvToaster />` near your app root so the queue renders. See\n[SvToaster](/help/ui-components/sv-toaster.md) for the toast API.\n\n## Cron reference\n\nScheduling parses standard 5-field cron: `*`, lists (`1,15`), ranges (`1-5`),\nand steps (`*/15`, `9-17/2`). Day-of-week is `0-6` with Sunday `0` (`7` also\nmeans Sunday). When **both** day-of-month and day-of-week are restricted, cron\nfires if *either* matches - the usual \"1st of the month **or** every Monday\"\nsemantics that most hand-rolled matchers get wrong.\n\n```\n ┌───────── minute (0-59)\n │ ┌─────── hour (0-23)\n │ │ ┌───── day-of-month (1-31)\n │ │ │ ┌─── month (1-12)\n │ │ │ │ ┌─ day-of-week (0-6, Sun=0)\n │ │ │ │ │\n 30 17 * * 1-5 -> weekdays at 17:30\n```\n\n| Cron | Fires |\n| ---- | ----- |\n| `* * * * *` | Every minute |\n| `*/15 * * * *` | Every 15 minutes |\n| `0 * * * *` | Hourly, on the hour |\n| `0 9 * * 1-5` | Weekdays at 09:00 |\n| `30 17 * * 1-5` | Weekdays at 17:30 |\n| `0 0 * * *` | Daily at midnight |\n| `0 8 * * 1` | Monday mornings at 08:00 |\n| `0 6 1 * *` | The 1st of each month at 06:00 |\n\nThe same list ships as `CRON_PRESETS` for populating a picker:\n\n```ts\nimport { CRON_PRESETS } from '@svgrid/enterprise'\n// [{ label: 'Weekdays at 17:30', cron: '30 17 * * 1-5' }, ...]\n```\n\nA malformed expression throws at `parseCron` / `createScheduler` setup time, not\nsilently at 3am - so a typo fails loudly where you can see it.\n\n### Time zones\n\nCron matches against the **browser's local time** (`getHours`, `getDay`, and so\non). A `30 17 * * *` schedule fires at 17:30 in whatever zone the user's machine\nis set to - which is usually what a person means by \"half five.\" If you need a\nfixed zone regardless of where the viewer sits (say, exchange hours), convert the\ntarget time into the user's local offset when you build the cron, or gate the\naction inside `onFire` on a zone-aware check.\n\n### Missed runs while the tab is closed\n\nThe scheduler only fires when a tick lands inside the matching minute. If the tab\nis closed at 17:30 and reopened at 17:45, that day's `30 17` run is **skipped** -\nit is not replayed on reopen. This is the honest consequence of client-side\ntiming, and the reason guaranteed delivery belongs on a server.\n\nWhen \"did we miss one while away?\" matters, persist the last time the app was\nlive and check it on startup with `nextRun`:\n\n```ts\nimport { nextRun } from '@svgrid/enterprise'\n\n// `lastSeen` (ms) is persisted when the app last ran; start from the minute\n// AFTER it so a run you already processed at that minute is not replayed.\nconst now = new Date()\nfor (const schedule of schedules) {\n const due = nextRun(schedule, new Date(lastSeen + 60_000))\n if (due && due <= now) {\n // A scheduled time elapsed while the app was closed - run catch-up.\n onFire(schedule, due)\n }\n}\n```\n\n## Persisting schedules\n\nBecause a `Schedule` is plain JSON, persistence is trivial - `localStorage`, a\ndatabase table, or a saved view. Read them back on load and hand the array\nstraight to `createScheduler`; the scheduler reads the array each tick, so you\ncan add, remove, or toggle schedules at runtime and the next tick picks up the\nchange.\n\n```ts\n// Restore\nconst schedules: Schedule[] = JSON.parse(localStorage.getItem('schedules') ?? '[]')\n// Save\nlocalStorage.setItem('schedules', JSON.stringify(schedules))\n```\n\n## Building a schedules panel\n\n`nextRun` and the scheduler's `upcoming()` give you the next fire time per\nschedule - everything a management UI needs. Here is the panel from the image\nabove, wired to the same `schedules` array the scheduler runs:\n\n```svelte\n<script lang=\"ts\">\n import { nextRun, type Schedule } from '@svgrid/enterprise'\n\n let { schedules = $bindable() }: { schedules: Schedule[] } = $props()\n const now = new Date()\n const fmt = (d: Date | null) =>\n d ? d.toLocaleString(undefined, { weekday: 'short', hour: '2-digit', minute: '2-digit' }) : '-'\n</script>\n\n<table class=\"schedules\">\n <thead>\n <tr><th>Name</th><th>Cadence</th><th>Next run</th><th>Status</th></tr>\n </thead>\n <tbody>\n {#each schedules as s (s.id)}\n <tr>\n <td>{s.name}</td>\n <td><code>{s.runAt ? 'Once' : s.cron}</code></td>\n <td>{fmt(nextRun(s, now))}</td>\n <td>\n <button onclick={() => (s.enabled = s.enabled === false)}>\n {s.enabled === false ? 'Off' : 'On'}\n </button>\n </td>\n </tr>\n {/each}\n </tbody>\n</table>\n```\n\nToggling a row flips `enabled` on the live array; the scheduler honors it on its\nnext tick, no restart needed.\n\n## Testing your schedules\n\nEverything except `createScheduler`'s timer is pure and clock-injectable, so you\ncan unit-test schedules against a fixed instant - no waiting on the wall clock,\nno flaky CI:\n\n```ts\nimport { isScheduleDue, nextRun, createScheduler } from '@svgrid/enterprise'\n\n// Is this schedule due at a specific minute?\nisScheduleDue({ id: 'eod', cron: '30 17 * * 1-5' }, new Date('2026-07-27T17:30')) // true\n\n// Drive the runtime with a fake clock and assert onFire ran once.\nlet clock = new Date('2026-07-27T17:30')\nconst fired: string[] = []\nconst scheduler = createScheduler({\n schedules: [{ id: 'eod', cron: '30 17 * * 1-5' }],\n onFire: (s) => fired.push(s.id),\n now: () => clock,\n})\nscheduler.tick()\nscheduler.tick() // same minute again\n// fired === ['eod'] (deduped to one fire per minute)\n```\n\n## API summary\n\n| Export | Purpose |\n| ------ | ------- |\n| `createScheduler({ schedules, onFire, now?, intervalMs? })` | The runtime. Returns `.start()`, `.stop()`, `.tick(at?)`, `.upcoming(at?)`. |\n| `cronMatches(expr, date)` | Does a cron expression match a `Date` to the minute? |\n| `isScheduleDue(schedule, date)` | Is a schedule (cron **or** one-off) due in that minute? |\n| `nextRun(schedule, from)` | Next fire time at/after `from`, or `null`. |\n| `parseCron(expr)` | Parse + validate a cron expression (throws on error). |\n| `CRON_PRESETS` | Common `{ label, cron }` pairs for a picker. |\n| `type Schedule`, `type Scheduler` | The data shape and the runtime handle. |\n\n## When to use a server instead\n\nClient-side scheduling is the right tool when the tab is reliably open and the\naction is local: download a file, show a reminder, refresh a view for the\nalways-on dashboard. Reach for a real backend job when you need delivery with\n**no browser open** (nightly emails to people who are asleep), an\n**authoritative audit trail**, or **fan-out to many recipients**. The two\ncompose cleanly: run the interactive, always-on schedules here and let the server\nown the guaranteed ones.\n"
|
|
3738
3738
|
},
|
|
3739
3739
|
{
|
|
3740
3740
|
"slug": "help/security",
|
|
@@ -3758,7 +3758,7 @@ export const docs = [
|
|
|
3758
3758
|
"slug": "help/server/server-filtering",
|
|
3759
3759
|
"path": "docs/help/server/server-filtering.md",
|
|
3760
3760
|
"title": "Server filtering",
|
|
3761
|
-
"markdown": "# Server filtering\n\nWhen the data lives on the server, the grid does not filter rows itself. It\nrecords what the user typed and emits a single **`ServerFilterModel`**, and your\nbackend turns that model into a `WHERE` clause. This page is a deep dive into\nthat model: its exact shape, the operator set, set-filter faceting, the global\nquick search, and how to map all of it to a **parameterized** query with the\n`normalizeFilters` helper from `@svgrid/enterprise`.\n\nIt builds on the [Server-Side Row Model](./server-row-model.md), where\n`createServerDataSource` owns the request lifecycle.\n\n\n\n## The `ServerFilterModel` shape\n\nEvery `getRows(request)` call receives the current filter as\n`request.filterModel`. It has two parts: a `global` quick-search string and a\n`columns` map keyed by column id.\n\n```ts\ntype ServerFilterModel = {\n global?: string // the quick-filter search box\n columns?: Record<string, { // keyed by column id\n operator: string // equals | contains | startsWith | greaterThan | lessThan | between | isBlank\n value: string\n valueTo?: string // second bound, for `between`\n selectedValues?: string[] // set-filter (facet checklist) selection\n }>\n}\n```\n\nA populated model:\n\n```json\n{\n \"global\": \"berlin\",\n \"columns\": {\n \"status\": { \"operator\": \"equals\", \"value\": \"active\" },\n \"age\": { \"operator\": \"between\", \"value\": \"18\", \"valueTo\": \"65\" },\n \"country\": { \"operator\": \"contains\", \"value\": \"\", \"selectedValues\": [\"DE\", \"FR\"] }\n }\n}\n```\n\nEach entry may carry an operator-style filter (`value` plus, for `between`, a\n`valueTo`) **or** a set-filter selection (`selectedValues`), or both. When\n`selectedValues` is present it wins - the checklist selection takes precedence\nover the operator value.\n\n## The operator set\n\n`operator` is one of seven values. Map each to a predicate:\n\n| `operator` | SQL |\n| -------------- | ------------------------------------- |\n| `equals` | `col = $value` |\n| `contains` | `col ILIKE '%' || $value || '%'` |\n| `startsWith` | `col ILIKE $value || '%'` |\n| `greaterThan` | `col > $value` |\n| `lessThan` | `col < $value` |\n| `between` | `col BETWEEN $value AND $valueTo` |\n| `isBlank` | `col IS NULL OR col = ''` |\n\nAny unrecognized operator is treated as `contains` - the safe, permissive\ndefault.\n\n## Set filters and faceting\n\nA set filter (facet checklist) is expressed with `selectedValues`: the list of\nvalues the user ticked. It maps to an `IN (...)` predicate:\n\n```sql\ncol IN ($v0, $v1, $v2) -- one bound parameter per selected value\n```\n\nBecause `selectedValues` takes precedence over `operator` / `value`, a column\nthat has both a checklist selection and a typed value filters by the checklist.\nBuild the facet list itself with a separate `SELECT DISTINCT col` (or a\npre-computed facet count) query - the model carries only the selection, not the\navailable options.\n\n## The global quick filter\n\n`global` is the free-text quick-search box. It is not scoped to one column: it\nis an **`OR` across your searchable columns**. You decide which columns are\nsearchable.\n\n```sql\n-- global = 'berlin'\n(name ILIKE '%' || $q || '%' OR city ILIKE '%' || $q || '%' OR country ILIKE '%' || $q || '%')\n```\n\nCombine the global `OR` group with the per-column predicates using `AND`: a row\nmust match the quick search **and** every active column filter.\n\n## Mapping to a parameterized WHERE\n\nThe one rule that matters: **never string-concatenate user values into SQL.**\nBind every value as a parameter so a value like `'; DROP TABLE ...` is data, not\ncode. The `IN (...)` list gets one placeholder per selected value; `between`\ngets two.\n\nYou do not have to hand-write the operator switch. `@svgrid/enterprise` ships\n`normalizeFilters(model)`, which flattens the model into one uniform list of\npredicates plus the trimmed search term - the same helper the built-in REST and\nSQL sources use.\n\n```ts\nimport { normalizeFilters } from '@svgrid/enterprise'\n\nconst { predicates, search } = normalizeFilters(filterModel)\n// predicates: Array of backend-neutral predicates over one column each -\n// { column, op: 'in', values } // set filter\n// { column, op: 'isNull' } // isBlank\n// { column, op: 'contains' | 'startsWith' | 'eq' | 'gt' | 'lt', value }\n// { column, op: 'between', value, valueTo }\n// search: the trimmed global term (or undefined)\n```\n\n`normalizeFilters` also does the tidying you would otherwise repeat in every\nbackend: it drops empty operator filters, trims values, prefers `selectedValues`\nwhen present, and for `between` fills a missing bound from the other. Turning\nthat neutral list into bound SQL is then a small, safe switch:\n\n```ts\nfunction buildWhere(filterModel, searchable) {\n const { predicates, search } = normalizeFilters(filterModel)\n const clauses = []\n const params = []\n\n for (const p of predicates) {\n switch (p.op) {\n case 'in': {\n // one bound placeholder per selected value\n const start = params.length\n p.values.forEach((v) => params.push(v))\n const list = p.values.map((_, i) => `$${start + i + 1}`).join(', ')\n clauses.push(`${p.column} IN (${list})`)\n break\n }\n case 'isNull': clauses.push(`(${p.column} IS NULL OR ${p.column} = '')`); break\n case 'contains': clauses.push(`${p.column} ILIKE '%' || $${params.push(p.value)} || '%'`); break\n case 'startsWith': clauses.push(`${p.column} ILIKE $${params.push(p.value)} || '%'`); break\n case 'eq': clauses.push(`${p.column} = $${params.push(p.value)}`); break\n case 'gt': clauses.push(`${p.column} > $${params.push(p.value)}`); break\n case 'lt': clauses.push(`${p.column} < $${params.push(p.value)}`); break\n case 'between': clauses.push(`${p.column} BETWEEN $${params.push(p.value)} AND $${params.push(p.valueTo)}`); break\n }\n }\n\n if (search) {\n const p = params.push(search)\n const or = searchable.map((c) => `${c} ILIKE '%' || $${p} || '%'`).join(' OR ')\n clauses.push(`(${or})`)\n }\n\n return { where: clauses.length ? 'WHERE ' + clauses.join(' AND ') : '', params }\n}\n```\n\nEvery value goes through `params.push`; nothing user-supplied is interpolated\ninto the SQL text.\n\n## Wiring the grid to the controller\n\nRun the grid with `externalFilter` so it emits intent instead of filtering\nlocally, and forward the change to `ctl.setFilter`. Debounce the rapid changes -\neach keystroke should not become its own round trip.\n\n```svelte\n<script lang=\"ts\">\n import { SvGrid } from '@svgrid/grid'\n import { createServerDataSource, type ServerFilterModel } from '@svgrid/grid'\n\n let view = $state(
|
|
3761
|
+
"markdown": "# Server filtering\n\nWhen the data lives on the server, the grid does not filter rows itself. It\nrecords what the user typed and emits a single **`ServerFilterModel`**, and your\nbackend turns that model into a `WHERE` clause. This page is a deep dive into\nthat model: its exact shape, the operator set, set-filter faceting, the global\nquick search, and how to map all of it to a **parameterized** query with the\n`normalizeFilters` helper from `@svgrid/enterprise`.\n\nIt builds on the [Server-Side Row Model](./server-row-model.md), where\n`createServerDataSource` owns the request lifecycle.\n\n\n\n## The `ServerFilterModel` shape\n\nEvery `getRows(request)` call receives the current filter as\n`request.filterModel`. It has two parts: a `global` quick-search string and a\n`columns` map keyed by column id.\n\n```ts\ntype ServerFilterModel = {\n global?: string // the quick-filter search box\n columns?: Record<string, { // keyed by column id\n operator: string // equals | contains | startsWith | greaterThan | lessThan | between | isBlank\n value: string\n valueTo?: string // second bound, for `between`\n selectedValues?: string[] // set-filter (facet checklist) selection\n }>\n}\n```\n\nA populated model:\n\n```json\n{\n \"global\": \"berlin\",\n \"columns\": {\n \"status\": { \"operator\": \"equals\", \"value\": \"active\" },\n \"age\": { \"operator\": \"between\", \"value\": \"18\", \"valueTo\": \"65\" },\n \"country\": { \"operator\": \"contains\", \"value\": \"\", \"selectedValues\": [\"DE\", \"FR\"] }\n }\n}\n```\n\nEach entry may carry an operator-style filter (`value` plus, for `between`, a\n`valueTo`) **or** a set-filter selection (`selectedValues`), or both. When\n`selectedValues` is present it wins - the checklist selection takes precedence\nover the operator value.\n\n## The operator set\n\n`operator` is one of seven values. Map each to a predicate:\n\n| `operator` | SQL |\n| -------------- | ------------------------------------- |\n| `equals` | `col = $value` |\n| `contains` | `col ILIKE '%' || $value || '%'` |\n| `startsWith` | `col ILIKE $value || '%'` |\n| `greaterThan` | `col > $value` |\n| `lessThan` | `col < $value` |\n| `between` | `col BETWEEN $value AND $valueTo` |\n| `isBlank` | `col IS NULL OR col = ''` |\n\nAny unrecognized operator is treated as `contains` - the safe, permissive\ndefault.\n\n## Set filters and faceting\n\nA set filter (facet checklist) is expressed with `selectedValues`: the list of\nvalues the user ticked. It maps to an `IN (...)` predicate:\n\n```sql\ncol IN ($v0, $v1, $v2) -- one bound parameter per selected value\n```\n\nBecause `selectedValues` takes precedence over `operator` / `value`, a column\nthat has both a checklist selection and a typed value filters by the checklist.\nBuild the facet list itself with a separate `SELECT DISTINCT col` (or a\npre-computed facet count) query - the model carries only the selection, not the\navailable options.\n\n## The global quick filter\n\n`global` is the free-text quick-search box. It is not scoped to one column: it\nis an **`OR` across your searchable columns**. You decide which columns are\nsearchable.\n\n```sql\n-- global = 'berlin'\n(name ILIKE '%' || $q || '%' OR city ILIKE '%' || $q || '%' OR country ILIKE '%' || $q || '%')\n```\n\nCombine the global `OR` group with the per-column predicates using `AND`: a row\nmust match the quick search **and** every active column filter.\n\n## Mapping to a parameterized WHERE\n\nThe one rule that matters: **never string-concatenate user values into SQL.**\nBind every value as a parameter so a value like `'; DROP TABLE ...` is data, not\ncode. The `IN (...)` list gets one placeholder per selected value; `between`\ngets two.\n\nYou do not have to hand-write the operator switch. `@svgrid/enterprise` ships\n`normalizeFilters(model)`, which flattens the model into one uniform list of\npredicates plus the trimmed search term - the same helper the built-in REST and\nSQL sources use.\n\n```ts\nimport { normalizeFilters } from '@svgrid/enterprise'\n\nconst { predicates, search } = normalizeFilters(filterModel)\n// predicates: Array of backend-neutral predicates over one column each -\n// { column, op: 'in', values } // set filter\n// { column, op: 'isNull' } // isBlank\n// { column, op: 'contains' | 'startsWith' | 'eq' | 'gt' | 'lt', value }\n// { column, op: 'between', value, valueTo }\n// search: the trimmed global term (or undefined)\n```\n\n`normalizeFilters` also does the tidying you would otherwise repeat in every\nbackend: it drops empty operator filters, trims values, prefers `selectedValues`\nwhen present, and for `between` fills a missing bound from the other. Turning\nthat neutral list into bound SQL is then a small, safe switch:\n\n```ts\nfunction buildWhere(filterModel, searchable) {\n const { predicates, search } = normalizeFilters(filterModel)\n const clauses = []\n const params = []\n\n for (const p of predicates) {\n switch (p.op) {\n case 'in': {\n // one bound placeholder per selected value\n const start = params.length\n p.values.forEach((v) => params.push(v))\n const list = p.values.map((_, i) => `$${start + i + 1}`).join(', ')\n clauses.push(`${p.column} IN (${list})`)\n break\n }\n case 'isNull': clauses.push(`(${p.column} IS NULL OR ${p.column} = '')`); break\n case 'contains': clauses.push(`${p.column} ILIKE '%' || $${params.push(p.value)} || '%'`); break\n case 'startsWith': clauses.push(`${p.column} ILIKE $${params.push(p.value)} || '%'`); break\n case 'eq': clauses.push(`${p.column} = $${params.push(p.value)}`); break\n case 'gt': clauses.push(`${p.column} > $${params.push(p.value)}`); break\n case 'lt': clauses.push(`${p.column} < $${params.push(p.value)}`); break\n case 'between': clauses.push(`${p.column} BETWEEN $${params.push(p.value)} AND $${params.push(p.valueTo)}`); break\n }\n }\n\n if (search) {\n const p = params.push(search)\n const or = searchable.map((c) => `${c} ILIKE '%' || $${p} || '%'`).join(' OR ')\n clauses.push(`(${or})`)\n }\n\n return { where: clauses.length ? 'WHERE ' + clauses.join(' AND ') : '', params }\n}\n```\n\nEvery value goes through `params.push`; nothing user-supplied is interpolated\ninto the SQL text.\n\n## Wiring the grid to the controller\n\nRun the grid with `externalFilter` so it emits intent instead of filtering\nlocally, and forward the change to `ctl.setFilter`. Debounce the rapid changes -\neach keystroke should not become its own round trip.\n\n```svelte\n<script lang=\"ts\">\n import { SvGrid } from '@svgrid/grid'\n import { createServerDataSource, type ServerFilterModel, type ServerState } from '@svgrid/grid'\n\n let view = $state<ServerState<Row>>()\n const ctl = createServerDataSource(source, {\n pageSize: 50,\n onChange: (s) => (view = s),\n })\n ctl.refresh()\n\n // Debounce so typing in the quick search does not fire a query per keystroke.\n let timer: ReturnType<typeof setTimeout>\n function applyFilter(model: ServerFilterModel) {\n clearTimeout(timer)\n timer = setTimeout(() => ctl.setFilter(model), 250)\n }\n\n // Adapt the grid's filter change into a ServerFilterModel.\n function onFiltersChange(f: { global?: string; columns?: any }) {\n applyFilter({ global: f.global, columns: toColumnModel(f.columns) })\n }\n</script>\n\n{#if view}\n <SvGrid\n data={view.rows}\n {columns} {features}\n filterable\n externalFilter\n loading={view.loading}\n pageable={false}\n {onFiltersChange}\n />\n{/if}\n```\n\n`setFilter` resets to page 0 and re-fetches, so a new filter always shows its\nfirst page of matches. The controller's monotonic request id means a slow\nresponse for an old filter can never land after a newer one.\n\n## Index the columns you filter\n\nThe model pushes filtering to the database, so the database has to be ready for\nit. Add an index on each column you filter or sort by. `contains`\n(`ILIKE '%x%'`) cannot use a plain B-tree index - reach for a trigram\n(`pg_trgm`) index or a full-text column for large tables, and prefer\n`startsWith` or `equals` where the UX allows, since those are index-friendly.\n\n## Set-filter values from the server\n\nA column's filter checklist normally lists the distinct values found in the rows\nthe grid has loaded - but in server mode that is only the current page, so values\nthat live on other pages never appear. Pass `serverFilterValues` and the grid\nfetches the full distinct list from your backend the first time a column's filter\nmenu opens (cached per column):\n\n```svelte\n<SvGrid\n {columns}\n serverFilterValues={async (columnId) => {\n const res = await fetch(`/api/values?column=${columnId}`) // SELECT DISTINCT col ...\n return res.json() // string[]\n }}\n/>\n```\n\nNow the checklist shows every value, not just the ones on screen; selecting them\ndrives `filterModel.columns[col].selectedValues` as usual.\n\n## See also\n\n- [Server-Side Row Model](./server-row-model.md) - the datasource contract and request lifecycle.\n- [Server editing](./server-editing.md) - the write side: create / update / delete with optimistic updates.\n"
|
|
3762
3762
|
},
|
|
3763
3763
|
{
|
|
3764
3764
|
"slug": "help/server/server-grouping",
|
|
@@ -3788,7 +3788,7 @@ export const docs = [
|
|
|
3788
3788
|
"slug": "help/server/server-tree-data",
|
|
3789
3789
|
"path": "docs/help/server/server-tree-data.md",
|
|
3790
3790
|
"title": "Server tree data (load on demand)",
|
|
3791
|
-
"markdown": "# Server tree data (load on demand)\n\nWhen a hierarchy is too large to ship up front - a file system, an org\nchart with tens of thousands of people, a geographic drill-down - you do\nnot want to fetch every node before the grid renders. The answer is to\n**seed only the roots**, then fetch each node's children the first time\nthe user expands it.\n\nThis page covers **self-referential tree data** (a node's children are more\nrows of the same shape, found by `parentId`). That is still a pattern you\nassemble over the grid's `data` prop: you keep a flat `allNodes` array plus an\n`expanded` map, derive the visible rows, and run an async `toggle(id)` that\nfetches, appends, and caches. If that flat-tree derivation is new to you, read\n[Tree data](../rows/tree-rows.md) first - this page is its lazy-loading\nextension.\n\n## First-class tree mode\n\n`createServerGroupModel` handles self-referential trees too - turn on\n`treeData` and give it `getRowId` (the node id) and `hasChildren` (whether a\nnode can expand). It owns the lazy expand/collapse, per-node caching, and\nrace-safety; each expand calls `getRows` with `groupKeys` set to the path of\nnode ids, and you return that node's direct children:\n\n```svelte\n<script lang=\"ts\">\n import { SvGrid, createServerGroupModel, serverGroupRows, SvGroupCell, renderComponent } from '@svgrid/grid'\n\n const ctl = createServerGroupModel<Node>(source, {\n treeData: true,\n getRowId: (n) => n.id,\n hasChildren: (n) => n.expandable,\n onChange: (s) => (view = s),\n })\n ctl.refresh() // load the roots (getRows with groupKeys: [])\n\n const rows = $derived(serverGroupRows(view))\n const columns = [\n { field: 'name', header: 'Name',\n cell: (ctx) => renderComponent(SvGroupCell, { row: ctx.row.original, onToggle: ctl.toggleGroup, leafField: 'name' }) },\n ]\n</script>\n\n<SvGrid data={rows} {columns} />\n```\n\nThe source's `getRows` returns a node's children for the requested path; a row\nis expandable when `hasChildren` returns true. See\n[Server grouping](./server-grouping.md) for the full contract and the aggregate\ncase. The rest of this page is the **roll-your-own** alternative, for when you\nwant to own the flat-row derivation yourself.\n\n<img src=\"/docs-media/server-tree-lazy.svg\" alt=\"Load-on-demand tree flow: only roots are seeded; expanding a node fires an async fetchChildren call that shows a loading placeholder; the fetched children are appended to the flat rows and cached so re-expanding is instant.\" width=\"100%\" />\n\n<div data-docs-demo=\"31-lazy-tree-load\" data-height=\"480\"></div>\n\n## The moving parts\n\nFour pieces of state, and one async action:\n\n- `allNodes` - a **flat** array of every node loaded *so far*. It starts\n as just the roots and grows as the user expands branches.\n- `expanded` - a `Record<string, boolean>` of which node ids are open.\n- each node carries a `loadState: 'unloaded' | 'loading' | 'loaded'` and\n an `expandable` flag, so you know whether a fetch is still needed.\n- `visibleRows` - derived: a depth-first walk from the roots that stops\n descending at collapsed nodes and injects a placeholder row while a\n node is loading.\n- `toggle(id)` - the async action that expands/collapses and fetches.\n\n## Seeding the roots\n\nLoad only the top level into `allNodes`. Every seeded node is marked\n`unloaded` so the first expand triggers a fetch:\n\n```ts\ntype LoadState = 'unloaded' | 'loading' | 'loaded'\n\ntype Node = {\n id: string\n parentId: string | null\n depth: number\n name: string\n expandable: boolean\n loadState: LoadState\n childIds: string[]\n}\n\nlet allNodes = $state<Node[]>(await fetchChildren('root')) // roots only\nlet expanded = $state<Record<string, boolean>>({})\n```\n\n`fetchChildren(id)` is **your** function - a `fetch()` to your API, a SQL\ncall in a SvelteKit endpoint, whatever. It returns that node's direct\nchildren, each seeded `unloaded` with an empty `childIds`.\n\n## Toggle: fetch on first expand, cache after\n\n`toggle` flips the expanded flag, and - only when opening a node that has\nnot been loaded yet - sets `loadState: 'loading'`, awaits\n`fetchChildren`, then marks it `'loaded'` and appends the children to the\nflat array. A node already `'loaded'` never fetches again, so re-expand\nis instant:\n\n```ts\nasync function toggle(id: string) {\n const isOpen = !!expanded[id]\n expanded = { ...expanded, [id]: !isOpen }\n if (isOpen) return // collapsing - nothing to fetch\n\n const node = allNodes.find((n) => n.id === id)\n if (!node || !node.expandable || node.loadState === 'loaded') return // cached\n\n allNodes = allNodes.map((n) => (n.id === id ? { ...n, loadState: 'loading' } : n))\n\n try {\n const children = await fetchChildren(id)\n allNodes = allNodes\n .map((n) =>\n n.id === id\n ? { ...n, loadState: 'loaded', childIds: children.map((c) => c.id) }\n : n,\n )\n .concat(children)\n } catch (err) {\n // roll back so the user can retry\n allNodes = allNodes.map((n) => (n.id === id ? { ...n, loadState: 'unloaded' } : n))\n console.error('fetchChildren failed', err)\n }\n}\n```\n\nTwo things worth calling out:\n\n- **Immutable updates.** Each step reassigns `allNodes` / `expanded` so\n Svelte's `$state` reactivity fires. Never mutate a node in place.\n- **The `catch` rolls `loadState` back to `'unloaded'`**, so a failed\n request leaves the node collapsible and retryable instead of stuck on\n a spinner.\n\n## The visible rows and the loading placeholder\n\nDerive what the grid renders. Walk from the roots; at an expanded node\nthat is still `loading`, push a synthetic placeholder row instead of\ndescending:\n\n```ts\ntype ViewRow = Node | { id: string; parentId: string; depth: number; placeholder: true }\n\nconst visibleRows = $derived.by(() => {\n const out: ViewRow[] = []\n const byId = new Map(allNodes.map((n) => [n.id, n]))\n function walk(id: string) {\n const node = byId.get(id)\n if (!node) return\n out.push(node)\n if (!expanded[id]) return\n if (node.loadState === 'loading') {\n out.push({ id: `${id}__loading`, parentId: id, depth: node.depth + 1, placeholder: true })\n return\n }\n for (const cid of node.childIds) walk(cid)\n }\n for (const root of allNodes.filter((n) => n.parentId === null)) walk(root.id)\n return out\n})\n```\n\nHand `visibleRows` to `<SvGrid data={visibleRows} ...>` like any other\ndataset. Render the placeholder as a spinner row inside the name-column\ncell snippet (see [Tree data](../rows/tree-rows.md) for the chevron +\nindentation cell), keying off a `placeholder` type guard.\n\n## Keyboard navigation\n\nGive the tree column standard tree-grid keys. Intercept at the window\nlevel **with a capture listener** so your handler runs before the grid's\nown arrow-key cell mover, and only act when the active cell is on the\nname column:\n\n```ts\nlet activeCol = $state('')\nlet activeRowIndex = $state(0)\n\n$effect(() => {\n function onKey(e: KeyboardEvent) {\n if (activeCol !== 'name') return\n const row = visibleRows[activeRowIndex]\n if (!row || 'placeholder' in row || !row.expandable) return\n const isOpen = !!expanded[row.id]\n if (e.key === 'ArrowRight' && !isOpen) { e.preventDefault(); void toggle(row.id) }\n else if (e.key === 'ArrowLeft' && isOpen) { e.preventDefault(); void toggle(row.id) }\n else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); void toggle(row.id) }\n }\n window.addEventListener('keydown', onKey, true)\n return () => window.removeEventListener('keydown', onKey, true)\n})\n```\n\n- **Right** expands a collapsed node (firing the fetch on first open).\n- **Left** collapses an expanded one.\n- **Enter** / **Space** toggle.\n\nTrack the active cell by wiring `onActiveCellChange` on the grid:\n\n```svelte\n<SvGrid\n data={visibleRows}\n {columns}\n onActiveCellChange={(args) => { activeCol = args.columnId; activeRowIndex = args.rowIndex }}\n/>\n```\n\nRegular arrow keys still move the active cell on non-tree columns - the\ntree keys only fire when focus is on the name column.\n\n## Why this stays a pattern\n\nBecause the tree lives entirely in your derived state, everything else -\nsorting, custom cells, roll-up totals, styling - is the same code you\nwould write for a fully-seeded tree. The only difference is that\n`allNodes` grows over time. There is no server-tree mode to configure and\nno contract to satisfy; you own the fetch, the cache, and the shape of\nthe rows.\n\n## See also\n\n- [Server-Side Row Model](./server-row-model.md) - the page-based\n datasource contract for flat data that lives on the server.\n- [Server grouping](./server-grouping.md) - the sibling pattern: the\n backend returns pre-aggregated group rows and expanding one drills into\n its detail rows.\n- [Tree data](../rows/tree-rows.md) - the flat-list + derived-visible-rows\n foundation this page extends, including the chevron cell and roll-ups.\n"
|
|
3791
|
+
"markdown": "# Server tree data (load on demand)\n\nWhen a hierarchy is too large to ship up front - a file system, an org\nchart with tens of thousands of people, a geographic drill-down - you do\nnot want to fetch every node before the grid renders. The answer is to\n**seed only the roots**, then fetch each node's children the first time\nthe user expands it.\n\nThis page covers **self-referential tree data** (a node's children are more\nrows of the same shape, found by `parentId`). That is still a pattern you\nassemble over the grid's `data` prop: you keep a flat `allNodes` array plus an\n`expanded` map, derive the visible rows, and run an async `toggle(id)` that\nfetches, appends, and caches. If that flat-tree derivation is new to you, read\n[Tree data](../rows/tree-rows.md) first - this page is its lazy-loading\nextension.\n\n## First-class tree mode\n\n`createServerGroupModel` handles self-referential trees too - turn on\n`treeData` and give it `getRowId` (the node id) and `hasChildren` (whether a\nnode can expand). It owns the lazy expand/collapse, per-node caching, and\nrace-safety; each expand calls `getRows` with `groupKeys` set to the path of\nnode ids, and you return that node's direct children:\n\n```svelte\n<script lang=\"ts\">\n import { SvGrid, createServerGroupModel, serverGroupRows, SvGroupCell, renderComponent } from '@svgrid/grid'\n\n type Node = { id: string; name: string; expandable: boolean }\n\n const ctl = createServerGroupModel<Node>(source, {\n treeData: true,\n getRowId: (n) => n.id,\n hasChildren: (n) => n.expandable,\n onChange: (s) => (view = s),\n })\n ctl.refresh() // load the roots (getRows with groupKeys: [])\n\n const rows = $derived(serverGroupRows(view))\n const columns = [\n { field: 'name', header: 'Name',\n cell: (ctx) => renderComponent(SvGroupCell, { row: ctx.row.original, onToggle: ctl.toggleGroup, leafField: 'name' }) },\n ]\n</script>\n\n<SvGrid data={rows} {columns} />\n```\n\nThe source's `getRows` returns a node's children for the requested path; a row\nis expandable when `hasChildren` returns true. See\n[Server grouping](./server-grouping.md) for the full contract and the aggregate\ncase. The rest of this page is the **roll-your-own** alternative, for when you\nwant to own the flat-row derivation yourself.\n\n<img src=\"/docs-media/server-tree-lazy.svg\" alt=\"Load-on-demand tree flow: only roots are seeded; expanding a node fires an async fetchChildren call that shows a loading placeholder; the fetched children are appended to the flat rows and cached so re-expanding is instant.\" width=\"100%\" />\n\n<div data-docs-demo=\"31-lazy-tree-load\" data-height=\"480\"></div>\n\n## The moving parts\n\nFour pieces of state, and one async action:\n\n- `allNodes` - a **flat** array of every node loaded *so far*. It starts\n as just the roots and grows as the user expands branches.\n- `expanded` - a `Record<string, boolean>` of which node ids are open.\n- each node carries a `loadState: 'unloaded' | 'loading' | 'loaded'` and\n an `expandable` flag, so you know whether a fetch is still needed.\n- `visibleRows` - derived: a depth-first walk from the roots that stops\n descending at collapsed nodes and injects a placeholder row while a\n node is loading.\n- `toggle(id)` - the async action that expands/collapses and fetches.\n\n## Seeding the roots\n\nLoad only the top level into `allNodes`. Every seeded node is marked\n`unloaded` so the first expand triggers a fetch:\n\n```ts\ntype LoadState = 'unloaded' | 'loading' | 'loaded'\n\ntype Node = {\n id: string\n parentId: string | null\n depth: number\n name: string\n expandable: boolean\n loadState: LoadState\n childIds: string[]\n}\n\nlet allNodes = $state<Node[]>(await fetchChildren('root')) // roots only\nlet expanded = $state<Record<string, boolean>>({})\n```\n\n`fetchChildren(id)` is **your** function - a `fetch()` to your API, a SQL\ncall in a SvelteKit endpoint, whatever. It returns that node's direct\nchildren, each seeded `unloaded` with an empty `childIds`.\n\n## Toggle: fetch on first expand, cache after\n\n`toggle` flips the expanded flag, and - only when opening a node that has\nnot been loaded yet - sets `loadState: 'loading'`, awaits\n`fetchChildren`, then marks it `'loaded'` and appends the children to the\nflat array. A node already `'loaded'` never fetches again, so re-expand\nis instant:\n\n```ts\nasync function toggle(id: string) {\n const isOpen = !!expanded[id]\n expanded = { ...expanded, [id]: !isOpen }\n if (isOpen) return // collapsing - nothing to fetch\n\n const node = allNodes.find((n) => n.id === id)\n if (!node || !node.expandable || node.loadState === 'loaded') return // cached\n\n allNodes = allNodes.map((n) => (n.id === id ? { ...n, loadState: 'loading' } : n))\n\n try {\n const children = await fetchChildren(id)\n allNodes = allNodes\n .map((n) =>\n n.id === id\n ? { ...n, loadState: 'loaded', childIds: children.map((c) => c.id) }\n : n,\n )\n .concat(children)\n } catch (err) {\n // roll back so the user can retry\n allNodes = allNodes.map((n) => (n.id === id ? { ...n, loadState: 'unloaded' } : n))\n console.error('fetchChildren failed', err)\n }\n}\n```\n\nTwo things worth calling out:\n\n- **Immutable updates.** Each step reassigns `allNodes` / `expanded` so\n Svelte's `$state` reactivity fires. Never mutate a node in place.\n- **The `catch` rolls `loadState` back to `'unloaded'`**, so a failed\n request leaves the node collapsible and retryable instead of stuck on\n a spinner.\n\n## The visible rows and the loading placeholder\n\nDerive what the grid renders. Walk from the roots; at an expanded node\nthat is still `loading`, push a synthetic placeholder row instead of\ndescending:\n\n```ts\ntype ViewRow = Node | { id: string; parentId: string; depth: number; placeholder: true }\n\nconst visibleRows = $derived.by(() => {\n const out: ViewRow[] = []\n const byId = new Map(allNodes.map((n) => [n.id, n]))\n function walk(id: string) {\n const node = byId.get(id)\n if (!node) return\n out.push(node)\n if (!expanded[id]) return\n if (node.loadState === 'loading') {\n out.push({ id: `${id}__loading`, parentId: id, depth: node.depth + 1, placeholder: true })\n return\n }\n for (const cid of node.childIds) walk(cid)\n }\n for (const root of allNodes.filter((n) => n.parentId === null)) walk(root.id)\n return out\n})\n```\n\nHand `visibleRows` to `<SvGrid data={visibleRows} ...>` like any other\ndataset. Render the placeholder as a spinner row inside the name-column\ncell snippet (see [Tree data](../rows/tree-rows.md) for the chevron +\nindentation cell), keying off a `placeholder` type guard.\n\n## Keyboard navigation\n\nGive the tree column standard tree-grid keys. Intercept at the window\nlevel **with a capture listener** so your handler runs before the grid's\nown arrow-key cell mover, and only act when the active cell is on the\nname column:\n\n```ts\nlet activeCol = $state('')\nlet activeRowIndex = $state(0)\n\n$effect(() => {\n function onKey(e: KeyboardEvent) {\n if (activeCol !== 'name') return\n const row = visibleRows[activeRowIndex]\n if (!row || 'placeholder' in row || !row.expandable) return\n const isOpen = !!expanded[row.id]\n if (e.key === 'ArrowRight' && !isOpen) { e.preventDefault(); void toggle(row.id) }\n else if (e.key === 'ArrowLeft' && isOpen) { e.preventDefault(); void toggle(row.id) }\n else if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); void toggle(row.id) }\n }\n window.addEventListener('keydown', onKey, true)\n return () => window.removeEventListener('keydown', onKey, true)\n})\n```\n\n- **Right** expands a collapsed node (firing the fetch on first open).\n- **Left** collapses an expanded one.\n- **Enter** / **Space** toggle.\n\nTrack the active cell by wiring `onActiveCellChange` on the grid:\n\n```svelte\n<SvGrid\n data={visibleRows}\n {columns}\n onActiveCellChange={(args) => { activeCol = args.columnId; activeRowIndex = args.rowIndex }}\n/>\n```\n\nRegular arrow keys still move the active cell on non-tree columns - the\ntree keys only fire when focus is on the name column.\n\n## Why this stays a pattern\n\nBecause the tree lives entirely in your derived state, everything else -\nsorting, custom cells, roll-up totals, styling - is the same code you\nwould write for a fully-seeded tree. The only difference is that\n`allNodes` grows over time. There is no server-tree mode to configure and\nno contract to satisfy; you own the fetch, the cache, and the shape of\nthe rows.\n\n## See also\n\n- [Server-Side Row Model](./server-row-model.md) - the page-based\n datasource contract for flat data that lives on the server.\n- [Server grouping](./server-grouping.md) - the sibling pattern: the\n backend returns pre-aggregated group rows and expanding one drills into\n its detail rows.\n- [Tree data](../rows/tree-rows.md) - the flat-list + derived-visible-rows\n foundation this page extends, including the chevron cell and roll-ups.\n"
|
|
3792
3792
|
},
|
|
3793
3793
|
{
|
|
3794
3794
|
"slug": "help/shadcn",
|
|
@@ -3842,7 +3842,7 @@ export const docs = [
|
|
|
3842
3842
|
"slug": "help/testing",
|
|
3843
3843
|
"path": "docs/help/testing.md",
|
|
3844
3844
|
"title": "Testing your grid",
|
|
3845
|
-
"markdown": "# Testing your grid\n\nHow to write tests that catch regressions before they ship. SvGrid is\ndesigned for both **fast unit tests** (Layer 2, the headless engine,\nruns in pure node) and **slow but accurate browser tests** (Layer 3,\nthe `<SvGrid>` component, needs a real DOM or jsdom).\n\n\n\n## Test pyramid for a grid app\n\n```\n /─────────────\\\n / Playwright \\ slow, ~5-15 / page\n / end-to-end \\ accurate\n /───────────────────\\\n / jsdom + svelte- \\\n / testing-library \\ ~15-60 / file\n / component tests \\\n /───────────────────────────\\\n / vitest engine tests \\ fast, ~50-200 / file\n / (Layer 2, no DOM, pure) \\ pure JS\n /─────────────────────────────────\\\n```\n\nYou want a wide base of fast tests (the engine surface), a narrower\nmiddle layer of component tests (mount + interact with the renderer),\nand a small top layer of e2e tests for the journeys that actually\nmatter to your users.\n\n## Engine tests (Layer 2, vitest)\n\nEvery helper in `@svgrid/grid` is a pure function. Test them\nwithout a DOM.\n\n```ts\nimport { describe, it, expect } from 'vitest'\nimport { createSvGrid, tableFeatures, rowSortingFeature } from '@svgrid/grid'\n\ndescribe('sort behaviour', () => {\n it('sorts by a single column ascending', () => {\n type Row = { id: number; name: string }\n const features = tableFeatures({ rowSortingFeature })\n const grid = createSvGrid<typeof features, Row>({\n data: [\n { id: 1, name: 'Charlie' },\n { id: 2, name: 'Alice' },\n { id: 3, name: 'Bob' },\n ],\n columns: [\n { field: 'id', header: 'ID' },\n { field: 'name', header: 'Name' },\n ],\n _features: features,\n })\n grid.setSort([{ id: 'name', desc: false }])\n const visible = grid.getRowModel().rows.map((r) => r.original.name)\n expect(visible).toEqual(['Alice', 'Bob', 'Charlie'])\n })\n})\n```\n\nEngine tests run at ~10k assertions/second on a modern laptop. The\n`@svgrid/grid` package itself ships [hundreds of these](https://github.com/sv-grid/sv-grid/tree/main/packages/grid/src) -\nyou can model yours after them.\n\n## Enterprise feature tests (vitest + jsdom)\n\nThe Enterprise helpers need `jsdom` because `importData` calls `Blob.text()`\nand `exportData` builds an `<a download>`. Set vitest's `environment`\nto `'jsdom'` for these files.\n\n```ts\nimport { describe, it, expect, beforeEach } from 'vitest'\nimport { importData, setLicenseKey } from '@svgrid/enterprise'\n\nbeforeEach(() => setLicenseKey('SVENTERPRISE-DEV-TEST'))\n\ndescribe('CSV import', () => {\n it('parses, coerces types, and rejects negative prices', async () => {\n const csv = 'id,price\\n1,-5\\n2,10\\n'\n const fakeApi = makeFakeApi() // see below\n const result = await importData(fakeApi, {\n file: csv,\n format: 'csv',\n validator: (row) => row.price < 0\n ? [{ field: 'price', message: 'must be >= 0' }]\n : [],\n })\n expect(result.rows).toHaveLength(2)\n expect(result.errors).toHaveLength(1)\n expect(result.errors[0].rowIndex).toBe(0)\n })\n})\n```\n\nThe 48-test suite in `packages/enterprise/src/*.test.ts` shows the\nfull pattern, including a `fakeApi` stub you can copy.\n\n## Component tests (svelte-testing-library + jsdom)\n\nFor \"does the grid actually render the rows\", mount the `<SvGrid>`\ncomponent in jsdom:\n\n```ts\nimport { render } from '@testing-library/svelte'\nimport { describe, it, expect } from 'vitest'\nimport { SvGrid, tableFeatures, rowSortingFeature, type ColumnDef } from '@svgrid/grid'\n\ntype Row = { id: number; name: string }\n\nconst features = tableFeatures({ rowSortingFeature })\nconst columns: ColumnDef<typeof features, Row>[] = [\n { field: 'id', header: 'ID' },\n { field: 'name', header: 'Name' },\n]\n\ndescribe('<SvGrid>', () => {\n it('renders one row per data entry', () => {\n const { container } = render(SvGrid, {\n props: {\n data: [{ id: 1, name: 'Ada' }, { id: 2, name: 'Linus' }],\n columns,\n features,\n },\n })\n const bodyRows = container.querySelectorAll('tbody tr')\n expect(bodyRows.length).toBe(2)\n })\n})\n```\n\nA few caveats:\n\n- **Vitest config:** `environment: 'jsdom'` plus\n `resolve.conditions: ['browser']` so vitest picks Svelte's client\n build. The `@svgrid/grid` repo's `vite.config.ts` shows the\n exact knobs.\n- **No virtualization in jsdom.** jsdom returns `0` for every layout\n metric, so the row virtualizer never advances. Test on small\n datasets (< 50 rows) at this layer; push virtualization tests to\n Playwright.\n- **No clipboard.** `document.execCommand('copy')` is a no-op in\n jsdom; if you're testing copy/paste, mock the clipboard or skip\n to Playwright.\n\n## End-to-end (Playwright)\n\nFor real-DOM behaviours: virtualization, scroll-driven chunk loading,\nfocus traps, clipboard, the `<SvGrid>`'s `ResizeObserver`-driven\nlayout.\n\n```ts\nimport { test, expect } from '@playwright/test'\n\ntest('Sort + filter + paginate together', async ({ page }) => {\n await page.goto('http://localhost:5180/#/demos/02-sort-filter-paginate')\n // Sort by Customer\n await page.locator('thead th', { hasText: 'Customer' }).click()\n // First row should now be alphabetically first.\n const first = await page.locator('tbody tr').first().textContent()\n expect(first?.startsWith('A')).toBe(true)\n // Apply a filter\n await page.locator('thead th', { hasText: 'Region' }).locator('button[aria-label*=Filter]').click()\n await page.locator('.sv-grid-menu-option', { hasText: 'EMEA' }).click()\n // Row count drops\n const visible = await page.locator('tbody tr').count()\n expect(visible).toBeLessThan(50)\n})\n```\n\nThe 53-demo gallery is the easiest target for e2e: every demo is a\nURL you can navigate, every behaviour is reachable from the keyboard.\nMirror your in-app test flows against a paired demo first; it surfaces\nbugs at the API layer before they hit your app's code.\n\n## Accessibility regression tests\n\nWrap [axe-core](https://github.com/dequelabs/axe-core) into your\nPlaywright suite to catch contrast / role / label regressions on every\ncommit:\n\n```ts\nimport { test, expect } from '@playwright/test'\nimport { injectAxe, checkA11y } from 'axe-playwright'\n\ntest('a11y: quick-start grid', async ({ page }) => {\n await page.goto('http://localhost:5180/#/demos/01-quick-start')\n await injectAxe(page)\n await checkA11y(page, '.sv-grid-shell', {\n detailedReport: false,\n axeOptions: {\n runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa'] },\n },\n })\n})\n```\n\nThe grid passes axe's WCAG 2.1 AA rules at the default theme; if your\ncustom theme breaks contrast, this test fails immediately.\n\n## Visual regression\n\nFor the small set of pixels that matter (header bar height, focus ring\nwidth, the \"selected row\" highlight), Playwright's `toHaveScreenshot()`\nis a good fit:\n\n```ts\ntest('focused cell matches the design system ring', async ({ page }) => {\n await page.goto('http://localhost:5180/#/demos/01-quick-start')\n await page.locator('tbody tr').first().locator('td').first().click()\n await expect(page.locator('.sv-grid-cell-active')).toHaveScreenshot('active-cell.png')\n})\n```\n\nPin the screenshot to a tight selector and a 1x device-pixel-ratio so\nyour team's various GPUs don't churn the baseline.\n\n## Performance regression\n\nThe benchmark script (`pnpm bench`) is meant to be run on every release.\nFor your own app, capture two numbers in CI:\n\n1. **Time to first paint** on your largest grid - run a Playwright\n trace, look at the timing of the first `tbody tr` appearing.\n2. **Sustained scroll p95 frame time** - use Playwright's\n `page.evaluate(() => performance.timing)` or the Chrome DevTools\n protocol's `Performance.getMetrics`.\n\nBoth can fail your CI with a 10% deviation threshold. See\n[Performance benchmarks](./benchmarks.md) for the documented numbers\non the published package.\n\n## Test data fixtures\n\nA common pitfall: ad-hoc test rows that drift across tests until\nnothing reuses them.\n\n```ts\n// tests/fixtures/orders.ts\nexport function makeOrder(overrides: Partial<Order> = {}): Order {\n return {\n id: 1,\n customer: 'Acme',\n total: 100,\n placedAt: '2024-01-01',\n status: 'pending',\n ...overrides,\n }\n}\n```\n\nEvery test uses `makeOrder()` with the diffs it cares about. When the\ndomain shape changes, ONE fixture changes, not 200 tests.\n\n## What NOT to do\n\n- **Don't grep DOM classes.** `.sv-grid-cell-active` is implementation\n detail (see [API stability](./api-stability.md)). Tests against it\n break on minor releases. Prefer `aria-selected=\"true\"` or a custom\n `data-testid`.\n- **Don't snapshot the entire rendered HTML.** Internal markup changes\n per release; snapshots become churn. Snapshot small specific\n fragments instead.\n- **Don't test the framework.** SvGrid is well-tested at the package\n level; you don't need to verify that \"click on a sort header sorts\".\n Test YOUR business rules - \"rejected orders never appear in the\n active queue\".\n\n## See also\n\n- [API stability](./api-stability.md) - what's safe to assert against.\n- [Architecture overview](./architecture.md) - which layer to test at.\n- [Performance benchmarks](./benchmarks.md) - reference numbers you\n can use as CI thresholds.\n- [Accessibility](./accessibility.md) - the a11y contract these tests\n enforce.\n"
|
|
3845
|
+
"markdown": "# Testing your grid\n\nHow to write tests that catch regressions before they ship. SvGrid is\ndesigned for both **fast unit tests** (Layer 2, the headless engine,\nruns in pure node) and **slow but accurate browser tests** (Layer 3,\nthe `<SvGrid>` component, needs a real DOM or jsdom).\n\n\n\n## Test pyramid for a grid app\n\n```\n /─────────────\\\n / Playwright \\ slow, ~5-15 / page\n / end-to-end \\ accurate\n /───────────────────\\\n / jsdom + svelte- \\\n / testing-library \\ ~15-60 / file\n / component tests \\\n /───────────────────────────\\\n / vitest engine tests \\ fast, ~50-200 / file\n / (Layer 2, no DOM, pure) \\ pure JS\n /─────────────────────────────────\\\n```\n\nYou want a wide base of fast tests (the engine surface), a narrower\nmiddle layer of component tests (mount + interact with the renderer),\nand a small top layer of e2e tests for the journeys that actually\nmatter to your users.\n\n## Engine tests (Layer 2, vitest)\n\nEvery helper in `@svgrid/grid` is a pure function. Test them\nwithout a DOM.\n\n```ts\nimport { describe, it, expect } from 'vitest'\nimport {\n createSvGrid, tableFeatures, rowSortingFeature,\n createCoreRowModel, createSortedRowModel, sortFns,\n} from '@svgrid/grid'\n\ndescribe('sort behaviour', () => {\n it('sorts by a single column ascending', () => {\n type Row = { id: number; name: string }\n const features = tableFeatures({ rowSortingFeature })\n const grid = createSvGrid<typeof features, Row>({\n data: [\n { id: 1, name: 'Charlie' },\n { id: 2, name: 'Alice' },\n { id: 3, name: 'Bob' },\n ],\n columns: [\n { field: 'id', header: 'ID' },\n { field: 'name', header: 'Name' },\n ],\n _features: features,\n // Opt into the row models you assert on. The headless grid composes\n // them explicitly, so a grid built without `sortedRowModel` returns\n // rows in source order no matter what `sorting` says.\n _rowModels: {\n coreRowModel: createCoreRowModel(),\n sortedRowModel: createSortedRowModel(sortFns),\n },\n state: { sorting: [{ id: 'name', desc: false }] },\n })\n const visible = grid.getRowModel().rows.map((r) => r.original.name)\n expect(visible).toEqual(['Alice', 'Bob', 'Charlie'])\n })\n})\n```\n\nEngine tests run at ~10k assertions/second on a modern laptop. The\n`@svgrid/grid` package itself ships [hundreds of these](https://github.com/sv-grid/sv-grid/tree/main/packages/grid/src) -\nyou can model yours after them.\n\n## Enterprise feature tests (vitest + jsdom)\n\nThe Enterprise helpers need `jsdom` because `importData` calls `Blob.text()`\nand `exportData` builds an `<a download>`. Set vitest's `environment`\nto `'jsdom'` for these files.\n\n```ts\nimport { describe, it, expect, beforeEach } from 'vitest'\nimport { importData, setLicenseKey } from '@svgrid/enterprise'\n\nbeforeEach(() => setLicenseKey('SVENTERPRISE-DEV-TEST'))\n\ndescribe('CSV import', () => {\n it('parses, coerces types, and rejects negative prices', async () => {\n const csv = 'id,price\\n1,-5\\n2,10\\n'\n const fakeApi = makeFakeApi() // see below\n const result = await importData(fakeApi, {\n file: csv,\n format: 'csv',\n validator: (row) => row.price < 0\n ? [{ field: 'price', message: 'must be >= 0' }]\n : [],\n })\n expect(result.rows).toHaveLength(2)\n expect(result.errors).toHaveLength(1)\n expect(result.errors[0].rowIndex).toBe(0)\n })\n})\n```\n\nThe 48-test suite in `packages/enterprise/src/*.test.ts` shows the\nfull pattern, including a `fakeApi` stub you can copy.\n\n## Component tests (svelte-testing-library + jsdom)\n\nFor \"does the grid actually render the rows\", mount the `<SvGrid>`\ncomponent in jsdom:\n\n```ts\nimport { render } from '@testing-library/svelte'\nimport { describe, it, expect } from 'vitest'\nimport { SvGrid, tableFeatures, rowSortingFeature, type ColumnDef } from '@svgrid/grid'\n\ntype Row = { id: number; name: string }\n\nconst features = tableFeatures({ rowSortingFeature })\nconst columns: ColumnDef<typeof features, Row>[] = [\n { field: 'id', header: 'ID' },\n { field: 'name', header: 'Name' },\n]\n\ndescribe('<SvGrid>', () => {\n it('renders one row per data entry', () => {\n const { container } = render(SvGrid, {\n props: {\n data: [{ id: 1, name: 'Ada' }, { id: 2, name: 'Linus' }],\n columns,\n features,\n },\n })\n const bodyRows = container.querySelectorAll('tbody tr')\n expect(bodyRows.length).toBe(2)\n })\n})\n```\n\nA few caveats:\n\n- **Vitest config:** `environment: 'jsdom'` plus\n `resolve.conditions: ['browser']` so vitest picks Svelte's client\n build. The `@svgrid/grid` repo's `vite.config.ts` shows the\n exact knobs.\n- **No virtualization in jsdom.** jsdom returns `0` for every layout\n metric, so the row virtualizer never advances. Test on small\n datasets (< 50 rows) at this layer; push virtualization tests to\n Playwright.\n- **No clipboard.** `document.execCommand('copy')` is a no-op in\n jsdom; if you're testing copy/paste, mock the clipboard or skip\n to Playwright.\n\n## End-to-end (Playwright)\n\nFor real-DOM behaviours: virtualization, scroll-driven chunk loading,\nfocus traps, clipboard, the `<SvGrid>`'s `ResizeObserver`-driven\nlayout.\n\n```ts\nimport { test, expect } from '@playwright/test'\n\ntest('Sort + filter + paginate together', async ({ page }) => {\n await page.goto('http://localhost:5180/#/demos/02-sort-filter-paginate')\n // Sort by Customer\n await page.locator('thead th', { hasText: 'Customer' }).click()\n // First row should now be alphabetically first.\n const first = await page.locator('tbody tr').first().textContent()\n expect(first?.startsWith('A')).toBe(true)\n // Apply a filter\n await page.locator('thead th', { hasText: 'Region' }).locator('button[aria-label*=Filter]').click()\n await page.locator('.sv-grid-menu-option', { hasText: 'EMEA' }).click()\n // Row count drops\n const visible = await page.locator('tbody tr').count()\n expect(visible).toBeLessThan(50)\n})\n```\n\nThe 53-demo gallery is the easiest target for e2e: every demo is a\nURL you can navigate, every behaviour is reachable from the keyboard.\nMirror your in-app test flows against a paired demo first; it surfaces\nbugs at the API layer before they hit your app's code.\n\n## Accessibility regression tests\n\nWrap [axe-core](https://github.com/dequelabs/axe-core) into your\nPlaywright suite to catch contrast / role / label regressions on every\ncommit:\n\n```ts\nimport { test, expect } from '@playwright/test'\nimport { injectAxe, checkA11y } from 'axe-playwright'\n\ntest('a11y: quick-start grid', async ({ page }) => {\n await page.goto('http://localhost:5180/#/demos/01-quick-start')\n await injectAxe(page)\n await checkA11y(page, '.sv-grid-shell', {\n detailedReport: false,\n axeOptions: {\n runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa'] },\n },\n })\n})\n```\n\nThe grid passes axe's WCAG 2.1 AA rules at the default theme; if your\ncustom theme breaks contrast, this test fails immediately.\n\n## Visual regression\n\nFor the small set of pixels that matter (header bar height, focus ring\nwidth, the \"selected row\" highlight), Playwright's `toHaveScreenshot()`\nis a good fit:\n\n```ts\ntest('focused cell matches the design system ring', async ({ page }) => {\n await page.goto('http://localhost:5180/#/demos/01-quick-start')\n await page.locator('tbody tr').first().locator('td').first().click()\n await expect(page.locator('.sv-grid-cell-active')).toHaveScreenshot('active-cell.png')\n})\n```\n\nPin the screenshot to a tight selector and a 1x device-pixel-ratio so\nyour team's various GPUs don't churn the baseline.\n\n## Performance regression\n\nThe benchmark script (`pnpm bench`) is meant to be run on every release.\nFor your own app, capture two numbers in CI:\n\n1. **Time to first paint** on your largest grid - run a Playwright\n trace, look at the timing of the first `tbody tr` appearing.\n2. **Sustained scroll p95 frame time** - use Playwright's\n `page.evaluate(() => performance.timing)` or the Chrome DevTools\n protocol's `Performance.getMetrics`.\n\nBoth can fail your CI with a 10% deviation threshold. See\n[Performance benchmarks](./benchmarks.md) for the documented numbers\non the published package.\n\n## Test data fixtures\n\nA common pitfall: ad-hoc test rows that drift across tests until\nnothing reuses them.\n\n```ts\n// tests/fixtures/orders.ts\nexport function makeOrder(overrides: Partial<Order> = {}): Order {\n return {\n id: 1,\n customer: 'Acme',\n total: 100,\n placedAt: '2024-01-01',\n status: 'pending',\n ...overrides,\n }\n}\n```\n\nEvery test uses `makeOrder()` with the diffs it cares about. When the\ndomain shape changes, ONE fixture changes, not 200 tests.\n\n## What NOT to do\n\n- **Don't grep DOM classes.** `.sv-grid-cell-active` is implementation\n detail (see [API stability](./api-stability.md)). Tests against it\n break on minor releases. Prefer `aria-selected=\"true\"` or a custom\n `data-testid`.\n- **Don't snapshot the entire rendered HTML.** Internal markup changes\n per release; snapshots become churn. Snapshot small specific\n fragments instead.\n- **Don't test the framework.** SvGrid is well-tested at the package\n level; you don't need to verify that \"click on a sort header sorts\".\n Test YOUR business rules - \"rejected orders never appear in the\n active queue\".\n\n## See also\n\n- [API stability](./api-stability.md) - what's safe to assert against.\n- [Architecture overview](./architecture.md) - which layer to test at.\n- [Performance benchmarks](./benchmarks.md) - reference numbers you\n can use as CI thresholds.\n- [Accessibility](./accessibility.md) - the a11y contract these tests\n enforce.\n"
|
|
3846
3846
|
},
|
|
3847
3847
|
{
|
|
3848
3848
|
"slug": "help/tokens",
|
|
@@ -3860,7 +3860,7 @@ export const docs = [
|
|
|
3860
3860
|
"slug": "help/ui-components/cli",
|
|
3861
3861
|
"path": "docs/help/ui-components/cli.md",
|
|
3862
3862
|
"title": "Add components with the CLI",
|
|
3863
|
-
"markdown": "# Add components with the CLI\n\n`@svgrid/ui` gets a SvGrid UI component into your app - and lets you *see* it -\nin one command.\n\n**See it first, no project needed:**\n\n```sh\nnpx @svgrid/ui try calendar # spins up a sandbox and opens it in your browser\n```\n\n**Put it in your app:**\n\n<div data-docs-add=\"add calendar\"></div>\n\n`add` writes the component and installs `@svgrid/grid` for you. Want to preview it\ninside your own app? Add `--preview` (SvelteKit) - see [See it](#see-it) below.\n\n## How it works\n\n`@svgrid/ui` is a **recipe scaffolder**, not a second component library. `add`\nwrites a minimal, ready-to-edit `.svelte` starter into your project that imports\nfrom [`@svgrid/grid`](./index.md), and installs the package for you.\n\nThe distinction matters: the components themselves live in `@svgrid/grid`, so you\nget bug fixes and new features by bumping one version - while the file `add` drops\nin is *yours* to restyle, rename and wire however you like. It is the fast start,\nnot a fork. Because each recipe is a self-contained demo, `try` and `--preview`\ncan render it immediately.\n\n```\nyour-app/\n src/lib/components/ui/\n calendar.svelte <- your copy, imports { SvCalendar } from '@svgrid/grid'\n```\n\n## See it\n\nTwo ways to render a component, not just drop its file in:\n\n```sh\n# zero setup: cached Vite + Svelte sandbox, opens http://localhost:5173\nnpx @svgrid/ui try button\n\n# try several at once - they all render in the one sandbox\nnpx @svgrid/ui try button calendar slider\n\n# or a whole family by its group id\nnpx @svgrid/ui try inputs\n\n# inside a SvelteKit app: also writes a /preview/button route (+ a /preview index)\nnpx @svgrid/ui add button --preview\n# -> start your dev server, open http://localhost:5173/preview/button\n```\n\n`try` needs no project - it caches a tiny sandbox under your temp dir (so repeat\nruns are instant) and opens the browser, with a theme picker (all
|
|
3863
|
+
"markdown": "# Add components with the CLI\n\n`@svgrid/ui` gets a SvGrid UI component into your app - and lets you *see* it -\nin one command.\n\n**See it first, no project needed:**\n\n```sh\nnpx @svgrid/ui try calendar # spins up a sandbox and opens it in your browser\n```\n\n**Put it in your app:**\n\n<div data-docs-add=\"add calendar\"></div>\n\n`add` writes the component and installs `@svgrid/grid` for you. Want to preview it\ninside your own app? Add `--preview` (SvelteKit) - see [See it](#see-it) below.\n\n## How it works\n\n`@svgrid/ui` is a **recipe scaffolder**, not a second component library. `add`\nwrites a minimal, ready-to-edit `.svelte` starter into your project that imports\nfrom [`@svgrid/grid`](./index.md), and installs the package for you.\n\nThe distinction matters: the components themselves live in `@svgrid/grid`, so you\nget bug fixes and new features by bumping one version - while the file `add` drops\nin is *yours* to restyle, rename and wire however you like. It is the fast start,\nnot a fork. Because each recipe is a self-contained demo, `try` and `--preview`\ncan render it immediately.\n\n```\nyour-app/\n src/lib/components/ui/\n calendar.svelte <- your copy, imports { SvCalendar } from '@svgrid/grid'\n```\n\n## See it\n\nTwo ways to render a component, not just drop its file in:\n\n```sh\n# zero setup: cached Vite + Svelte sandbox, opens http://localhost:5173\nnpx @svgrid/ui try button\n\n# try several at once - they all render in the one sandbox\nnpx @svgrid/ui try button calendar slider\n\n# or a whole family by its group id\nnpx @svgrid/ui try inputs\n\n# inside a SvelteKit app: also writes a /preview/button route (+ a /preview index)\nnpx @svgrid/ui add button --preview\n# -> start your dev server, open http://localhost:5173/preview/button\n```\n\n`try` needs no project - it caches a tiny sandbox under your temp dir (so repeat\nruns are instant) and opens the browser, with a theme picker (all 20 presets) and\na light/dark toggle so you can preview the component in your target theme. Pass\n**several component ids** (or a group id) and they all render together in the same\nsandbox - handy for comparing a set side by side. `--preview` drops a\n`src/routes/preview/<id>` page into an existing SvelteKit app so it renders in your\nrunning dev server. `add` also prints the exact `try` command for whatever you just\nadded, so the \"see it\" step is always one copy-paste away.\n\n## Commands\n\n```sh\n# add one component (installs @svgrid/grid for you)\nnpx @svgrid/ui add calendar\n\n# add several at once, into a folder you choose\nnpx @svgrid/ui add calendar time-picker --dir src/lib/ui\n\n# add a whole family in one go\nnpx @svgrid/ui add date-time\n\n# add + a /preview route you can open in your dev server (SvelteKit)\nnpx @svgrid/ui add calendar --preview\n\n# just write the file, don't run the package manager\nnpx @svgrid/ui add calendar --no-install\n\n# see it with zero setup (one component, several, or a whole family)\nnpx @svgrid/ui try calendar\nnpx @svgrid/ui try calendar time-picker date-range-input\nnpx @svgrid/ui try date-time\n\n# list everything you can add\nnpx @svgrid/ui list\n```\n\n> Running an older CLI? `npx` caches by name - pin the latest with\n> `npx @svgrid/ui@latest ...` (the `try` command and `--preview` flag arrived in 0.3.0).\n\n## Options\n\n| Flag | Description |\n| ---------------- | ---------------------------------------------------------------------------------------------------- |\n| `--preview`, `-p`| (with `add`) Also write a `src/routes/preview/<id>` route so you can see it in your dev server. SvelteKit apps only. |\n| `--dir <path>` | Where to write files. Default: `src/lib/components/ui`, or the `componentsDir` in a project `svgrid.json`. |\n| `--force` | Overwrite files that already exist (otherwise existing files are left untouched). |\n| `--no-install` | Skip installing the dependency; just print the install command. |\n\n## Available components\n\nThe whole [catalogue](./index.md#the-catalogue) is available - every component id\nmaps to its tutorial page. Add them individually (`add calendar`), several at once\n(`add calendar time-picker`), or a full family with its group id:\n\n| Group | Adds |\n| ------------ | ---------------------------------------------------------------- |\n| `date-time` | calendar, time-picker, date-time-picker, date-range-input |\n| `buttons` | button, button-group, repeat-button, toggle-button, switch-button, check-box, radio-group, rating |\n| `inputs` | text-input, text-area, number-input, password-input, masked-input, phone-input, color-input, otp-input, duration-input, tags-input |\n| `selection` | list-box, drop-down-list, combo-box, auto-complete, multi-select, tree-select, grid-select, country-input |\n| `range` | slider, gauge, progress, circular-progress, sparkline, stat |\n| `overlays` | popover, tooltip, modal, drawer, toaster, context-menu, menu |\n| `layout` | tabs, accordion, splitter, dock-layout, dock-manager, card, divider, scroll-area, grid-chart, form, field, file-upload |\n| `feedback` | badge, skeleton, alert, empty-state, chip, timeline, avatar, avatar-group, carousel |\n| `navigation` | breadcrumb, pagination, stepper, nav-pane, tree, command, tour, rich-text |\n\nRun `npx @svgrid/ui list` for the full set with descriptions. Every component is\nalso a plain import from `@svgrid/grid` - the CLI is a convenience, never the only\nway in.\n\n## See also\n\n- [SvGrid UI overview](./index.md) - the full component catalogue.\n- [Headless editors](./headless-editors.md) - build fully custom markup on the same cores.\n"
|
|
3864
3864
|
},
|
|
3865
3865
|
{
|
|
3866
3866
|
"slug": "help/ui-components/date-time",
|
|
@@ -3878,7 +3878,7 @@ export const docs = [
|
|
|
3878
3878
|
"slug": "help/ui-components/headless-editors",
|
|
3879
3879
|
"path": "docs/help/ui-components/headless-editors.md",
|
|
3880
3880
|
"title": "Headless editors",
|
|
3881
|
-
"markdown": "# Headless editors\n\nEvery editor is **headless-first**, exactly like the grid (`createSvGrid` /\n`<SvGrid>`). Each `Sv*` component is a thin styled renderer over a framework-free\nrunes core named `create<Editor>`. Import the core to render your own markup with\nthe kit's state machine, keyboard handling and ARIA - and none of its styles.\n\n## The pattern\n\nA core is a factory that takes **reactive getters** for its inputs and returns\nreactive state, actions, and **prop-getters** you spread onto your own elements.\n\n```svelte\n<script lang=\"ts\">\n import { createListbox } from '@svgrid/grid'\n\n let value = $state<
|
|
3881
|
+
"markdown": "# Headless editors\n\nEvery editor is **headless-first**, exactly like the grid (`createSvGrid` /\n`<SvGrid>`). Each `Sv*` component is a thin styled renderer over a framework-free\nrunes core named `create<Editor>`. Import the core to render your own markup with\nthe kit's state machine, keyboard handling and ARIA - and none of its styles.\n\n## The pattern\n\nA core is a factory that takes **reactive getters** for its inputs and returns\nreactive state, actions, and **prop-getters** you spread onto your own elements.\n\n```svelte\n<script lang=\"ts\">\n import { createListbox, type ListboxValue } from '@svgrid/grid'\n\n // `ListboxValue` covers every selection shape the primitive can hand back:\n // a single value, an array, or a Set when `multiple` is on.\n let value = $state<ListboxValue>(null)\n const options = [\n { value: 'a', label: 'Apple' },\n { value: 'b', label: 'Banana' },\n ]\n\n // Reactive inputs are getters; callbacks are closures.\n const lb = createListbox({\n options: () => options,\n value: () => value,\n onChange: (v) => (value = v),\n })\n</script>\n\n<!-- Your markup, the kit's behavior (roving focus, keyboard, aria-*) -->\n<ul {...lb.rootProps()}>\n {#each options as opt, i (opt.value)}\n <li {...lb.optionProps(i)} class:mine-selected={lb.isSelected(opt.value)}>\n {opt.label}\n </li>\n {/each}\n</ul>\n```\n\n`rootProps()` / `optionProps(i)` return attribute + event bundles (including\n`role`, `aria-*`, `tabindex`, and the `onkeydown`/`onclick` handlers). Spread\nthem and you get the full WAI-ARIA listbox behavior on your own DOM.\n\n## What lives where\n\n- **The core** owns state, selection/parse/format math, keyboard, and ARIA. It\n never touches the DOM.\n- **The styled component** owns rendering concerns only: the `--sg-*` styling,\n portalling/positioning for popovers, scroll-into-view, and DOM measurement.\n\nSo `createNumberInput` clamps/formats/steps and exposes `inputProps()`, while\n`<SvNumberInput>` adds the box, spinner buttons and theme; `createCombobox`\nruns the filter + open/active state, while `<SvComboBox>` adds the portalled\npanel. You can always drop to the core when you need a bespoke look.\n\n## Available cores\n\nSelection: `createListbox`, `createCombobox`, `createDropdownList`,\n`createAutocomplete`, `createTagsInput`, `createCountryInput`, `createButtonGroup`.\nInputs: `createNumberInput`, `createMaskedInput`, `createPhoneInput`,\n`createColorInput`, `createPasswordInput`.\nButtons/toggles: `createToggle`, `createSwitch`, `createCheckbox`,\n`createRadioGroup`, `createRating`.\nDate/time: `createCalendar`, `createTimePicker`, `createDateTimePicker`.\nLayout/range: `createTabs`, `createTree`, `createSlider`, `createGauge`,\n`createAccordion`, `createSplitter`, `createFileUpload`.\nNavigation: `createPagination` (pager), `createStepper` (wizard steps),\n`createCarousel` (slideshow with autoplay). `createPagination` and `createStepper`\nare pure - no runes - so you can unit-test and even run them server-side.\nForms: `createForm` (schema-driven values/errors/touched + validation + submit).\nCommand palette: `createCommand` (fuzzy filter + roving + global hotkey) - pair it\nwith `createOverlay` for the focus-trap/scroll-lock/dismissal, as `SvCommand` does.\nOverlays/menu: `createMenu` (the roving-focus + submenu state machine behind\n`SvMenu` / `SvMenuList` / `SvContextMenu`), `createOverlay` (the dialog lifecycle\nbehind `SvModal` / `SvDrawer`), `createTooltip` (the hover/focus + show-delay +\nEscape state machine behind `SvTooltip`).\n\n**Toasts are already headless.** `SvToaster` is a thin renderer over the exported\n`toastStore` + `toast()` / `dismissToast` / `pauseToast` / `resumeToast` /\n`clearToasts` queue - the state machine (timers, pause-on-hover, live-region\nannouncement) lives in the store, so you can drive it from anywhere or render your\nown toaster over the same store.\n\n### Dialog overlays: `createOverlay`\n\n`SvModal` and `SvDrawer` share one lifecycle core, `createOverlay`: when `open`\nflips true it wires a focus trap, a body scroll-lock, and Escape/backdrop\ndismissal (via the shared dismissable-layer stack, so nested overlays close\ntop-first), and tears them all down on close. Your component renders the backdrop\nand panel and spreads `dialogProps()` for the ARIA:\n\n```svelte\n<script lang=\"ts\">\n import { createOverlay } from '@svgrid/grid'\n let open = $state(false)\n let dialogEl = $state<HTMLElement | null>(null)\n const overlay = createOverlay({\n open: () => open,\n getDialog: () => dialogEl,\n onClose: () => (open = false),\n })\n</script>\n\n{#if open}\n <div class=\"backdrop\">\n <div bind:this={dialogEl} {...overlay.dialogProps({ labelledBy: titleId })}>…</div>\n </div>\n{/if}\n```\n\n### Anchored-panel selects: `createPopoverSelect`\n\nThe dropdown selects that render a panel to `<body>` - `SvMultiSelect`,\n`SvTreeSelect`, `SvGridSelect` - share one engine, `createPopoverSelect`. It owns\nopen/close, the `anchoredRect` positioning (reposition on scroll/resize),\noutside/Escape dismissal via the shared layer stack, a roving `active` index, and\nthe WAI-ARIA combobox wiring (`aria-expanded` / `aria-controls` /\n`aria-activedescendant`). Your component owns the item rendering and passes in the\ntrigger/panel refs as getters:\n\n```svelte\n<script lang=\"ts\">\n import { createPopoverSelect } from '@svgrid/grid'\n let trigger = $state<HTMLElement | null>(null)\n let panel = $state<HTMLElement | null>(null)\n\n const sel = createPopoverSelect({\n itemCount: () => options.length,\n onSelect: (i) => choose(options[i]),\n getTrigger: () => trigger,\n getPanel: () => panel,\n })\n</script>\n\n<button bind:this={trigger} {...sel.triggerProps('listbox')}>Choose</button>\n{#if sel.open}\n <div bind:this={panel} {...sel.focusOwnerProps('listbox')}>\n {#each options as opt, i}\n <div {...sel.itemProps(i)} class:active={sel.isActive(i)}>{opt.label}</div>\n {/each}\n </div>\n{/if}\n```\n\n## Data-model cores (dock)\n\nA few composite components are headless in a second sense: instead of a\n`create*` factory with prop-getters, they expose a **plain serializable state\ntree plus pure, immutable transforms**. The state *is* the headless core - you\nown it, `bind` it, persist it, and drive it from the exported functions; the\n`Sv*` component is only the styled renderer.\n\n- **`SvDockLayout`** - state is a `DockNode` tree (`dockGroup` / `dockTabs` /\n `dockPane`); transforms include `movePane`, `removePane`, `dockInto`,\n `dockSetActive`, `dockSetSizes`, `allPaneIds`.\n- **`SvDockManager`** - state is a `DockManagerState` (`main` / `floating` /\n `autoHide`); transforms include `floatPane`, `dockPaneOnto`, `reorderTab`,\n `autoHideLeaf`, `pinAutoHidden`, `dockManagerClosePane`, `allManagerPaneIds`.\n\nEvery transform returns a new tree and is unit-tested independently of the DOM,\nso you can compute or restore a whole workspace on the server or in a worker.\nSee [SvDockLayout](./sv-dock-layout.md) and [SvDockManager](./sv-dock-manager.md).\n\nPure helpers are exported too - `enabledIndices`/`wrapMove` (the roving-focus\nnavigation math shared by the listbox, menu and popover-select cores),\n`filterOptions`, `groupOptions`, `nextTypeaheadIndex`,\n`virtualRange`/`scrollToIndex` (windowing), `moveTreeNode`/`sortTreeNodes`,\n`rules`/`runRules` (validation), `phoneDigitsValid` - so you can build entirely\ncustom controls on the same foundation.\n\n## Shared editor contract\n\nThe styled editors also share a small props contract (`SvEditorProps`):\n`disabled`, `readonly`, `required`, `invalid`, `error`, `label`, `hint`, `size`,\n`dir`, `name`, `id`, `ariaLabel`. The `editorAria(...)` helper turns that state\ninto the right `aria-invalid` / `aria-required` / `aria-describedby` attributes,\nand `<SvField>` renders the label + hint + error chrome - both exported if you\nwant them on your own markup.\n"
|
|
3882
3882
|
},
|
|
3883
3883
|
{
|
|
3884
3884
|
"slug": "help/ui-components/i18n-rtl",
|
|
@@ -4010,7 +4010,7 @@ export const docs = [
|
|
|
4010
4010
|
"slug": "help/ui-components/sv-chip",
|
|
4011
4011
|
"path": "docs/help/ui-components/sv-chip.md",
|
|
4012
4012
|
"title": "SvChip",
|
|
4013
|
-
"markdown": "# SvChip\n\nA compact, interactive pill for a selected value, filter token, or entity: an\noptional leading avatar or icon, a label, and an optional remove button.\n\n`SvChip` is the interactive cousin of [SvBadge](sv-badge.md). Where a badge is a\nstatic status marker, a chip can be clicked to select and dismissed to remove -\nperfect for tag inputs, active-filter rows, and \"assigned to\" pickers. It draws\nfrom the grid's `--sg-*` semantic tokens in both soft and solid fills.\n\nRelated: [SvBadge](sv-badge.md) · [SvAvatar](sv-avatar.md) · [Feedback & display overview](feedback.md)\n\n## Installation\n\nAdd it with the CLI - this drops a ready-to-edit `SvChip` starter into your app:\n\n<div data-docs-add=\"add chip\"></div>\n\nPrefer to see it first? `npx @svgrid/ui try chip` opens it in a throwaway sandbox - no project needed.\n\nOr install the package and import it directly. `SvChip` ships free in\n`@svgrid/grid` (dependency-free):\n\n<div data-docs-install=\"@svgrid/grid\"></div>\n\n```ts\nimport { SvChip } from '@svgrid/grid'\n```\n\n## Example\n\n<div data-docs-demo=\"339-status-display\" data-height=\"420\" data-code></div>\n\n```svelte\n<script lang=\"ts\">\n import { SvChip } from '@svgrid/grid'\n let tags = $state(['design', 'urgent'])\n</script>\n\n{#each tags as tag}\n <SvChip removable onRemove={() => (tags = tags.filter((t) => t !== tag))}>{tag}</SvChip>\n{/each}\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n| ----------- | ---------------------------------------------------------------------- | --------- | -------------------------------------------------------------- |\n| `variant` | `neutral` \\| `accent` \\| `success` \\| `warning` \\| `danger` \\| `info` | `neutral` | Semantic color, from the matching `--sg-*` token. |\n| `size` | `sm` \\| `md` | `md` | Height, font size, and padding. |\n| `removable` | `boolean` | `false` | Shows a trailing remove (x) button. |\n| `onRemove` | `() => void` | - | Called when the remove button is clicked. |\n| `onClick` | `() => void` | - | Makes the label a button and fires on activation. |\n| `solid` | `boolean` | `false` | Filled solid fill instead of the default soft tint. |\n| `disabled` | `boolean` | `false` | Dims the chip and blocks interaction. |\n| `children` | `Snippet` | - | The chip label. |\n| `leading` | `Snippet` | - | Leading content such as an avatar or icon. |\n\n## Examples\n\n### Removable tags\n\nWire `removable` and `onRemove` to build a tag list from an array:\n\n```svelte\n{#each labels as label (label)}\n <SvChip variant=\"accent\" removable onRemove={() => remove(label)}>{label}</SvChip>\n{/each}\n```\n\n### Clickable filter chips\n\nGive `onClick` to turn a chip into a toggle for an active-filter bar:\n\n```svelte\n<SvChip variant={active ? 'accent' : 'neutral'} solid={active} onClick={toggle}>\n In stock\n</SvChip>\n```\n\n### Entity chips with an avatar\n\nThe `leading` snippet slots an [SvAvatar](sv-avatar.md) for people and entities:\n\n```svelte\n<SvChip onClick={openProfile}>\n {#snippet leading()}<SvAvatar size=\"sm\" name=\"Ada Lovelace\" />{/snippet}\n Ada Lovelace\n</SvChip>\n```\n\n### Active-filter token row\n\nReflect the current filters as a removable chip set: each chip clears its own\nfilter, and a trailing action resets them all:\n\n```svelte\n<script lang=\"ts\">\n import { SvChip, SvButton } from '@svgrid/grid'\n let filters = $state([\n { key: 'status', label: 'Status: Open', variant: 'accent' },\n { key: 'owner', label: 'Owner: Me', variant: 'info' },\n { key: 'due', label: 'Overdue', variant: 'danger' },\n ]
|
|
4013
|
+
"markdown": "# SvChip\n\nA compact, interactive pill for a selected value, filter token, or entity: an\noptional leading avatar or icon, a label, and an optional remove button.\n\n`SvChip` is the interactive cousin of [SvBadge](sv-badge.md). Where a badge is a\nstatic status marker, a chip can be clicked to select and dismissed to remove -\nperfect for tag inputs, active-filter rows, and \"assigned to\" pickers. It draws\nfrom the grid's `--sg-*` semantic tokens in both soft and solid fills.\n\nRelated: [SvBadge](sv-badge.md) · [SvAvatar](sv-avatar.md) · [Feedback & display overview](feedback.md)\n\n## Installation\n\nAdd it with the CLI - this drops a ready-to-edit `SvChip` starter into your app:\n\n<div data-docs-add=\"add chip\"></div>\n\nPrefer to see it first? `npx @svgrid/ui try chip` opens it in a throwaway sandbox - no project needed.\n\nOr install the package and import it directly. `SvChip` ships free in\n`@svgrid/grid` (dependency-free):\n\n<div data-docs-install=\"@svgrid/grid\"></div>\n\n```ts\nimport { SvChip } from '@svgrid/grid'\n```\n\n## Example\n\n<div data-docs-demo=\"339-status-display\" data-height=\"420\" data-code></div>\n\n```svelte\n<script lang=\"ts\">\n import { SvChip } from '@svgrid/grid'\n let tags = $state(['design', 'urgent'])\n</script>\n\n{#each tags as tag}\n <SvChip removable onRemove={() => (tags = tags.filter((t) => t !== tag))}>{tag}</SvChip>\n{/each}\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n| ----------- | ---------------------------------------------------------------------- | --------- | -------------------------------------------------------------- |\n| `variant` | `neutral` \\| `accent` \\| `success` \\| `warning` \\| `danger` \\| `info` | `neutral` | Semantic color, from the matching `--sg-*` token. |\n| `size` | `sm` \\| `md` | `md` | Height, font size, and padding. |\n| `removable` | `boolean` | `false` | Shows a trailing remove (x) button. |\n| `onRemove` | `() => void` | - | Called when the remove button is clicked. |\n| `onClick` | `() => void` | - | Makes the label a button and fires on activation. |\n| `solid` | `boolean` | `false` | Filled solid fill instead of the default soft tint. |\n| `disabled` | `boolean` | `false` | Dims the chip and blocks interaction. |\n| `children` | `Snippet` | - | The chip label. |\n| `leading` | `Snippet` | - | Leading content such as an avatar or icon. |\n\n## Examples\n\n### Removable tags\n\nWire `removable` and `onRemove` to build a tag list from an array:\n\n```svelte\n{#each labels as label (label)}\n <SvChip variant=\"accent\" removable onRemove={() => remove(label)}>{label}</SvChip>\n{/each}\n```\n\n### Clickable filter chips\n\nGive `onClick` to turn a chip into a toggle for an active-filter bar:\n\n```svelte\n<SvChip variant={active ? 'accent' : 'neutral'} solid={active} onClick={toggle}>\n In stock\n</SvChip>\n```\n\n### Entity chips with an avatar\n\nThe `leading` snippet slots an [SvAvatar](sv-avatar.md) for people and entities:\n\n```svelte\n<SvChip onClick={openProfile}>\n {#snippet leading()}<SvAvatar size=\"sm\" name=\"Ada Lovelace\" />{/snippet}\n Ada Lovelace\n</SvChip>\n```\n\n### Active-filter token row\n\nReflect the current filters as a removable chip set: each chip clears its own\nfilter, and a trailing action resets them all:\n\n```svelte\n<script lang=\"ts\">\n import { SvChip, SvButton } from '@svgrid/grid'\n type Filter = { key: string; label: string; variant: 'accent' | 'info' | 'danger' }\n let filters = $state<Filter[]>([\n { key: 'status', label: 'Status: Open', variant: 'accent' },\n { key: 'owner', label: 'Owner: Me', variant: 'info' },\n { key: 'due', label: 'Overdue', variant: 'danger' },\n ])\n\n const remove = (key: string) => (filters = filters.filter((f) => f.key !== key))\n</script>\n\n<div style=\"display:flex; flex-wrap:wrap; gap:6px; align-items:center;\">\n {#each filters as f (f.key)}\n <SvChip variant={f.variant} size=\"sm\" removable onRemove={() => remove(f.key)}>\n {f.label}\n </SvChip>\n {/each}\n {#if filters.length}\n <SvButton size=\"sm\" variant=\"ghost\" onclick={() => (filters = [])}>Clear all</SvButton>\n {/if}\n</div>\n```\n\n> Tip: the remove control is a separate `<button>` from the label, so a chip can\n> be both clickable (`onClick`) and removable (`removable`) at once without the\n> two hit areas overlapping.\n\n## Accessibility\n\n- The remove control is a real `<button>` with `aria-label=\"Remove\"`, reachable\n by keyboard.\n- When `onClick` is set the label becomes a `<button>`, so it is focusable and\n activates with `Enter` / `Space`.\n- `disabled` blocks pointer interaction and dims the chip; keep a text label so\n the chip's meaning does not rely on color alone.\n\n## See also\n\n- [Feedback overview](feedback.md) - the whole status and display layer at a glance.\n- [SvBadge](sv-badge.md) - a static status pill without the interactions.\n- [SvAvatar](sv-avatar.md) - the leading avatar for entity chips.\n"
|
|
4014
4014
|
},
|
|
4015
4015
|
{
|
|
4016
4016
|
"slug": "help/ui-components/sv-circular-progress",
|
|
@@ -4046,7 +4046,7 @@ export const docs = [
|
|
|
4046
4046
|
"slug": "help/ui-components/sv-context-menu",
|
|
4047
4047
|
"path": "docs/help/ui-components/sv-context-menu.md",
|
|
4048
4048
|
"title": "SvContextMenu",
|
|
4049
|
-
"markdown": "# SvContextMenu\n\nWraps a region and opens a menu at the pointer on right-click or long-press.\n\n`SvContextMenu` turns any content into a right-clickable zone. It reuses\n[SvMenuList](sv-menu-list.md) for the surface itself - submenus, keyboard, icons,\nand shortcuts - clamps the menu so it stays on-screen, portals it to `<body>`,\nand closes through the shared dismissable layer stack. You describe the menu with\na plain `MenuItem[]` array and handle picks in `onSelect`.\n\nRelated: [SvMenu](sv-menu.md) · [SvMenuList](sv-menu-list.md) · [Overlays & menus overview](overlays.md)\n\n## Installation\n\nAdd it with the CLI - this drops a ready-to-edit `SvContextMenu` starter into your app:\n\n<div data-docs-add=\"add context-menu\"></div>\n\nPrefer to see it first? `npx @svgrid/ui try context-menu` opens it in a throwaway sandbox - no project needed.\n\nOr install the package and import it directly. `SvContextMenu` ships free in\n`@svgrid/grid` (dependency-free):\n\n<div data-docs-install=\"@svgrid/grid\"></div>\n\n```ts\nimport { SvContextMenu } from '@svgrid/grid'\n```\n\n## Example\n\n<div data-docs-demo=\"331-app-overlays\" data-height=\"440\" data-code></div>\n\n```svelte\n<script lang=\"ts\">\n import { SvContextMenu, type MenuItem } from '@svgrid/grid'\n const items: MenuItem[] = [\n { label: 'Copy', shortcut: 'Ctrl+C', onSelect: () => copy() },\n { label: 'Duplicate', onSelect: () => duplicate() },\n { separator: true },\n { label: 'Delete', onSelect: () => remove() },\n ]\n</script>\n\n<SvContextMenu {items} onSelect={(i) => console.log(i.label)}>\n <div class=\"drop-zone\">Right-click me</div>\n</SvContextMenu>\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n| ----------- | ----------------------------- | ------- | -------------------------------------------------- |\n| `items` | `ReadonlyArray<MenuItem>` | - | The menu contents (see `MenuItem` below). |\n| `onSelect` | `(item: MenuItem) => void` | - | Fires when a leaf item is chosen. |\n| `disabled` | `boolean` | `false` | Suppress the context menu entirely. |\n| `ariaLabel` | `string` | - | Accessible name for the menu surface. |\n| `children` | `Snippet` | - | The region that responds to right-click. |\n\n### MenuItem\n\n| Field | Type | Description |\n| ----------- | ----------------- | ---------------------------------------------------------- |\n| `label` | `string` | Item text. Omit and set `separator` for a divider. |\n| `separator` | `boolean` | Render a divider instead of an item. |\n| `icon` | `Snippet` | Leading icon. |\n| `shortcut` | `string` | Right-aligned hint, e.g. a keyboard shortcut. |\n| `disabled` | `boolean` | Dim and skip the item. |\n| `children` | `MenuItem[]` | Nested submenu items (renders a flyout). |\n| `onSelect` | `() => void` | Per-item callback fired when the item is chosen. |\n\n## Examples\n\n### Per-item handlers\n\nGive each item its own `onSelect` so the array reads like a command list, and use\nthe top-level `onSelect` only for cross-cutting logging:\n\n```ts\nconst items: MenuItem[] = [\n { label: 'Edit', icon: pencil, onSelect: () => edit(row) },\n { label: 'Archive', onSelect: () => archive(row) },\n]\n```\n\n### Submenus and separators\n\nGroup related actions under a `children` array for a flyout, and break sections\nwith `{ separator: true }`:\n\n```ts\nconst items: MenuItem[] = [\n { label: 'Move to', children: [\n { label: 'Inbox', onSelect: () => move('inbox') },\n { label: 'Archive', onSelect: () => move('archive') },\n ]},\n { separator: true },\n { label: 'Delete', disabled: locked, onSelect: () => remove() },\n]\n```\n\n### Row context menu on a grid\n\nBuild the item array from the row under the pointer so each action closes over\nthe right record, and rebuild it when the selection changes. Disable actions\nthat do not apply rather than hiding them, so the menu keeps a stable shape:\n\n```svelte\n<script lang=\"ts\">\n import { SvContextMenu, type MenuItem } from '@svgrid/grid'\n let row = $state({ id: 1, name: 'Ada', archived: false })\n\n const items: MenuItem[] = $derived([\n { label: 'Open', shortcut: 'Enter', onSelect: () =>
|
|
4049
|
+
"markdown": "# SvContextMenu\n\nWraps a region and opens a menu at the pointer on right-click or long-press.\n\n`SvContextMenu` turns any content into a right-clickable zone. It reuses\n[SvMenuList](sv-menu-list.md) for the surface itself - submenus, keyboard, icons,\nand shortcuts - clamps the menu so it stays on-screen, portals it to `<body>`,\nand closes through the shared dismissable layer stack. You describe the menu with\na plain `MenuItem[]` array and handle picks in `onSelect`.\n\nRelated: [SvMenu](sv-menu.md) · [SvMenuList](sv-menu-list.md) · [Overlays & menus overview](overlays.md)\n\n## Installation\n\nAdd it with the CLI - this drops a ready-to-edit `SvContextMenu` starter into your app:\n\n<div data-docs-add=\"add context-menu\"></div>\n\nPrefer to see it first? `npx @svgrid/ui try context-menu` opens it in a throwaway sandbox - no project needed.\n\nOr install the package and import it directly. `SvContextMenu` ships free in\n`@svgrid/grid` (dependency-free):\n\n<div data-docs-install=\"@svgrid/grid\"></div>\n\n```ts\nimport { SvContextMenu } from '@svgrid/grid'\n```\n\n## Example\n\n<div data-docs-demo=\"331-app-overlays\" data-height=\"440\" data-code></div>\n\n```svelte\n<script lang=\"ts\">\n import { SvContextMenu, type MenuItem } from '@svgrid/grid'\n const items: MenuItem[] = [\n { label: 'Copy', shortcut: 'Ctrl+C', onSelect: () => copy() },\n { label: 'Duplicate', onSelect: () => duplicate() },\n { separator: true },\n { label: 'Delete', onSelect: () => remove() },\n ]\n</script>\n\n<SvContextMenu {items} onSelect={(i) => console.log(i.label)}>\n <div class=\"drop-zone\">Right-click me</div>\n</SvContextMenu>\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n| ----------- | ----------------------------- | ------- | -------------------------------------------------- |\n| `items` | `ReadonlyArray<MenuItem>` | - | The menu contents (see `MenuItem` below). |\n| `onSelect` | `(item: MenuItem) => void` | - | Fires when a leaf item is chosen. |\n| `disabled` | `boolean` | `false` | Suppress the context menu entirely. |\n| `ariaLabel` | `string` | - | Accessible name for the menu surface. |\n| `children` | `Snippet` | - | The region that responds to right-click. |\n\n### MenuItem\n\n| Field | Type | Description |\n| ----------- | ----------------- | ---------------------------------------------------------- |\n| `label` | `string` | Item text. Omit and set `separator` for a divider. |\n| `separator` | `boolean` | Render a divider instead of an item. |\n| `icon` | `Snippet` | Leading icon. |\n| `shortcut` | `string` | Right-aligned hint, e.g. a keyboard shortcut. |\n| `disabled` | `boolean` | Dim and skip the item. |\n| `children` | `MenuItem[]` | Nested submenu items (renders a flyout). |\n| `onSelect` | `() => void` | Per-item callback fired when the item is chosen. |\n\n## Examples\n\n### Per-item handlers\n\nGive each item its own `onSelect` so the array reads like a command list, and use\nthe top-level `onSelect` only for cross-cutting logging:\n\n```ts\nconst items: MenuItem[] = [\n { label: 'Edit', icon: pencil, onSelect: () => edit(row) },\n { label: 'Archive', onSelect: () => archive(row) },\n]\n```\n\n### Submenus and separators\n\nGroup related actions under a `children` array for a flyout, and break sections\nwith `{ separator: true }`:\n\n```ts\nconst items: MenuItem[] = [\n { label: 'Move to', children: [\n { label: 'Inbox', onSelect: () => move('inbox') },\n { label: 'Archive', onSelect: () => move('archive') },\n ]},\n { separator: true },\n { label: 'Delete', disabled: locked, onSelect: () => remove() },\n]\n```\n\n### Row context menu on a grid\n\nBuild the item array from the row under the pointer so each action closes over\nthe right record, and rebuild it when the selection changes. Disable actions\nthat do not apply rather than hiding them, so the menu keeps a stable shape:\n\n```svelte\n<script lang=\"ts\">\n import { SvContextMenu, type MenuItem } from '@svgrid/grid'\n let row = $state({ id: 1, name: 'Ada', archived: false })\n\n const items: MenuItem[] = $derived([\n { label: 'Open', shortcut: 'Enter', onSelect: () => openRow(row) },\n { label: 'Duplicate', onSelect: () => duplicate(row) },\n { separator: true },\n { label: 'Archive', disabled: row.archived, onSelect: () => archive(row) },\n { label: 'Delete', onSelect: () => remove(row) },\n ])\n</script>\n\n<SvContextMenu {items} ariaLabel=\"Row actions\" onSelect={(i) => log(i.label)}>\n <div class=\"row\" onpointerdown={() => (row = pickRowUnderPointer())}>\n {row.name}\n </div>\n</SvContextMenu>\n```\n\nTip: the menu closes on page scroll, since a scrolled-away anchor would strand\nit. That means it is meant to be picked from promptly - do not rely on it staying\nopen across a scroll.\n\n### Conditional menus\n\nBind `disabled` to context - for example, no menu on read-only rows - to skip the\nhandler without unwrapping the region.\n\n## Accessibility\n\n- The surface is a WAI-ARIA `menu`; the first enabled item is focused on open and\n arrow keys, Home/End, Enter, and Escape all work (via SvMenuList).\n- The menu portals to `<body>` and is clamped to the viewport, so it never opens\n off-screen or is clipped by an ancestor.\n- A page scroll closes the menu, since a moved anchor would leave it stranded.\n\n## See also\n\n- [Overlays overview](overlays.md) - the whole floating-surface family.\n- [SvMenu](sv-menu.md) - the same menu surface opened from a trigger button.\n- [SvMenuList](sv-menu-list.md) - the recursive surface both share.\n"
|
|
4050
4050
|
},
|
|
4051
4051
|
{
|
|
4052
4052
|
"slug": "help/ui-components/sv-country-input",
|
|
@@ -4430,7 +4430,7 @@ export const docs = [
|
|
|
4430
4430
|
"slug": "help/ui-components/sv-tree",
|
|
4431
4431
|
"path": "docs/help/ui-components/sv-tree.md",
|
|
4432
4432
|
"title": "SvTree",
|
|
4433
|
-
"markdown": "# SvTree\n\nA WAI-ARIA tree view: expand/collapse, single-select highlight, cascading\ntri-state checkboxes, keyboard navigation, and - for large or remote data -\nfixed-row virtualization and lazy loading.\n\n`SvTree` renders hierarchical data (file explorers, org charts, category pickers).\nThe behavior - flattening, cascade math, keyboard, and ARIA - lives in the\nheadless `createTree` core; the component owns the DOM concerns: windowing, focus,\ninline rename, drag-drop reorder, and merging lazily-loaded children. Turn on\n`virtual` to scale to tens of thousands of nodes, or `loadChildren` to fetch each\nfolder's children on first expand. Colors come from the `--sg-*` tokens.\n\nRelated: [SvNavPane](sv-nav-pane.md) · [SvTreeSelect](sv-tree-select.md) · [Navigation & rich overview](navigation.md)\n\n## Installation\n\nAdd it with the CLI - this drops a ready-to-edit `SvTree` starter into your app:\n\n<div data-docs-add=\"add tree\"></div>\n\nPrefer to see it first? `npx @svgrid/ui try tree` opens it in a throwaway sandbox - no project needed.\n\nOr install the package and import it directly. `SvTree` ships free in\n`@svgrid/grid` (dependency-free):\n\n<div data-docs-install=\"@svgrid/grid\"></div>\n\n```ts\nimport { SvTree } from '@svgrid/grid'\n```\n\n## Example\n\n<div data-docs-demo=\"321-tree\" data-height=\"440\" data-code></div>\n\n```svelte\n<script lang=\"ts\">\n import { SvTree, type
|
|
4433
|
+
"markdown": "# SvTree\n\nA WAI-ARIA tree view: expand/collapse, single-select highlight, cascading\ntri-state checkboxes, keyboard navigation, and - for large or remote data -\nfixed-row virtualization and lazy loading.\n\n`SvTree` renders hierarchical data (file explorers, org charts, category pickers).\nThe behavior - flattening, cascade math, keyboard, and ARIA - lives in the\nheadless `createTree` core; the component owns the DOM concerns: windowing, focus,\ninline rename, drag-drop reorder, and merging lazily-loaded children. Turn on\n`virtual` to scale to tens of thousands of nodes, or `loadChildren` to fetch each\nfolder's children on first expand. Colors come from the `--sg-*` tokens.\n\nRelated: [SvNavPane](sv-nav-pane.md) · [SvTreeSelect](sv-tree-select.md) · [Navigation & rich overview](navigation.md)\n\n## Installation\n\nAdd it with the CLI - this drops a ready-to-edit `SvTree` starter into your app:\n\n<div data-docs-add=\"add tree\"></div>\n\nPrefer to see it first? `npx @svgrid/ui try tree` opens it in a throwaway sandbox - no project needed.\n\nOr install the package and import it directly. `SvTree` ships free in\n`@svgrid/grid` (dependency-free):\n\n<div data-docs-install=\"@svgrid/grid\"></div>\n\n```ts\nimport { SvTree } from '@svgrid/grid'\n```\n\n## Example\n\n<div data-docs-demo=\"321-tree\" data-height=\"440\" data-code></div>\n\n```svelte\n<script lang=\"ts\">\n import { SvTree, type SvTreeNode } from '@svgrid/grid'\n let selected = $state<string | null>(null)\n const nodes: SvTreeNode[] = [\n { id: 'src', label: 'src', children: [\n { id: 'app', label: 'App.svelte' },\n { id: 'main', label: 'main.ts' },\n ] },\n ]\n</script>\n\n<SvTree {nodes} bind:selected onSelect={(id) => open(id)} />\n```\n\n## Props\n\n| Prop | Type | Default | Description |\n| ----------------- | ------------------------------------------------------- | -------------- | -------------------------------------------------------------- |\n| `nodes` | `ReadonlyArray<SvTreeNode>` | - | The tree data. |\n| `selected` | `string \\| null` | `null` | Selected node id (single-select highlight). |\n| `onSelect` | `(id: string) => void` | - | Fires when a node is selected. |\n| `expandedIds` | `string[]` | - | Controlled/initial expanded ids. |\n| `onToggle` | `(id: string, expanded: boolean) => void` | - | Fires on expand/collapse. |\n| `checkable` | `boolean` | `false` | Show cascading tri-state checkboxes. |\n| `checked` | `string[]` | `[]` | The set of checked ids. |\n| `onCheck` | `(ids: string[]) => void` | - | Fires with the new checked set. |\n| `ariaLabel` | `string` | - | Accessible name for the tree. |\n| `dir` | `EditorDir` | - | Text direction; `rtl` mirrors indentation and flips arrows. |\n| `virtual` | `boolean` | `false` | Window rows (render only the visible slice). Needs `height`. |\n| `rowHeight` | `number` | `30` | Fixed row height in px (must match the CSS). |\n| `height` | `number` | - | Scroll-viewport height in px. Required for `virtual`. |\n| `loadChildren` | `(node: SvTreeNode) => Promise<SvTreeNode[]>` | - | Lazy-load a `lazy` node's children on first expand. |\n| `filter` | `string` | - | Show only matching nodes and their ancestors, auto-expanded. |\n| `searchable` | `boolean` | `false` | Render a built-in search box that drives the filter. |\n| `searchPlaceholder` | `string` | `Search...` | Placeholder for the search box. |\n| `sort` | `asc` \\| `desc` \\| `(a, b) => number` | - | Sort siblings by label or a custom comparator. |\n| `editable` | `boolean` | `false` | Allow inline rename (double-click or F2); fires `onRename`. |\n| `onRename` | `(id: string, label: string) => void` | - | Fires with the new label after an inline edit. |\n| `reorderable` | `boolean` | `false` | Allow drag-drop reorder; fires `onMove`. |\n| `onMove` | `(dragId, targetId, position) => void` | - | Drop event; apply with the exported `moveTreeNode` helper. |\n\n`SvTreeNode` (exported): `{ id: string; label: string; children?: SvTreeNode[]; disabled?: boolean; lazy?: boolean }`.\nSet `lazy: true` on a node to show an expand arrow before its children exist and\nload them via `loadChildren`.\n\n## Examples\n\n### Cascading checkboxes\n\nTurn on `checkable` and hold the checked set yourself; parent/child state cascades\ntri-state automatically:\n\n```svelte\n<SvTree {nodes} checkable checked={checked} onCheck={(ids) => (checked = ids)} />\n```\n\n### Scaling with virtualization\n\nFor thousands of nodes, set `virtual` with a `height` so only the visible rows\nrender:\n\n```svelte\n<SvTree {nodes} virtual height={400} rowHeight={30} />\n```\n\n### Lazy loading\n\nMark nodes `lazy` and fetch on first expand:\n\n```svelte\n<SvTree {nodes} loadChildren={async (node) => await api.children(node.id)} />\n```\n\n### Drag-drop reorder\n\nEnable `reorderable` and apply the drop with `moveTreeNode`:\n\n```svelte\n<script lang=\"ts\">\n import { SvTree, moveTreeNode } from '@svgrid/grid'\n</script>\n\n<SvTree {nodes} reorderable onMove={(d, t, pos) => (nodes = moveTreeNode(nodes, d, t, pos))} />\n```\n\n### A remote file explorer\n\nSeed the tree with the top-level folders you know up front, mark each `lazy`, and\nresolve its children from an API on first expand. Any child that is itself a\nfolder is returned `lazy` too, so the tree fills in level by level. A `searchable`\nbox filters what has loaded so far:\n\n```svelte\n<script lang=\"ts\">\n import { SvTree, type SvTreeNode } from '@svgrid/grid'\n\n let nodes = $state<SvTreeNode[]>([\n { id: 'docs', label: 'Documents', lazy: true },\n { id: 'media', label: 'Media', lazy: true },\n ])\n let selected = $state<string | null>(null)\n\n async function loadChildren(node: SvTreeNode): Promise<SvTreeNode[]> {\n const res = await fetch(`/api/fs?dir=${node.id}`)\n const entries: { id: string; name: string; folder: boolean }[] = await res.json()\n return entries.map((e) => ({ id: e.id, label: e.name, lazy: e.folder }))\n }\n</script>\n\n<SvTree\n {nodes}\n bind:selected\n {loadChildren}\n searchable\n searchPlaceholder=\"Find a file...\"\n onSelect={(id) => openFile(id)}\n/>\n```\n\nTip: `loadChildren` runs once per node, on its first expand; the returned nodes\nare merged in and cached, so re-collapsing and re-expanding will not refetch.\nReach for `virtual` with a `height` only when a single loaded level can hold\nthousands of rows.\n\n## Accessibility\n\n- Emits WAI-ARIA `tree` / `treeitem` roles with roving focus.\n- Arrow Up / Down move between rows; Left / Right collapse / expand (mirrored in\n RTL); Enter / Space select.\n- Checkboxes expose their tri-state; disabled nodes are not focusable.\n- Inline rename opens on F2 and commits on Enter, cancels on Escape.\n\n## See also\n\n- [Navigation overview](navigation.md) - the wayfinding family at a glance.\n- [SvNavPane](sv-nav-pane.md) - a flatter app-shell sidebar.\n- [SvTreeSelect](sv-tree-select.md) - a tree inside a select dropdown.\n"
|
|
4434
4434
|
},
|
|
4435
4435
|
{
|
|
4436
4436
|
"slug": "help/ui-components/sv-typography",
|
|
@@ -4778,13 +4778,13 @@ export const docs = [
|
|
|
4778
4778
|
"slug": "reference/bundle-size",
|
|
4779
4779
|
"path": "docs/reference/bundle-size.md",
|
|
4780
4780
|
"title": "Bundle size",
|
|
4781
|
-
"markdown": "# Bundle size\r\n\r\nWhat SvGrid costs in your bundle, how to reproduce the number on your\r\nbranch, and what to do if size matters.\r\n\r\n## Measured\r\n\r\nRe-measured **2026-08-
|
|
4781
|
+
"markdown": "# Bundle size\r\n\r\nWhat SvGrid costs in your bundle, how to reproduce the number on your\r\nbranch, and what to do if size matters.\r\n\r\n## Measured\r\n\r\nRe-measured **2026-08-20** with the script that ships in the repo:\r\n\r\n```bash\r\nnode packages/grid/scripts/measure-size.mjs\r\n```\r\n\r\n| Target | Base JS (gzip) | CSS (gzip) | Loaded on demand |\r\n| --- | ---: | ---: | ---: |\r\n| Headless core (`createGrid`) | **2.3 kB** | - | - |\r\n| Full render component (`<SvGrid>`) | **78.2 kB** | **9.0 kB** | 77.1 kB |\r\n\r\nSvelte is a peer dependency and is excluded from every figure. Builds are\r\nminified and gzipped at level 9.\r\n\r\nThe \"loaded on demand\" column is code that is reachable only through\r\n`import()`, so it never lands in your initial bundle. As measured, that\r\nsplits into:\r\n\r\n| Chunk | gzip | Loads when |\r\n| --- | ---: | --- |\r\n| `SvDateTimePicker` | 18.2 kB | a date / datetime / time cell editor opens |\r\n| `SvGridChart` | 15.7 kB | a chart renders |\r\n| `chart` (engine) | 11.7 kB | charting is enabled |\r\n| `GridMenus` | 11.7 kB | a header or context menu opens |\r\n| `SvGridChartPanel` | 7.5 kB | the chart panel opens |\r\n| `SvGridDropdown` | 5.2 kB | a list / chips cell editor or the page-size picker opens |\r\n| `dismissable` | 3.7 kB | any popover, menu or dropdown layer opens |\r\n| `export-format` | 1.8 kB | CSV / TSV / JSON export or clipboard copy runs |\r\n| `popover` | 0.9 kB | a popover is positioned |\r\n| `SvGridChartView` | 0.7 kB | the grid switches to chart view |\r\n\r\nThe Kanban board and the scheduler / calendar view are not in either\r\nfigure: their renderers live in `@svgrid/enterprise` and register into the\r\nfree grid through the board and scheduler view seams.\r\n\r\n## Reproduce on your branch\r\n\r\n`measure-size.mjs` runs two isolated Vite library builds, one per target,\r\nwith Svelte marked external, then gzips each emitted chunk and classifies\r\nit as `base` (statically reachable from the entry) or `lazy` (reachable\r\nonly via `import()`).\r\n\r\n```bash\r\nnode packages/grid/scripts/measure-size.mjs\r\n# or, from the repo root:\r\npnpm size\r\n```\r\n\r\nTo see where the weight sits inside the base bundle:\r\n\r\n```bash\r\ncorepack pnpm --filter @svgrid/grid build\r\nnpx source-map-explorer packages/grid/dist/index.js\r\n```\r\n\r\nA treemap opens in your browser. Each block is a source file scaled by its\r\nbyte cost in the final bundle.\r\n\r\n## @svgrid/enterprise\r\n\r\nThe Enterprise pack is a separate install and a separate bundle. `xlsx`\r\nexport pulls JSZip and PDF export pulls pdfmake as optional peer\r\ndependencies, imported on the first `api.exportData(...)` call rather than\r\nat module load, so neither is in your synchronous bundle.\r\n\r\n## What to do if size matters\r\n\r\n1. **Use the headless engine for read-only views.** When you only need to\r\n display server-side data with no interaction, `createGrid` plus a short\r\n `<table>` renderer is 2.3 kB instead of 78.2 kB. See the\r\n [headless engine reference](./headless-engine.md).\r\n2. **Register only the features you use.** The grid is feature-gated:\r\n sorting, filtering, grouping, pagination, expansion, and selection are\r\n each opt-in and tree-shake out when not imported. See the\r\n [features reference](./features.md).\r\n3. **Let the lazy chunks stay lazy.** Charts, date/time editors, menus,\r\n and export already split themselves. Importing their modules directly\r\n at the top level pulls them back into your base bundle.\r\n4. **Code-split the Enterprise pack.** `installEnterprise(api)` is\r\n async-safe, so import it in the route that needs export rather than at\r\n module load:\r\n `const { installEnterprise } = await import('@svgrid/enterprise')`.\r\n\r\n## See also\r\n\r\n- [Features reference](./features.md) - what each feature does\r\n- [Headless engine reference](./headless-engine.md) - skip the renderer entirely\r\n- [Going to production guide](../getting-started/6-going-to-production.md)\r\n"
|
|
4782
4782
|
},
|
|
4783
4783
|
{
|
|
4784
4784
|
"slug": "reference/enterprise",
|
|
4785
4785
|
"path": "docs/reference/enterprise.md",
|
|
4786
4786
|
"title": "Enterprise reference",
|
|
4787
|
-
"markdown": "# Enterprise reference\r\n\r\nEverything that `@svgrid/enterprise` adds on top of the Community surface.\r\nInstall + license: see [Enterprise feature pack](../enterprise/README.md).\r\n\r\n```ts\r\nimport {\r\n installEnterprise, setLicenseKey,\r\n exportGrid, printGrid, importData,\r\n createPivotModel, pivotAggregators,\r\n
|
|
4787
|
+
"markdown": "# Enterprise reference\r\n\r\nEverything that `@svgrid/enterprise` adds on top of the Community surface.\r\nInstall + license: see [Enterprise feature pack](../enterprise/README.md).\r\n\r\n```ts\r\nimport {\r\n installEnterprise, setLicenseKey,\r\n exportGrid, printGrid, importData,\r\n createPivotModel, pivotAggregators,\r\n type EnterpriseGridApi, type ExportOptions, type ImportOptions, type PivotConfig,\r\n} from '@svgrid/enterprise'\r\n\r\n// The AI helpers are free and live in the community package. Enterprise only\r\n// registers its export engine so an AI-planned Excel / PDF export can run.\r\nimport { setAIProvider, aiFilter, aiSmartFill, aiSummarize, aiClassify } from '@svgrid/grid'\r\n```\r\n\r\n## License\r\n\r\n### `setLicenseKey(key)` / `clearLicenseKey()` / `isLicenseKeySet()`\r\n\r\n```ts\r\nsetLicenseKey('SVENTERPRISE-XXXX-XXXX-XXXX')\r\n```\r\n\r\nCall once at app startup. Without a key the Enterprise methods still work\r\nbut the grid shows an \"unlicensed\" watermark + a one-time console\r\nnudge.\r\n\r\n### `dismissUnlicensedNudge()`\r\n\r\nHide the console nudge for the rest of the session (useful in tests).\r\n\r\n## Augmenting the API\r\n\r\n### `installEnterprise(api)`\r\n\r\n```ts\r\nfunction installEnterprise<TFeatures, TData>(\r\n api: SvGridApi<TFeatures, TData>,\r\n): EnterpriseGridApi<TFeatures, TData>\r\n```\r\n\r\nMutates and returns the same api object. After install, the api has\r\nthe new methods listed below.\r\n\r\n```svelte\r\n<SvGrid\r\n data={rows} columns={columns} features={features}\r\n onApiReady={(next) => (api = installEnterprise(next))}\r\n/>\r\n```\r\n\r\n## Export\r\n\r\n### `pro.exportData(opts)`\r\n\r\n```ts\r\nexportData(opts: ExportOptions<TData>): Promise<void>\r\n```\r\n\r\nWrites the current view to a file and triggers a download.\r\n\r\n```ts\r\ntype ExportOptions<TData> = {\r\n format: 'xlsx' | 'pdf' | 'csv' | 'tsv' | 'html'\r\n filename?: string // base name; extension auto-appended\r\n columns?: ReadonlyArray<ExportColumn> // defaults to every key on the first row\r\n rows?: ReadonlyArray<TData> // defaults to api.getDisplayedRows()\r\n pageOrientation?: 'portrait' | 'landscape' // pdf only\r\n\r\n // Style + branding\r\n styles?: ExportStyles\r\n header?: ReadonlyArray<ExportHeaderFooterLine>\r\n footer?: ReadonlyArray<ExportHeaderFooterLine>\r\n\r\n // Embedded images\r\n imageFields?: ReadonlyArray<string>\r\n\r\n // Multi-sheet workbook (xlsx only)\r\n sheets?: ReadonlyArray<ExportSheet<TData>>\r\n}\r\n```\r\n\r\nSee [Data export](../help/export.md) for the option-by-option\r\nwalk-through and the demo links.\r\n\r\n### `pro.print(opts?)`\r\n\r\n```ts\r\nprint(opts?: PrintOptions<TData>): Promise<void>\r\n```\r\n\r\nOpens a printable view in a new window with repeat-on-page headers,\r\noptional cover page, configurable page-size + orientation, and a\r\nprint-CSS theme.\r\n\r\n### Static helpers\r\n\r\nIf you don't want to install Enterprise onto the api, use the module-level\r\nhelpers directly:\r\n\r\n```ts\r\nimport { exportGrid, printGrid } from '@svgrid/enterprise'\r\nawait exportGrid(api, { format: 'xlsx' })\r\nawait printGrid(api)\r\n```\r\n\r\n## Import\r\n\r\n### `pro.importData(opts)`\r\n\r\n```ts\r\nimportData(opts: ImportOptions<TData>): Promise<ImportResult<TData>>\r\n\r\ntype ImportOptions<TData> = {\r\n format?: ImportFormat // auto-detected from filename/MIME\r\n source: File | Blob | string // file picker result, drag-drop, or inline text\r\n columns?: ImportColumnMap<TData> // header-row → field map\r\n types?: ImportColumnTypes<TData> // per-field type coercion\r\n validate?: ImportValidator<TData> // per-row validation\r\n preview?: boolean // return parsed rows + errors; do NOT commit\r\n}\r\n\r\ntype ImportResult<TData> = {\r\n rows: ReadonlyArray<TData>\r\n errors: ReadonlyArray<ImportRowError>\r\n summary: { read: number; accepted: number; rejected: number }\r\n}\r\n```\r\n\r\nSee [Data import](../help/import.md).\r\n\r\n## AI\r\n\r\n### `setAIProvider(provider)`\r\n\r\n```ts\r\ntype AIProvider = (req: AIRequest) => Promise<unknown>\r\n\r\nsetAIProvider(async (req) => {\r\n // call your model - OpenAI, Anthropic, Ollama, local. ANY provider.\r\n return { /* shape depends on req.task */ }\r\n})\r\n```\r\n\r\nYou write one adapter; the four helpers below route through it.\r\n\r\n### `pro.ai.filter(query, opts?)`\r\n\r\nNatural-language to filter + sort plan.\r\n\r\n```ts\r\nawait pro.ai.filter('show me churned customers from Q3 sorted by ARR', {\r\n apply: true, // also call api.setFilter/setSort with the result\r\n})\r\n```\r\n\r\n### `pro.ai.smartFill(opts)`\r\n\r\nPropose values for empty cells from a few worked examples.\r\n\r\n### `pro.ai.summarize(opts)`\r\n\r\nOne-paragraph + bullets summary of a row, selection, group, or the\r\nwhole view.\r\n\r\n### `pro.ai.classify(opts)`\r\n\r\nClassify free-text cells into one of a known label set.\r\n\r\nFull options + return shapes: [AI assistant](../help/ai.md).\r\n\r\n## Pivot\r\n\r\n### `createPivotModel(data, config)`\r\n\r\n```ts\r\nfunction createPivotModel<TFeatures, TData>(\r\n data: ReadonlyArray<TData>,\r\n config: PivotConfig<TData>,\r\n): PivotResult<TFeatures>\r\n\r\ntype PivotResult<TFeatures> = {\r\n rows: PivotRow[]\r\n columns: Array<ColumnDef<TFeatures, PivotRow>>\r\n}\r\n```\r\n\r\nPure - no DOM, no api required. Hand the result to a regular\r\n`<SvGrid>` instance.\r\n\r\n### `pro.pivot.build(config)` / `pro.pivot.buildFrom(data, config)`\r\n\r\nSame model, with the live api's `getData()` (or an arbitrary array)\r\nas the source.\r\n\r\n### `pivotAggregators`\r\n\r\n```ts\r\ntype PivotAggregatorId =\r\n | 'sum' | 'avg' | 'min' | 'max'\r\n | 'count' | 'countDistinct'\r\n | 'first' | 'last'\r\n```\r\n\r\nA custom aggregator is a plain function: `agg: (values) => something`.\r\n\r\nFull reference: [Pivot tables](../help/pivot.md).\r\n\r\n## Subpath imports\r\n\r\nThe package ships per-feature subpaths so you can tree-shake:\r\n\r\n```ts\r\nimport { exportGrid } from '@svgrid/enterprise/export'\r\nimport { importData } from '@svgrid/enterprise/import'\r\nimport { aiFilter } from '@svgrid/grid'\r\nimport { createPivotModel } from '@svgrid/enterprise/pivot'\r\n```\r\n\r\nIf you only need export, the AI plumbing, pivot engine, and import\r\nparser don't ship in your bundle.\r\n\r\n## Types\r\n\r\n```ts\r\nimport type {\r\n EnterpriseGridApi, // SvGridApi + Enterprise methods\r\n EnterpriseAIApi, // .ai namespace shape\r\n EnterprisePivotApi, // .pivot namespace shape\r\n\r\n ExportFormat,\r\n ExportOptions,\r\n ExportColumn,\r\n ExportCellStyle,\r\n ExportStyles,\r\n ExportHeaderFooterLine,\r\n ExportSheet,\r\n\r\n ImportFormat,\r\n ImportOptions,\r\n ImportResult,\r\n ImportColumnMap,\r\n ImportColumnTypes,\r\n ImportRowError,\r\n ImportValidator,\r\n ImportFieldType,\r\n\r\n PivotAggregator,\r\n PivotAggregatorId,\r\n PivotConfig,\r\n PivotResult,\r\n PivotRow,\r\n PivotRowKind,\r\n PivotValueConfig,\r\n} from '@svgrid/enterprise'\r\n\r\n// AI types come from the community package alongside the helpers themselves.\r\nimport type {\r\n AIProvider,\r\n AIRequest,\r\n AITask,\r\n AIFilterOptions,\r\n AIFilterResult,\r\n AIFilterClause,\r\n AISortClause,\r\n AISmartFillOptions,\r\n AISmartFillResult,\r\n AISmartFillExample,\r\n AISummarizeOptions,\r\n AISummarizeTarget,\r\n AISummary,\r\n AIClassifyOptions,\r\n AIClassifyResult,\r\n} from '@svgrid/grid'\r\n```\r\n"
|
|
4788
4788
|
},
|
|
4789
4789
|
{
|
|
4790
4790
|
"slug": "reference/features",
|