@svgrid/enterprise 2.6.2 → 2.6.4

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 (34) hide show
  1. package/README.md +1 -1
  2. package/dist/cdn/svgrid-enterprise.svelte-external.js +1682 -1677
  3. package/dist/designer/assets/{GridMenus-Bjr19Tz_.js → GridMenus-BIU7Nfbr.js} +1 -1
  4. package/dist/designer/assets/SvGrid.helpers-C6wxF55r.js +1 -0
  5. package/dist/designer/assets/SvGridCellEditor-CMtmQINJ.js +2 -0
  6. package/dist/designer/assets/SvListBox-D-_I_Aff.js +1 -0
  7. package/dist/designer/assets/editor-registry-BB1Qb5mp.js +1 -0
  8. package/dist/designer/assets/{index-xjfKckuH.js → index-DxBZuosR.js} +42 -42
  9. package/dist/designer/assets/js-scroller.svelte-03aI6ovu.js +110 -0
  10. package/dist/designer/assets/row-drag-touch-IuJPEt0z.js +1 -0
  11. package/dist/designer/assets/src-D8qr49rE.css +1 -0
  12. package/dist/designer/assets/src-Dn1Axx6T.js +2896 -0
  13. package/dist/designer/index.html +7 -5
  14. package/dist/license-core.d.ts +1 -1
  15. package/dist/license-core.js +1 -1
  16. package/dist/node/studio.js +114 -78
  17. package/dist/studio/emit-project.js +16 -7
  18. package/dist/studio/emit-schema.d.ts +1 -4
  19. package/dist/studio/emit-schema.js +7 -29
  20. package/dist/studio/init-flow.js +2 -2
  21. package/dist/studio/scaffold-app.d.ts +8 -0
  22. package/dist/studio/scaffold-app.js +7 -2
  23. package/dist/studio/scaffold.d.ts +8 -0
  24. package/dist/studio/scaffold.js +5 -0
  25. package/dist/studio/themes.d.ts +17 -1
  26. package/dist/studio/themes.js +26 -1
  27. package/dist/studio/ui-components.generated.js +8 -1
  28. package/dist/version.d.ts +1 -1
  29. package/dist/version.js +1 -1
  30. package/package.json +6 -6
  31. package/dist/designer/assets/SvListBox-BB8rt4aP.js +0 -1
  32. package/dist/designer/assets/js-scroller.svelte-CGkUUXwV.js +0 -110
  33. package/dist/designer/assets/src-BT3LjWJL.css +0 -1
  34. package/dist/designer/assets/src-DDhAK0WP.js +0 -2893
@@ -4,11 +4,13 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>SvGrid Studio - Designer</title>
7
- <script type="module" crossorigin src="./assets/index-xjfKckuH.js"></script>
7
+ <script type="module" crossorigin src="./assets/index-DxBZuosR.js"></script>
8
8
  <link rel="modulepreload" crossorigin href="./assets/rolldown-runtime-Dd_uD5pT.js">
9
9
  <link rel="modulepreload" crossorigin href="./assets/disclose-version-CIm4S9xS.js">
10
10
  <link rel="modulepreload" crossorigin href="./assets/export-format-DwjuTfrQ.js">
11
- <link rel="modulepreload" crossorigin href="./assets/js-scroller.svelte-CGkUUXwV.js">
11
+ <link rel="modulepreload" crossorigin href="./assets/js-scroller.svelte-03aI6ovu.js">
12
+ <link rel="modulepreload" crossorigin href="./assets/SvGrid.helpers-C6wxF55r.js">
13
+ <link rel="modulepreload" crossorigin href="./assets/editor-registry-BB1Qb5mp.js">
12
14
  <link rel="modulepreload" crossorigin href="./assets/dismissable-2EZKoens.js">
13
15
  <link rel="modulepreload" crossorigin href="./assets/popover-CUkLsvRX.js">
14
16
  <link rel="modulepreload" crossorigin href="./assets/SvDateTimePicker-DOBYyLeN.js">
@@ -16,13 +18,13 @@
16
18
  <link rel="modulepreload" crossorigin href="./assets/easing-zaypKrr0.js">
17
19
  <link rel="modulepreload" crossorigin href="./assets/chart-DHEgt4lN.js">
