@svgrid/enterprise 3.1.17 → 3.1.19

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 (62) hide show
  1. package/dist/SvGridEditPanel.svelte +14 -1
  2. package/dist/SvGridScheduler.svelte +15 -6
  3. package/dist/SvSheet.svelte +2 -0
  4. package/dist/cdn/svgrid-enterprise.svelte-external.js +8253 -8166
  5. package/dist/designer/assets/{index-C1Ujg62Z.js → index-DA9jVAjf.js} +359 -348
  6. package/dist/designer/assets/index-WCxj6WfJ.css +1 -0
  7. package/dist/designer/assets/{src-hO8VnnI5.js → src-fddEt37f.js} +2 -2
  8. package/dist/designer/index.html +3 -3
  9. package/dist/export-pdf.d.ts +8 -0
  10. package/dist/export-pdf.js +14 -0
  11. package/dist/export.js +2 -2
  12. package/dist/fields/SvBase.svelte +36 -33
  13. package/dist/fields/base-connect.js +50 -9
  14. package/dist/fields/base-sql.d.ts +8 -0
  15. package/dist/fields/base-sql.js +12 -3
  16. package/dist/install.js +3 -3
  17. package/dist/node/studio.js +207 -62
  18. package/dist/sheet-enable.d.ts +8 -2
  19. package/dist/sheet-enable.js +21 -9
  20. package/dist/sources/introspect-supabase.d.ts +6 -3
  21. package/dist/sources/introspect-supabase.js +29 -11
  22. package/dist/sources/supabase.d.ts +10 -0
  23. package/dist/sources/supabase.js +11 -2
  24. package/dist/studio/cli.d.ts +9 -0
  25. package/dist/studio/cli.js +41 -7
  26. package/dist/studio/emit-project.d.ts +3 -1
  27. package/dist/studio/emit-project.js +84 -33
  28. package/dist/studio/emit-schema.js +9 -2
  29. package/dist/studio/index.d.ts +1 -1
  30. package/dist/studio/init-flow.d.ts +7 -0
  31. package/dist/studio/init-flow.js +23 -2
  32. package/dist/studio/introspect-db.d.ts +15 -1
  33. package/dist/studio/introspect-db.js +29 -5
  34. package/dist/studio/scaffold.js +4 -2
  35. package/dist/sveltekit/sql-source.d.ts +24 -0
  36. package/dist/sveltekit/sql-source.js +34 -1
  37. package/dist/sveltekit/sql.js +14 -2
  38. package/dist/version.d.ts +2 -2
  39. package/dist/version.js +2 -2
  40. package/dist/wc/GridFooter-BgoA77TJ.js +145 -0
  41. package/dist/wc/{GridMenus-q9qYpzKh.js → GridMenus-DVGuqp2l.js} +430 -430
  42. package/dist/wc/{PinSplitter-D-qtDhSM.js → PinSplitter-Btco0txr.js} +24 -24
  43. package/dist/wc/{SvChartMenu-CQNrQGT2.js → SvChartMenu-Ue-HFR5c.js} +58 -58
  44. package/dist/wc/{SvDateRangeInput-BYNOfYxD.js → SvDateRangeInput-CJZohgBe.js} +43 -43
  45. package/dist/wc/{SvDateTimePicker-Bq9N0Oiy.js → SvDateTimePicker-DmK_QkkU.js} +241 -241
  46. package/dist/wc/{SvField-B_kurx_P.js → SvField-C-OodMMv.js} +71 -71
  47. package/dist/wc/SvGridCellEditor-C7ios4i_.js +582 -0
  48. package/dist/wc/{SvGridChart-DrmqEen2.js → SvGridChart-DWRqlhCV.js} +549 -549
  49. package/dist/wc/{SvGridChartBuilder-XFvW1BdT.js → SvGridChartBuilder-BXW3yUR0.js} +337 -337
  50. package/dist/wc/{SvGridChartPanel-oJJtUKG6.js → SvGridChartPanel-28n4tomx.js} +354 -354
  51. package/dist/wc/{SvGridChartView-Cetuj70H.js → SvGridChartView-CbG_WrU-.js} +7 -7
  52. package/dist/wc/{SvGridDropdown-45WsObLL.js → SvGridDropdown-BVnpNU_I.js} +130 -130
  53. package/dist/wc/{SvModal-DrfOYYyX.js → SvModal-DLppAQED.js} +91 -91
  54. package/dist/wc/{client-DQIzjDBG.js → client-GWAV79ON.js} +479 -476
  55. package/dist/wc/{date-format-DS-z_h9v.js → date-format-4_QoS4zZ.js} +393 -393
  56. package/dist/wc/{grid-extension.svelte-Bufq4B4C.js → grid-extension.svelte-CucBS4FL.js} +1 -1
  57. package/dist/wc/sv-sheet-element.js +10474 -10460
  58. package/dist/wc/{svelte-virtualizer.svelte-CkZaFakp.js → svelte-virtualizer.svelte-BDcouWqw.js} +3 -3
  59. package/package.json +18 -5
  60. package/dist/designer/assets/index-DdmxO6Dd.css +0 -1
  61. package/dist/wc/GridFooter-4T29pfok.js +0 -145
  62. package/dist/wc/SvGridCellEditor-Nnl8C3Yj.js +0 -582
@@ -22,9 +22,12 @@ export type IntrospectSupabaseOptions = {
22
22
  fetch?: typeof fetch;
23
23
  };
24
24
  /** List a Supabase project's tables from its PostgREST OpenAPI doc (URL + anon key).
25
- * Returns the exposed table names (sorted), or `[]` when the doc can't be read
26
- * (CORS-blocked, bad key). Lets the Studio wizard offer a table picker instead of
27
- * making the user hand-type the table name. */
25
+ * Returns the exposed table names (sorted). `[]` means the project answered and
26
+ * lists none, or answered 401/403 without naming the key as invalid (the doc is
27
+ * restricted for this key; each table can still be introspected by name).
28
+ * Throws, naming the URL, when the project cannot be reached (wrong URL, CORS),
29
+ * answers with another error status, says the key is invalid, or sends something
30
+ * that is not an API description. */
28
31
  export declare function listSupabaseTables(url: string, key: string, fetchFn?: typeof fetch): Promise<string[]>;
29
32
  /** Introspect a Supabase/PostgREST table into an EntitySchema, or null. */
30
33
  export declare function introspectSupabaseTable<TData extends RowData = RowData>(options: IntrospectSupabaseOptions): Promise<EntitySchema<TData> | null>;
@@ -1,4 +1,5 @@
1
1
  import { buildEntitySchema } from './schema-from-columns.js';
2
+ import { hideBaseOrderColumn } from '../studio/introspect-db.js';
2
3
  const mapOpenApiType = (prop) => {
3
4
  if (Array.isArray(prop.enum))
4
5
  return 'enum';
@@ -98,28 +99,44 @@ async function viaSample(o, base) {
98
99
  }));
