@svgrid/enterprise 2.6.3 → 2.7.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.
Files changed (70) hide show
  1. package/dist/SvGridBulkEditDrawer.svelte +158 -0
  2. package/dist/SvGridBulkEditDrawer.svelte.d.ts +9 -0
  3. package/dist/SvGridSelectionBar.svelte +452 -0
  4. package/dist/SvGridSelectionBar.svelte.d.ts +6 -0
  5. package/dist/SvPivotDesigner.svelte +3 -3
  6. package/dist/SvPivotDesigner.svelte.d.ts +3 -3
  7. package/dist/bulk-edit.d.ts +64 -0
  8. package/dist/bulk-edit.js +152 -0
  9. package/dist/cdn/svgrid-enterprise.svelte-external.js +6232 -5824
  10. package/dist/designer/assets/GridMenus-Cgilexpk.js +8 -0
  11. package/dist/designer/assets/SvDateRangeInput-BV5OXYQ2.css +1 -0
  12. package/dist/designer/assets/SvDateRangeInput-CVn-yqrQ.js +1 -0
  13. package/dist/designer/assets/SvDateTimePicker-BdioTrJQ.js +1 -0
  14. package/dist/designer/assets/SvDateTimePicker-CY8TK4BT.css +1 -0
  15. package/dist/designer/assets/SvField-BsqiA5me.js +1 -0
  16. package/dist/designer/assets/SvGridCellEditor-dLY7_kk9.js +2 -0
  17. package/dist/designer/assets/SvListBox-CVYQcLX9.js +1 -0
  18. package/dist/designer/assets/{SvListBox-BMSfFJJT.css → SvListBox-DgN8kvjt.css} +1 -1
  19. package/dist/designer/assets/column-resize-C_inr5zT.js +1 -0
  20. package/dist/designer/assets/date-format-4DJvNtG2.js +1 -0
  21. package/dist/designer/assets/{SvDateTimePicker-Cygij-dr.css → date-format-Dzy4mTGh.css} +1 -1
  22. package/dist/designer/assets/dismissable-BO547MH9.js +1 -0
  23. package/dist/designer/assets/editor-registry-BjP16H0v.js +1 -0
  24. package/dist/designer/assets/{index-aTVomkp-.js → index-BvaLmQSd.js} +128 -128
  25. package/dist/designer/assets/index-Cp7fDJ3b.css +1 -0
  26. package/dist/designer/assets/{js-scroller.svelte-C7LaeUEW.js → js-scroller.svelte-Bhxqb3-c.js} +2 -2
  27. package/dist/designer/assets/preload-helper-CG0n22O7.js +1 -0
  28. package/dist/designer/assets/row-resize-B0FWHcNr.js +1 -0
  29. package/dist/designer/assets/src-7qOYsBug.js +2896 -0
  30. package/dist/designer/assets/src-B7DNcoQK.css +1 -0
  31. package/dist/designer/assets/validate-h-aslRV9.js +1 -0
  32. package/dist/designer/index.html +18 -13
  33. package/dist/export.d.ts +4 -0
  34. package/dist/import.d.ts +7 -0
  35. package/dist/index.d.ts +3 -0
  36. package/dist/index.js +8 -0
  37. package/dist/install.d.ts +3 -0
  38. package/dist/install.js +2 -0
  39. package/dist/license-core.d.ts +1 -1
  40. package/dist/license-core.js +1 -1
  41. package/dist/node/studio.js +178 -80
  42. package/dist/pivot.d.ts +11 -0
  43. package/dist/pivot.js +6 -1
  44. package/dist/selection-bar.d.ts +7 -0
  45. package/dist/selection-bar.js +42 -0
  46. package/dist/studio/emit-project.js +22 -7
  47. package/dist/studio/emit-schema.d.ts +1 -4
  48. package/dist/studio/emit-schema.js +7 -29
  49. package/dist/studio/init-flow.js +2 -2
  50. package/dist/studio/scaffold-app.d.ts +8 -0
  51. package/dist/studio/scaffold-app.js +7 -2
  52. package/dist/studio/scaffold.d.ts +8 -0
  53. package/dist/studio/scaffold.js +5 -0
  54. package/dist/studio/themes.d.ts +17 -1
  55. package/dist/studio/themes.js +26 -1
  56. package/dist/studio/ui-components.generated.js +72 -3
  57. package/dist/version.d.ts +1 -1
  58. package/dist/version.js +1 -1
  59. package/package.json +10 -5
  60. package/dist/designer/assets/GridMenus-DuogVgW0.js +0 -7
  61. package/dist/designer/assets/SvDateTimePicker-DOBYyLeN.js +0 -1
  62. package/dist/designer/assets/SvGrid.helpers-C6wxF55r.js +0 -1
  63. package/dist/designer/assets/SvGridCellEditor-CMtmQINJ.js +0 -2
  64. package/dist/designer/assets/SvListBox-CWCIPM4P.js +0 -1
  65. package/dist/designer/assets/dismissable-2EZKoens.js +0 -1
  66. package/dist/designer/assets/editor-registry-BB1Qb5mp.js +0 -1
  67. package/dist/designer/assets/index-BfzoL904.css +0 -1
  68. package/dist/designer/assets/src-B5lLfIhK.css +0 -1
  69. package/dist/designer/assets/src-DLoHsbp5.js +0 -2893
  70. /package/dist/designer/assets/{dismissable-BF-9TJSd.css → SvField-BF-9TJSd.css} +0 -0
