@svgrid/enterprise 3.0.3 → 3.1.1

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 (53) hide show
  1. package/dist/SvSheet.svelte +87 -8
  2. package/dist/cdn/svgrid-enterprise.svelte-external.js +13269 -13209
  3. package/dist/designer/assets/{GridMenus-8gTNssgV.js → GridMenus-DGZJKFvX.js} +1 -1
  4. package/dist/designer/assets/SvGrid.helpers-DE5mmORW.js +1 -0
  5. package/dist/designer/assets/{SvGridCellEditor-DMoWJjEI.js → SvGridCellEditor-C7NWcnNC.js} +1 -1
  6. package/dist/designer/assets/{SvGridChart-Cy1ATrWh.js → SvGridChart-Fg43Yi0M.js} +2 -2
  7. package/dist/designer/assets/{SvGridChartBuilder-eQ3mn0L9.js → SvGridChartBuilder-BqGuA3yU.js} +1 -1
  8. package/dist/designer/assets/{SvGridChartPanel-CATdVc9X.js → SvGridChartPanel-BsiXRAnx.js} +3 -3
  9. package/dist/designer/assets/{SvGridChartView-CmsZtCED.js → SvGridChartView-BgttK0tR.js} +1 -1
  10. package/dist/designer/assets/{SvListBox-BDS66lmZ.js → SvListBox-v2jm0-vf.js} +1 -1
  11. package/dist/designer/assets/cell-formatting-BWrW_O0n.js +1 -0
  12. package/dist/designer/assets/{chart-Cn7MnNqA.js → chart-BIl-dIRv.js} +1 -1
  13. package/dist/designer/assets/{chart-panel-messages-Dz7PTIqT.js → chart-panel-messages-4cF2vw7L.js} +1 -1
  14. package/dist/designer/assets/{chart-summary-BD1qYB2R.js → chart-summary-B7bTtSCC.js} +1 -1
  15. package/dist/designer/assets/{chart-trend-B8xr5LuD.js → chart-trend-CLsn5Hgd.js} +1 -1
  16. package/dist/designer/assets/export-format-DGscOjS9.js +6 -0
  17. package/dist/designer/assets/{index-D5bVbuzI.js → index-DkhD2kIe.js} +314 -314
  18. package/dist/designer/assets/index-l21iGkpF.css +1 -0
  19. package/dist/designer/assets/src-Mu7AGqT8.js +58 -0
  20. package/dist/designer/index.html +12 -12
  21. package/dist/export-ooxml.d.ts +10 -0
  22. package/dist/export-ooxml.js +22 -5
  23. package/dist/import.js +11 -2
  24. package/dist/node/studio.js +221 -129
  25. package/dist/sheet/objects.d.ts +47 -0
  26. package/dist/sheet/objects.js +33 -0
  27. package/dist/studio/cli.js +55 -1
  28. package/dist/studio/init-flow.d.ts +6 -1
  29. package/dist/studio/init-flow.js +43 -2
  30. package/dist/studio/scaffold-app.d.ts +6 -0
  31. package/dist/studio/scaffold-app.js +5 -3
  32. package/dist/studio/scaffold.js +29 -12
  33. package/dist/version.d.ts +1 -1
  34. package/dist/version.js +1 -1
  35. package/dist/wc/{GridMenus-By9vPuq0.js → GridMenus-6_fF5jIm.js} +2 -2
  36. package/dist/wc/{SvGrid.helpers-BdVDtjUm.js → SvGrid.helpers-zBkDTryR.js} +36 -37
  37. package/dist/wc/{SvGridCellEditor-Bnf1Kr5C.js → SvGridCellEditor-CzUKEbkI.js} +1 -1
  38. package/dist/wc/{SvGridChart-BqUJh2pG.js → SvGridChart-C6UvNIbN.js} +3 -3
  39. package/dist/wc/{SvGridChartBuilder-CGycC5cn.js → SvGridChartBuilder-ikf_5LyW.js} +3 -3
  40. package/dist/wc/{SvGridChartPanel-CRnizsHD.js → SvGridChartPanel-Dc_jj7B-.js} +4 -4
  41. package/dist/wc/{SvGridChartView-D1pa_xMy.js → SvGridChartView-thkznAOb.js} +2 -2
  42. package/dist/wc/{cell-formatting-CBfgahv1.js → cell-formatting-hb3vzxhJ.js} +27 -5
  43. package/dist/wc/{chart-6OXlybVT.js → chart-CV5D7_k8.js} +1 -1
  44. package/dist/wc/{chart-summary-2m3ia1-n.js → chart-summary-DGx0QhJa.js} +1 -1
  45. package/dist/wc/{chart-trend-CQOQtTMl.js → chart-trend-BIRH_xH8.js} +1 -1
  46. package/dist/wc/sv-sheet-element.js +7444 -7347
  47. package/dist/wc/{svelte-virtualizer.svelte-CXtQy2TQ.js → svelte-virtualizer.svelte-Du4tjrIP.js} +7 -3
  48. package/package.json +3 -3
  49. package/dist/designer/assets/SvGrid.helpers-BgT-gnCo.js +0 -1
  50. package/dist/designer/assets/cell-formatting-BbubJTVz.js +0 -1
  51. package/dist/designer/assets/export-format-DGckmgKr.js +0 -6
  52. package/dist/designer/assets/index-_JVFxt02.css +0 -1
  53. package/dist/designer/assets/src-DUQ4xE3x.js +0 -58