99
100
  }
100
101
  /** List a Supabase project's tables from its PostgREST OpenAPI doc (URL + anon key).
101
- * Returns the exposed table names (sorted), or `[]` when the doc can't be read
102
- * (CORS-blocked, bad key). Lets the Studio wizard offer a table picker instead of
103
- * making the user hand-type the table name. */
102
+ * Returns the exposed table names (sorted). `[]` means the project answered and
103
+ * lists none, or answered 401/403 without naming the key as invalid (the doc is
104
+ * restricted for this key; each table can still be introspected by name).
105
+ * Throws, naming the URL, when the project cannot be reached (wrong URL, CORS),
106
+ * answers with another error status, says the key is invalid, or sends something
107
+ * that is not an API description. */
104
108
  export async function listSupabaseTables(url, key, fetchFn = globalThis.fetch) {
105
109
  const base = url.replace(/\/+$/, '');
110
+ const at = `${base}/rest/v1/`;
106
111
  let res;
107
112
  try {
108
- res = await fetchFn(`${base}/rest/v1/`, { headers: { apikey: key, Authorization: `Bearer ${key}`, Accept: 'application/json' } });
113
+ res = await fetchFn(at, { headers: { apikey: key, Authorization: `Bearer ${key}`, Accept: 'application/json' } });
109
114
  }
110
- catch {
111
- return [];
115
+ catch (e) {
116
+ throw new Error(`Could not reach ${at} (${e instanceof Error ? e.message : String(e)})`, { cause: e });
117
+ }
118
+ if (!res.ok) {
119
+ let body = '';
120
+ try {
121
+ body = await res.text();
122
+ }
123
+ catch { /* no body */ }
124
+ const badKey = /invalid api key/i.test(body);
125
+ if ((res.status === 401 || res.status === 403) && !badKey)
126
+ return [];
127
+ throw new Error(`${at} answered ${res.status}${badKey ? ' (invalid API key)' : ''}`);
112
128
  }
113
- if (!res.ok)
114
- return [];
115
129
  let spec;
116
130
  try {
117
131
  spec = await res.json();
118
132
  }
119
133
  catch {
120
- return [];
134
+ spec = null;
121
135
  }
122
- const defs = spec?.definitions ?? spec?.components?.schemas ?? {};
136
+ if (!spec || typeof spec !== 'object' || !(spec.definitions || spec.components || spec.paths)) {
137
+ throw new Error(`${at} did not answer with an API description: is this a Supabase project URL?`);
138
+ }
139
+ const defs = spec.definitions ?? spec.components?.schemas ?? {};
123
140
  // PostgREST also lists RPCs under `paths` as `/rpc/...`; `definitions` are the tables/views.
124
141
  return Object.keys(defs).sort((a, b) => a.localeCompare(b));
125
142
  }
@@ -133,5 +150,6 @@ export async function introspectSupabaseTable(options) {
133
150
  const columns = (await viaOpenApi(o, base)) ?? (await viaSample(o, base)) ?? (await viaCsv(o, base));
134
151
  if (!columns || !columns.length)
135
152
  return null;
136
- return buildEntitySchema(o.table, columns);
153
+ const schema = buildEntitySchema(o.table, columns);
154
+ return { ...schema, fields: schema.fields.map((f) => hideBaseOrderColumn(f)) };
137
155
  }
@@ -38,5 +38,15 @@ export type SupabaseDataSourceConfig<TData extends RowData> = {
38
38
  * pivoted request is answered as plain grouping, with a warning once.
39
39
  */
40
40
  pivot?: (request: ServerRequest) => Promise<ServerResult<TData>>;
41
+ /**
42
+ * How rows come back when the request has no sort, e.g. creation order.
43
+ * Default: a `created_at` / `inserted_at` field when the schema has one.
44
+ * The primary key is appended to every leaf order as the tie-breaker, so
45
+ * paging with `range` is deterministic.
46
+ */
47
+ defaultOrder?: ReadonlyArray<{
48
+ field: string;
49
+ desc?: boolean;
50
+ }>;
41
51
  };
42
52
  export declare function createSupabaseDataSource<TData extends RowData>(config: SupabaseDataSourceConfig<TData>): WritableDataSource<TData>;
@@ -1,4 +1,5 @@
1
1
  import { resolveIdField } from '../schema.js';
2
+ import { unsortedOrder } from '../sveltekit/sql-source.js';
2
3
  import { nudgeEnterpriseFor } from '../license.js';
3
4
  import { normalizeFilters } from './filters.js';
4
5
  const sanitize = (term) => term.replace(/[%,()]/g, '');
@@ -9,6 +10,8 @@ export function createSupabaseDataSource(config) {
9
10
  const searchColumns = config.searchColumns ??
10
11
  schema.fields.filter((f) => f.type === 'text' || f.type === 'enum').map((f) => String(f.field));
11
12
  let warnedPivot = false;
13
+ const key = schema.fields.some((f) => f.field === idField) ? idField : null;
14
+ const unsorted = unsortedOrder(schema, config.defaultOrder);
12
15
  return {
13
16
  async getRows(request) {
14
17
  // ---- Server-side pivot ----------------------------------------------
@@ -112,8 +115,14 @@ export function createSupabaseDataSource(config) {
112
115
  }
113
116
  }
114
117
  else {
115
- for (const s of request.sortModel)
116
- q = q.order(s.id, { ascending: !s.desc });
118
+ // Without an order PostgREST pages in no fixed order. Unsorted rows go
119
+ // in creation order where it is known; the key breaks ties.
120
+ const sorted = request.sortModel.map((s) => ({ field: s.id, desc: !!s.desc }));
121
+ const order = sorted.length ? sorted : unsorted;
122
+ for (const o of order)
123
+ q = q.order(o.field, { ascending: !o.desc });
124
+ if (key && !order.some((o) => o.field === key))
125
+ q = q.order(key, { ascending: true });
117
126
  }
118
127
  const { data, count, error } = await q.range(request.startRow, request.endRow - 1);
119
128
  if (error)
@@ -35,6 +35,8 @@ export type AddResult = {
35
35
  schema: EntitySchema;
36
36
  /** Paths written, in order. */
37
37
  written: string[];
38
+ /** Existing files without managed markers, left alone; the new version went to `parkedAs`. */
39
+ conflicts: ParkedFile[];
38
40
  verify: VerifyResult;
39
41
  };
40
42
  /** Resolve an EntitySchema from whichever input was provided. */
@@ -44,9 +46,16 @@ export declare function resolveSchema(options: AddOptions, io: StudioIO): Promis
44
46
  * --from` path), linked so foreign keys become working relation lookups.
45
47
  */
46
48
  export declare function resolveSchemas(from: string, io: StudioIO): Promise<EntitySchema[]>;
49
+ /** A generated file that was not written over an existing one, and where it went instead. */
50
+ export type ParkedFile = {
51
+ path: string;
52
+ parkedAs: string;
53
+ };
47
54
  export declare function runStudioAdd(options: AddOptions, io: StudioIO): Promise<AddResult>;
48
55
  export type AddAppResult = {
49
56
  written: string[];
57
+ /** Existing files without managed markers, left alone; the new version went to `parkedAs`. */
58
+ conflicts: ParkedFile[];
50
59
  verify: VerifyResult;
51
60
  };
52
61
  /**
@@ -1,6 +1,6 @@
1
1
  import { introspectDrizzle, introspectDrizzleAll, introspectJson } from './introspect.js';
2
2
  import { introspectPrisma, introspectPrismaAll } from './introspect-prisma.js';
3
- import { mergeManaged, scaffold, skipUserOwned } from './scaffold.js';
3
+ import { MANAGED_START, mergeManaged, scaffold, skipUserOwned } from './scaffold.js';
4
4
  import { scaffoldApp } from './scaffold-app.js';
5
5
  import { verifyScaffold } from './verify.js';
6
6
  /** A `.prisma` file (by extension or a `model X {` block) parses as Prisma; otherwise Drizzle. */
@@ -34,9 +34,37 @@ export async function resolveSchemas(from, io) {
34
34
  throw new Error(`runStudioAddApp: schema file not found: ${from}`);
35
35
  return isPrismaSource(from, src) ? introspectPrismaAll(src) : introspectDrizzleAll(src);
36
36
  }
37
- /** Merge-write a set of generated files, preserving user edits outside managed regions. */
37
+ /**
38
+ * Where to park a generated file next to `path`: `<path>.new`, unless that holds
39
+ * something else (an earlier run's copy the user may be merging), then the first
40
+ * free `<path>.new.1`, `.new.2`, ... A slot already holding these exact contents
41
+ * is reused, so re-running does not pile up copies.
42
+ */
43
+ async function freeNewPath(path, contents, io) {
44
+ for (let i = 0;; i++) {
45
+ const candidate = i === 0 ? `${path}.new` : `${path}.new.${i}`;
46
+ const there = await io.readFile(candidate);
47
+ if (there == null || there === contents)
48
+ return candidate;
49
+ }
50
+ }
51
+ /**
52
+ * Merge-write a set of generated files, preserving user edits outside managed
53
+ * regions.
54
+ *
55
+ * A file already on disk with no managed markers is somebody's own code (the
56
+ * create-studio starter's nav layout, a hand-written page), and a merge would
57
+ * replace it whole. Those get the new version beside them as `<file>.new`, the
58
+ * same convention the designer uses for a hand-edited file, and come back in
59
+ * `conflicts`. Unlike the designer, an existing `.new` with other contents is
60
+ * kept: the copy goes to `.new.1` and up (see freeNewPath). A folder `init` generated (it has studio.config.json) is the
61
+ * exception: its files are Studio's own output, and regenerating them is the
62
+ * point.
63
+ */
38
64
  async function writeAll(files, io) {
39
65
  const written = [];
66
+ const conflicts = [];
67
+ const studioApp = (await io.readFile('studio.config.json')) != null;
40
68
  for (const file of files) {
41
69
  const existing = await io.readFile(file.path);
42
70
  // User-owned companions (a screen's `handlers.ts`) carry no managed markers -
@@ -45,18 +73,24 @@ async function writeAll(files, io) {
45
73
  // HANDLERS-DESIGN.md (the round-trip contract).
46
74
  if (skipUserOwned(file, existing != null))
47
75
  continue;
76
+ if (existing != null && !studioApp && existing !== file.contents && !existing.includes(MANAGED_START)) {
77
+ const parkedAs = await freeNewPath(file.path, file.contents, io);
78
+ await io.writeFile(parkedAs, file.contents);
79
+ conflicts.push({ path: file.path, parkedAs });
80
+ continue;
81
+ }
48
82
  // Regenerating? Replace only the managed region, keep the user's edits.
49
83
  await io.writeFile(file.path, mergeManaged(existing, file.contents));
50
84
  written.push(file.path);
51
85
  }
52
- return written;
86
+ return { written, conflicts };
53
87
  }
54
88
  export async function runStudioAdd(options, io) {
55
89
  const schema = await resolveSchema(options, io);
56
90
  const { files } = scaffold(schema, options);
57
- const written = await writeAll(files, io);
91
+ const { written, conflicts } = await writeAll(files, io);
58
92
  const verify = await verifyScaffold(files);
59
- return { schema, written, verify };
93
+ return { schema, written, conflicts, verify };
60
94
  }
61
95
  /** The stylesheets an `init`-generated layout imports, in cascade order. */
62
96
  const APP_STYLESHEETS = ['app.css', 'custom.css'];
@@ -118,7 +152,7 @@ async function appShellDefaults(options, io) {
118
152
  */
119
153
  export async function runStudioAddApp(schemas, options, io) {
120
154
  const { files } = scaffoldApp(schemas, await appShellDefaults(options, io));
121
- const written = await writeAll(files, io);
155
+ const { written, conflicts } = await writeAll(files, io);
122
156
  const verify = await verifyScaffold(files);
123
- return { written, verify };
157
+ return { written, conflicts, verify };
124
158
  }
@@ -65,7 +65,9 @@ export declare function studioDeployInfo(project: StudioProject): {
65
65
  /** The runtime npm deps the generated source imports, keyed by package -> range.
66
66
  * One source of truth for package.json AND the fragment-mode dependency report. */
67
67
  export declare function runtimeDeps(project: StudioProject, allSource: string): Record<string, string>;
68
- export declare function emitStudioAppBundle(project: StudioProject): GeneratedFile[];
68
+ export declare function emitStudioAppBundle(project: StudioProject, opts?: {
69
+ databaseUrl?: string;
70
+ }): GeneratedFile[];
69
71
  /**
70
72
  * Emit ONLY the app content (`src/routes` + `src/lib` + `db`) to drop into an
71
73
  * EXISTING SvelteKit app - not a whole new app. Everything emitStudioProject
@@ -38,20 +38,31 @@ function columnFormatExpr(fmt) {
38
38
  }
39
39
  }
40
40
  /** The visible grid columns: configured order, per-column header/width/align overrides, editability per mode. */
41
- function gridColumnsExpr(schemaVar, block, typeName) {
41
+ function gridColumnsExpr(schemaVar, block, typeName, relDisplay) {
42
42
  if (block.config.kind !== 'grid')
43
43
  return `schemaToColumns(${schemaVar})`;
44
44
  const cfg = block.config;
45
45
  const idSafe = block.id.replace(/-/g, '_');
46
- const visible = cfg.columns.filter((c) => c.show);
46
+ const shown = cfg.columns.filter((c) => c.show);
47
+ // The prepared schema hides a relation's FK from the grid and shows its label
48
+ // column instead (`customer_id` -> `customer`), so a configured FK column maps
49
+ // to the label column - unless that one is configured too.
50
+ const shownFields = new Set(shown.map((c) => c.field));
51
+ const seen = new Set();
52
+ const visible = shown
53
+ .map((c) => {
54
+ const d = relDisplay?.get(c.field);
55
+ return { c, col: d && !shownFields.has(d) ? d : c.field };
56
+ })
57
+ .filter((v) => !seen.has(v.col) && !!seen.add(v.col));
47
58
  // Only inline editing keeps cells editable; form / read-only grids are not editable in place.
48
59
  const editablePart = cfg.editing === 'inline' ? '' : ', editable: false';
49
60
  if (visible.length === 0) {
50
61
  return editablePart ? `schemaToColumns(${schemaVar}).map((c) => ({ ...c${editablePart} }))` : `schemaToColumns(${schemaVar})`;
51
62
  }
52
- const order = visible.map((c) => `'${c.field}'`);
63
+ const order = visible.map((v) => `'${v.col}'`);
53
64
  const ovEntries = visible
54
- .map((c) => {
65
+ .map(({ c, col }) => {
55
66
  const parts = [];
56
67
  if (c.header)
57
68
  parts.push(`header: ${JSON.stringify(c.header)}`);
@@ -70,7 +81,7 @@ function gridColumnsExpr(schemaVar, block, typeName) {
70
81
  parts.push(`cell: (ctx: CellContext<${typeName}>) => renderSnippet(${cellSnippetName(idSafe, c.field)}, { value: ctx.getValue() })`);
71
82
  else if (c.format)
72
83
  parts.push(`format: ${columnFormatExpr(c.format)}`);
73
- return parts.length ? `'${c.field}': { ${parts.join(', ')} }` : null;
84
+ return parts.length ? `'${col}': { ${parts.join(', ')} }` : null;
74
85
  })
75
86
  .filter((x) => !!x);
76
87
  const ovDecl = ovEntries.length ? ` const ov: Record<string, Partial<(typeof all)[number]>> = { ${ovEntries.join(', ')} };` : '';
@@ -2151,7 +2162,7 @@ function screenPage(schema, rawSchema, screen, resolve, rawResolve, accessEnable
2151
2162
  for (const b of blocks) {
2152
2163
  if (b.config.kind === 'grid') {
2153
2164
  const idSafe = b.id.replace(/-/g, '_');
2154
- const colExpr = gridColumnsExpr(n.schemaVar, b, n.type);
2165
+ const colExpr = gridColumnsExpr(n.schemaVar, b, n.type, relationDisplayFields(rawSchema, resolve));
2155
2166
  let colValue = i18nEnabled ? `localizeCols(${colExpr}, ${JSON.stringify(schema.name)}, $t)` : colExpr;
2156
2167
  // An "edit" action needs the form; drop it if this grid has no edit modal.
2157
2168
  const actions = (b.config.rowActions ?? []).filter((a) => a.kind !== 'edit' || wantsForm);
@@ -2532,7 +2543,7 @@ function attrs(f) {
2532
2543
  return out.length ? ' ' + out.join(' ') : '';
2533
2544
  }
2534
2545
  /** The `+page.server.ts` + SSR `+page.svelte` for a single-grid CRUD screen. */
2535
- function emitSsrGridScreen(schema, screen, routed, accessEnabled, screenIds, byName) {
2546
+ function emitSsrGridScreen(schema, screen, routed, accessEnabled, screenIds, byName, rawSchema = schema) {
2536
2547
  const n = namesFor(schema);
2537
2548
  // Routed (SQL, or in-memory rows held on the server): a same-origin client over
2538
2549
  // the entity's /api route. Otherwise REST, whose source is imported from
@@ -2561,24 +2572,35 @@ function emitSsrGridScreen(schema, screen, routed, accessEnabled, screenIds, byN
2561
2572
  const fieldTypesLit = `{ ${formFields.map((f) => `${jsStr(f.field)}: ${jsStr(normType(f.type))}`).join(', ')} }`;
2562
2573
  // Relation fields render as a native <select> whose options are prefetched in
2563
2574
  // load - from the related entity's /api route when it has one, else from its
2564
- // in-process source in $lib/data (see relSource below).
2575
+ // in-process source in $lib/data (see relSourceOf below).
2565
2576
  const isRel = (f) => f.type === 'relation' && !!f.relation && byName.has(f.relation.entity);
2577
+ const relSourceOf = (rs) => routed(rs.name)
2578
+ ? `createKitDataSource<Record<string, unknown>>({ endpoint: ${jsStr('/api/' + namesFor(rs).route)}, fetch })`
2579
+ : namesFor(rs).sourceVar;
2580
+ // The grid shows each relation as its label column (`customer_id` -> `customer`).
2581
+ // The $lib/data sources fill it with withRelationLabels; rows read through the
2582
+ // /api route come back without it, so load wraps that client the same way:
2583
+ // one batched `id IN (...)` lookup per relation for the ids on this page only.
2584
+ // Names come from the RAW schema, as everywhere else that has to match
2585
+ // withRelationLabels.
2586
+ const relDisplay = relationDisplayFields(rawSchema, (name) => byName.get(name));
2587
+ const labelFields = isSql ? schema.fields.filter((f) => isRel(f) && schema.fields.some((x) => x.field === relDisplay.get(f.field))) : [];
2566
2588
  const relFields = formFields.filter(isRel);
2567
2589
  const relRouted = relFields.some((f) => routed(relSchemaOf(f).name));
2568
- const relSourceVars = [...new Set(relFields.filter((f) => !routed(relSchemaOf(f).name)).map((f) => namesFor(relSchemaOf(f)).sourceVar))];
2590
+ const relSourceVars = [...new Set([...relFields, ...labelFields].filter((f) => !routed(relSchemaOf(f).name)).map((f) => namesFor(relSchemaOf(f)).sourceVar))];
2569
2591
  const relPrefetch = relFields
2570
2592
  .map((f) => {
2571
2593
  const rs = relSchemaOf(f);
2572
- const rel = namesFor(rs);
2573
2594
  const relId = relIdOf(rs);
2574
2595
  const labelF = f.relation.labelField ?? relId;
2575
- const relSource = routed(rs.name)
2576
- ? `createKitDataSource<Record<string, unknown>>({ endpoint: ${jsStr('/api/' + rel.route)}, fetch })`
2577
- : rel.sourceVar;
2578
- return ` const ${f.field}Options = (await ${relSource}.getRows({ startRow: 0, endRow: 100, pageIndex: 0, pageSize: 100, sortModel: [], filterModel: {} })).rows.map((r: Record<string, unknown>) => ({ value: String(r[${jsStr(relId)}] ?? ''), label: String(r[${jsStr(labelF)}] ?? '') }))`;
2596
+ return ` const ${f.field}Options = (await ${relSourceOf(rs)}.getRows({ startRow: 0, endRow: 100, pageIndex: 0, pageSize: 100, sortModel: [], filterModel: {} })).rows.map((r: Record<string, unknown>) => ({ value: String(r[${jsStr(relId)}] ?? ''), label: String(r[${jsStr(labelF)}] ?? '') }))`;
2579
2597
  })
2580
2598
  .join('\n');
2581
2599
  const relReturn = relFields.map((f) => `, ${f.field}Options`).join('');
2600
+ const labelSpecs = labelFields.map((f) => {
2601
+ const rs = relSchemaOf(f);
2602
+ return `{ fk: ${jsStr(f.field)}, as: ${jsStr(relDisplay.get(f.field))}, lookup: createRelationLookup({ source: ${relSourceOf(rs)}, valueField: ${jsStr(relIdOf(rs))}, labelField: ${jsStr(f.relation.labelField ?? relIdOf(rs))} }) }`;
2603
+ });
2582
2604
  // Source acquisition:
2583
2605
  // - routed: build a same-origin client over the entity's /api/<entity> route
2584
2606
  // with SvelteKit's event.fetch, so validation / RBAC / triggers / audit stay
@@ -2596,6 +2618,7 @@ function emitSsrGridScreen(schema, screen, routed, accessEnabled, screenIds, byN
2596
2618
  'validateAll',
2597
2619
  ...(conditional ? ['stripHiddenValues'] : []),
2598
2620
  ...(isSql || relRouted ? ['createKitDataSource'] : []),
2621
+ ...(labelSpecs.length ? ['createRelationLookup', 'withRelationLabels'] : []),
2599
2622
  ].join(', ');
2600
2623
  const dataImports = [...new Set([...(isSql ? [] : [n.sourceVar]), ...relSourceVars])];
2601
2624
  const sourceImport = dataImports.length ? `import { ${dataImports.join(', ')} } from '$lib/data'\n` : '';
@@ -2637,7 +2660,7 @@ function formToValues(fd: FormData): Record<string, unknown> {
2637
2660
 
2638
2661
  export const load: PageServerLoad = async ({ url${loadFetchArg}${localsArg} }) => {
2639
2662
  ${readGuard} const plan = planFromSearchParams(url, ${pageSize})
2640
- const { rows, rowCount } = await ${src}.getRows(plan)
2663
+ const { rows, rowCount } = await ${labelSpecs.length ? `withRelationLabels(${src}, [\n ${labelSpecs.join(',\n ')},\n ])` : src}.getRows(plan)
2641
2664
  ${relPrefetch ? relPrefetch + '\n' : ''} return { rows, total: rowCount, page: plan.pageIndex, size: plan.pageSize, sort: plan.sortModel${relReturn} }
2642
2665
  }
2643
2666
 
@@ -3050,7 +3073,7 @@ export function emitStudioProject(project) {
3050
3073
  // load-only server file, and the SAME page markup renders from data.*.
3051
3074
  if (isSsrScreen(project, screen)) {
3052
3075
  if (ssrScreenShape(project, screen) === 'grid') {
3053
- pages.push(...emitSsrGridScreen(schema, screen, routed, accessEnabled, screensByEntity.get(screen.entity) ?? [], byName));
3076
+ pages.push(...emitSsrGridScreen(schema, screen, routed, accessEnabled, screensByEntity.get(screen.entity) ?? [], byName, rawByName.get(screen.entity) ?? schema));
3054
3077
  }
3055
3078
  else {
3056
3079
  pages.push(ssrReadServerFile(schema, screen, routed, accessEnabled, screensByEntity.get(screen.entity) ?? [], resolve), screenPage(schema, rawByName.get(screen.entity) ?? schema, screen, resolve, (name) => rawByName.get(name), accessEnabled, i18nEnabled, routeById, drillEnabled, true));
@@ -5005,7 +5028,7 @@ jobs:
5005
5028
  - uses: actions/checkout@v4
5006
5029
  - uses: actions/setup-node@v4
5007
5030
  with:
5008
- node-version: 20
5031
+ node-version: 22
5009
5032
  - run: npm install
5010
5033
  - run: npm run build
5011
5034
  - run: npm test
@@ -5024,7 +5047,7 @@ jobs:
5024
5047
  - uses: actions/checkout@v4
5025
5048
  - uses: actions/setup-node@v4
5026
5049
  with:
5027
- node-version: 20
5050
+ node-version: 22
5028
5051
  - run: npm install
5029
5052
  ${steps}`;
5030
5053
  }
@@ -5097,7 +5120,7 @@ function deployPlan(project) {
5097
5120
  adapterModule: '@sveltejs/adapter-node',
5098
5121
  adapterDep: ['@sveltejs/adapter-node', '^5.2.0'],
5099
5122
  files: [
5100
- { path: 'Dockerfile', description: 'Container image (multi-stage: build then run `node build`).', contents: `# Build stage\nFROM node:20-alpine AS build\nWORKDIR /app\nCOPY package*.json ./\nRUN npm install\nCOPY . .\nRUN npm run build\nRUN npm prune --omit=dev\n\n# Run stage\nFROM node:20-alpine\nWORKDIR /app\nENV NODE_ENV=production\nCOPY --from=build /app/build ./build\nCOPY --from=build /app/node_modules ./node_modules\nCOPY --from=build /app/package.json ./package.json\nEXPOSE 3000\nCMD ["node", "build"]\n` },
5123
+ { path: 'Dockerfile', description: 'Container image (multi-stage: build then run `node build`).', contents: `# Build stage\nFROM node:22-alpine AS build\nWORKDIR /app\nCOPY package*.json ./\nRUN npm install\nCOPY . .\nRUN npm run build\nRUN npm prune --omit=dev\n\n# Run stage\nFROM node:22-alpine\nWORKDIR /app\nENV NODE_ENV=production\nCOPY --from=build /app/build ./build\nCOPY --from=build /app/node_modules ./node_modules\nCOPY --from=build /app/package.json ./package.json\nEXPOSE 3000\nCMD ["node", "build"]\n` },
5101
5124
  { path: '.dockerignore', description: 'Files kept out of the image.', contents: `node_modules\n.svelte-kit\nbuild\n.env\n.env.*\n.git\n.github\n` },
5102
5125
  ],
5103
5126
  cli: 'npm run build && node build',
@@ -5126,7 +5149,7 @@ export function studioDeployInfo(project) {
5126
5149
  /** DEPLOY.md - the runbook: required env vars, then Git-integration / CI / CLI paths. */
5127
5150
  function deployDocs(project, plan, envKeys, hasMigrations, opts = {}) {
5128
5151
  const envBlock = envKeys.length
5129
- ? `## Environment variables\n\nSet these on your host (and locally in \`.env\` - copy \`.env.example\`):\n\n${envKeys.map((k) => `- \`${k}\``).join('\n')}\n\nOptional: \`VITE_SVPRO_KEY\` (your SvGrid license key) removes the unlicensed watermark - call \`setLicenseKey(import.meta.env.VITE_SVPRO_KEY)\` once at startup (see the docs).\n\n`
5152
+ ? `## Environment variables\n\nSet these on your host (and locally in \`.env\` - copy \`.env.example\`):\n\n${envKeys.map((k) => `- \`${k}\``).join('\n')}\n\nOptional: \`VITE_SVPRO_KEY\` (your SvGrid license key) removes the unlicensed watermark - the root layout passes it to \`setLicenseKey\`. It is read at build time, so set it before \`npm run build\`.\n\n`
5130
5153
  : '';
5131
5154
  const migrations = hasMigrations
5132
5155
  ? `## Database migrations\n\nThis app has a typed Drizzle schema (\`src/lib/server/db/schema.ts\`). Create + apply the tables:\n\n\`\`\`bash\nnpm run db:generate # SQL migrations from the schema\nnpm run db:migrate # apply them to DATABASE_URL\n${opts.hasSeedScript ? 'npm run db:seed # optional: insert the sample rows\n' : ''}\`\`\`\n\n(Run these against your production database as part of your release step.)\n\n`
@@ -5134,16 +5157,22 @@ function deployDocs(project, plan, envKeys, hasMigrations, opts = {}) {
5134
5157
  const ddl = opts.hasDdl
5135
5158
  ? `## Database tables\n\nThis app's SQL entities expect their tables to exist. Run \`db/schema.sql\` against the database your \`DATABASE_URL\` points at (psql / mysql / sqlite3 / sqlcmd, or your provider's SQL console) before first use - and optionally \`db/seed.sql\` for sample rows. Tip: enable the Drizzle data layer in Studio for real, versioned migrations instead.\n\n`
5136
5159
  : '';
5160
+ // Letter the options that exist, so a target without a dashboard or a deploy
5161
+ // workflow does not skip from A to C.
5162
+ const letters = ['A', 'B', 'C'];
5163
+ const gitLetter = plan.dashboard ? letters.shift() : '';
5164
+ const ciLetter = plan.deployWorkflow ? letters.shift() : '';
5165
+ const cliLetter = letters.shift();
5137
5166
  const gitIntegration = plan.dashboard
5138
- ? `## Option A - Git integration (simplest, no secrets)\n\n1. Push this repo to GitHub.\n2. Import it at <${plan.dashboard}> - the SvelteKit build is auto-detected.\n3. Add the environment variables above in the host's dashboard.\n\nEvery push to \`main\` then redeploys automatically.\n\n`
5167
+ ? `## Option ${gitLetter} - Git integration (simplest, no CI secrets)\n\n1. Push this repo to GitHub.\n2. Import it at <${plan.dashboard}> - the SvelteKit build is auto-detected.\n${envKeys.length ? "3. Add the environment variables above in the host's dashboard.\n" : ''}\nEvery push to \`main\` then redeploys automatically.\n\n`
5139
5168
  : '';
5140
5169
  const ci = plan.deployWorkflow
5141
- ? `## Option B - GitHub Actions (included)\n\n\`.github/workflows/deploy.yml\` deploys on every push to \`main\`. Add these repository secrets to enable it (Settings -> Secrets and variables -> Actions):\n\n${plan.secrets.map((s) => `- \`${s}\``).join('\n')}\n\nUntil the secrets are set the deploy step is skipped, so CI stays green. Prefer Option A? Delete \`deploy.yml\`.\n\n`
5170
+ ? `## Option ${ciLetter} - GitHub Actions (included)\n\n\`.github/workflows/deploy.yml\` deploys on every push to \`main\`. Add these repository secrets to enable it (Settings -> Secrets and variables -> Actions):\n\n${plan.secrets.map((s) => `- \`${s}\``).join('\n')}\n\nUntil the secrets are set the deploy step is skipped, so CI stays green.${gitLetter ? ` Prefer Option ${gitLetter}? Delete \`deploy.yml\`.` : ''}\n\n`
5142
5171
  : '';
5143
5172
  const dockerNote = project.deploy === 'node'
5144
5173
  ? `## Container\n\nA \`Dockerfile\` is included:\n\n\`\`\`bash\ndocker build -t ${appSlug(project.title)} .\ndocker run -p 3000:3000 --env-file .env ${appSlug(project.title)}\n\`\`\`\n\nThe server listens on \`PORT\` (default 3000).\n\n`