package/dist/pivot.js CHANGED
@@ -233,7 +233,7 @@ parentId) {
233
233
  // regardless of `rowSubtotals` so the grouping hierarchy and the
234
234
  // expand/collapse parent chain stay intact; the flag only controls whether
235
235
  // the header carries its aggregate (subtotal) values or renders as a
236
- // label-only row. This matches Excel / AG Grid, where hiding subtotals never
236
+ // label-only row. This matches Excel, where hiding subtotals never
237
237
  // removes a grouping level from the row axis.
238
238
  const isRealGroup = node.level > 0 && node.level < rowFields.length;
239
239
  let nextParentId = parentId;
@@ -259,6 +259,11 @@ parentId) {
259
259
  }
260
260
  }
261
261
  // ------------------------------------------------------------ public API
262
+ /**
263
+ * Build a pivot table from flat rows: cross the row axis with the column axis,
264
+ * aggregate each intersection, and return rows plus generated columns ready to
265
+ * hand to `<SvGrid>`.
266
+ */
262
267
  export function createPivotModel(data, config) {
263
268
  if (config.values.length === 0) {
264
269
  throw new Error('pivot: at least one value config is required');
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Register the Enterprise selection bar. Idempotent - safe to call from every
3
+ * component that uses `selectionBar`, or once at app start. Also invoked by
4
+ * {@link installEnterprise} so wiring the Pro API turns the bar on too.
5
+ */
6
+ export declare function enableSelectionBar(): void;
7
+ export { default as SvGridSelectionBar } from './SvGridSelectionBar.svelte';
@@ -0,0 +1,42 @@
1
+ /**
2
+ * selection bar - registers the Enterprise bulk-action bar with the grid, so
3
+ * `<SvGrid selectionBar={...}>` floats a bar over the rows while any are
4
+ * selected: the count, the actions that apply to the whole selection, an
5
+ * overflow menu once there are more than a handful, and a clear button.
6
+ *
7
+ * The grid ships the `selectionBar` prop and its config types for free; the
8
+ * *renderer* (SvGridSelectionBar) is Pro and plugs in through the grid's
9
+ * `registerSelectionBarView` seam. Without it the grid shows a short upsell
10
+ * note where the bar would be.
11
+ *
12
+ * Soft-gated like the rest of Enterprise: it works without a license key but
13
+ * the grid shows the "unlicensed" watermark + a one-time console nudge.
14
+ *
15
+ * ```ts
16
+ * import { setLicenseKey, enableSelectionBar } from '@svgrid/enterprise'
17
+ * setLicenseKey('YOUR-KEY')
18
+ * enableSelectionBar()
19
+ * // then:
20
+ * // <SvGrid {data} {columns} showRowSelection
21
+ * // selectionBar={{ position: 'bottom', actions: [...] }} />
22
+ * ```
23
+ */
24
+ import { registerSelectionBarView } from '@svgrid/grid';
25
+ import { isLicenseKeySet } from './license';
26
+ import { emitUnlicensedNudge } from './watermark';
27
+ import SvGridSelectionBar from './SvGridSelectionBar.svelte';
28
+ let enabled = false;
29
+ /**
30
+ * Register the Enterprise selection bar. Idempotent - safe to call from every
31
+ * component that uses `selectionBar`, or once at app start. Also invoked by
32
+ * {@link installEnterprise} so wiring the Pro API turns the bar on too.
33
+ */
34
+ export function enableSelectionBar() {
35
+ if (enabled)
36
+ return;
37
+ enabled = true;
38
+ registerSelectionBarView(SvGridSelectionBar);
39
+ if (!isLicenseKeySet())
40
+ emitUnlicensedNudge();
41
+ }
42
+ export { default as SvGridSelectionBar } from './SvGridSelectionBar.svelte';
@@ -1,7 +1,7 @@
1
1
  import { tenantField, isTenantScoped } from './project.js';
2
2
  import { blockColumns, blockStyleCss, blockClassName, sanitizeClassName, componentHandleName, componentHasBindings, entityDataSource, flattenBlocks, serializeProject, seedUsers, compileHandlerSteps, rowSelectSlot, eventSlot, FORM_SUBMIT, GRID_EVENTS, screenLayoutOf, isPaneLayout, canvasRectOf, CANVAS_ROW_PX, CANVAS_GAP_PX, gridOpts, stackOpts, splitOpts, dockOpts, canvasOpts, stateInitExpr, stateTsType, reconcileDock, ON_LOAD, ON_DESTROY, isSsrScreen, ssrScreenShape } from './project.js';
3
3
  import { uiComponentSpec, gridApiSettableProps, STANDARD_UI_EVENTS } from './ui-components.js';
4
- import { resolveThemeTokens, resolveThemeTokensFor, isDarkTheme } from './themes.js';
4
+ import { isDarkTheme, themeTokenCss } from './themes.js';
5
5
  import { hasFieldConditions } from '../edit-panel.js';
6
6
  import { SVGRID_VERSION } from '../version.js';
7
7
  import { emitEntityModules, homeFile, layoutFile, lookupVar, namesFor, prepareEntities, relationDisplayFields, sqlDdlFiles } from './emit-schema.js';
@@ -195,6 +195,12 @@ function blockMarkup(entity, schemaVar, typeName, block, resolve, ctx = { hasRec
195
195
  return fire ? `(${param}) => { ${base}${fire} }` : `(${param}) => ${base}`;
196
196
  };
197
197
  const lines = [`data={${dataExpr}}`, `columns={${colVar}}`, `loading={${tree || grouped ? '!allRowsReady' : 'view.loading'}}`, `loadingOverlay`, `emptyMessage=${JSON.stringify(emptyMsg)}`, `fitColumns`];
198
+ // Emitted explicitly in BOTH directions on purpose. The grid now defaults
199
+ // this off and takes `summary` as a shortcut, but generated apps install
200
+ // the PUBLISHED @svgrid/grid, where the default is still on and `summary`
201
+ // does not exist yet. Switching to `summary` (or dropping the `false`)
202
+ // before that release would silently give every generated screen the
203
+ // wrong footer. Revisit once the grid carrying the flip is published.
198
204
  lines.push(`enableRowSummaries={${cfg.rowSummaries ? 'true' : 'false'}}`);
199
205
  if (cfg.striped)
200
206
  lines.push(`zebraRows`);
@@ -3048,7 +3054,7 @@ export function emitStudioProject(project) {
3048
3054
  navExtras = [...navExtras, { href: '/users', label: 'Users', id: '__users__' }];
3049
3055
  const i18nFiles = i18nEnabled ? [i18nModule(project)] : [];
3050
3056
  const handleFiles = project.screens.some(screenHasCode) ? [handlesModuleFile()] : [];
3051
- return [...files, ...accessFiles, ...authFileList, ...dataLayerList, ...ddlFiles, ...auditFiles, ...jobFileList, ...tenantFileList, ...i18nFiles, ...actionRouteFiles, ...ssrHelpers, ...pages, ...companions, ...handleFiles, layoutFile(navExtras, { accent: project.theme?.accent, shell: project.theme?.shell, title: project.title, themeVars: resolveThemeTokens(project.theme), lightVars: resolveThemeTokensFor(project.theme, 'light'), darkVars: resolveThemeTokensFor(project.theme, 'dark'), dark: isDarkTheme(project.theme), access: accessEnabled, auth: authEnabled && !supabaseAuth, supabaseAuth, authRoutes: publicAuthRoutes, authAccount: dbBackedAuth, i18n: i18nEnabled, appClass: project.theme?.appClass }), homeFile(navExtras)];
3057
+ return [...files, ...accessFiles, ...authFileList, ...dataLayerList, ...ddlFiles, ...auditFiles, ...jobFileList, ...tenantFileList, ...i18nFiles, ...actionRouteFiles, ...ssrHelpers, ...pages, ...companions, ...handleFiles, layoutFile(navExtras, { shell: project.theme?.shell, title: project.title, themeSwitch: true, dark: isDarkTheme(project.theme), access: accessEnabled, auth: authEnabled && !supabaseAuth, supabaseAuth, authRoutes: publicAuthRoutes, authAccount: dbBackedAuth, i18n: i18nEnabled, appClass: project.theme?.appClass }), homeFile(navExtras)];
3052
3058
  }
3053
3059
  /** The default-locale (`en`) message catalog, keyed for nav, screen titles, the
3054
3060
  * New button, and grid column headers - the strings the app renders. */
@@ -4832,8 +4838,15 @@ const SCAFFOLD_STATIC = [
4832
4838
  { path: '.npmrc', description: 'npm config.', contents: `engine-strict=false\n` },
4833
4839
  { path: '.gitignore', description: 'git ignore.', contents: `node_modules\n.svelte-kit\n/build\n.env\n.env.*\n!.env.example\n.DS_Store\n` },
4834
4840
  ];
4835
- const APP_CSS = `:root { --sg-accent: #4f46e5; color-scheme: light dark; }
4836
- * { box-sizing: border-box; }
4841
+ /** `src/app.css`: the project's theme tokens (see `themeTokenCss`) followed by the
4842
+ * `.st-*` page styles every generated screen uses. This is the ONE place the tokens
4843
+ * are emitted, so the fragment (which drops the layout) and the full app are
4844
+ * themed alike. `layer` is for drop-in outputs - see `themeTokenCss`. */
4845
+ function appCss(project, opts = {}) {
4846
+ return `${themeTokenCss(project.theme, opts)}
4847
+ ${APP_CSS_BODY}`;
4848
+ }
4849
+ const APP_CSS_BODY = `* { box-sizing: border-box; }
4837
4850
  html, body { margin: 0; height: 100%; }
4838
4851
  body { font-family: var(--sg-font, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif); color: var(--sg-fg, #0f172a); background: var(--sg-bg, #fff); }
4839
4852
  .st__title { margin: 0; font-size: 22px; font-weight: 720; letter-spacing: -0.015em; }
@@ -5390,7 +5403,7 @@ export function emitStudioAppBundle(project) {
5390
5403
  ...SCAFFOLD_STATIC,
5391
5404
  ...(envExample(allSource) ? [{ path: '.env.example', description: 'Environment variables the app reads (copy to .env and fill in).', contents: envExample(allSource) }] : []),
5392
5405
  ...(envDotFile(allSource) ? [{ path: '.env', description: 'Local env (git-ignored): a real random SESSION_SECRET is pre-filled so sessions are secure out of the box; fill in the rest.', contents: envDotFile(allSource) }] : []),
5393
- { path: 'src/app.css', description: 'App theme + page styles.', contents: APP_CSS },
5406
+ { path: 'src/app.css', description: 'App theme tokens + page styles.', contents: appCss(project) },
5394
5407
  // Your styles: a dedicated CSS file the layout imports AFTER app.css, so its rules
5395
5408
  // win. Edited in the designer's Styles panel; regenerated from the model.
5396
5409
  { path: 'src/custom.css', description: 'Your custom styles (edited in the designer; imported after app.css so it overrides).', contents: `/* Your custom styles. Edit in the designer's Styles panel, or here after ejecting.\n Imported after app.css, so these rules override the defaults. */\n${project.theme?.customCss ? `\n${project.theme.customCss}\n` : ''}` },
@@ -5445,7 +5458,9 @@ ${depList}
5445
5458
 
5446
5459
  ## 3. Styles
5447
5460
  Import \`src/app.css\` once (e.g. in your root \`+layout.svelte\`) - it carries the
5448
- \`.st-*\` page styles the screens use and the \`--sg-*\` design tokens.
5461
+ \`.st-*\` page styles the screens use and the \`--sg-*\` design tokens of the theme
5462
+ picked in Studio. The tokens sit in a cascade layer (\`@layer svgrid-studio\`), so if
5463
+ your app already defines its own \`--sg-*\` tokens, yours keep winning.
5449
5464
 
5450
5465
  ## 4. What your app must provide
5451
5466
  The full-app nav shell, home redirect, auth guard, RBAC bootstrap, i18n provider,
@@ -5457,7 +5472,7 @@ ${libFiles.join('\n')}
5457
5472
  `;
5458
5473
  return [
5459
5474
  ...content,
5460
- { path: 'src/app.css', description: 'Page styles (.st-*) + design tokens - import once in your layout.', contents: APP_CSS },
5475
+ { path: 'src/app.css', description: 'Theme tokens (layered, so yours win) + page styles (.st-*) - import once in your layout.', contents: appCss(project, { layer: 'svgrid-studio' }) },
5461
5476
  { path: 'FRAGMENT.md', description: 'How to drop this fragment into an existing SvelteKit app.', contents: fragmentMd },
5462
5477
  ];
5463
5478
  }
@@ -58,12 +58,9 @@ export type NavItem = {
58
58
  id?: string;
59
59
  };
60
60
  export declare function layoutFile(nav: NavItem[], opts?: {
61
- accent?: string;
62
61
  shell?: ShellConfig;
63
62
  title?: string;
64
- themeVars?: Record<string, string>;
65
- lightVars?: Record<string, string>;
66
- darkVars?: Record<string, string>;
63
+ themeSwitch?: boolean;
67
64
  dark?: boolean;
68
65
  access?: boolean;
69
66
  auth?: boolean;
@@ -666,34 +666,13 @@ export function layoutFile(nav, opts = {}) {
666
666
  const brand = (shell.brand ?? '').trim() || opts.title || 'My Studio App';
667
667
  const footer = shell.footer === undefined ? 'Built with SvGrid Studio' : shell.footer;
668
668
  const right = style === 'sidebar' && shell.navPosition === 'right';
669
- // Emit the full theme token bundle so the generated app matches the look chosen
670
- // in the designer. When both light + dark token sets are supplied we ship a
671
- // built-in light/dark switcher: both sets are scoped by [data-theme] on <html>,
672
- // and the bare :root falls back to the mode picked in Studio (pre-hydration).
669
+ // The theme tokens themselves live in src/app.css (imported below) - see
670
+ // `themeTokenCss` - so the fragment, which drops this file, stays themed too.
671
+ // `themeSwitch` ships the built-in light/dark toggle: app.css scopes both
672
+ // palettes by [data-theme] on <html>, and the bare :root falls back to the mode
673
+ // picked in Studio (pre-hydration).
673
674
  const defaultMode = opts.dark ? 'dark' : 'light';
674
- const withAccent = (m) => {
675
- const v = { ...(m ?? {}) };
676
- if (opts.accent)
677
- v['--sg-accent'] = opts.accent;
678
- return v;
679
- };
680
- const declLines = (m) => Object.entries(m).map(([k, v]) => `${k}: ${v};`).join(' ');
681
- const hasSwitch = !!(opts.lightVars && opts.darkVars);
682
- let themeHead = '';
683
- if (hasSwitch) {
684
- const lv = withAccent(opts.lightVars);
685
- const dv = withAccent(opts.darkVars);
686
- const defRule = [declLines(defaultMode === 'dark' ? dv : lv), `color-scheme: ${defaultMode};`].join(' ');
687
- const lightRule = [declLines(lv), 'color-scheme: light;'].join(' ');
688
- const darkRule = [declLines(dv), 'color-scheme: dark;'].join(' ');
689
- themeHead = `\n<svelte:head><style>:root { ${defRule} }\n:root[data-theme="light"] { ${lightRule} }\n:root[data-theme="dark"] { ${darkRule} }</style></svelte:head>\n`;
690
- }
691
- else {
692
- const vars = withAccent(opts.themeVars);
693
- const varLines = declLines(vars);
694
- const rootRule = [varLines, opts.dark ? 'color-scheme: dark;' : ''].filter(Boolean).join(' ');
695
- themeHead = rootRule ? `\n<svelte:head><style>:root { ${rootRule} }</style></svelte:head>\n` : '';
696
- }
675
+ const hasSwitch = !!opts.themeSwitch;
697
676
  // i18n: translate nav labels via `nav.<id>` keys (Home has no id -> literal).
698
677
  const navLabel = opts.i18n ? `{item.id ? $t('nav.' + item.id, item.label) : item.label}` : '{item.label}';
699
678
  const anchor = `<a class="sv-app__link" class:is-active={page.url.pathname === item.href} href={item.href}>${navLabel}</a>`;
@@ -932,7 +911,7 @@ export function layoutFile(nav, opts = {}) {
932
911
  $effect(() => { void page.url.pathname; navOpen = false })${hasSwitch ? `
933
912
  // Light/dark switcher: defaults to the mode picked in Studio, then honours the
934
913
  // visitor's saved choice. Applies via [data-theme] on <html> (see the token
935
- // sets in <svelte:head>), and persists per browser.
914
+ // sets in app.css), and persists per browser.
936
915
  let theme = $state<'light' | 'dark'>('${defaultMode}')
937
916
  function applyTheme(t: 'light' | 'dark') {
938
917
  theme = t
@@ -970,7 +949,6 @@ export function layoutFile(nav, opts = {}) {
970
949
  // Close both menus on route change.
971
950
  $effect(() => { void page.url.pathname; bellOpen = false; menuOpen = false })` : ''}
972
951
  </script>
973
- ${themeHead}
974
952
 
975
953
  ${opts.auth ? `{#if ${JSON.stringify(opts.authRoutes ?? ['/login'])}.includes(page.url.pathname)}
976
954
  {@render children()}
@@ -335,14 +335,14 @@ async function askScreens(schemas, flags, prompts) {
335
335
  async function askTheme(flags, prompts) {
336
336
  let preset = flags.theme?.trim().toLowerCase();
337
337
  if (preset && !getStudioTheme(preset)) {
338
- prompts.say(` ! Unknown theme "${preset}" - using tailwind.`);
338
+ prompts.say(` ! Unknown theme "${preset}" - using ember.`);
339
339
  preset = undefined;
340
340
  }
341
341
  if (!preset && !flags.yes) {
342
342
  const i = await pick(prompts, 'Theme:', studioThemes.map((t) => ({ label: t.name, hint: t.id })));
343
343
  preset = studioThemes[i]?.id;
344
344
  }
345
- preset ?? (preset = 'tailwind');
345
+ preset ?? (preset = 'ember');
346
346
  const dark = flags.dark ?? (flags.yes ? false : isYes(await prompts.ask('Dark mode? (y/N)', 'n'), false));
347
347
  return { preset, mode: dark ? 'dark' : 'light' };
348
348
  }
@@ -16,6 +16,14 @@ export type ScaffoldAppOptions = {
16
16
  dataSource?: ScaffoldOptions['dataSource'];
17
17
  /** App title shown in the nav + home. Default `'Data App'`. */
18
18
  appTitle?: string;
19
+ /**
20
+ * Theme preset id (one of `@svgrid/grid/themes`; default: the house Ember
21
+ * preset) and starting mode. The tokens are emitted in the page's
22
+ * `<svelte:head>` inside `@layer svgrid-studio`, so an app that already
23
+ * defines its own `--sg-*` tokens keeps its look.
24
+ */
25
+ theme?: string;
26
+ dark?: boolean;
19
27
  };
20
28
  export declare function scaffoldApp(schemas: EntitySchema[], options?: ScaffoldAppOptions): {
21
29
  files: GeneratedFile[];
@@ -11,9 +11,10 @@
11
11
  */
12
12
  import { linkRelationLabels, titleCase } from '../schema.js';
13
13
  import { scaffold, MANAGED_START, MANAGED_END } from './scaffold.js';
14
+ import { themeTokenCss } from './themes.js';
14
15
  const MANAGED_NOTE = '// Regenerated by SvGrid Studio. Edits inside these markers are overwritten.';
15
16
  const managed = (body) => `${MANAGED_START}\n${MANAGED_NOTE}\n${body}\n${MANAGED_END}`;
16
- function layoutFile(nav, appTitle) {
17
+ function layoutFile(nav, appTitle, theme) {
17
18
  return {
18
19
  path: 'src/routes/+layout.svelte',
19
20
  description: 'App shell: a collapsible nav sidebar linking every entity screen (an off-canvas drawer on tablet/phone), with the routed page in the main area.',
@@ -40,6 +41,10 @@ ${managed(` let { children } = $props()
40
41
  })`)}
41
42
  </script>
42
43
 
44
+ <svelte:head><style>
45
+ ${themeTokenCss(theme, { layer: 'svgrid-studio' })}
46
+ </style></svelte:head>
47
+
43
48
  <div class="app-shell" class:is-drawer={drawer} class:is-navopen={navOpen}>
44
49
  <header class="app-shell__mobilebar">
45
50
  <button type="button" class="app-shell__burger" aria-label="Toggle navigation" aria-expanded={navOpen} onclick={() => (navOpen = !navOpen)}>
@@ -138,6 +143,6 @@ export function scaffoldApp(schemas, options = {}) {
138
143
  files.push(...scaffold(schema, { dataSource: options.dataSource }).files);
139
144
  }
140
145
  const nav = linked.map((s) => ({ href: `/${s.name}`, label: s.label ?? titleCase(s.name) }));
141
- files.push(layoutFile(nav, appTitle), homeFile(nav, appTitle));
146
+ files.push(layoutFile(nav, appTitle, { preset: options.theme, mode: options.dark ? 'dark' : 'light' }), homeFile(nav, appTitle));
142
147
  return { files };
143
148
  }
@@ -27,6 +27,14 @@ export type GeneratedFile = {
27
27
  export type ScaffoldOptions = {
28
28
  /** Route segment for the page, defaults to the schema name. e.g. `'customers'`. */
29
29
  route?: string;
30
+ /**
31
+ * Theme preset id (one of `@svgrid/grid/themes`; default: the house Ember
32
+ * preset) and starting mode. The tokens are emitted in the page's
33
+ * `<svelte:head>` inside `@layer svgrid-studio`, so an app that already
34
+ * defines its own `--sg-*` tokens keeps its look.
35
+ */
36
+ theme?: string;
37
+ dark?: boolean;
30
38
  /** API route the page talks to, defaults to `/api/{route}`. */
31
39
  apiRoute?: string;
32
40
  /** `$lib` path for the generated schema module, defaults to `$lib/{route}.schema`. */
@@ -14,6 +14,7 @@
14
14
  * omitted from the emitted schema with a note to re-add them by hand.
15
15
  */
16
16
  import { resolveIdField } from '../schema.js';
17
+ import { themeTokenCss } from './themes.js';
17
18
  const DRIVERS = {
18
19
  postgres: {
19
20
  imports: `import pg from 'pg'`,
@@ -257,6 +258,10 @@ ${managed(serverBody)}
257
258
  ${managed(pageBody)}
258
259
  </script>
259
260
 
261
+ <svelte:head><style>
262
+ ${themeTokenCss({ preset: options.theme, mode: options.dark ? 'dark' : 'light' }, { layer: 'svgrid-studio' })}
263
+ </style></svelte:head>
264
+
260
265
  <div class="page">
261
266
  <header class="page__toolbar">
262
267
  <h2 class="page__title">${label}</h2>
@@ -9,7 +9,9 @@
9
9
  * shell, grids, charts, KPIs, forms - not just the accent.
10
10
  *
11
11
  * Pure + node-safe (studio subtree): plain data + helpers, so the same tokens
12
- * drive the browser preview and the emitted `+layout.svelte` / `app.css`.
12
+ * drive the browser preview and every emitted output - `app.css` for the full
13
+ * app and the fragment, the `<svelte:head>` of the CLI `add` scaffolds - via
14
+ * `themeTokenCss` below, the single place the token CSS is built.
13
15
  */
14
16
  import { type ThemeMode, type ThemePalette, type ThemePreset } from '@svgrid/grid/themes';
15
17
  import type { ProjectTheme } from './project.js';
@@ -30,5 +32,19 @@ export declare function resolveThemeTokens(theme?: ProjectTheme): Record<string,
30
32
  /** Resolve the `--sg-*` tokens for a project theme in an EXPLICIT mode (for the
31
33
  * generated app's light/dark switcher, which ships both token sets). */
32
34
  export declare function resolveThemeTokensFor(theme: ProjectTheme | undefined, mode: 'light' | 'dark'): Record<string, string>;
35
+ /**
36
+ * The `--sg-*` token rules for a project theme, as CSS text. `:root` carries
37
+ * the mode picked in Studio (what the server-rendered page shows before
38
+ * hydration), and both palettes are scoped by `[data-theme]` on `<html>` for
39
+ * the generated app's light/dark switcher. An explicit accent override is
40
+ * applied to both. No theme -> the default preset, so every output is themed.
41
+ *
42
+ * `layer` wraps the rules in a cascade layer. Use it for drop-in outputs (the
43
+ * fragment, `svgrid-studio add`): layered declarations lose to unlayered ones,
44
+ * so a host app that already defines its own `--sg-*` tokens keeps winning.
45
+ */
46
+ export declare function themeTokenCss(theme?: ProjectTheme, opts?: {
47
+ layer?: string;
48
+ }): string;
33
49
  /** An inline `style` string (`--sg-x: y; ...; color-scheme; font-family`) for previews. */
34
50
  export declare function themeStyleString(theme?: ProjectTheme): string;
@@ -9,7 +9,9 @@
9
9
  * shell, grids, charts, KPIs, forms - not just the accent.
10
10
  *
11
11
  * Pure + node-safe (studio subtree): plain data + helpers, so the same tokens
12
- * drive the browser preview and the emitted `+layout.svelte` / `app.css`.
12
+ * drive the browser preview and every emitted output - `app.css` for the full
13
+ * app and the fragment, the `<svelte:head>` of the CLI `add` scaffolds - via
14
+ * `themeTokenCss` below, the single place the token CSS is built.
13
15
  */
14
16
  import { themePresets, defaultThemePreset, getThemePreset, resolveThemeTokens as resolveTokens, } from '@svgrid/grid/themes';
15
17
  /** The built-in Studio themes, in gallery order (default first). */
@@ -33,6 +35,29 @@ export function resolveThemeTokensFor(theme, mode) {
33
35
  const preset = getStudioTheme(theme?.preset) ?? defaultStudioTheme;
34
36
  return resolveTokens(preset, mode, theme?.accent);
35
37
  }
38
+ /**
39
+ * The `--sg-*` token rules for a project theme, as CSS text. `:root` carries
40
+ * the mode picked in Studio (what the server-rendered page shows before
41
+ * hydration), and both palettes are scoped by `[data-theme]` on `<html>` for
42
+ * the generated app's light/dark switcher. An explicit accent override is
43
+ * applied to both. No theme -> the default preset, so every output is themed.
44
+ *
45
+ * `layer` wraps the rules in a cascade layer. Use it for drop-in outputs (the
46
+ * fragment, `svgrid-studio add`): layered declarations lose to unlayered ones,
47
+ * so a host app that already defines its own `--sg-*` tokens keeps winning.
48
+ */
49
+ export function themeTokenCss(theme, opts = {}) {
50
+ const decl = (m) => Object.entries(m).map(([k, v]) => ` ${k}: ${v};`).join('\n');
51
+ const mode = isDarkTheme(theme) ? 'dark' : 'light';
52
+ const light = resolveThemeTokensFor(theme, 'light');
53
+ const dark = resolveThemeTokensFor(theme, 'dark');
54
+ const rules = [
55
+ `:root {\n${decl(mode === 'dark' ? dark : light)}\n color-scheme: ${mode};\n}`,
56
+ `:root[data-theme="light"] {\n${decl(light)}\n color-scheme: light;\n}`,
57
+ `:root[data-theme="dark"] {\n${decl(dark)}\n color-scheme: dark;\n}`,
58
+ ].join('\n');
59
+ return opts.layer ? `@layer ${opts.layer} {\n${rules}\n}` : rules;
60
+ }
36
61
  /** An inline `style` string (`--sg-x: y; ...; color-scheme; font-family`) for previews. */
37
62
  export function themeStyleString(theme) {
38
63
  const tokens = resolveThemeTokens(theme);