18
20
  <link rel="modulepreload" crossorigin href="./assets/SvGridChart-BEDNTdc5.js">
19
- <link rel="modulepreload" crossorigin href="./assets/src-DDhAK0WP.js">
20
- <link rel="modulepreload" crossorigin href="./assets/SvListBox-BB8rt4aP.js">
21
+ <link rel="modulepreload" crossorigin href="./assets/src-Dn1Axx6T.js">
22
+ <link rel="modulepreload" crossorigin href="./assets/SvListBox-D-_I_Aff.js">
21
23
  <link rel="stylesheet" crossorigin href="./assets/dismissable-BF-9TJSd.css">
22
24
  <link rel="stylesheet" crossorigin href="./assets/SvDateTimePicker-Cygij-dr.css">
23
25
  <link rel="stylesheet" crossorigin href="./assets/SvGridDropdown-Crwb2P3Q.css">
24
26
  <link rel="stylesheet" crossorigin href="./assets/SvGridChart-Vu4Nvjiv.css">
25
- <link rel="stylesheet" crossorigin href="./assets/src-BT3LjWJL.css">
27
+ <link rel="stylesheet" crossorigin href="./assets/src-D8qr49rE.css">
26
28
  <link rel="stylesheet" crossorigin href="./assets/SvListBox-BMSfFJJT.css">
27
29
  <link rel="stylesheet" crossorigin href="./assets/index-BfzoL904.css">
28
30
  </head>
@@ -29,7 +29,7 @@ export type LicenseInfo = {
29
29
  * in that position) yields null, which reads as "no expiry encoded".
30
30
  *
31
31
  * Boundaries are UTC so a key does not lapse at a different instant per
32
- * timezone. Kept in sync with `tools/issue-license.mjs`.
32
+ * timezone. Kept in sync with the (maintainer-side) key issuer.
33
33
  */
34
34
  export declare function parseLicenseExpiry(key: string | null | undefined): Date | null;
35
35
  /** Classify a license key string. Pure and side-effect-free.
@@ -23,7 +23,7 @@ export const VALID_PREFIX = 'SVENTERPRISE-';
23
23
  * in that position) yields null, which reads as "no expiry encoded".
24
24
  *
25
25
  * Boundaries are UTC so a key does not lapse at a different instant per
26
- * timezone. Kept in sync with `tools/issue-license.mjs`.
26
+ * timezone. Kept in sync with the (maintainer-side) key issuer.
27
27
  */