@@ -1721,7 +1721,8 @@ ${managed$1(serverBody)}
1721
1721
  const pageBody = ` const source = createKitDataSource<${rowType}>({ endpoint: '${apiRoute}' })
1722
1722
  const columns = schemaToColumns(${constName})${hasLookups ? `\n\n${lookupDecls}\n${lookupsMap}` : ""}
1723
1723
 
1724
- let state = $state<ServerState<${rowType}>>({
1724
+ // Not "state": a local of that name makes svelte-check read $state as a store of it.
1725
+ let view = $state<ServerState<${rowType}>>({
1725
1726
  rows: [], total: 0, loading: false, saving: false, error: null,
1726
1727
  pageIndex: 0, pageSize: 25, pageCount: 1, sortModel: [], filterModel: {},
1727
1728
  })
@@ -1729,7 +1730,7 @@ ${managed$1(serverBody)}
1729
1730
  pageSize: 25,
1730
1731
  optimistic: true,
1731
1732
  getRowId: (r) => String(r.${idField}),
1732
- onChange: (s) => (state = s),
1733
+ onChange: (s) => (view = s),
1733
1734
  })
1734
1735
  controller.refresh()
1735
1736
 
@@ -1739,7 +1740,7 @@ ${managed$1(serverBody)}
1739
1740
  async function save({ mode, id, values }: { mode: 'create' | 'edit'; id: string | null; values: Partial<${rowType}> }) {
1740
1741
  if (mode === 'create') {
1741
1742
  await controller.createRow(values)
1742
- controller.setPage(state.pageCount - 1) // jump to the new row (appended at the end)
1743
+ controller.setPage(view.pageCount - 1) // jump to the new row (appended at the end)
1743
1744
  } else if (id) {
1744
1745
  await controller.updateRow(id, values)
1745
1746
  }
@@ -1757,12 +1758,11 @@ ${managed$1(serverBody)}
1757
1758
  path: `src/routes/${route}/+page.svelte`,
1758
1759
  description: "The data screen: sort, filter, search, paging, multi-select delete, and create/edit - wired to the API route.",
1759
1760
  contents: `<script lang="ts">
1760
- import { SvGrid, createServerDataSource } from '@svgrid/grid'
1761
+ import { SvGrid, createServerDataSource, type ServerState } from '@svgrid/grid'
1761
1762
  import {
1762
1763
  SvGridEditPanel,
1763
1764
  createKitDataSource,${hasLookups ? "\n createRelationLookup," : ""}
1764
1765
  schemaToColumns,
1765
- type ServerState,
1766
1766
  } from '@svgrid/enterprise'
1767
1767
  import { ${constName}, type ${rowType} } from '${schemaImport}'
1768
1768
 