5145
5174
  : '';
5146
- const cli = `## Option C - Deploy from your machine\n\n\`\`\`bash\nnpm run deploy\n\`\`\`\n\n(runs \`${plan.deployScript}\`)\n`;
5175
+ const cli = `## Option ${cliLetter} - Deploy from your machine\n\n\`\`\`bash\nnpm run deploy\n\`\`\`\n\n(runs \`${plan.deployScript}\`)\n`;
5147
5176
  return `# Deploying ${project.title}\n\nConfigured for **${plan.label}** (SvelteKit \`${plan.adapterModule}\`). A CI workflow (\`.github/workflows/ci.yml\`) builds + tests every push.\n\n${envBlock}${ddl}${migrations}${gitIntegration}${ci}${dockerNote}${cli}`;
5148
5177
  }
5149
5178
  /** The env-var keys the generated app reads (for DEPLOY.md). */
@@ -5190,15 +5219,23 @@ function supabaseConnOf(project) {
5190
5219
  * random `SESSION_SECRET` filled in, so the app is secure out of the box (no
5191
5220
  * hardcoded dev secret), and the Supabase URL + anon key the model already
5192
5221
  * holds, so a Supabase app runs as generated instead of failing on an empty
5193
- * URL. Only emitted when there is something to fill in. */
5194
- function envDotFile(allSource, project) {
5222
+ * URL. Only emitted when there is something to fill in.
5223
+ *
5224
+ * `databaseUrl` is the connection the app was built from (`studio init --db
5225
+ * sqlite --url ./shop.db`). Only a SQLite file path is written: it is not a
5226
+ * secret, and without it the app opens an empty data.db. Server URLs carry a
5227
+ * password, so those stay for the user to paste. */
5228
+ function envDotFile(allSource, project, databaseUrl) {
5195
5229
  const example = envExample(allSource);
5196
5230
  if (!example)
5197
5231
  return null;
5198
5232
  const supa = allSource.includes('PUBLIC_SUPABASE_URL') ? supabaseConnOf(project) : null;
5199
- if (!allSource.includes('SESSION_SECRET') && !supa)
5233
+ const sqliteFile = databaseUrl && allSource.includes("import Database from 'better-sqlite3'") && /^DATABASE_URL=$/m.test(example) ? databaseUrl : null;
5234
+ if (!allSource.includes('SESSION_SECRET') && !supa && !sqliteFile)
5200
5235
  return null;
5201
5236
  let env = example.replace('SESSION_SECRET=change-me-to-a-long-random-string', `SESSION_SECRET=${randomSecret()}`);
5237
+ if (sqliteFile)
5238
+ env = env.replace(/^DATABASE_URL=$/m, `DATABASE_URL=${sqliteFile}`);
5202
5239
  // The anon key is public / browser-safe; access is governed by RLS.
5203
5240
  if (supa)
5204
5241
  env = env.replace(/^PUBLIC_SUPABASE_URL=$/m, `PUBLIC_SUPABASE_URL=${supa.url}`).replace(/^PUBLIC_SUPABASE_ANON_KEY=$/m, `PUBLIC_SUPABASE_ANON_KEY=${supa.key}`);
@@ -5296,8 +5333,11 @@ export function runtimeDeps(project, allSource) {
5296
5333
  dependencies['mysql2'] = '^3.9.0';
5297
5334
  if (allSource.includes("import mssql from 'mssql'"))
5298
5335
  dependencies['mssql'] = '^10.0.0';
5336
+ // 11.x and 12.0.0 ship no Node 24 prebuild, so npm falls back to node-gyp and
5337
+ // the install fails without a C++ toolchain. 12.1+ has one; 13.x bundles N-API
5338
+ // prebuilds that load on any Node >= 22.
5299
5339
  if (allSource.includes("import Database from 'better-sqlite3'"))
5300
- dependencies['better-sqlite3'] = '^11.0.0';
5340
+ dependencies['better-sqlite3'] = BETTER_SQLITE3_RANGE;
5301
5341
  if (allSource.includes("from '@libsql/client'"))
5302
5342
  dependencies['@libsql/client'] = '^0.14.0';
5303
5343
  if (allSource.includes("from '@electric-sql/pglite'"))
@@ -5415,12 +5455,20 @@ import { ${imports.join(', ')} } from './schemas'
5415
5455
  ${blocks.join('\n\n')}
5416
5456
  `;
5417
5457
  }
5458
+ /** The better-sqlite3 range a generated app installs. Mirrored in the studio
5459
+ * package's designer-server.ts RUNTIME_DEPS. */
5460
+ const BETTER_SQLITE3_RANGE = '^12.1.0 || ^13.0.0';
5418
5461
  const VITEST_CONFIG = `import { defineConfig } from 'vitest/config'
5462
+ import { svelte } from '@sveltejs/vite-plugin-svelte'
5419
5463
 
5420
- // Node-only test runner for the generated smoke tests (no Svelte/DOM needed).
5421
- // Kept separate from vite.config so the SvelteKit plugin doesn't load here.
5464
+ // Test runner for the generated smoke tests, in Node (no DOM). Kept separate from
5465
+ // vite.config so the SvelteKit plugin doesn't load here. The tests import the
5466
+ // @svgrid/enterprise root, which re-exports .svelte components: the svelte plugin
5467
+ // compiles them, and inlining @svgrid/* routes them through it instead of Node's
5468
+ // own loader (which rejects the .svelte extension).
5422
5469
  export default defineConfig({
5423
- test: { environment: 'node', include: ['src/**/*.test.ts'] },
5470
+ plugins: [svelte()],
5471
+ test: { environment: 'node', include: ['src/**/*.test.ts'], server: { deps: { inline: [/@svgrid\\//] } } },
5424
5472
  })
5425
5473
  `;
5426
5474
  /**
@@ -5486,14 +5534,17 @@ jobs:
5486
5534
  ${steps}`,
5487
5535
  }];
5488
5536
  }
5489
- export function emitStudioAppBundle(project) {
5537
+ export function emitStudioAppBundle(project, opts = {}) {
5490
5538
  const generated = emitStudioProject(project);
5491
5539
  const allSource = generated.map((f) => f.contents).join('\n');
5540
+ const dotEnv = envDotFile(allSource, project, opts.databaseUrl);
5541
+ const hasConnections = generated.some((f) => f.path === 'src/lib/connections.ts');
5542
+ const readmeConnections = `SQL entities read their connection from \`.env\` (\`DATABASE_URL\`)${hasConnections ? ';\nSupabase entities use the shared client in `src/lib/connections.ts`, which reads\n`PUBLIC_SUPABASE_URL` and `PUBLIC_SUPABASE_ANON_KEY` from `.env`' : ''}.\n`;
5492
5543
  const plan = deployPlan(project);
5493
5544
  const deploySteps = plan.dashboard
5494
5545
  ? `1. Push this folder to a Git repo (GitHub / GitLab / Bitbucket).\n2. Import it at <${plan.dashboard}> - build settings are detected automatically.\n\nOr deploy straight from your machine with the CLI:\n\n\`\`\`bash\n${plan.cli}\n\`\`\``
5495
5546
  : `Build and run the server:\n\n\`\`\`bash\n${plan.cli}\n\`\`\``;
5496
- const readme = `# ${project.title}\n\nGenerated with SvGrid Studio.\n\n\`\`\`bash\nnpm install\nnpm run dev\n\`\`\`\n\nSQL / Supabase entities read their connection from \`.env\` (\`DATABASE_URL\`) or\n\`src/lib/connections.ts\`. Everything else runs on seeded in-memory data.\n\n## Deploy (${plan.label})\n\nThis app is configured for **${plan.label}** (SvelteKit \`${plan.adapterModule}\`).\n\n${deploySteps}\n\nTo target a different host, pick another **Deploy target** in the designer and\nre-generate, or swap the adapter in \`svelte.config.js\`. Entities on **Local\ndatabase** (PGlite) or **In-memory** need no server env; SQL / Supabase entities\nneed their connection set in the host's environment variables.\n\n## Round-tripping back into the designer\n\nThis app ships its own design model in \`studio.config.json\`. To keep editing\nvisually, open the SvGrid Studio designer and **Load** that file - your entities,\nscreens, blocks, theme, RBAC, i18n, etc. come back exactly as generated, and you\ncan re-generate from there.\n\nThe designer regenerates the files under \`src/routes\` and \`src/lib\` from the\nmodel, so **keep your own custom code in new files/modules and import it**, rather\nthan editing the generated screens in place - that way a re-generate never\nclobbers your work. (The CLI workflow, \`npx @svgrid/studio add\`, is the\nalternative: it wraps generated code in \`svgrid:managed\` markers and preserves\nanything you write outside them.)\n`;
5547
+ const readme = `# ${project.title}\n\nGenerated with SvGrid Studio.\n\n\`\`\`bash\nnpm install\nnpm run dev\n\`\`\`\n\n${readmeConnections}Everything else runs on seeded in-memory data.\n\n## Deploy (${plan.label})\n\nThis app is configured for **${plan.label}** (SvelteKit \`${plan.adapterModule}\`).\n\n${deploySteps}\n\nTo target a different host, pick another **Deploy target** in the designer and\nre-generate, or swap the adapter in \`svelte.config.js\`. Entities on **Local\ndatabase** (PGlite) or **In-memory** need no server env; SQL / Supabase entities\nneed their connection set in the host's environment variables.\n\n## Round-tripping back into the designer\n\nThis app ships its own design model in \`studio.config.json\`. To keep editing\nvisually, open the SvGrid Studio designer and **Load** that file - your entities,\nscreens, blocks, theme, RBAC, i18n, etc. come back exactly as generated, and you\ncan re-generate from there.\n\nThe designer regenerates the files under \`src/routes\` and \`src/lib\` from the\nmodel, so **keep your own custom code in new files/modules and import it**, rather\nthan editing the generated screens in place - that way a re-generate never\nclobbers your work. (The CLI workflow, \`npx @svgrid/studio add\`, is the\nalternative: it wraps generated code in \`svgrid:managed\` markers and preserves\nanything you write outside them.)\n`;
5497
5548
  const scaffold = [
5498
5549
  { path: 'package.json', description: 'Dependencies + scripts (npm install, npm run dev).', contents: packageJson(project, allSource) },
5499
5550
  { path: 'svelte.config.js', description: `SvelteKit config (${plan.adapterModule}).`, contents: svelteConfig(plan) },
@@ -5504,7 +5555,7 @@ export function emitStudioAppBundle(project) {
5504
5555
  rootLayoutModule(project),
5505
5556
  ...SCAFFOLD_STATIC,
5506
5557
  ...(envExample(allSource) ? [{ path: '.env.example', description: 'Environment variables the app reads (copy to .env and fill in).', contents: envExample(allSource) }] : []),
5507
- ...(envDotFile(allSource, project) ? [{ path: '.env', description: 'Local env (git-ignored): a real random SESSION_SECRET and the Supabase connection the design holds are pre-filled, so the app runs as generated; fill in the rest.', contents: envDotFile(allSource, project) }] : []),
5558
+ ...(dotEnv ? [{ path: '.env', description: 'Local env (git-ignored): a real random SESSION_SECRET, the Supabase connection the design holds and a SQLite file path are pre-filled, so the app runs as generated; fill in the rest.', contents: dotEnv }] : []),
5508
5559
  { path: 'src/app.css', description: 'App theme tokens + page styles.', contents: appCss(project) },
5509
5560
  // Your styles: a dedicated CSS file the layout imports AFTER app.css, so its rules
5510
5561
  // win. Edited in the designer's Styles panel; regenerated from the model.
@@ -986,7 +986,11 @@ export function layoutFile(nav, opts = {}) {
986
986
  contents: `<script lang="ts">
987
987
  import '../app.css'
988
988
  import '../custom.css'
989
- import { page } from '$app/state'${opts.access ? `\n import { currentRole, canScreen } from '../lib/access'` : ''}${opts.i18n ? `\n import { t, currentLocale, locales } from '../lib/i18n'` : ''}${opts.auth ? `\n import type { LayoutData } from './$types'` : ''}${opts.supabaseAuth ? `\n import { SvAuthGate, type SupabaseAuthClientLike } from '@svgrid/enterprise'\n import { supabaseClient } from '../lib/connections'` : ''}
989
+ import { page } from '$app/state'
990
+ import { setLicenseKey } from '@svgrid/enterprise'${opts.access ? `\n import { currentRole, canScreen } from '../lib/access'` : ''}${opts.i18n ? `\n import { t, currentLocale, locales } from '../lib/i18n'` : ''}${opts.auth ? `\n import type { LayoutData } from './$types'` : ''}${opts.supabaseAuth ? `\n import { SvAuthGate, type SupabaseAuthClientLike } from '@svgrid/enterprise'\n import { supabaseClient } from '../lib/connections'` : ''}
991
+
992
+ // VITE_SVPRO_KEY in .env is your SvGrid license key; it removes the unlicensed watermark.
993
+ if (import.meta.env.VITE_SVPRO_KEY) setLicenseKey(import.meta.env.VITE_SVPRO_KEY)
990
994
 
991
995
  let { children${opts.auth ? ', data' : ''} }${opts.auth ? ": { children: import('svelte').Snippet; data: LayoutData }" : ''} = $props()
992
996
  const nav = ${JSON.stringify(links)}
@@ -1201,5 +1205,8 @@ export function emitStudioApp(schemas) {
1201
1205
  const n = namesFor(s);
1202
1206
  return { href: `/${n.route}`, label: n.label };
1203
1207
  });
1204
- return [...files, ...prepared.map((s) => entityScreenPage(s)), layoutFile(nav), homeFile(nav)];
1208
+ // The layout imports ../custom.css after app.css; emitStudioAppBundle writes it
1209
+ // too, so an app built here needs its own copy or the build cannot resolve it.
1210
+ const customCss = { path: 'src/custom.css', description: 'Your custom styles (imported after app.css so it overrides).', userOwned: true, contents: '/* Your custom styles. Imported after app.css, so these rules override the defaults. */\n' };
1211
+ return [...files, ...prepared.map((s) => entityScreenPage(s)), layoutFile(nav), homeFile(nav), customCss];
1205
1212
  }
@@ -31,6 +31,6 @@ export { verifyScaffold, summarizeVerify, type VerifyResult, type VerifyIssue, t
31
31
  export { UI_COMPONENT_REGISTRY, uiComponentSpec, gridPropSurface, gridApiSettableProps, gridIconGroups, GRID_CURATED_PROPS, STANDARD_UI_EVENTS, type UiPropType, type UiPropGroup, type UiComponentEvent, type UiComponentProp, type UiComponentSpec, } from './ui-components.js';
32
32
  export { buildCopilotMessages, projectFromModelText, type CopilotMessages } from './copilot-core.js';
33
33
  export { resolveDeployTarget, deployCommands, missingEnvKeys, type DeployProvider, type DeployResolution, type DeployPlanCommands, } from './deploy-cli.js';
34
- export { runStudioAdd, runStudioAddApp, resolveSchema, resolveSchemas, type StudioIO, type AddOptions, type AddResult, type AddAppResult, } from './cli.js';
34
+ export { runStudioAdd, runStudioAddApp, resolveSchema, resolveSchemas, type StudioIO, type AddOptions, type AddResult, type AddAppResult, type ParkedFile, } from './cli.js';
35
35
  export { resolveIdField, schemaToColumns, schemaToFormFields, isFieldHidden, linkRelationLabels, pickLabelField, type EntitySchema, type EntityField, type EntityFieldType, } from '../schema.js';
36
36
  export { checkLicenseKey, type LicenseInfo, type LicenseStatus } from '../license-core.js';
@@ -59,6 +59,13 @@ export type InitResult = {
59
59
  };
60
60
  /** Parse "all", "none", a comma list of names, or a list of 1-based numbers. */
61
61
  export declare function parseSelection(answer: string, items: string[]): string[];
62
+ /**
63
+ * A file path given relative to where init ran, re-expressed relative to the
64
+ * app folder, which is where the app runs (`npm run dev` in `outDir`). Absolute
65
+ * paths pass through. An absolute or `..` out dir has no relative answer without
66
+ * the cwd, so the path is made absolute when the runtime has one.
67
+ */
68
+ export declare function fileFromApp(file: string, outDir: string): string;
62
69
  /**
63
70
  * The whole guided flow: ask, build, write. Returns the project and what was
64
71
  * written so the caller can print next steps (or assert them in a test).