28
28
  export function parseLicenseExpiry(key) {
29
29
  if (!key)
@@ -1,5 +1,5 @@
1
- import { isRangeOperator, isSetOperator, isValuelessOperator } from "@svgrid/grid/filtering";
2
1
  import { defaultThemePreset, getThemePreset, resolveThemeTokens as resolveThemeTokens$1, themePresets } from "@svgrid/grid/themes";
2
+ import { isRangeOperator, isSetOperator, isValuelessOperator } from "@svgrid/grid/filtering";
3
3
  //#region src/schema.ts
4
4
  /** `first_name` / `firstName` / `first-name` -> `First Name`. */
5
5
  function titleCase$2(field) {
@@ -1479,6 +1479,72 @@ function redactConnectionString(value) {
1479
1479
  }
1480
1480
  }
1481
1481
  //#endregion
1482
+ //#region src/studio/themes.ts
1483
+ /**
1484
+ * The Studio look-and-feel themes: a `ProjectTheme`-aware wrapper around
1485
+ * `@svgrid/grid`'s built-in design-system presets (shadcn/ui, Tailwind,
1486
+ * Material 3, Excel, Fluent 2, ...) - the single source of truth for these
1487
+ * tokens, also used directly (as plain CSS) by app code that isn't going
1488
+ * through Studio's project model.
1489
+ *
1490
+ * Picking a preset (and a light/dark mode) restyles the WHOLE generated app -
1491
+ * shell, grids, charts, KPIs, forms - not just the accent.
1492
+ *
1493
+ * Pure + node-safe (studio subtree): plain data + helpers, so the same tokens
1494
+ * drive the browser preview and every emitted output - `app.css` for the full
1495
+ * app and the fragment, the `<svelte:head>` of the CLI `add` scaffolds - via
1496
+ * `themeTokenCss` below, the single place the token CSS is built.
1497
+ */
1498
+ /** The built-in Studio themes, in gallery order (default first). */
1499
+ var studioThemes = themePresets;
1500
+ /** The default theme (used when a project has no `preset`). */
1501
+ var defaultStudioTheme = defaultThemePreset;
1502
+ /** Look up a theme by id. */
1503
+ var getStudioTheme = (id) => getThemePreset(id);
1504
+ /** True when the project's theme is rendering in dark mode. */
1505
+ var isDarkTheme = (theme) => theme?.mode === "dark";
1506
+ /** Resolve the effective `--sg-*` tokens for a project theme: the chosen preset
1507
+ * (default if none) in the chosen mode, with the accent override applied. */
1508
+ function resolveThemeTokens(theme) {
1509
+ const preset = getStudioTheme(theme?.preset) ?? defaultStudioTheme;
1510
+ const mode = theme?.mode === "dark" ? "dark" : "light";
1511
+ return resolveThemeTokens$1(preset, mode, theme?.accent);
1512
+ }
1513
+ /** Resolve the `--sg-*` tokens for a project theme in an EXPLICIT mode (for the
1514
+ * generated app's light/dark switcher, which ships both token sets). */
1515
+ function resolveThemeTokensFor(theme, mode) {
1516
+ const preset = getStudioTheme(theme?.preset) ?? defaultStudioTheme;
1517
+ return resolveThemeTokens$1(preset, mode, theme?.accent);
1518
+ }
1519
+ /**
1520
+ * The `--sg-*` token rules for a project theme, as CSS text. `:root` carries
1521
+ * the mode picked in Studio (what the server-rendered page shows before
1522
+ * hydration), and both palettes are scoped by `[data-theme]` on `<html>` for
1523
+ * the generated app's light/dark switcher. An explicit accent override is
1524
+ * applied to both. No theme -> the default preset, so every output is themed.
1525
+ *
1526
+ * `layer` wraps the rules in a cascade layer. Use it for drop-in outputs (the
1527
+ * fragment, `svgrid-studio add`): layered declarations lose to unlayered ones,
1528
+ * so a host app that already defines its own `--sg-*` tokens keeps winning.
1529
+ */
1530
+ function themeTokenCss(theme, opts = {}) {
1531
+ const decl = (m) => Object.entries(m).map(([k, v]) => ` ${k}: ${v};`).join("\n");
1532
+ const mode = isDarkTheme(theme) ? "dark" : "light";
1533
+ const light = resolveThemeTokensFor(theme, "light");
1534
+ const dark = resolveThemeTokensFor(theme, "dark");
1535
+ const rules = [
1536
+ `:root {\n${decl(mode === "dark" ? dark : light)}\n color-scheme: ${mode};\n}`,
1537
+ `:root[data-theme="light"] {\n${decl(light)}\n color-scheme: light;\n}`,
1538
+ `:root[data-theme="dark"] {\n${decl(dark)}\n color-scheme: dark;\n}`
1539
+ ].join("\n");
1540
+ return opts.layer ? `@layer ${opts.layer} {\n${rules}\n}` : rules;
1541
+ }
1542
+ /** An inline `style` string (`--sg-x: y; ...; color-scheme; font-family`) for previews. */
1543
+ function themeStyleString(theme) {
1544
+ const tokens = resolveThemeTokens(theme);
1545
+ return `${Object.entries(tokens).map(([k, v]) => `${k}: ${v}`).join("; ")}; color-scheme: ${isDarkTheme(theme) ? "dark" : "light"}; font-family: ${tokens["--sg-font"]}`;
1546
+ }
1547
+ //#endregion
1482
1548
  //#region src/studio/scaffold.ts
1483
1549
  /**
1484
1550
  * scaffold - turn an `EntitySchema` into runnable SvelteKit files. The shared
@@ -1704,6 +1770,13 @@ ${managed$1(serverBody)}
1704
1770
  ${managed$1(pageBody)}
1705
1771
  <\/script>
1706
1772
 
1773
+ <svelte:head><style>
1774
+ ${themeTokenCss({
1775
+ preset: options.theme,
1776
+ mode: options.dark ? "dark" : "light"
1777
+ }, { layer: "svgrid-studio" })}
1778
+ </style></svelte:head>
1779
+
1707
1780
  <div class="page">
1708
1781
  <header class="page__toolbar">
1709
1782
  <h2 class="page__title">${label}</h2>
@@ -1803,7 +1876,7 @@ function mergeManaged(existing, generated) {
1803
1876
  */
1804
1877
  var MANAGED_NOTE = "// Regenerated by SvGrid Studio. Edits inside these markers are overwritten.";
1805
1878
  var managed = (body) => `${MANAGED_START}\n${MANAGED_NOTE}\n${body}\n${MANAGED_END}`;
1806
- function layoutFile$1(nav, appTitle) {
1879
+ function layoutFile$1(nav, appTitle, theme) {
1807
1880
  return {
1808
1881
  path: "src/routes/+layout.svelte",
1809
1882
  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.",
@@ -1830,6 +1903,10 @@ ${managed(` let { children } = $props()
1830
1903
  })`)}
1831
1904
  <\/script>
1832
1905
 
1906
+ <svelte:head><style>
1907
+ ${themeTokenCss(theme, { layer: "svgrid-studio" })}
1908
+ </style></svelte:head>
1909
+
1833
1910
  <div class="app-shell" class:is-drawer={drawer} class:is-navopen={navOpen}>
1834
1911
  <header class="app-shell__mobilebar">
1835
1912
  <button type="button" class="app-shell__burger" aria-label="Toggle navigation" aria-expanded={navOpen} onclick={() => (navOpen = !navOpen)}>
@@ -1927,7 +2004,10 @@ function scaffoldApp(schemas, options = {}) {
1927
2004
  href: `/${s.name}`,
1928
2005
  label: s.label ?? titleCase$2(s.name)
1929
2006
  }));
1930
- files.push(layoutFile$1(nav, appTitle), homeFile$1(nav, appTitle));
2007
+ files.push(layoutFile$1(nav, appTitle, {
2008
+ preset: options.theme,
2009
+ mode: options.dark ? "dark" : "light"
2010
+ }), homeFile$1(nav, appTitle));
1931
2011
  return { files };
1932
2012
  }
1933
2013
  //#endregion
@@ -8594,7 +8674,7 @@ var GENERATED_UI_SURFACE = {
8594
8674
  "key": "sortable",
8595
8675
  "label": "Sortable",
8596
8676
  "type": "boolean",
8597
- "description": "Convenience shortcuts to switch a whole capability on without wiring `features` or the finer-grained props by hand. Every capability is OFF by default - a bare grid is a plain read-only table - so set the shortcut `true` to opt in. (`false` / omitted both leave it off; the shortcut is mainly there to turn things ON.) `sortable` - column sorting (injects `rowSortingFeature`) `filterable` - column filtering (injects `columnFilteringFeature`) `editable` - inline cell editing (alias of `enableInlineEditing`) `groupable` - row grouping controls(alias of `showGroupingControls`, also injects `columnGroupingFeature`) `pageable` - pagination footer (alias of `showPagination`) Fine-grained props (`enableInlineEditing`, `showPagination`, ...) still work; the shortcut wins only when it is explicitly set.",
8677
+ "description": "Convenience shortcuts to switch a whole capability on without wiring `features` or the finer-grained props by hand. Every capability is OFF by default - a bare grid is a plain read-only table - so set the shortcut `true` to opt in. (`false` / omitted both leave it off; the shortcut is mainly there to turn things ON.) `sortable` - column sorting (injects `rowSortingFeature`) `filterable` - column filtering (injects `columnFilteringFeature`) `editable` - inline cell editing (alias of `enableInlineEditing`) `selectable` - cell selection (alias of `enableCellSelection`) `groupable` - row grouping controls(alias of `showGroupingControls`, also injects `columnGroupingFeature`) `pageable` - pagination footer (alias of `showPagination`) Fine-grained props (`enableInlineEditing`, `showPagination`, ...) still work; the shortcut wins only when it is explicitly set.",
8598
8678
  "group": "common"
8599
8679
  },
8600
8680
  {
@@ -8609,6 +8689,13 @@ var GENERATED_UI_SURFACE = {
8609
8689
  "type": "boolean",
8610
8690
  "group": "common"
8611
8691
  },
8692
+ {
8693
+ "key": "selectable",
8694
+ "label": "Selectable",
8695
+ "type": "boolean",
8696
+ "description": "Cell selection - click a cell to select it, drag or shift-click to extend the selection to a range. Alias of `enableCellSelection`, and the highest priority of the three inputs that decide it: `selectable` -> `enableCellSelection` -> `selectionMode`. NOTE the asymmetry with its siblings, which are all off until you opt in: this one is **ON by default**, because `selectionMode` defaults to `'both'`. It is named here so the capability is discoverable next to the other shortcuts, and so `selectable={false}` is a one-word way to turn cell selection off (the longhand being `selectionMode=\"none\"`, which also removes the row-selection surface). This is the CELL surface only. The selection **checkbox column** is the other half of `selectionMode`, controlled by `showRowSelection` - also on by default, and it needs no feature import (verified: the column renders and toggles rows with an empty `features` set). Use `selectionMode` when you want to move both surfaces at once.",
8697
+ "group": "common"
8698
+ },
8612
8699
  {
8613
8700
  "key": "treeData",
8614
8701
  "label": "Tree Data",
@@ -15320,22 +15407,7 @@ function layoutFile(nav, opts = {}) {
15320
15407
  const footer = shell.footer === void 0 ? "Built with SvGrid Studio" : shell.footer;
15321
15408
  const right = style === "sidebar" && shell.navPosition === "right";
15322
15409
  const defaultMode = opts.dark ? "dark" : "light";
15323
- const withAccent = (m) => {
15324
- const v = { ...m ?? {} };
15325
- if (opts.accent) v["--sg-accent"] = opts.accent;
15326
- return v;
15327
- };
15328
- const declLines = (m) => Object.entries(m).map(([k, v]) => `${k}: ${v};`).join(" ");
15329
- const hasSwitch = !!(opts.lightVars && opts.darkVars);
15330
- let themeHead = "";
15331
- if (hasSwitch) {
15332
- const lv = withAccent(opts.lightVars);
15333
- const dv = withAccent(opts.darkVars);
15334
- themeHead = `\n<svelte:head><style>:root { ${[declLines(defaultMode === "dark" ? dv : lv), `color-scheme: ${defaultMode};`].join(" ")} }\n:root[data-theme="light"] { ${[declLines(lv), "color-scheme: light;"].join(" ")} }\n:root[data-theme="dark"] { ${[declLines(dv), "color-scheme: dark;"].join(" ")} }</style></svelte:head>\n`;
15335
- } else {
15336
- const rootRule = [declLines(withAccent(opts.themeVars)), opts.dark ? "color-scheme: dark;" : ""].filter(Boolean).join(" ");
15337
- themeHead = rootRule ? `\n<svelte:head><style>:root { ${rootRule} }</style></svelte:head>\n` : "";
15338
- }
15410
+ const hasSwitch = !!opts.themeSwitch;
15339
15411
  const anchor = `<a class="sv-app__link" class:is-active={page.url.pathname === item.href} href={item.href}>${opts.i18n ? `{item.id ? $t('nav.' + item.id, item.label) : item.label}` : "{item.label}"}</a>`;
15340
15412
  const linkGate = opts.access ? `{#if !item.id || canScreen($currentRole, item.id)}${anchor}{/if}` : anchor;
15341
15413
  const localeSwitcher = opts.i18n ? `\n <select class="sv-app__locale" aria-label="Language" onchange={(e) => currentLocale.set(e.currentTarget.value as typeof $currentLocale)}>
@@ -15524,7 +15596,7 @@ function layoutFile(nav, opts = {}) {
15524
15596
  $effect(() => { void page.url.pathname; navOpen = false })${hasSwitch ? `
15525
15597
  // Light/dark switcher: defaults to the mode picked in Studio, then honours the
15526
15598
  // visitor's saved choice. Applies via [data-theme] on <html> (see the token
15527
- // sets in <svelte:head>), and persists per browser.
15599
+ // sets in app.css), and persists per browser.
15528
15600
  let theme = $state<'light' | 'dark'>('${defaultMode}')
15529
15601
  function applyTheme(t: 'light' | 'dark') {
15530
15602
  theme = t
@@ -15562,7 +15634,6 @@ function layoutFile(nav, opts = {}) {
15562
15634
  // Close both menus on route change.
15563
15635
  $effect(() => { void page.url.pathname; bellOpen = false; menuOpen = false })` : ""}
15564
15636
  <\/script>
15565
- ${themeHead}
15566
15637
 
15567
15638
  ${opts.auth ? `{#if ${JSON.stringify(opts.authRoutes ?? ["/login"])}.includes(page.url.pathname)}
15568
15639
  {@render children()}
@@ -15728,47 +15799,6 @@ function emitStudioApp(schemas) {
15728
15799
  ];
15729
15800
  }
15730
15801
  //#endregion
15731
- //#region src/studio/themes.ts
15732
- /**
15733
- * The Studio look-and-feel themes: a `ProjectTheme`-aware wrapper around
15734
- * `@svgrid/grid`'s built-in design-system presets (shadcn/ui, Tailwind,
15735
- * Material 3, Excel, Fluent 2, ...) - the single source of truth for these
15736
- * tokens, also used directly (as plain CSS) by app code that isn't going
15737
- * through Studio's project model.
15738
- *
15739
- * Picking a preset (and a light/dark mode) restyles the WHOLE generated app -
15740
- * shell, grids, charts, KPIs, forms - not just the accent.
15741
- *
15742
- * Pure + node-safe (studio subtree): plain data + helpers, so the same tokens
15743
- * drive the browser preview and the emitted `+layout.svelte` / `app.css`.
15744
- */
15745
- /** The built-in Studio themes, in gallery order (default first). */
15746
- var studioThemes = themePresets;
15747
- /** The default theme (used when a project has no `preset`). */
15748
- var defaultStudioTheme = defaultThemePreset;
15749
- /** Look up a theme by id. */
15750
- var getStudioTheme = (id) => getThemePreset(id);
15751
- /** True when the project's theme is rendering in dark mode. */
15752
- var isDarkTheme = (theme) => theme?.mode === "dark";
15753
- /** Resolve the effective `--sg-*` tokens for a project theme: the chosen preset
15754
- * (default if none) in the chosen mode, with the accent override applied. */
15755
- function resolveThemeTokens(theme) {
15756
- const preset = getStudioTheme(theme?.preset) ?? defaultStudioTheme;
15757
- const mode = theme?.mode === "dark" ? "dark" : "light";
15758
- return resolveThemeTokens$1(preset, mode, theme?.accent);
15759
- }
15760
- /** Resolve the `--sg-*` tokens for a project theme in an EXPLICIT mode (for the
15761
- * generated app's light/dark switcher, which ships both token sets). */
15762
- function resolveThemeTokensFor(theme, mode) {
15763
- const preset = getStudioTheme(theme?.preset) ?? defaultStudioTheme;
15764
- return resolveThemeTokens$1(preset, mode, theme?.accent);
15765
- }
15766
- /** An inline `style` string (`--sg-x: y; ...; color-scheme; font-family`) for previews. */
15767
- function themeStyleString(theme) {
15768
- const tokens = resolveThemeTokens(theme);
15769
- return `${Object.entries(tokens).map(([k, v]) => `${k}: ${v}`).join("; ")}; color-scheme: ${isDarkTheme(theme) ? "dark" : "light"}; font-family: ${tokens["--sg-font"]}`;
15770
- }
15771
- //#endregion
15772
15802
  //#region src/edit-panel.ts
15773
15803
  /** True when any field's behaviour depends on the values (i.e. has a `when`). */
15774
15804
  function hasFieldConditions(schema) {
@@ -15788,7 +15818,7 @@ function hasFieldConditions(schema) {
15788
15818
  *
15789
15819
  * `version.test.ts` asserts this matches `package.json`, so the two cannot drift.
15790
15820
  */
15791
- var SVGRID_VERSION = "2.6.2";
15821
+ var SVGRID_VERSION = "2.6.4";
15792
15822
  //#endregion
15793
15823
  //#region src/studio/emit-project.ts
15794
15824
  var has = (blocks, kind) => blocks.some((b) => b.config.kind === kind);
@@ -18310,12 +18340,9 @@ function emitStudioProject(project) {
18310
18340
  ...companions,
18311
18341
  ...handleFiles,
18312
18342
  layoutFile(navExtras, {
18313
- accent: project.theme?.accent,
18314
18343
  shell: project.theme?.shell,
18315
18344
  title: project.title,
18316
- themeVars: resolveThemeTokens(project.theme),
18317
- lightVars: resolveThemeTokensFor(project.theme, "light"),
18318
- darkVars: resolveThemeTokensFor(project.theme, "dark"),
18345
+ themeSwitch: true,
18319
18346
  dark: isDarkTheme(project.theme),
18320
18347
  access: accessEnabled,
18321
18348
  auth: authEnabled && !supabaseAuth,
@@ -20268,8 +20295,15 @@ var SCAFFOLD_STATIC = [
20268
20295
  contents: `node_modules\n.svelte-kit\n/build\n.env\n.env.*\n!.env.example\n.DS_Store\n`
20269
20296
  }
20270
20297
  ];
20271
- var APP_CSS = `:root { --sg-accent: #4f46e5; color-scheme: light dark; }
20272
- * { box-sizing: border-box; }
20298
+ /** `src/app.css`: the project's theme tokens (see `themeTokenCss`) followed by the
20299
+ * `.st-*` page styles every generated screen uses. This is the ONE place the tokens
20300
+ * are emitted, so the fragment (which drops the layout) and the full app are
20301
+ * themed alike. `layer` is for drop-in outputs - see `themeTokenCss`. */
20302
+ function appCss(project, opts = {}) {
20303
+ return `${themeTokenCss(project.theme, opts)}
20304
+ ${APP_CSS_BODY}`;
20305
+ }
20306
+ var APP_CSS_BODY = `* { box-sizing: border-box; }
20273
20307
  html, body { margin: 0; height: 100%; }
20274
20308
  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); }
20275
20309
  .st__title { margin: 0; font-size: 22px; font-weight: 720; letter-spacing: -0.015em; }
@@ -20813,8 +20847,8 @@ function emitStudioAppBundle(project) {
20813
20847
  }] : [],
20814
20848
  {
20815
20849
  path: "src/app.css",
20816
- description: "App theme + page styles.",
20817
- contents: APP_CSS
20850
+ description: "App theme tokens + page styles.",
20851
+ contents: appCss(project)
20818
20852
  },
20819
20853
  {
20820
20854
  path: "src/custom.css",
@@ -20889,7 +20923,9 @@ ${depList}
20889
20923
 
20890
20924
  ## 3. Styles
20891
20925
  Import \`src/app.css\` once (e.g. in your root \`+layout.svelte\`) - it carries the
20892
- \`.st-*\` page styles the screens use and the \`--sg-*\` design tokens.
20926
+ \`.st-*\` page styles the screens use and the \`--sg-*\` design tokens of the theme
20927
+ picked in Studio. The tokens sit in a cascade layer (\`@layer svgrid-studio\`), so if
20928
+ your app already defines its own \`--sg-*\` tokens, yours keep winning.
20893
20929
 
20894
20930
  ## 4. What your app must provide
20895
20931
  The full-app nav shell, home redirect, auth guard, RBAC bootstrap, i18n provider,
@@ -20903,8 +20939,8 @@ ${libFiles.join("\n")}
20903
20939
  ...content,
20904
20940
  {
20905
20941
  path: "src/app.css",
20906
- description: "Page styles (.st-*) + design tokens - import once in your layout.",
20907
- contents: APP_CSS
20942
+ description: "Theme tokens (layered, so yours win) + page styles (.st-*) - import once in your layout.",
20943
+ contents: appCss(project, { layer: "svgrid-studio" })
20908
20944
  },
20909
20945
  {
20910
20946
  path: "FRAGMENT.md",
@@ -40918,14 +40954,14 @@ async function askScreens(schemas, flags, prompts) {
40918
40954
  async function askTheme(flags, prompts) {
40919
40955
  let preset = flags.theme?.trim().toLowerCase();
40920
40956
  if (preset && !getStudioTheme(preset)) {
40921
- prompts.say(` ! Unknown theme "${preset}" - using tailwind.`);
40957
+ prompts.say(` ! Unknown theme "${preset}" - using ember.`);
40922
40958
  preset = void 0;
40923
40959
  }
40924
40960
  if (!preset && !flags.yes) preset = studioThemes[await pick(prompts, "Theme:", studioThemes.map((t) => ({
40925
40961
  label: t.name,
40926
40962
  hint: t.id
40927
40963
  })))]?.id;
40928
- preset ??= "tailwind";
40964
+ preset ??= "ember";
40929
40965
  const dark = flags.dark ?? (flags.yes ? false : isYes(await prompts.ask("Dark mode? (y/N)", "n"), false));
40930
40966
  return {
40931
40967
  preset,
@@ -41438,7 +41474,7 @@ var REVOKED_KEYS = /* @__PURE__ */ new Set([]);
41438
41474
  * in that position) yields null, which reads as "no expiry encoded".
41439
41475
  *
41440
41476
  * Boundaries are UTC so a key does not lapse at a different instant per
41441
- * timezone. Kept in sync with `tools/issue-license.mjs`.
41477
+ * timezone. Kept in sync with the (maintainer-side) key issuer.
41442
41478
  */
41443
41479
  function parseLicenseExpiry(key) {
41444
41480
  if (!key) return null;
@@ -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';
@@ -3048,7 +3048,7 @@ export function emitStudioProject(project) {
3048
3048
  navExtras = [...navExtras, { href: '/users', label: 'Users', id: '__users__' }];
3049
3049
  const i18nFiles = i18nEnabled ? [i18nModule(project)] : [];
3050
3050
  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)];
3051
+ 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
3052
  }
3053
3053
  /** The default-locale (`en`) message catalog, keyed for nav, screen titles, the
3054
3054
  * New button, and grid column headers - the strings the app renders. */
@@ -4832,8 +4832,15 @@ const SCAFFOLD_STATIC = [
4832
4832
  { path: '.npmrc', description: 'npm config.', contents: `engine-strict=false\n` },
4833
4833
  { 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
4834
  ];
4835
- const APP_CSS = `:root { --sg-accent: #4f46e5; color-scheme: light dark; }
4836
- * { box-sizing: border-box; }
4835
+ /** `src/app.css`: the project's theme tokens (see `themeTokenCss`) followed by the
4836
+ * `.st-*` page styles every generated screen uses. This is the ONE place the tokens
4837
+ * are emitted, so the fragment (which drops the layout) and the full app are
4838
+ * themed alike. `layer` is for drop-in outputs - see `themeTokenCss`. */
4839
+ function appCss(project, opts = {}) {
4840
+ return `${themeTokenCss(project.theme, opts)}
4841
+ ${APP_CSS_BODY}`;
4842
+ }
4843
+ const APP_CSS_BODY = `* { box-sizing: border-box; }
4837
4844
  html, body { margin: 0; height: 100%; }
4838
4845
  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
4846
  .st__title { margin: 0; font-size: 22px; font-weight: 720; letter-spacing: -0.015em; }
@@ -5390,7 +5397,7 @@ export function emitStudioAppBundle(project) {
5390
5397
  ...SCAFFOLD_STATIC,
5391
5398
  ...(envExample(allSource) ? [{ path: '.env.example', description: 'Environment variables the app reads (copy to .env and fill in).', contents: envExample(allSource) }] : []),
5392
5399
  ...(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 },
5400
+ { path: 'src/app.css', description: 'App theme tokens + page styles.', contents: appCss(project) },
5394
5401
  // Your styles: a dedicated CSS file the layout imports AFTER app.css, so its rules
5395
5402
  // win. Edited in the designer's Styles panel; regenerated from the model.
5396
5403
  { 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 +5452,9 @@ ${depList}
5445
5452
 
5446
5453
  ## 3. Styles
5447
5454
  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.
5455
+ \`.st-*\` page styles the screens use and the \`--sg-*\` design tokens of the theme
5456
+ picked in Studio. The tokens sit in a cascade layer (\`@layer svgrid-studio\`), so if
5457
+ your app already defines its own \`--sg-*\` tokens, yours keep winning.
5449
5458
 
5450
5459
  ## 4. What your app must provide
5451
5460
  The full-app nav shell, home redirect, auth guard, RBAC bootstrap, i18n provider,
@@ -5457,7 +5466,7 @@ ${libFiles.join('\n')}
5457
5466
  `;
5458
5467
  return [
5459
5468
  ...content,
5460
- { path: 'src/app.css', description: 'Page styles (.st-*) + design tokens - import once in your layout.', contents: APP_CSS },
5469
+ { 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
5470
  { path: 'FRAGMENT.md', description: 'How to drop this fragment into an existing SvelteKit app.', contents: fragmentMd },
5462
5471
  ];
5463
5472
  }
@@ -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>