@@ -1787,12 +1787,12 @@ ${themeTokenCss({
1787
1787
  </div>
1788
1788
  </header>
1789
1789
 
1790
- {#if state.error}<p class="page__error" role="alert">{state.error}</p>{/if}
1790
+ {#if view.error}<p class="page__error" role="alert">{view.error}</p>{/if}
1791
1791
 
1792
1792
  <SvGrid
1793
- data={state.rows}
1793
+ data={view.rows}
1794
1794
  {columns}
1795
- loading={state.loading}
1795
+ loading={view.loading}
1796
1796
  loadingOverlay
1797
1797
  fitColumns
1798
1798
  enableRowSummaries={false}
@@ -1815,11 +1815,11 @@ ${themeTokenCss({
1815
1815
  onRowSelectionChange={(_sel, rows) => (selected = rows)}
1816
1816
  showPagination
1817
1817
  externalPagination
1818
- rowCount={state.total}
1819
- pageIndex={state.pageIndex}
1820
- pageSize={state.pageSize}
1818
+ rowCount={view.total}
1819
+ pageIndex={view.pageIndex}
1820
+ pageSize={view.pageSize}
1821
1821
  onPaginationChange={({ pageIndex, pageSize }) => {
1822
- if (pageSize !== state.pageSize) controller.setPageSize(pageSize)
1822
+ if (pageSize !== view.pageSize) controller.setPageSize(pageSize)
1823
1823
  else controller.setPage(pageIndex)
1824
1824
  }}
1825
1825
  emptyMessage="No ${label} yet"
@@ -1851,6 +1851,7 @@ ${themeTokenCss({
1851
1851
  */
1852
1852
  function mergeManaged(existing, generated) {
1853
1853
  if (!existing) return generated;
1854
+ existing = migrateStateLocal(existing);
1854
1855
  const startIdx = existing.indexOf(MANAGED_START);
1855
1856
  const endIdx = existing.indexOf(MANAGED_END);
1856
1857
  if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) return generated;
@@ -1860,6 +1861,21 @@ function mergeManaged(existing, generated) {
1860
1861
  const newRegion = generated.slice(genStart, genEnd + 21);
1861
1862
  return existing.slice(0, startIdx) + newRegion + existing.slice(endIdx + 21);
1862
1863
  }
1864
+ /** The managed line pages scaffolded before the rename carried. */
1865
+ var OLD_STATE_LOCAL = /\blet state = \$state<ServerState</;
1866
+ /**
1867
+ * Pages scaffolded before the rename kept their server state in a local named
1868
+ * `state`, which svelte-check reads as a store subscription whenever `$state`
1869
+ * appears. The managed region now calls it `view`, but the markup below the
1870
+ * region is the user's and still says `state.rows`; a plain merge would leave it
1871
+ * pointing at nothing. Rename the references too. Nothing else in such a page
1872
+ * could be called `state` (the old local would have clashed), and member reads
1873
+ * like `page.state` are skipped.
1874
+ */
1875
+ function migrateStateLocal(existing) {
1876
+ if (!OLD_STATE_LOCAL.test(existing)) return existing;
1877
+ return existing.replace(/(?<![.\w$])state(?=\s*[.?[]|\s*=[^=>])/g, "view");
1878
+ }
1863
1879
  //#endregion
1864
1880
  //#region src/studio/scaffold-app.ts
1865
1881
  /**
@@ -1875,12 +1891,12 @@ function mergeManaged(existing, generated) {
1875
1891
  */
1876
1892
  var MANAGED_NOTE = "// Regenerated by SvGrid Studio. Edits inside these markers are overwritten.";
1877
1893
  var managed = (body) => `${MANAGED_START}\n${MANAGED_NOTE}\n${body}\n${MANAGED_END}`;
1878
- function layoutFile$1(nav, appTitle, theme) {
1894
+ function layoutFile$1(nav, appTitle, theme, stylesheets) {
1879
1895
  return {
1880
1896
  path: "src/routes/+layout.svelte",
1881
1897
  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.",
1882
1898
  contents: `<script lang="ts">
1883
- ${managed(` let { children } = $props()
1899
+ ${managed(`${stylesheets.map((css) => ` import '../${css}'\n`).join("")} let { children } = $props()
1884
1900
  const appTitle = ${JSON.stringify(appTitle)}
1885
1901
  const nav = ${JSON.stringify(nav)}
1886
1902
 
@@ -2003,10 +2019,11 @@ function scaffoldApp(schemas, options = {}) {
2003
2019
  href: `/${s.name}`,
2004
2020
  label: s.label ?? titleCase$2(s.name)
2005
2021
  }));
2006
- files.push(layoutFile$1(nav, appTitle, {
2022
+ const theme = {
2007
2023
  preset: options.theme,
2008
2024
  mode: options.dark ? "dark" : "light"
2009
- }), homeFile$1(nav, appTitle));
2025
+ };
2026
+ files.push(layoutFile$1(nav, appTitle, theme, options.stylesheets ?? []), homeFile$1(nav, appTitle));
2010
2027
  return { files };
2011
2028
  }
2012
2029
  //#endregion
@@ -16028,7 +16045,7 @@ function hasFieldConditions(schema) {
16028
16045
  *
16029
16046
  * `version.test.ts` asserts this matches `package.json`, so the two cannot drift.
16030
16047
  */
16031
- var SVGRID_VERSION = "3.0.3";
16048
+ var SVGRID_VERSION = "3.1.1";
16032
16049
  //#endregion
16033
16050
  //#region src/studio/emit-project.ts
16034
16051
  var has = (blocks, kind) => blocks.some((b) => b.config.kind === kind);
@@ -40702,6 +40719,153 @@ function getStarterDataset(id) {
40702
40719
  return starterDatasets.find((d) => d.id === id);
40703
40720
  }
40704
40721
  //#endregion
40722
+ //#region src/studio/verify.ts
40723
+ async function verifyScaffold(files) {
40724
+ const issues = [];
40725
+ let checked = 0;
40726
+ let skipped = 0;
40727
+ let compile;
40728
+ try {
40729
+ compile = (await import("svelte/compiler")).compile;
40730
+ } catch {
40731
+ compile = void 0;
40732
+ }
40733
+ for (const file of files) {
40734
+ if (!file.path.endsWith(".svelte") || !compile) {
40735
+ skipped++;
40736
+ continue;
40737
+ }
40738
+ try {
40739
+ const { warnings } = compile(file.contents, {
40740
+ filename: file.path,
40741
+ generate: "client"
40742
+ });
40743
+ checked++;
40744
+ for (const w of warnings) issues.push({
40745
+ path: file.path,
40746
+ severity: "warning",
40747
+ message: w.message ?? "warning"
40748
+ });
40749
+ } catch (err) {
40750
+ checked++;
40751
+ issues.push({
40752
+ path: file.path,
40753
+ severity: "error",
40754
+ message: err instanceof Error ? err.message : String(err)
40755
+ });
40756
+ }
40757
+ }
40758
+ return {
40759
+ ok: !issues.some((i) => i.severity === "error"),
40760
+ issues,
40761
+ checked,
40762
+ skipped
40763
+ };
40764
+ }
40765
+ /** One-line human summary of a verify result, for tool output / logs. */
40766
+ function summarizeVerify(result) {
40767
+ if (result.checked === 0) return "verification skipped (Svelte compiler unavailable) - run svelte-check in your project.";
40768
+ const errors = result.issues.filter((i) => i.severity === "error");
40769
+ if (errors.length === 0) return `verified: ${result.checked} file(s) compile.`;
40770
+ return `VERIFICATION FAILED (${errors.length} error(s)):\n` + errors.map((e) => ` ${e.path}: ${e.message}`).join("\n");
40771
+ }
40772
+ //#endregion
40773
+ //#region src/studio/cli.ts
40774
+ /** A `.prisma` file (by extension or a `model X {` block) parses as Prisma; otherwise Drizzle. */
40775
+ function isPrismaSource(path, source) {
40776
+ return /\.prisma$/i.test(path) || /\bmodel\s+\w+\s*\{/.test(source);
40777
+ }
40778
+ /** Resolve an EntitySchema from whichever input was provided. */
40779
+ async function resolveSchema(options, io) {
40780
+ if (options.schema) return options.schema;
40781
+ if (options.from) {
40782
+ const src = await io.readFile(options.from);
40783
+ if (src == null) throw new Error(`runStudioAdd: schema file not found: ${options.from}`);
40784
+ return isPrismaSource(options.from, src) ? introspectPrisma(src, options.table) : introspectDrizzle(src, options.table);
40785
+ }
40786
+ if (options.rows) return introspectJson(options.name ?? options.route ?? "entity", options.rows);
40787
+ throw new Error("runStudioAdd: provide `from` (a Drizzle or Prisma schema file), `rows`, or `schema`");
40788
+ }
40789
+ /**
40790
+ * Resolve **every** entity from a Drizzle or Prisma schema file (the `--all
40791
+ * --from` path), linked so foreign keys become working relation lookups.
40792
+ */
40793
+ async function resolveSchemas(from, io) {
40794
+ const src = await io.readFile(from);
40795
+ if (src == null) throw new Error(`runStudioAddApp: schema file not found: ${from}`);
40796
+ return isPrismaSource(from, src) ? introspectPrismaAll(src) : introspectDrizzleAll(src);
40797
+ }
40798
+ /** Merge-write a set of generated files, preserving user edits outside managed regions. */
40799
+ async function writeAll$1(files, io) {
40800
+ const written = [];
40801
+ for (const file of files) {
40802
+ const existing = await io.readFile(file.path);
40803
+ if (skipUserOwned(file, existing != null)) continue;
40804
+ await io.writeFile(file.path, mergeManaged(existing, file.contents));
40805
+ written.push(file.path);
40806
+ }
40807
+ return written;
40808
+ }
40809
+ async function runStudioAdd(options, io) {
40810
+ const schema = await resolveSchema(options, io);
40811
+ const { files } = scaffold(schema, options);
40812
+ return {
40813
+ schema,
40814
+ written: await writeAll$1(files, io),
40815
+ verify: await verifyScaffold(files)
40816
+ };
40817
+ }
40818
+ /** The stylesheets an `init`-generated layout imports, in cascade order. */
40819
+ var APP_STYLESHEETS = ["app.css", "custom.css"];
40820
+ /**
40821
+ * Fill the app-shell options the caller left out from the app already on disk,
40822
+ * so `add --all` in a folder `init` generated keeps its stylesheets, title and
40823
+ * theme. The shell layout from `init` carries no managed markers, so the merge
40824
+ * write replaces it whole; anything it imported or named has to be carried
40825
+ * over here or it is lost.
40826
+ *
40827
+ * Title: studio.config.json (shell brand, then title), else the `appTitle`
40828
+ * constant of a layout an earlier `add --all` wrote.
40829
+ */
40830
+ async function appShellDefaults(options, io) {
40831
+ const stylesheets = options.stylesheets ?? [];
40832
+ if (!options.stylesheets) {
40833
+ for (const css of APP_STYLESHEETS) if (await io.readFile(`src/${css}`) != null) stylesheets.push(css);
40834
+ }
40835
+ let title;
40836
+ let theme;
40837
+ const config = await io.readFile("studio.config.json");
40838
+ if (config != null) try {
40839
+ const parsed = JSON.parse(config);
40840
+ theme = parsed.theme;
40841
+ title = (typeof theme?.shell?.brand === "string" ? theme.shell.brand.trim() : "") || (typeof parsed.title === "string" ? parsed.title : void 0);
40842
+ } catch {}
40843
+ if (!title) {
40844
+ const m = (await io.readFile("src/routes/+layout.svelte"))?.match(/const appTitle = ("(?:[^"\\]|\\.)*")/);
40845
+ if (m) try {
40846
+ title = JSON.parse(m[1]);
40847
+ } catch {}
40848
+ }
40849
+ return {
40850
+ ...options,
40851
+ stylesheets,
40852
+ ...options.appTitle == null && title ? { appTitle: title } : {},
40853
+ ...options.theme == null && typeof theme?.preset === "string" ? { theme: theme.preset } : {},
40854
+ ...options.dark == null && theme?.mode === "dark" ? { dark: true } : {}
40855
+ };
40856
+ }
40857
+ /**
40858
+ * Scaffold a whole multi-entity app - every entity's screen plus a nav layout
40859
+ * and home page - and merge-write it. The `--all` path behind `svgrid-studio`.
40860
+ */
40861
+ async function runStudioAddApp(schemas, options, io) {
40862
+ const { files } = scaffoldApp(schemas, await appShellDefaults(options, io));
40863
+ return {
40864
+ written: await writeAll$1(files, io),
40865
+ verify: await verifyScaffold(files)
40866
+ };
40867
+ }
40868
+ //#endregion
40705
40869
  //#region src/sources/introspect-supabase.ts
40706
40870
  var mapOpenApiType = (prop) => {
40707
40871
  if (Array.isArray(prop.enum)) return "enum";
@@ -40966,7 +41130,8 @@ function unwrapRows(body) {
40966
41130
  return [];
40967
41131
  }
40968
41132
  /** Step 1-2: where does the data come from, and what tables does it have? */
40969
- async function gatherData(flags, prompts, db, fetchText) {
41133
+ async function gatherData(flags, prompts, db, fetchText, io) {
41134
+ if (flags.from) return gatherFromSchemaFile(flags, flags.from, prompts, io);
40970
41135
  let source = "sample";
40971
41136
  if (flags.db) source = "db";
40972
41137
  else if (flags.supabaseUrl) source = "supabase";
@@ -40998,6 +41163,41 @@ async function gatherData(flags, prompts, db, fetchText) {
40998
41163
  title: dataset.name
40999
41164
  };
41000
41165
  }
41166
+ /**
41167
+ * A schema file names the tables and columns but holds no rows and no
41168
+ * connection, so the app runs on the in-memory source and the emitter seeds it
41169
+ * with sample rows. `--db` + `--url` is the path to a live database.
41170
+ */
41171
+ async function gatherFromSchemaFile(flags, from, prompts, io) {
41172
+ const clash = flags.db ? "--db" : flags.supabaseUrl ? "--supabase-url" : flags.dataset ? "--dataset" : null;
41173
+ if (clash) throw new UserError(`--from and ${clash} both name the data source.`, "Pass one of them.");
41174
+ if (await io.readFile(from) == null) throw new UserError(`Schema file not found: ${from}`, "Pass the path to a Drizzle schema (.ts) or a Prisma schema (.prisma).");
41175
+ let all;
41176
+ try {
41177
+ all = await resolveSchemas(from, io);
41178
+ } catch (e) {
41179
+ throw new UserError(`Could not read ${from}: ${e instanceof Error ? e.message : String(e)}`, "Pass a Drizzle schema (.ts) or a Prisma schema (.prisma).");
41180
+ }
41181
+ if (all.length === 0) throw new UserError(`No tables or models found in ${from}.`, "Drizzle tables are read from pgTable / mysqlTable / sqliteTable calls, Prisma models from `model X {` blocks.");
41182
+ let picked = all.map((s) => s.name);
41183
+ if (!flags.yes) {
41184
+ prompts.say("");
41185
+ prompts.say(`Found ${all.length} ${all.length === 1 ? "table" : "tables"} in ${from}:`);
41186
+ all.forEach((s, i) => prompts.say(` ${i + 1}. ${s.name}`));
41187
+ picked = parseSelection(await prompts.ask("Which tables? (all, or a comma list of names/numbers)", "all"), picked);
41188
+ }
41189
+ const schemas = all.filter((s) => picked.includes(s.name));
41190
+ if (schemas.length === 0) throw new UserError("No tables picked - nothing to build.");
41191
+ const sources = {};
41192
+ for (const s of schemas) sources[s.name] = { kind: "memory" };
41193
+ prompts.say(` Read ${schemas.length} ${schemas.length === 1 ? "table" : "tables"} from ${from} (in-memory data with sample rows).`);
41194
+ return {
41195
+ schemas,
41196
+ sources,
41197
+ seed: {},
41198
+ kind: "memory"
41199
+ };
41200
+ }
41001
41201
  async function gatherFromDatabase(flags, prompts, db) {
41002
41202
  if (!db) throw new UserError("Connecting to a database is not available here.");
41003
41203
  let dialect = flags.db;
@@ -41263,7 +41463,7 @@ async function confirmTargetFolder(outDir, flags, prompts, io, before) {
41263
41463
  if (!isYes(await prompts.ask("Overwrite? (y/N)", "n"), false)) throw new UserError("Stopped without writing anything.", "Re-run with --out <dir> to build somewhere else.");
41264
41464
  }
41265
41465
  /** Merge-write the generated files, preserving edits outside managed regions. */
41266
- async function writeAll$1(files, outDir, io) {
41466
+ async function writeAll(files, outDir, io) {
41267
41467
  const written = [];
41268
41468
  for (const file of files) {
41269
41469
  const path = inDir(outDir, file.path);
@@ -41282,7 +41482,7 @@ async function runStudioInit(flags, prompts, db, io, fetchText = null) {
41282
41482
  prompts.say("SvGrid Studio - let's build your app.");
41283
41483
  const candidateDir = flags.out ?? ".";
41284
41484
  const markersBefore = await markersIn(candidateDir, io);
41285
- const data = await gatherData(flags, prompts, db, fetchText);
41485
+ const data = await gatherData(flags, prompts, db, fetchText, io);
41286
41486
  const perEntity = await askScreens(data.schemas, flags, prompts);
41287
41487
  const theme = await askTheme(flags, prompts);
41288
41488
  const defaultTitle = flags.title ?? data.title ?? "My app";
@@ -41298,7 +41498,7 @@ async function runStudioInit(flags, prompts, db, io, fetchText = null) {
41298
41498
  overviewDashboard: true,
41299
41499
  ...theme ? { theme } : {}
41300
41500
  });
41301
- const written = await writeAll$1(emitStudioAppBundle(project), outDir, io);
41501
+ const written = await writeAll(emitStudioAppBundle(project), outDir, io);
41302
41502
  const configPath = inDir(outDir, "studio.config.json");
41303
41503
  await io.writeFile(configPath, serializeProject(project));
41304
41504
  written.push(configPath);
@@ -41466,57 +41666,6 @@ function buildStudioBugReport(input) {
41466
41666
  };
41467
41667
  }
41468
41668
  //#endregion
41469
- //#region src/studio/verify.ts
41470
- async function verifyScaffold(files) {
41471
- const issues = [];
41472
- let checked = 0;
41473
- let skipped = 0;
41474
- let compile;
41475
- try {
41476
- compile = (await import("svelte/compiler")).compile;
41477
- } catch {
41478
- compile = void 0;
41479
- }
41480
- for (const file of files) {
41481
- if (!file.path.endsWith(".svelte") || !compile) {
41482
- skipped++;
41483
- continue;
41484
- }
41485
- try {
41486
- const { warnings } = compile(file.contents, {
41487
- filename: file.path,
41488
- generate: "client"
41489
- });
41490
- checked++;
41491
- for (const w of warnings) issues.push({
41492
- path: file.path,
41493
- severity: "warning",
41494
- message: w.message ?? "warning"
41495
- });
41496
- } catch (err) {
41497
- checked++;
41498
- issues.push({
41499
- path: file.path,
41500
- severity: "error",
41501
- message: err instanceof Error ? err.message : String(err)
41502
- });
41503
- }
41504
- }
41505
- return {
41506
- ok: !issues.some((i) => i.severity === "error"),
41507
- issues,
41508
- checked,
41509
- skipped
41510
- };
41511
- }
41512
- /** One-line human summary of a verify result, for tool output / logs. */
41513
- function summarizeVerify(result) {
41514
- if (result.checked === 0) return "verification skipped (Svelte compiler unavailable) - run svelte-check in your project.";
41515
- const errors = result.issues.filter((i) => i.severity === "error");
41516
- if (errors.length === 0) return `verified: ${result.checked} file(s) compile.`;
41517
- return `VERIFICATION FAILED (${errors.length} error(s)):\n` + errors.map((e) => ` ${e.path}: ${e.message}`).join("\n");
41518
- }
41519
- //#endregion
41520
41669
  //#region src/studio/copilot-core.ts
41521
41670
  /**
41522
41671
  * Studio Copilot - the pure, testable core. Builds the model request for "edit
@@ -41660,63 +41809,6 @@ function missingEnvKeys(envExample, envLocal) {
41660
41809
  return declared.filter((k) => !present.has(k));
41661
41810
  }
41662
41811
  //#endregion
41663
- //#region src/studio/cli.ts
41664
- /** A `.prisma` file (by extension or a `model X {` block) parses as Prisma; otherwise Drizzle. */
41665
- function isPrismaSource(path, source) {
41666
- return /\.prisma$/i.test(path) || /\bmodel\s+\w+\s*\{/.test(source);
41667
- }
41668
- /** Resolve an EntitySchema from whichever input was provided. */
41669
- async function resolveSchema(options, io) {
41670
- if (options.schema) return options.schema;
41671
- if (options.from) {
41672
- const src = await io.readFile(options.from);
41673
- if (src == null) throw new Error(`runStudioAdd: schema file not found: ${options.from}`);
41674
- return isPrismaSource(options.from, src) ? introspectPrisma(src, options.table) : introspectDrizzle(src, options.table);
41675
- }
41676
- if (options.rows) return introspectJson(options.name ?? options.route ?? "entity", options.rows);
41677
- throw new Error("runStudioAdd: provide `from` (a Drizzle or Prisma schema file), `rows`, or `schema`");
41678
- }
41679
- /**
41680
- * Resolve **every** entity from a Drizzle or Prisma schema file (the `--all
41681
- * --from` path), linked so foreign keys become working relation lookups.
41682
- */
41683
- async function resolveSchemas(from, io) {
41684
- const src = await io.readFile(from);
41685
- if (src == null) throw new Error(`runStudioAddApp: schema file not found: ${from}`);
41686
- return isPrismaSource(from, src) ? introspectPrismaAll(src) : introspectDrizzleAll(src);
41687
- }
41688
- /** Merge-write a set of generated files, preserving user edits outside managed regions. */
41689
- async function writeAll(files, io) {
41690
- const written = [];
41691
- for (const file of files) {
41692
- const existing = await io.readFile(file.path);
41693
- if (skipUserOwned(file, existing != null)) continue;
41694
- await io.writeFile(file.path, mergeManaged(existing, file.contents));
41695
- written.push(file.path);
41696
- }
41697
- return written;
41698
- }
41699
- async function runStudioAdd(options, io) {
41700
- const schema = await resolveSchema(options, io);
41701
- const { files } = scaffold(schema, options);
41702
- return {
41703
- schema,
41704
- written: await writeAll(files, io),
41705
- verify: await verifyScaffold(files)
41706
- };
41707
- }
41708
- /**
41709
- * Scaffold a whole multi-entity app - every entity's screen plus a nav layout
41710
- * and home page - and merge-write it. The `--all` path behind `svgrid-studio`.
41711
- */
41712
- async function runStudioAddApp(schemas, options, io) {
41713
- const { files } = scaffoldApp(schemas, options);
41714
- return {
41715
- written: await writeAll(files, io),
41716
- verify: await verifyScaffold(files)
41717
- };
41718
- }
41719
- //#endregion
41720
41812
  //#region src/revoked.ts
41721
41813
  var REVOKED_KEYS = /* @__PURE__ */ new Set([]);
41722
41814
  /**
@@ -92,6 +92,53 @@ export declare function objectAt(objects: ReadonlyArray<SheetObject>, point: {
92
92
  width: number;
93
93
  height: number;
94
94
  } | null): SheetObject | null;
95
+ /**
96
+ * The four panes Freeze Panes splits a sheet into: the frozen corner, the
97
+ * frozen rows (which scroll across), the frozen columns (which scroll
98
+ * down), and the body (which scrolls both ways).
99
+ */
100
+ export type SheetPane = 'corner' | 'rows' | 'cols' | 'body';
101
+ /**
102
+ * The pane an object belongs to: the one its anchor cell is in. Excel keeps
103
+ * an object with that pane, so one hung from a frozen cell is drawn over
104
+ * the frozen cells and stays put while the sheet scrolls under it.
105
+ */
106
+ export declare function paneOf(anchor: {
107
+ row: number;
108
+ col: number;
109
+ }, freeze: {
110
+ rows: number;
111
+ cols: number;
112
+ }): SheetPane;
113
+ /**
114
+ * How much of an object its pane cuts off, in pixels from each edge of its
115
+ * box. `box` and `edges` are in the same frame: `left` measured from the
116
+ * inline start, `top` from the top.
117
+ *
118
+ * `edges.start` and `edges.top` are where the scrolling cells begin (past
119
+ * the frozen columns, or the row numbers when none are frozen; under the
120
+ * frozen rows, or the header), and `edges.end` and `edges.bottom` where the
121
+ * sheet's window ends. An object is cut on the sides its pane scrolls from,
122
+ * so it slides under the frozen cells, and always at the window's far
123
+ * edges. A frozen pane never scrolls past its start, so nothing is cut
124
+ * there and the object is drawn whole over its frozen cells.
125
+ */
126
+ export declare function paneClip(pane: SheetPane, box: {
127
+ left: number;
128
+ top: number;
129
+ width: number;
130
+ height: number;
131
+ }, edges: {
132
+ start: number;
133
+ top: number;
134
+ end: number;
135
+ bottom: number;
136
+ }): {
137
+ start: number;
138
+ top: number;
139
+ end: number;
140
+ bottom: number;
141
+ };
95
142
  /**
96
143
  * The chart's spec, read from its range through `valueAt` (the computed
97
144
  * value) and `textAt` (what the cell shows, for the labels). A chart of a
@@ -49,6 +49,39 @@ export function objectAt(objects, point, boxOf) {
49
49
  }
50
50
  return null;
51
51
  }
52
+ /**
53
+ * The pane an object belongs to: the one its anchor cell is in. Excel keeps
54
+ * an object with that pane, so one hung from a frozen cell is drawn over
55
+ * the frozen cells and stays put while the sheet scrolls under it.
56
+ */
57
+ export function paneOf(anchor, freeze) {
58
+ const row = anchor.row < freeze.rows;
59
+ const col = anchor.col < freeze.cols;
60
+ return row && col ? 'corner' : row ? 'rows' : col ? 'cols' : 'body';
61
+ }
62
+ /**
63
+ * How much of an object its pane cuts off, in pixels from each edge of its
64
+ * box. `box` and `edges` are in the same frame: `left` measured from the
65
+ * inline start, `top` from the top.
66
+ *
67
+ * `edges.start` and `edges.top` are where the scrolling cells begin (past
68
+ * the frozen columns, or the row numbers when none are frozen; under the
69
+ * frozen rows, or the header), and `edges.end` and `edges.bottom` where the
70
+ * sheet's window ends. An object is cut on the sides its pane scrolls from,
71
+ * so it slides under the frozen cells, and always at the window's far
72
+ * edges. A frozen pane never scrolls past its start, so nothing is cut
73
+ * there and the object is drawn whole over its frozen cells.
74
+ */
75
+ export function paneClip(pane, box, edges) {
76
+ const across = pane === 'rows' || pane === 'body';
77
+ const down = pane === 'cols' || pane === 'body';
78
+ return {
79
+ start: across ? Math.max(0, edges.start - box.left) : 0,
80
+ top: down ? Math.max(0, edges.top - box.top) : 0,
81
+ end: Math.max(0, box.left + box.width - edges.end),
82
+ bottom: Math.max(0, box.top + box.height - edges.bottom),
83
+ };
84
+ }
52
85
  /** A cell's value as a number, or null when it is not one. */
53
86
  function numberOf(value) {
54
87
  if (typeof value === 'number')
@@ -58,12 +58,66 @@ export async function runStudioAdd(options, io) {
58
58
  const verify = await verifyScaffold(files);
59
59
  return { schema, written, verify };
60
60
  }
61
+ /** The stylesheets an `init`-generated layout imports, in cascade order. */
62
+ const APP_STYLESHEETS = ['app.css', 'custom.css'];
63
+ /**
64
+ * Fill the app-shell options the caller left out from the app already on disk,
65
+ * so `add --all` in a folder `init` generated keeps its stylesheets, title and
66
+ * theme. The shell layout from `init` carries no managed markers, so the merge
67
+ * write replaces it whole; anything it imported or named has to be carried
68
+ * over here or it is lost.
69
+ *
70
+ * Title: studio.config.json (shell brand, then title), else the `appTitle`
71
+ * constant of a layout an earlier `add --all` wrote.
72
+ */
73
+ async function appShellDefaults(options, io) {
74
+ const stylesheets = options.stylesheets ?? [];
75
+ if (!options.stylesheets) {
76
+ for (const css of APP_STYLESHEETS) {
77
+ if ((await io.readFile(`src/${css}`)) != null)
78
+ stylesheets.push(css);
79
+ }
80
+ }
81
+ let title;
82
+ let theme;
83
+ const config = await io.readFile('studio.config.json');
84
+ if (config != null) {
85
+ try {
86
+ const parsed = JSON.parse(config);
87
+ theme = parsed.theme;
88
+ const brand = typeof theme?.shell?.brand === 'string' ? theme.shell.brand.trim() : '';
89
+ title = brand || (typeof parsed.title === 'string' ? parsed.title : undefined);
90
+ }
91
+ catch {
92
+ /* unreadable config: fall back to the layout below */
93
+ }
94
+ }
95
+ if (!title) {
96
+ const layout = await io.readFile('src/routes/+layout.svelte');
97
+ const m = layout?.match(/const appTitle = ("(?:[^"\\]|\\.)*")/);
98
+ if (m) {
99
+ try {
100
+ title = JSON.parse(m[1]);
101
+ }
102
+ catch {
103
+ /* not a JSON string literal: keep the default */
104
+ }
105
+ }
106
+ }
107
+ return {
108
+ ...options,
109
+ stylesheets,
110
+ ...(options.appTitle == null && title ? { appTitle: title } : {}),
111
+ ...(options.theme == null && typeof theme?.preset === 'string' ? { theme: theme.preset } : {}),
112
+ ...(options.dark == null && theme?.mode === 'dark' ? { dark: true } : {}),
113
+ };
114
+ }
61
115
  /**
62
116
  * Scaffold a whole multi-entity app - every entity's screen plus a nav layout
63
117
  * and home page - and merge-write it. The `--all` path behind `svgrid-studio`.
64
118
  */
65
119
  export async function runStudioAddApp(schemas, options, io) {
66
- const { files } = scaffoldApp(schemas, options);
120
+ const { files } = scaffoldApp(schemas, await appShellDefaults(options, io));
67
121
  const written = await writeAll(files, io);
68
122
  const verify = await verifyScaffold(files);
69
123
  return { written, verify };
@@ -1,4 +1,4 @@
1
- import type { StudioIO } from './cli.js';
1
+ import { type StudioIO } from './cli.js';
2
2
  import { type DbExecute, type SqlDialectName } from './introspect-db.js';
3
3
  import { type StudioProject } from './project.js';
4
4
  /** Terminal I/O, injected so the flow can be scripted in tests. */
@@ -30,6 +30,11 @@ export type InitFlags = {
30
30
  supabaseUrl?: string;
31
31
  /** Supabase anon key. */
32
32
  supabaseKey?: string;
33
+ /**
34
+ * A Drizzle (.ts) or Prisma (.prisma) schema file. Skips the source question
35
+ * and builds the app from its tables, served from memory with sample rows.
36
+ */
37
+ from?: string;
33
38
  /** Pick a starter dataset by id, skipping the source question. */
34
39
  dataset?: string;
35
40
  /** Theme preset id. */