@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.
- package/dist/SvGridBulkEditDrawer.svelte +158 -0
- package/dist/SvGridBulkEditDrawer.svelte.d.ts +9 -0
- package/dist/SvGridSelectionBar.svelte +452 -0
- package/dist/SvGridSelectionBar.svelte.d.ts +6 -0
- package/dist/SvPivotDesigner.svelte +3 -3
- package/dist/SvPivotDesigner.svelte.d.ts +3 -3
- package/dist/bulk-edit.d.ts +64 -0
- package/dist/bulk-edit.js +152 -0
- package/dist/cdn/svgrid-enterprise.svelte-external.js +6232 -5824
- package/dist/designer/assets/GridMenus-Cgilexpk.js +8 -0
- package/dist/designer/assets/SvDateRangeInput-BV5OXYQ2.css +1 -0
- package/dist/designer/assets/SvDateRangeInput-CVn-yqrQ.js +1 -0
- package/dist/designer/assets/SvDateTimePicker-BdioTrJQ.js +1 -0
- package/dist/designer/assets/SvDateTimePicker-CY8TK4BT.css +1 -0
- package/dist/designer/assets/SvField-BsqiA5me.js +1 -0
- package/dist/designer/assets/SvGridCellEditor-dLY7_kk9.js +2 -0
- package/dist/designer/assets/SvListBox-CVYQcLX9.js +1 -0
- package/dist/designer/assets/{SvListBox-BMSfFJJT.css → SvListBox-DgN8kvjt.css} +1 -1
- package/dist/designer/assets/column-resize-C_inr5zT.js +1 -0
- package/dist/designer/assets/date-format-4DJvNtG2.js +1 -0
- package/dist/designer/assets/{SvDateTimePicker-Cygij-dr.css → date-format-Dzy4mTGh.css} +1 -1
- package/dist/designer/assets/dismissable-BO547MH9.js +1 -0
- package/dist/designer/assets/editor-registry-BjP16H0v.js +1 -0
- package/dist/designer/assets/{index-aTVomkp-.js → index-BvaLmQSd.js} +128 -128
- package/dist/designer/assets/index-Cp7fDJ3b.css +1 -0
- package/dist/designer/assets/{js-scroller.svelte-C7LaeUEW.js → js-scroller.svelte-Bhxqb3-c.js} +2 -2
- package/dist/designer/assets/preload-helper-CG0n22O7.js +1 -0
- package/dist/designer/assets/row-resize-B0FWHcNr.js +1 -0
- package/dist/designer/assets/src-7qOYsBug.js +2896 -0
- package/dist/designer/assets/src-B7DNcoQK.css +1 -0
- package/dist/designer/assets/validate-h-aslRV9.js +1 -0
- package/dist/designer/index.html +18 -13
- package/dist/export.d.ts +4 -0
- package/dist/import.d.ts +7 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +8 -0
- package/dist/install.d.ts +3 -0
- package/dist/install.js +2 -0
- package/dist/license-core.d.ts +1 -1
- package/dist/license-core.js +1 -1
- package/dist/node/studio.js +178 -80
- package/dist/pivot.d.ts +11 -0
- package/dist/pivot.js +6 -1
- package/dist/selection-bar.d.ts +7 -0
- package/dist/selection-bar.js +42 -0
- package/dist/studio/emit-project.js +22 -7
- package/dist/studio/emit-schema.d.ts +1 -4
- package/dist/studio/emit-schema.js +7 -29
- package/dist/studio/init-flow.js +2 -2
- package/dist/studio/scaffold-app.d.ts +8 -0
- package/dist/studio/scaffold-app.js +7 -2
- package/dist/studio/scaffold.d.ts +8 -0
- package/dist/studio/scaffold.js +5 -0
- package/dist/studio/themes.d.ts +17 -1
- package/dist/studio/themes.js +26 -1
- package/dist/studio/ui-components.generated.js +72 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +10 -5
- package/dist/designer/assets/GridMenus-DuogVgW0.js +0 -7
- package/dist/designer/assets/SvDateTimePicker-DOBYyLeN.js +0 -1
- package/dist/designer/assets/SvGrid.helpers-C6wxF55r.js +0 -1
- package/dist/designer/assets/SvGridCellEditor-CMtmQINJ.js +0 -2
- package/dist/designer/assets/SvListBox-CWCIPM4P.js +0 -1
- package/dist/designer/assets/dismissable-2EZKoens.js +0 -1
- package/dist/designer/assets/editor-registry-BB1Qb5mp.js +0 -1
- package/dist/designer/assets/index-BfzoL904.css +0 -1
- package/dist/designer/assets/src-B5lLfIhK.css +0 -1
- package/dist/designer/assets/src-DLoHsbp5.js +0 -2893
- /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
|
|
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 {
|
|
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, {
|
|
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
|
-
|
|
4836
|
-
*
|
|
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:
|
|
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: '
|
|
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
|
-
|
|
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
|
-
//
|
|
670
|
-
//
|
|
671
|
-
// built-in light/dark
|
|
672
|
-
// and the bare :root falls back to the mode
|
|
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
|
|
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
|
|
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()}
|
package/dist/studio/init-flow.js
CHANGED
|
@@ -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
|
|
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 = '
|
|
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`. */
|
package/dist/studio/scaffold.js
CHANGED
|
@@ -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>
|
package/dist/studio/themes.d.ts
CHANGED
|
@@ -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
|
|
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;
|
package/dist/studio/themes.js
CHANGED
|
@@ -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
|
|
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);
|