@svgrid/enterprise 2.5.3 → 2.6.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.
- package/dist/SvAlertRuleEditor.svelte.d.ts +13 -0
- package/dist/SvAlertsManager.svelte.d.ts +14 -0
- package/dist/SvAlertsPanel.svelte.d.ts +9 -0
- package/dist/SvAuthGate.svelte.d.ts +22 -0
- package/dist/SvBoard.svelte.d.ts +35 -0
- package/dist/SvExportMenu.svelte.d.ts +61 -0
- package/{src → dist}/SvExpressionEditor.svelte +4 -1
- package/dist/SvExpressionEditor.svelte.d.ts +12 -0
- package/dist/SvFileInput.svelte.d.ts +20 -0
- package/dist/SvGridAlerts.svelte.d.ts +77 -0
- package/dist/SvGridBoard.svelte.d.ts +30 -0
- package/{src → dist}/SvGridEditPanel.svelte +173 -28
- package/dist/SvGridEditPanel.svelte.d.ts +75 -0
- package/dist/SvGridMasterDetail.svelte.d.ts +49 -0
- package/dist/SvGridScheduler.svelte.d.ts +30 -0
- package/dist/SvImportDialog.svelte.d.ts +88 -0
- package/dist/SvLookupInput.svelte.d.ts +21 -0
- package/dist/SvPivotDesigner.svelte.d.ts +176 -0
- package/dist/SvRecordDetail.svelte.d.ts +58 -0
- package/dist/SvSchedule.svelte.d.ts +29 -0
- package/dist/SvSchemaChart.svelte.d.ts +61 -0
- package/dist/SvSchemaDashboard.svelte.d.ts +46 -0
- package/dist/alerts/alert-engine-attach.d.ts +79 -0
- package/dist/alerts/alert-engine-attach.js +106 -0
- package/dist/alerts/alert-engine.d.ts +42 -0
- package/dist/alerts/alert-engine.js +196 -0
- package/dist/alerts/alert-formats.d.ts +26 -0
- package/dist/alerts/alert-formats.js +58 -0
- package/dist/alerts/alert-observer.d.ts +54 -0
- package/dist/alerts/alert-observer.js +156 -0
- package/dist/alerts/alert-scheduler.d.ts +30 -0
- package/dist/alerts/alert-scheduler.js +54 -0
- package/dist/alerts/alert-storage.d.ts +31 -0
- package/dist/alerts/alert-storage.js +92 -0
- package/dist/alerts/alert-store.svelte.d.ts +24 -0
- package/dist/alerts/alert-store.svelte.js +69 -0
- package/dist/alerts/alert-types.d.ts +86 -0
- package/dist/alerts/alert-types.js +1 -0
- package/dist/alerts.d.ts +2 -0
- package/{src/alerts.ts → dist/alerts.js} +9 -9
- package/dist/board.d.ts +7 -0
- package/{src/board.ts → dist/board.js} +13 -14
- package/dist/cdn/svgrid-enterprise.svelte-external.js +3220 -2607
- package/dist/designer/assets/index-BfzoL904.css +1 -0
- package/dist/designer/assets/{index-CBYU-KNn.js → index-CKdwDseq.js} +139 -139
- package/dist/designer/assets/src-DCKhP5Xy.css +1 -0
- package/dist/designer/assets/{src-i309BeJb.js → src-DLsSdh3J.js} +213 -193
- package/dist/designer/index.html +4 -4
- package/dist/edit-panel.d.ts +105 -0
- package/dist/edit-panel.js +350 -0
- package/dist/export-conditional.d.ts +31 -0
- package/dist/export-conditional.js +65 -0
- package/dist/export-ooxml.d.ts +93 -0
- package/dist/export-ooxml.js +385 -0
- package/dist/export-pdf.d.ts +107 -0
- package/dist/export-pdf.js +144 -0
- package/dist/export-print.d.ts +55 -0
- package/dist/export-print.js +91 -0
- package/{src/export-serialize.ts → dist/export-serialize.d.ts} +11 -24
- package/dist/export-serialize.js +11 -0
- package/dist/export-xls.d.ts +26 -0
- package/dist/export-xls.js +157 -0
- package/dist/export.d.ts +323 -0
- package/dist/export.js +1021 -0
- package/dist/expressions/evaluate.d.ts +17 -0
- package/dist/expressions/evaluate.js +214 -0
- package/dist/expressions/expression-columns.d.ts +61 -0
- package/dist/expressions/expression-columns.js +72 -0
- package/dist/expressions/expression-types.d.ts +125 -0
- package/dist/expressions/expression-types.js +1 -0
- package/dist/expressions/parse.d.ts +25 -0
- package/dist/expressions/parse.js +537 -0
- package/dist/import.d.ts +232 -0
- package/dist/import.js +846 -0
- package/dist/index.d.ts +66 -0
- package/dist/index.js +91 -0
- package/dist/install.d.ts +70 -0
- package/dist/install.js +61 -0
- package/dist/license-core.d.ts +37 -0
- package/dist/license-core.js +81 -0
- package/dist/license.d.ts +30 -0
- package/dist/license.js +139 -0
- package/dist/master-detail.d.ts +23 -0
- package/dist/master-detail.js +35 -0
- package/dist/node/studio.js +683 -13
- package/dist/pivot-chart.d.ts +28 -0
- package/dist/pivot-chart.js +71 -0
- package/dist/pivot-designer.d.ts +69 -0
- package/dist/pivot-designer.js +38 -0
- package/dist/pivot-enable.d.ts +6 -0
- package/{src/pivot-enable.ts → dist/pivot-enable.js} +13 -18
- package/dist/pivot.d.ts +111 -0
- package/dist/pivot.js +353 -0
- package/dist/print.d.ts +33 -0
- package/dist/print.js +61 -0
- package/dist/revoked.d.ts +1 -0
- package/{src/revoked.ts → dist/revoked.js} +5 -6
- package/dist/scheduler-assignments.d.ts +64 -0
- package/dist/scheduler-assignments.js +92 -0
- package/dist/scheduler-axis.d.ts +81 -0
- package/dist/scheduler-axis.js +180 -0
- package/{src/scheduler-booking.ts → dist/scheduler-booking.d.ts} +10 -43
- package/dist/scheduler-booking.js +50 -0
- package/dist/scheduler-config.d.ts +177 -0
- package/dist/scheduler-config.js +1 -0
- package/dist/scheduler-dependencies.d.ts +89 -0
- package/dist/scheduler-dependencies.js +179 -0
- package/dist/scheduler-freebusy.d.ts +19 -0
- package/{src/scheduler-freebusy.ts → dist/scheduler-freebusy.js} +9 -19
- package/{src/scheduler-heatmap.ts → dist/scheduler-heatmap.d.ts} +12 -35
- package/dist/scheduler-heatmap.js +41 -0
- package/dist/scheduler-resource-tree.d.ts +48 -0
- package/dist/scheduler-resource-tree.js +78 -0
- package/dist/scheduler-slots.d.ts +37 -0
- package/dist/scheduler-slots.js +80 -0
- package/dist/scheduler-summary.d.ts +41 -0
- package/dist/scheduler-summary.js +54 -0
- package/dist/scheduler.d.ts +7 -0
- package/{src/scheduler.ts → dist/scheduler.js} +13 -14
- package/dist/scheduling.d.ts +132 -0
- package/dist/scheduling.js +228 -0
- package/dist/schema-designer.d.ts +42 -0
- package/dist/schema-designer.js +117 -0
- package/dist/schema.d.ts +408 -0
- package/dist/schema.js +225 -0
- package/dist/smart-shim.d.ts +62 -0
- package/dist/smart-shim.js +34 -0
- package/dist/sources/aggregate.d.ts +43 -0
- package/dist/sources/aggregate.js +60 -0
- package/dist/sources/auth-supabase.d.ts +69 -0
- package/dist/sources/auth-supabase.js +39 -0
- package/dist/sources/dashboard.d.ts +65 -0
- package/dist/sources/dashboard.js +95 -0
- package/dist/sources/field-inference.d.ts +23 -0
- package/dist/sources/field-inference.js +52 -0
- package/dist/sources/filters.d.ts +35 -0
- package/dist/sources/filters.js +52 -0
- package/dist/sources/index.d.ts +18 -0
- package/dist/sources/index.js +18 -0
- package/dist/sources/introspect-supabase.d.ts +30 -0
- package/dist/sources/introspect-supabase.js +134 -0
- package/dist/sources/realtime-supabase.d.ts +37 -0
- package/dist/sources/realtime-supabase.js +52 -0
- package/dist/sources/relation-lookup.d.ts +50 -0
- package/dist/sources/relation-lookup.js +77 -0
- package/dist/sources/rest-adapters.d.ts +48 -0
- package/dist/sources/rest-adapters.js +85 -0
- package/dist/sources/rest.d.ts +38 -0
- package/dist/sources/rest.js +86 -0
- package/dist/sources/schema-from-columns.d.ts +37 -0
- package/dist/sources/schema-from-columns.js +48 -0
- package/dist/sources/supabase.d.ts +33 -0
- package/dist/sources/supabase.js +74 -0
- package/dist/sources/with-entity-rules.d.ts +25 -0
- package/dist/sources/with-entity-rules.js +47 -0
- package/dist/sources/with-relation-labels.d.ts +26 -0
- package/dist/sources/with-relation-labels.js +38 -0
- package/dist/staged-editing.d.ts +85 -0
- package/dist/staged-editing.js +0 -0
- package/dist/studio/bug-report.d.ts +61 -0
- package/dist/studio/bug-report.js +126 -0
- package/dist/studio/cli.d.ts +56 -0
- package/dist/studio/cli.js +70 -0
- package/dist/studio/copilot-core.d.ts +22 -0
- package/{src/studio/copilot-core.ts → dist/studio/copilot-core.js} +34 -38
- package/dist/studio/csv.d.ts +36 -0
- package/dist/studio/csv.js +173 -0
- package/dist/studio/db-connect-string.d.ts +45 -0
- package/dist/studio/db-connect-string.js +113 -0
- package/dist/studio/deploy-cli.d.ts +35 -0
- package/dist/studio/deploy-cli.js +81 -0
- package/dist/studio/emit-project.d.ts +77 -0
- package/dist/studio/emit-project.js +5463 -0
- package/dist/studio/emit-schema.d.ts +110 -0
- package/dist/studio/emit-schema.js +1128 -0
- package/dist/studio/index.d.ts +35 -0
- package/dist/studio/index.js +41 -0
- package/dist/studio/init-flow.d.ts +58 -0
- package/dist/studio/init-flow.js +451 -0
- package/dist/studio/introspect-db.d.ts +47 -0
- package/dist/studio/introspect-db.js +255 -0
- package/dist/studio/introspect-openapi.d.ts +19 -0
- package/dist/studio/introspect-openapi.js +244 -0
- package/dist/studio/introspect-prisma.d.ts +25 -0
- package/dist/studio/introspect-prisma.js +164 -0
- package/dist/studio/introspect.d.ts +35 -0
- package/dist/studio/introspect.js +306 -0
- package/dist/studio/project.d.ts +1448 -0
- package/dist/studio/project.js +2187 -0
- package/dist/studio/sample-data.d.ts +19 -0
- package/dist/studio/sample-data.js +207 -0
- package/dist/studio/samples/ats.d.ts +2 -0
- package/dist/studio/samples/ats.js +194 -0
- package/dist/studio/samples/clinic.d.ts +2 -0
- package/dist/studio/samples/clinic.js +171 -0
- package/dist/studio/samples/crm.d.ts +2 -0
- package/dist/studio/samples/crm.js +280 -0
- package/dist/studio/samples/datasets.d.ts +38 -0
- package/dist/studio/samples/datasets.js +296 -0
- package/dist/studio/samples/ecommerce.d.ts +2 -0
- package/dist/studio/samples/ecommerce.js +178 -0
- package/dist/studio/samples/events.d.ts +2 -0
- package/dist/studio/samples/events.js +192 -0
- package/dist/studio/samples/fleet.d.ts +2 -0
- package/dist/studio/samples/fleet.js +203 -0
- package/dist/studio/samples/gym.d.ts +2 -0
- package/dist/studio/samples/gym.js +205 -0
- package/dist/studio/samples/hr.d.ts +2 -0
- package/dist/studio/samples/hr.js +186 -0
- package/dist/studio/samples/index.d.ts +13 -0
- package/dist/studio/samples/index.js +44 -0
- package/dist/studio/samples/insurance.d.ts +2 -0
- package/dist/studio/samples/insurance.js +220 -0
- package/dist/studio/samples/inventory.d.ts +2 -0
- package/dist/studio/samples/inventory.js +204 -0
- package/dist/studio/samples/invoicing.d.ts +2 -0
- package/dist/studio/samples/invoicing.js +154 -0
- package/dist/studio/samples/library.d.ts +2 -0
- package/dist/studio/samples/library.js +171 -0
- package/dist/studio/samples/live-data.d.ts +5 -0
- package/dist/studio/samples/live-data.js +261 -0
- package/dist/studio/samples/projects.d.ts +2 -0
- package/dist/studio/samples/projects.js +203 -0
- package/dist/studio/samples/realestate.d.ts +2 -0
- package/dist/studio/samples/realestate.js +187 -0
- package/dist/studio/samples/restaurant.d.ts +2 -0
- package/dist/studio/samples/restaurant.js +180 -0
- package/dist/studio/samples/school.d.ts +2 -0
- package/dist/studio/samples/school.js +188 -0
- package/dist/studio/samples/shared.d.ts +142 -0
- package/dist/studio/samples/shared.js +164 -0
- package/dist/studio/samples/starter.d.ts +4 -0
- package/dist/studio/samples/starter.js +237 -0
- package/dist/studio/samples/subscriptions.d.ts +2 -0
- package/dist/studio/samples/subscriptions.js +175 -0
- package/dist/studio/samples/support.d.ts +2 -0
- package/dist/studio/samples/support.js +209 -0
- package/dist/studio/scaffold-app.d.ts +22 -0
- package/{src/studio/scaffold-app.ts → dist/studio/scaffold-app.js} +45 -63
- package/dist/studio/scaffold.d.ts +63 -0
- package/dist/studio/scaffold.js +346 -0
- package/dist/studio/screen-suites.d.ts +193 -0
- package/dist/studio/screen-suites.js +297 -0
- package/dist/studio/themes.d.ts +34 -0
- package/dist/studio/themes.js +41 -0
- package/dist/studio/ui-components.d.ts +89 -0
- package/dist/studio/ui-components.generated.d.ts +29 -0
- package/dist/studio/ui-components.generated.js +7319 -0
- package/dist/studio/ui-components.js +673 -0
- package/dist/studio/user-error.d.ts +23 -0
- package/{src/studio/user-error.ts → dist/studio/user-error.js} +10 -13
- package/dist/studio/verify.d.ts +34 -0
- package/dist/studio/verify.js +47 -0
- package/dist/sveltekit/in-memory.d.ts +14 -0
- package/dist/sveltekit/in-memory.js +113 -0
- package/dist/sveltekit/index.d.ts +6 -0
- package/dist/sveltekit/index.js +5 -0
- package/dist/sveltekit/query-plan.d.ts +44 -0
- package/dist/sveltekit/query-plan.js +76 -0
- package/dist/sveltekit/sql-source.d.ts +47 -0
- package/dist/sveltekit/sql-source.js +106 -0
- package/dist/sveltekit/sql.d.ts +48 -0
- package/dist/sveltekit/sql.js +73 -0
- package/dist/sveltekit/transport.d.ts +209 -0
- package/dist/sveltekit/transport.js +219 -0
- package/{src/sveltekit/types.ts → dist/sveltekit/types.d.ts} +8 -11
- package/dist/sveltekit/types.js +1 -0
- package/dist/upgrade-prompt.d.ts +16 -0
- package/dist/upgrade-prompt.js +141 -0
- package/dist/version.d.ts +13 -0
- package/{src/version.ts → dist/version.js} +1 -1
- package/dist/watermark.d.ts +2 -0
- package/dist/watermark.js +146 -0
- package/package.json +28 -26
- package/dist/designer/assets/index-oYok52zd.css +0 -1
- package/dist/designer/assets/src-D-O2F805.css +0 -1
- package/src/SvBoard.dom.test.ts +0 -67
- package/src/SvGridAlerts.dom.test.ts +0 -113
- package/src/SvGridEditPanel.dom.test.ts +0 -320
- package/src/SvRecordDetail.dom.test.ts +0 -137
- package/src/SvSchedule.dom.test.ts +0 -57
- package/src/ai-export-pdf.dom.test.ts +0 -77
- package/src/ai-export-xlsx.dom.test.ts +0 -90
- package/src/ai-export.dom.test.ts +0 -114
- package/src/alerts/alert-engine-attach.ts +0 -165
- package/src/alerts/alert-engine.test.ts +0 -135
- package/src/alerts/alert-engine.ts +0 -260
- package/src/alerts/alert-formats.test.ts +0 -87
- package/src/alerts/alert-formats.ts +0 -77
- package/src/alerts/alert-observer.test.ts +0 -189
- package/src/alerts/alert-observer.ts +0 -208
- package/src/alerts/alert-scheduler.ts +0 -96
- package/src/alerts/alert-storage.test.ts +0 -54
- package/src/alerts/alert-storage.ts +0 -116
- package/src/alerts/alert-store.svelte.ts +0 -80
- package/src/alerts/alert-types.ts +0 -94
- package/src/board.dom.test.ts +0 -941
- package/src/edit-panel.test.ts +0 -322
- package/src/edit-panel.ts +0 -379
- package/src/export-conditional.test.ts +0 -60
- package/src/export-conditional.ts +0 -94
- package/src/export-ooxml.test.ts +0 -152
- package/src/export-ooxml.ts +0 -489
- package/src/export-pdf.test.ts +0 -138
- package/src/export-pdf.ts +0 -245
- package/src/export-print.test.ts +0 -66
- package/src/export-print.ts +0 -132
- package/src/export-serialize.test.ts +0 -56
- package/src/export-xls.ts +0 -199
- package/src/export-xlsx-roundtrip.test.ts +0 -120
- package/src/export-xlsx.test.ts +0 -150
- package/src/export.test.ts +0 -349
- package/src/export.ts +0 -1464
- package/src/expressions/evaluate.test.ts +0 -111
- package/src/expressions/evaluate.ts +0 -237
- package/src/expressions/expression-columns.ts +0 -133
- package/src/expressions/expression-types.ts +0 -84
- package/src/expressions/parse.test.ts +0 -106
- package/src/expressions/parse.ts +0 -610
- package/src/import-automap.test.ts +0 -131
- package/src/import-hardening.test.ts +0 -158
- package/src/import.test.ts +0 -415
- package/src/import.ts +0 -1044
- package/src/index.ts +0 -569
- package/src/install.ts +0 -154
- package/src/license-core.test.ts +0 -23
- package/src/license-core.ts +0 -107
- package/src/license-expiry.test.ts +0 -144
- package/src/license.ts +0 -157
- package/src/master-detail.test.ts +0 -61
- package/src/master-detail.ts +0 -42
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/_metadata.json +0 -178
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/attachments-ZrPsKxob.js +0 -3875
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/attachments-ZrPsKxob.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/constants-ACqvWGE2.js +0 -21
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/constants-ACqvWGE2.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/dev-D3w5jt92.js +0 -1938
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/dev-D3w5jt92.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/events-CC2V3H0y.js +0 -205
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/events-CC2V3H0y.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/flags-BXP37tAa.js +0 -18
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/flags-BXP37tAa.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/hydration-DwubnU5b.js +0 -264
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/hydration-DwubnU5b.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/legacy-client-hD7f6PUz.js +0 -697
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/legacy-client-hD7f6PUz.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte.js +0 -134
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_animate.js +0 -55
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_animate.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_attachments.js +0 -2
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_compiler.js +0 -46843
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_compiler.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_easing.js +0 -237
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_easing.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_events.js +0 -2
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal.js +0 -4
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_client.js +0 -7
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_disclose-version.js +0 -4
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_disclose-version.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_async.js +0 -5
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_async.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_legacy.js +0 -5
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_legacy.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_tracing.js +0 -5
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_tracing.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_server.js +0 -6
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_legacy.js +0 -83
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_legacy.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_motion.js +0 -559
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_motion.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity.js +0 -24
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity_window.js +0 -122
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity_window.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_server.js +0 -2
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_store.js +0 -89
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_store.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_transition.js +0 -241
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_transition.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-DBx1SCN6.js +0 -4938
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-DBx1SCN6.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-gjfpSTFS.js +0 -495
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-gjfpSTFS.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/validate-DKoOKg8S.js +0 -190
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/validate-DKoOKg8S.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/version-C0MiR7eR.js +0 -8
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/version-C0MiR7eR.js.map +0 -1
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/warnings-B6Y7P0Q-.js +0 -138
- package/src/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/warnings-B6Y7P0Q-.js.map +0 -1
- package/src/pivot-chart.test.ts +0 -86
- package/src/pivot-chart.ts +0 -112
- package/src/pivot-designer.ts +0 -101
- package/src/pivot.test.ts +0 -335
- package/src/pivot.ts +0 -578
- package/src/print.ts +0 -107
- package/src/scheduler-assignments.test.ts +0 -97
- package/src/scheduler-assignments.ts +0 -134
- package/src/scheduler-axis.test.ts +0 -108
- package/src/scheduler-axis.ts +0 -238
- package/src/scheduler-booking.test.ts +0 -57
- package/src/scheduler-config.ts +0 -179
- package/src/scheduler-dependencies.test.ts +0 -155
- package/src/scheduler-dependencies.ts +0 -223
- package/src/scheduler-freebusy.test.ts +0 -41
- package/src/scheduler-heatmap.test.ts +0 -39
- package/src/scheduler-resource-tree.test.ts +0 -84
- package/src/scheduler-resource-tree.ts +0 -107
- package/src/scheduler-slots.test.ts +0 -53
- package/src/scheduler-slots.ts +0 -94
- package/src/scheduler-summary.test.ts +0 -47
- package/src/scheduler-summary.ts +0 -81
- package/src/scheduling.test.ts +0 -194
- package/src/scheduling.ts +0 -293
- package/src/schema-designer.test.ts +0 -121
- package/src/schema-designer.ts +0 -142
- package/src/schema.test.ts +0 -268
- package/src/schema.ts +0 -562
- package/src/smart-shim.ts +0 -107
- package/src/sources/aggregate.test.ts +0 -79
- package/src/sources/aggregate.ts +0 -102
- package/src/sources/auth-supabase.test.ts +0 -80
- package/src/sources/auth-supabase.ts +0 -87
- package/src/sources/dashboard.kpi.test.ts +0 -50
- package/src/sources/dashboard.test.ts +0 -61
- package/src/sources/dashboard.ts +0 -136
- package/src/sources/field-inference.test.ts +0 -60
- package/src/sources/field-inference.ts +0 -63
- package/src/sources/filters.test.ts +0 -58
- package/src/sources/filters.ts +0 -70
- package/src/sources/index.ts +0 -67
- package/src/sources/introspect-supabase.test.ts +0 -112
- package/src/sources/introspect-supabase.ts +0 -139
- package/src/sources/realtime-supabase.test.ts +0 -93
- package/src/sources/realtime-supabase.ts +0 -99
- package/src/sources/relation-lookup.test.ts +0 -99
- package/src/sources/relation-lookup.ts +0 -128
- package/src/sources/rest-adapters.test.ts +0 -95
- package/src/sources/rest-adapters.ts +0 -120
- package/src/sources/rest.test.ts +0 -104
- package/src/sources/rest.ts +0 -129
- package/src/sources/schema-from-columns.test.ts +0 -106
- package/src/sources/schema-from-columns.ts +0 -88
- package/src/sources/supabase.test.ts +0 -110
- package/src/sources/supabase.ts +0 -99
- package/src/sources/with-entity-rules.test.ts +0 -104
- package/src/sources/with-entity-rules.ts +0 -78
- package/src/sources/with-relation-labels.test.ts +0 -75
- package/src/sources/with-relation-labels.ts +0 -73
- package/src/staged-editing.ts +0 -0
- package/src/studio/bug-report.test.ts +0 -101
- package/src/studio/bug-report.ts +0 -165
- package/src/studio/cli.test.ts +0 -187
- package/src/studio/cli.ts +0 -117
- package/src/studio/copilot-core.test.ts +0 -45
- package/src/studio/csv.test.ts +0 -90
- package/src/studio/csv.ts +0 -165
- package/src/studio/db-connect-string.test.ts +0 -94
- package/src/studio/db-connect-string.ts +0 -128
- package/src/studio/deploy-cli.test.ts +0 -56
- package/src/studio/deploy-cli.ts +0 -100
- package/src/studio/emit-project.test.ts +0 -3324
- package/src/studio/emit-project.ts +0 -5377
- package/src/studio/emit-schema.test.ts +0 -111
- package/src/studio/emit-schema.ts +0 -1181
- package/src/studio/index.ts +0 -380
- package/src/studio/init-flow.test.ts +0 -514
- package/src/studio/init-flow.ts +0 -556
- package/src/studio/introspect-db.test.ts +0 -186
- package/src/studio/introspect-db.ts +0 -312
- package/src/studio/introspect-openapi.test.ts +0 -84
- package/src/studio/introspect-openapi.ts +0 -252
- package/src/studio/introspect-prisma.test.ts +0 -99
- package/src/studio/introspect-prisma.ts +0 -175
- package/src/studio/introspect.test.ts +0 -172
- package/src/studio/introspect.ts +0 -310
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/_svelte_metadata.json +0 -1
- package/src/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/package.json +0 -3
- package/src/studio/pipeline.test.ts +0 -42
- package/src/studio/project-robust.test.ts +0 -157
- package/src/studio/project.test.ts +0 -929
- package/src/studio/project.ts +0 -2394
- package/src/studio/sample-data.test.ts +0 -58
- package/src/studio/sample-data.ts +0 -203
- package/src/studio/samples/ats.ts +0 -202
- package/src/studio/samples/clinic.ts +0 -179
- package/src/studio/samples/crm.ts +0 -291
- package/src/studio/samples/datasets.test.ts +0 -84
- package/src/studio/samples/datasets.ts +0 -340
- package/src/studio/samples/ecommerce.ts +0 -187
- package/src/studio/samples/events.ts +0 -201
- package/src/studio/samples/fleet.ts +0 -213
- package/src/studio/samples/gym.ts +0 -215
- package/src/studio/samples/hr.ts +0 -195
- package/src/studio/samples/index.ts +0 -55
- package/src/studio/samples/insurance.ts +0 -229
- package/src/studio/samples/inventory.ts +0 -213
- package/src/studio/samples/invoicing.ts +0 -162
- package/src/studio/samples/library.ts +0 -180
- package/src/studio/samples/live-data.test.ts +0 -99
- package/src/studio/samples/live-data.ts +0 -306
- package/src/studio/samples/projects.ts +0 -212
- package/src/studio/samples/realestate.ts +0 -196
- package/src/studio/samples/restaurant.ts +0 -189
- package/src/studio/samples/samples.test.ts +0 -304
- package/src/studio/samples/school.ts +0 -197
- package/src/studio/samples/seed-floor.test.ts +0 -25
- package/src/studio/samples/shared.ts +0 -237
- package/src/studio/samples/starter.ts +0 -251
- package/src/studio/samples/subscriptions.ts +0 -183
- package/src/studio/samples/support.ts +0 -216
- package/src/studio/scaffold-app.test.ts +0 -91
- package/src/studio/scaffold.test.ts +0 -192
- package/src/studio/scaffold.ts +0 -390
- package/src/studio/screen-suites.test.ts +0 -269
- package/src/studio/screen-suites.ts +0 -448
- package/src/studio/themes.ts +0 -62
- package/src/studio/ui-components-surface.test.ts +0 -185
- package/src/studio/ui-components.generated.ts +0 -7154
- package/src/studio/ui-components.ts +0 -742
- package/src/studio/verify.test.ts +0 -47
- package/src/studio/verify.ts +0 -79
- package/src/sveltekit/in-memory.test.ts +0 -104
- package/src/sveltekit/in-memory.ts +0 -129
- package/src/sveltekit/index.ts +0 -24
- package/src/sveltekit/query-plan.test.ts +0 -109
- package/src/sveltekit/query-plan.ts +0 -125
- package/src/sveltekit/sql-source.test.ts +0 -198
- package/src/sveltekit/sql-source.ts +0 -171
- package/src/sveltekit/sql.test.ts +0 -80
- package/src/sveltekit/sql.ts +0 -131
- package/src/sveltekit/transport-scope.test.ts +0 -125
- package/src/sveltekit/transport.test.ts +0 -201
- package/src/sveltekit/transport.ts +0 -373
- package/src/upgrade-prompt.test.ts +0 -71
- package/src/upgrade-prompt.ts +0 -162
- package/src/version.test.ts +0 -17
- package/src/watermark.test.ts +0 -97
- package/src/watermark.ts +0 -156
- /package/{src → dist}/SvAlertRuleEditor.svelte +0 -0
- /package/{src → dist}/SvAlertsManager.svelte +0 -0
- /package/{src → dist}/SvAlertsPanel.svelte +0 -0
- /package/{src → dist}/SvAuthGate.svelte +0 -0
- /package/{src → dist}/SvBoard.svelte +0 -0
- /package/{src → dist}/SvExportMenu.svelte +0 -0
- /package/{src → dist}/SvFileInput.svelte +0 -0
- /package/{src → dist}/SvGridAlerts.svelte +0 -0
- /package/{src → dist}/SvGridBoard.svelte +0 -0
- /package/{src → dist}/SvGridMasterDetail.svelte +0 -0
- /package/{src → dist}/SvGridScheduler.svelte +0 -0
- /package/{src → dist}/SvImportDialog.svelte +0 -0
- /package/{src → dist}/SvLookupInput.svelte +0 -0
- /package/{src → dist}/SvPivotDesigner.svelte +0 -0
- /package/{src → dist}/SvRecordDetail.svelte +0 -0
- /package/{src → dist}/SvSchedule.svelte +0 -0
- /package/{src → dist}/SvSchemaChart.svelte +0 -0
- /package/{src → dist}/SvSchemaDashboard.svelte +0 -0
- /package/{src → dist}/pdfmake-shims.d.ts +0 -0
- /package/{src → dist}/smart.export.d.ts +0 -0
- /package/{src → dist}/smart.export.js +0 -0
- /package/{src → dist}/studio/HANDLERS-DESIGN.md +0 -0
- /package/{src → dist/studio}/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/_svelte_metadata.json +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/_metadata.json +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/attachments-BOv1rBKp.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/attachments-BOv1rBKp.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/constants-B9vgIdCd.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/constants-B9vgIdCd.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/dev-T1qbW_qD.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/dev-T1qbW_qD.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/events-Bo11SfVK.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/events-Bo11SfVK.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/flags-X7D4vfrZ.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/flags-X7D4vfrZ.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/hydration-oyUje5i1.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/hydration-oyUje5i1.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/legacy-client-DeSQgSFm.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/legacy-client-DeSQgSFm.js.map +0 -0
- /package/{src → dist/studio}/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/package.json +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_animate.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_animate.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_attachments.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_compiler.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_compiler.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_easing.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_easing.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_events.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_client.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_disclose-version.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_disclose-version.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_async.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_async.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_legacy.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_legacy.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_tracing.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_flags_tracing.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_internal_server.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_legacy.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_legacy.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_motion.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_motion.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity_window.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_reactivity_window.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_server.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_store.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_store.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_transition.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/svelte_transition.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-BbWyXNk1.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-BbWyXNk1.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-DI_fC7vt.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/utils-DI_fC7vt.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/validate-Dyr3lQv9.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/validate-Dyr3lQv9.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/version-_9FHH-LF.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/version-_9FHH-LF.js.map +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/warnings-CUbSPnyP.js +0 -0
- /package/{src → dist}/studio/node_modules/.vite/vitest/be65d27ae088a0e03fd8e1331d90b01649464cb6/deps___vitest__/warnings-CUbSPnyP.js.map +0 -0
|
@@ -1,1181 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* emitStudioApp - turn a set of `EntitySchema`s into the source files of a
|
|
3
|
-
* runnable **create-studio** app (the EntityScreen-style template). This is what
|
|
4
|
-
* `npm create @svgrid/studio --from <schema>` writes instead of the seeded
|
|
5
|
-
* Customers/Orders: a `schemas.ts` + `data.ts` + one route per entity + the nav
|
|
6
|
-
* shell, all driven by the user's real Drizzle / Prisma schema.
|
|
7
|
-
*
|
|
8
|
-
* Pure string building (like `scaffold` / `scaffoldApp`), and node-safe - it
|
|
9
|
-
* lives in the `./studio` subtree so `create-studio` gets it from the Node
|
|
10
|
-
* build. It targets the template's conventions: `$lib/EntityScreen.svelte`,
|
|
11
|
-
* `$lib/schemas`, `$lib/data`, and the `st-*` / `.home` CSS classes.
|
|
12
|
-
*/
|
|
13
|
-
import { resolveIdField, titleCase, type EntityField, type EntitySchema, type ValidationRuleSpec } from '../schema.js'
|
|
14
|
-
import type { GeneratedFile } from './scaffold.js'
|
|
15
|
-
import type { EntityDataSource, RestAdapterConfig, RestSource, ShellConfig, ShellStyle, SqlDialectKind, SqlSource, EntityTriggers, TriggerEvent } from './project.js'
|
|
16
|
-
import { sanitizeClassName, compileTriggerSteps, TRIGGER_EVENTS } from './project.js'
|
|
17
|
-
|
|
18
|
-
/** Build the createKitHandlers `hooks: {...}` option from an entity's triggers.
|
|
19
|
-
* before-hooks alias the mutable payload as `v`; after-hooks alias the saved row. */
|
|
20
|
-
function triggerHooksOpt(triggers: EntityTriggers | undefined): string | null {
|
|
21
|
-
if (!triggers) return null
|
|
22
|
-
const SHAPE: Record<TriggerEvent, { param: string; alias: string }> = {
|
|
23
|
-
beforeCreate: { param: '{ values }', alias: 'const v = values as Record<string, unknown>' },
|
|
24
|
-
beforeUpdate: { param: '{ patch }', alias: 'const v = patch as Record<string, unknown>' },
|
|
25
|
-
beforeDelete: { param: '{ id }', alias: 'const v = { id } as Record<string, unknown>' },
|
|
26
|
-
afterCreate: { param: '{ row }', alias: 'const v = row as Record<string, unknown>' },
|
|
27
|
-
afterUpdate: { param: '{ row }', alias: 'const v = row as Record<string, unknown>' },
|
|
28
|
-
afterDelete: { param: '{ id }', alias: 'const v = { id } as Record<string, unknown>' },
|
|
29
|
-
}
|
|
30
|
-
const entries: string[] = []
|
|
31
|
-
for (const ev of TRIGGER_EVENTS) {
|
|
32
|
-
const steps = triggers[ev]
|
|
33
|
-
if (!steps?.length) continue
|
|
34
|
-
const { param, alias } = SHAPE[ev]
|
|
35
|
-
const body = compileTriggerSteps(steps).split('\n').map((l) => (l ? ' ' + l : l)).join('\n')
|
|
36
|
-
entries.push(` ${ev}: async (${param}) => {\n ${alias}\n${body}\n }`)
|
|
37
|
-
}
|
|
38
|
-
return entries.length ? `hooks: {\n${entries.join(',\n')},\n }` : null
|
|
39
|
-
}
|
|
40
|
-
import { generateValue } from './sample-data.js'
|
|
41
|
-
|
|
42
|
-
const pascal = (name: string): string =>
|
|
43
|
-
titleCase(name).replace(/\s+/g, '')
|
|
44
|
-
|
|
45
|
-
const camel = (name: string): string => {
|
|
46
|
-
const p = pascal(name)
|
|
47
|
-
return p.charAt(0).toLowerCase() + p.slice(1)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Entity field type -> a TS property type for the generated row type. */
|
|
51
|
-
function tsType(field: EntityField): string {
|
|
52
|
-
// A chips/tags editor stores multiple values (string[]) - its seed rows + form
|
|
53
|
-
// value are arrays, so the row type must be string[] even on a `text` field.
|
|
54
|
-
if (field.input?.editorType === 'chips') return 'string[]'
|
|
55
|
-
const type = field.type
|
|
56
|
-
if (type === 'number') return 'number'
|
|
57
|
-
if (type === 'boolean') return 'boolean'
|
|
58
|
-
if (type === 'json') return 'unknown'
|
|
59
|
-
return 'string' // text / date / dateString / datetime / enum / relation
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** How many sample rows to seed per entity. */
|
|
63
|
-
const SEED_COUNT = 6
|
|
64
|
-
|
|
65
|
-
type Names = {
|
|
66
|
-
type: string // PascalCase row type, e.g. `User`
|
|
67
|
-
schemaVar: string // e.g. `userSchema`
|
|
68
|
-
sourceVar: string // e.g. `userSource`
|
|
69
|
-
label: string // display label
|
|
70
|
-
route: string // route segment (= schema name)
|
|
71
|
-
idPrefix: string // seed id prefix for nextId()
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function namesFor(schema: EntitySchema): Names {
|
|
75
|
-
return {
|
|
76
|
-
type: pascal(schema.name),
|
|
77
|
-
schemaVar: `${camel(schema.name)}Schema`,
|
|
78
|
-
sourceVar: `${camel(schema.name)}Source`,
|
|
79
|
-
label: schema.label ?? titleCase(schema.name),
|
|
80
|
-
route: schema.name,
|
|
81
|
-
idPrefix: schema.name.charAt(0).toLowerCase() || 'r',
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Lookup variable name for a relation field, e.g. `postAuthorIdLookup`. */
|
|
86
|
-
export const lookupVar = (schema: EntitySchema, field: string) => `${camel(schema.name)}${pascal(field)}Lookup`
|
|
87
|
-
|
|
88
|
-
function rowType(schema: EntitySchema, derived: Set<string>): string {
|
|
89
|
-
// Runtime-filled columns are optional in the row type: relation labels (`derived`)
|
|
90
|
-
// and computed / no-code-formula fields (not present on seed rows / inserts).
|
|
91
|
-
// So is a field the form only asks for under a condition - a row that never met
|
|
92
|
-
// the condition simply does not carry it (a won deal has no "lost reason").
|
|
93
|
-
const optional = (f: EntityField) =>
|
|
94
|
-
derived.has(f.field) || !!f.formula || !!f.computed || !!f.when?.visible
|
|
95
|
-
const lines = schema.fields.map((f) => ` ${f.field}${optional(f) ? '?' : ''}: ${tsType(f)}`)
|
|
96
|
-
return `export type ${pascal(schema.name)} = {\n${lines.join('\n')}\n}`
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** Base (unwrapped) in-memory source variable, e.g. `userStore`. */
|
|
100
|
-
const storeVar = (schema: EntitySchema) => `${camel(schema.name)}Store`
|
|
101
|
-
|
|
102
|
-
type RelationInfo = { fkField: string; displayField: string; related: EntitySchema }
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Map each relation FK field (whose target resolves) to the denormalized display
|
|
106
|
-
* field name that `withRelationLabels` fills onto every row - e.g. `companyId` ->
|
|
107
|
-
* `company`, else `<fk>Label`. Used by the grid (via `relationInfos`) and by any
|
|
108
|
-
* view that renders a relation as text (board/calendar titles) so it shows the
|
|
109
|
-
* related label instead of the raw FK id. Order-dependent collision handling must
|
|
110
|
-
* match `relationInfos` exactly, so both go through here.
|
|
111
|
-
*/
|
|
112
|
-
export function relationDisplayFields(schema: EntitySchema, resolve: (name: string) => EntitySchema | undefined): Map<string, string> {
|
|
113
|
-
const existing = new Set(schema.fields.map((f) => f.field))
|
|
114
|
-
const out = new Map<string, string>()
|
|
115
|
-
for (const f of schema.fields) {
|
|
116
|
-
if (f.type !== 'relation' || !f.relation) continue
|
|
117
|
-
if (!resolve(f.relation.entity)) continue
|
|
118
|
-
// Show the label under a friendly name: `authorId` -> `author`, else `<fk>Label`.
|
|
119
|
-
let display = /(_id|Id)$/.test(f.field) ? f.field.replace(/(_id|Id)$/, '') : `${f.field}Label`
|
|
120
|
-
if (!display || existing.has(display)) display = `${f.field}Label`
|
|
121
|
-
while (existing.has(display)) display += '_'
|
|
122
|
-
existing.add(display)
|
|
123
|
-
out.set(f.field, display)
|
|
124
|
-
}
|
|
125
|
-
return out
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** Relation fields of a schema whose target is present in the set. */
|
|
129
|
-
function relationInfos(schema: EntitySchema, byName: Map<string, EntitySchema>): RelationInfo[] {
|
|
130
|
-
const display = relationDisplayFields(schema, (name) => byName.get(name))
|
|
131
|
-
const infos: RelationInfo[] = []
|
|
132
|
-
for (const f of schema.fields) {
|
|
133
|
-
if (f.type !== 'relation' || !f.relation) continue
|
|
134
|
-
const related = byName.get(f.relation.entity)
|
|
135
|
-
if (!related) continue
|
|
136
|
-
infos.push({ fkField: f.field, displayField: display.get(f.field)!, related })
|
|
137
|
-
}
|
|
138
|
-
return infos
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Rewrite a schema for a linked grid: hide each FK column from the grid (it
|
|
143
|
-
* stays a lookup in the form) and append a read-only display column that shows
|
|
144
|
-
* the related label (filled by `withRelationLabels` in data.ts).
|
|
145
|
-
*/
|
|
146
|
-
function prepareSchema(schema: EntitySchema, infos: RelationInfo[]): EntitySchema {
|
|
147
|
-
const pkId = resolveIdField(schema)
|
|
148
|
-
const fkFields = new Set(infos.map((i) => i.fkField))
|
|
149
|
-
const fields: EntityField[] = schema.fields.map((f) => {
|
|
150
|
-
let nf = f
|
|
151
|
-
// The in-memory starter uses string ids, so the key is a text field.
|
|
152
|
-
if (f.field === pkId && f.type !== 'text') nf = { ...nf, type: 'text' }
|
|
153
|
-
if (fkFields.has(f.field)) {
|
|
154
|
-
const prev = typeof nf.hidden === 'object' ? nf.hidden : {}
|
|
155
|
-
nf = { ...nf, hidden: { ...prev, grid: true } }
|
|
156
|
-
}
|
|
157
|
-
return nf
|
|
158
|
-
})
|
|
159
|
-
for (const i of infos) {
|
|
160
|
-
fields.push({ field: i.displayField, type: 'text', label: titleCase(i.displayField), readonly: true, hidden: { form: true } })
|
|
161
|
-
}
|
|
162
|
-
return { ...schema, fields }
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
type Prepared = { schema: EntitySchema; infos: RelationInfo[] }
|
|
166
|
-
|
|
167
|
-
/** N sample rows per entity, with valid (string) ids + foreign-key references. */
|
|
168
|
-
function generateSeed(entries: Prepared[]): Map<string, Array<Record<string, unknown>>> {
|
|
169
|
-
const seed = new Map<string, Array<Record<string, unknown>>>()
|
|
170
|
-
for (const { schema, infos } of entries) {
|
|
171
|
-
const pkId = resolveIdField(schema)
|
|
172
|
-
const idPrefix = namesFor(schema).idPrefix
|
|
173
|
-
const relPrefix = new Map(infos.map((i) => [i.fkField, namesFor(i.related).idPrefix]))
|
|
174
|
-
const display = new Set(infos.map((i) => i.displayField))
|
|
175
|
-
const rows: Array<Record<string, unknown>> = []
|
|
176
|
-
for (let i = 0; i < SEED_COUNT; i++) {
|
|
177
|
-
const row: Record<string, unknown> = {}
|
|
178
|
-
for (const f of schema.fields) {
|
|
179
|
-
if (display.has(f.field)) continue // relation label, filled at runtime
|
|
180
|
-
if (f.field === pkId) row[f.field] = `${idPrefix}${i + 1}`
|
|
181
|
-
else if (f.type === 'relation') {
|
|
182
|
-
const rp = relPrefix.get(f.field)
|
|
183
|
-
row[f.field] = rp ? `${rp}${(i % SEED_COUNT) + 1}` : ''
|
|
184
|
-
} else row[f.field] = generateValue(f, i)
|
|
185
|
-
}
|
|
186
|
-
rows.push(row)
|
|
187
|
-
}
|
|
188
|
-
seed.set(schema.name, rows)
|
|
189
|
-
}
|
|
190
|
-
return seed
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/** Compile a no-code formula into a JS expression: bare field names -> `$.field`
|
|
194
|
-
* (where `$` is the row, typed loosely so arithmetic never trips svelte-check). */
|
|
195
|
-
function compileFormula(formula: string, fieldNames: string[]): string {
|
|
196
|
-
const names = [...fieldNames].sort((a, b) => b.length - a.length).map((f) => f.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
|
197
|
-
if (!names.length) return formula
|
|
198
|
-
return formula.replace(new RegExp('\\b(' + names.join('|') + ')\\b', 'g'), '$.$1')
|
|
199
|
-
}
|
|
200
|
-
/** The condition a validation rule asserts must hold (fail -> attach its message). */
|
|
201
|
-
function compileValidation(r: ValidationRuleSpec): string {
|
|
202
|
-
const lhs = `$[${JSON.stringify(r.field)}]`
|
|
203
|
-
const num = (v: unknown) => (r.compareTo ? `Number($[${JSON.stringify(r.compareTo)}])` : Number(v))
|
|
204
|
-
const str = (v: unknown) => (r.compareTo ? `String($[${JSON.stringify(r.compareTo)}])` : JSON.stringify(String(v ?? '')))
|
|
205
|
-
switch (r.op) {
|
|
206
|
-
case 'eq': return `String(${lhs}) === ${str(r.value)}`
|
|
207
|
-
case 'ne': return `String(${lhs}) !== ${str(r.value)}`
|
|
208
|
-
case 'lt': return `Number(${lhs}) < ${num(r.value)}`
|
|
209
|
-
case 'lte': return `Number(${lhs}) <= ${num(r.value)}`
|
|
210
|
-
case 'gt': return `Number(${lhs}) > ${num(r.value)}`
|
|
211
|
-
case 'gte': return `Number(${lhs}) >= ${num(r.value)}`
|
|
212
|
-
case 'required': return `${lhs} != null && ${lhs} !== ''`
|
|
213
|
-
case 'maxLen': return `String(${lhs} ?? '').length <= ${Number(r.value)}`
|
|
214
|
-
case 'minLen': return `String(${lhs} ?? '').length >= ${Number(r.value)}`
|
|
215
|
-
default: return 'true'
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
function schemasModule(entries: Prepared[]): GeneratedFile {
|
|
220
|
-
const body = entries
|
|
221
|
-
.map(({ schema: s, infos }) => {
|
|
222
|
-
const n = namesFor(s)
|
|
223
|
-
const derived = new Set(infos.map((i) => i.displayField))
|
|
224
|
-
const fieldNames = s.fields.map((f) => f.field)
|
|
225
|
-
// Strip the no-code specs from the emitted DATA literal; compile them below
|
|
226
|
-
// into real `computed` / `hooks.validate` functions (JSON drops functions).
|
|
227
|
-
const clean: EntitySchema = {
|
|
228
|
-
...s,
|
|
229
|
-
fields: s.fields.map((f) => { const { formula: _f, ...rest } = f; return rest }),
|
|
230
|
-
}
|
|
231
|
-
delete (clean as { validations?: unknown }).validations
|
|
232
|
-
let out = `${rowType(s, derived)}\n\nexport const ${n.schemaVar}: EntitySchema<${n.type}> = ${JSON.stringify(clean, null, 2)}`
|
|
233
|
-
|
|
234
|
-
// Computed fields from formulas.
|
|
235
|
-
const aug: string[] = []
|
|
236
|
-
for (const f of s.fields) {
|
|
237
|
-
if (!f.formula || f.computed) continue
|
|
238
|
-
aug.push(`;(${n.schemaVar}.fields.find((f) => f.field === ${JSON.stringify(f.field)})!).computed = (row) => { const $ = row as Record<string, any>; return (${compileFormula(f.formula, fieldNames)}) }`)
|
|
239
|
-
}
|
|
240
|
-
// Cross-field validation rules -> hooks.validate.
|
|
241
|
-
if (s.validations?.length) {
|
|
242
|
-
const checks = s.validations.map((r) => ` if (!(${compileValidation(r)})) errors[${JSON.stringify(r.field)}] = ${JSON.stringify(r.message)}`).join('\n')
|
|
243
|
-
aug.push(`${n.schemaVar}.hooks = {\n ...${n.schemaVar}.hooks,\n validate: (values) => {\n const $ = values as Record<string, any>\n const errors: Record<string, string> = {}\n${checks}\n return Object.keys(errors).length ? errors : null\n },\n}`)
|
|
244
|
-
}
|
|
245
|
-
if (aug.length) out += '\n\n' + aug.join('\n')
|
|
246
|
-
return out
|
|
247
|
-
})
|
|
248
|
-
.join('\n\n')
|
|
249
|
-
return {
|
|
250
|
-
path: 'src/lib/schemas.ts',
|
|
251
|
-
description: 'One EntitySchema per entity, generated from your schema. Add computed fields, hooks, or validators here.',
|
|
252
|
-
contents: `/**
|
|
253
|
-
* Generated from your schema by \`npm create @svgrid/studio --from\`. Each
|
|
254
|
-
* EntitySchema drives the grid columns, the edit form, and validation. Add a
|
|
255
|
-
* field here (and to its type) and it shows up in the grid and the form.
|
|
256
|
-
*/
|
|
257
|
-
import type { EntitySchema } from '@svgrid/enterprise'
|
|
258
|
-
|
|
259
|
-
${body}
|
|
260
|
-
`,
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// --- REST / SQL source-expression helpers ----------------------------------
|
|
265
|
-
|
|
266
|
-
const BACKTICK = String.fromCharCode(96)
|
|
267
|
-
|
|
268
|
-
/** Join a base URL and a path, tolerating leading/trailing slashes + empty base. */
|
|
269
|
-
function joinUrl(base: string, path: string): string {
|
|
270
|
-
const b = (base ?? '').replace(/\/+$/, '')
|
|
271
|
-
const p = (path ?? '').replace(/^\/+/, '')
|
|
272
|
-
if (!b) return p
|
|
273
|
-
if (!p) return b
|
|
274
|
-
return `${b}/${p}`
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/** `body` + `data.items` -> `body?.data?.items` (safe access into a response). */
|
|
278
|
-
function dottedAccess(root: string, dotted: string): string {
|
|
279
|
-
return dotted.split('.').filter(Boolean).reduce((acc, key) => `${acc}?.${key}`, root)
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/** The REST collection URL with static path params substituted (`{id}` -> value). */
|
|
283
|
-
function buildRestUrl(src: RestSource): string {
|
|
284
|
-
let path = src.path
|
|
285
|
-
for (const p of src.params) {
|
|
286
|
-
if (p.location === 'path' && p.value) path = path.split(`{${p.name}}`).join(p.value)
|
|
287
|
-
}
|
|
288
|
-
return joinUrl(src.baseUrl, path)
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/** A `SqlDialect` object literal for the dialect, or null to use adapter defaults. */
|
|
292
|
-
function sqlDialectExpr(dialect?: SqlDialectKind): string | null {
|
|
293
|
-
switch (dialect) {
|
|
294
|
-
case 'postgres':
|
|
295
|
-
case 'supabase': return "{ placeholders: '$', ilike: true }"
|
|
296
|
-
case 'mysql': return `{ quote: '${BACKTICK}', placeholders: '?' }`
|
|
297
|
-
case 'mssql': return "{ placeholders: '@' }"
|
|
298
|
-
case 'turso': return "{ placeholders: '?' }" // libSQL / SQLite positional args
|
|
299
|
-
default: return null // sqlite / undefined -> adapter defaults
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
const sq = (s: string): string => `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
|
|
304
|
-
|
|
305
|
-
/** The generator name for a REST adapter kind. */
|
|
306
|
-
export function restAdapterImport(a: RestAdapterConfig): string {
|
|
307
|
-
return a.kind === 'dummyjson' ? 'dummyJsonAdapter' : a.kind === 'jsonServer' ? 'jsonServerAdapter' : 'offsetLimitAdapter'
|
|
308
|
-
}
|
|
309
|
-
/** The adapter spread emitted into the createRestDataSource(...) call - it supplies
|
|
310
|
-
* a `buildQuery` (real server paging/sort) + `parse` (row/total unwrap). */
|
|
311
|
-
function restAdapterExpr(a: RestAdapterConfig, T: string): string {
|
|
312
|
-
if (a.kind === 'dummyjson') return `...dummyJsonAdapter<${T}>(${a.rowsKey ? sq(a.rowsKey) : ''})`
|
|
313
|
-
if (a.kind === 'jsonServer') return `...jsonServerAdapter<${T}>()`
|
|
314
|
-
const fields: string[] = []
|
|
315
|
-
for (const k of ['offsetParam', 'limitParam', 'sortByParam', 'orderParam', 'searchParam', 'rowsKey', 'totalKey'] as const) {
|
|
316
|
-
const v = a[k]
|
|
317
|
-
if (v) fields.push(`${k}: ${sq(v)}`)
|
|
318
|
-
}
|
|
319
|
-
return `...offsetLimitAdapter<${T}>(${fields.length ? `{ ${fields.join(', ')} }` : ''})`
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/** The `createRestDataSource(...)` call for one entity's REST binding. */
|
|
323
|
-
function restStoreExpr(src: RestSource, storeName: string, T: string, schemaVar: string): string {
|
|
324
|
-
const opts: string[] = [`url: ${sq(buildRestUrl(src))}`, `schema: ${schemaVar}`]
|
|
325
|
-
if (src.idField) opts.push(`idField: ${sq(src.idField)}`)
|
|
326
|
-
const headers = src.params.filter((p) => p.location === 'header' && p.value !== undefined && p.value !== '')
|
|
327
|
-
if (headers.length) opts.push(`headers: { ${headers.map((h) => `${sq(h.name)}: ${sq(h.value ?? '')}`).join(', ')} }`)
|
|
328
|
-
const query = src.params.filter((p) => p.location === 'query' && p.value !== undefined && p.value !== '')
|
|
329
|
-
if (query.length) opts.push(`query: { ${query.map((q) => `${sq(q.name)}: ${sq(q.value ?? '')}`).join(', ')} }`)
|
|
330
|
-
// An adapter provides both buildQuery (server paging/sort) + parse, so it wins over
|
|
331
|
-
// the legacy manual rowsPath/totalPath (which only unwraps a single fetched page).
|
|
332
|
-
if (src.adapter) {
|
|
333
|
-
opts.push(restAdapterExpr(src.adapter, T))
|
|
334
|
-
} else if (src.rowsPath || src.totalPath) {
|
|
335
|
-
const rowsExpr = src.rowsPath ? dottedAccess('body', src.rowsPath) : 'body'
|
|
336
|
-
const totalExpr = src.totalPath ? dottedAccess('body', src.totalPath) : `(${rowsExpr})?.length`
|
|
337
|
-
opts.push(`parse: (body: any) => ({ rows: ${rowsExpr} ?? [], rowCount: Number(${totalExpr} ?? 0) })`)
|
|
338
|
-
}
|
|
339
|
-
return `const ${storeName} = createRestDataSource<${T}>({ ${opts.join(', ')} })`
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
type DataModuleNeeds = {
|
|
343
|
-
supabase: boolean
|
|
344
|
-
supabaseUrl?: string
|
|
345
|
-
supabaseKey?: string
|
|
346
|
-
/** SQL-bound entities get a connected `+server.ts` API route each. */
|
|
347
|
-
sqlRoutes: Array<{ schema: EntitySchema; table: string; dialect?: SqlDialectKind; dbSchema?: string }>
|
|
348
|
-
/** Embedded-Postgres (PGlite) entities: one shared client + a table each. */
|
|
349
|
-
pglite?: boolean
|
|
350
|
-
pgliteTables?: Array<{ schema: EntitySchema; table: string; seed?: Record<string, unknown>[] }>
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
/** Per-dialect server driver wiring for a connected `+server.ts` (reads `$env DATABASE_URL`). */
|
|
354
|
-
const SQL_DRIVERS: Record<'postgres' | 'mysql' | 'mssql' | 'sqlite' | 'turso', { dep: string; imports: string; setup: string; exec: string }> = {
|
|
355
|
-
postgres: { dep: 'pg', imports: `import pg from 'pg'`, setup: `const pool = new pg.Pool({ connectionString: env.DATABASE_URL })`, exec: `const result = await pool.query(text, params)\n return result.rows` },
|
|
356
|
-
mysql: { dep: 'mysql2', imports: `import mysql from 'mysql2/promise'`, setup: `const pool = mysql.createPool(env.DATABASE_URL ?? '')`, exec: `const [rows] = await pool.query(text, params)\n return rows as Record<string, unknown>[]` },
|
|
357
|
-
mssql: { dep: 'mssql', imports: `import mssql from 'mssql'`, setup: `const poolPromise = mssql.connect(env.DATABASE_URL ?? '')`, exec: `const pool = await poolPromise\n const request = pool.request()\n params.forEach((p, i) => request.input('p' + (i + 1), p))\n const result = await request.query(text)\n return result.recordset as Record<string, unknown>[]` },
|
|
358
|
-
sqlite: { dep: 'better-sqlite3', imports: `import Database from 'better-sqlite3'`, setup: `const db = new Database(env.DATABASE_URL ?? 'data.db')`, exec: `return db.prepare(text).all(...params) as Record<string, unknown>[]` },
|
|
359
|
-
// Turso / libSQL: hosted SQLite over HTTP. DATABASE_URL is the libsql:// URL,
|
|
360
|
-
// DATABASE_AUTH_TOKEN the database token. Runs on serverless + edge.
|
|
361
|
-
turso: { dep: '@libsql/client', imports: `import { createClient } from '@libsql/client'`, setup: `const client = createClient({ url: env.DATABASE_URL ?? '', authToken: env.DATABASE_AUTH_TOKEN })`, exec: `const rs = await client.execute({ sql: text, args: params })\n return rs.rows as unknown as Record<string, unknown>[]` },
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/** A fully-connected SvelteKit API route for a SQL-bound entity. When RBAC is on,
|
|
365
|
-
* the route imports the shared access policy and rejects unauthorized writes -
|
|
366
|
-
* server-enforced, so a tampered client can't bypass it. When audit is on, every
|
|
367
|
-
* successful write is recorded. */
|
|
368
|
-
function sqlRouteFile(schema: EntitySchema, table: string, dialect?: SqlDialectKind, feat: { access?: boolean; audit?: boolean; screenIds?: string[]; triggers?: EntityTriggers; dbSchema?: string; tenantField?: string } = {}): GeneratedFile {
|
|
369
|
-
const n = namesFor(schema)
|
|
370
|
-
const key = (dialect === 'supabase' ? 'postgres' : (dialect ?? 'postgres')) as 'postgres' | 'mysql' | 'mssql' | 'sqlite' | 'turso'
|
|
371
|
-
const driver = SQL_DRIVERS[key]
|
|
372
|
-
const dialectLiteral = sqlDialectExpr(dialect)
|
|
373
|
-
const accessImport = feat.access ? `\nimport { authorizeAction, getServerRole } from '$lib/access'` : ''
|
|
374
|
-
const auditImport = feat.audit ? `\nimport { recordAudit } from '$lib/audit'` : ''
|
|
375
|
-
const tenantImport = feat.tenantField ? `\nimport { requireTenant } from '$lib/server/tenant'` : ''
|
|
376
|
-
// Every connected route validates writes against the schema server-side, and
|
|
377
|
-
// (when enabled) authorizes them by role + records an audit entry.
|
|
378
|
-
const opts = [`schema: ${n.schemaVar}`, `source`, `validate: true`]
|
|
379
|
-
if (feat.access) opts.push(`// Server-enforced RBAC: the caller's role comes from the session (event.locals). Reads\n // are allowed only if the role can open one of this entity's own screens.\n authorize: ({ action, event }) => authorizeAction(getServerRole(event), action, ${JSON.stringify(feat.screenIds ?? [])})`)
|
|
380
|
-
if (feat.tenantField) opts.push(`// Multi-tenancy, enforced server-side: reads are filtered to the caller's tenant,\n // creates are stamped with it, and update/delete re-read the target under the\n // scope first - so a guessed id cannot reach another tenant's row.\n scope: ({ event }) => ({ field: ${JSON.stringify(feat.tenantField)}, value: requireTenant(event) })`)
|
|
381
|
-
if (feat.audit) opts.push(`// Record every successful write to the audit trail.\n audit: (e) => recordAudit({ entity: ${JSON.stringify(schema.name)}, action: e.action, recordId: e.id, values: e.values as Record<string, unknown> | undefined, actor: String(e.event.locals?.role ?? e.event.locals?.user ?? 'system') })`)
|
|
382
|
-
// Server-enforced business rules: the entity's triggers compiled to lifecycle hooks.
|
|
383
|
-
const hooks = triggerHooksOpt(feat.triggers)
|
|
384
|
-
if (hooks) opts.push(`// Business rules, enforced server-side (a client that skips them still can't write bad data).\n ${hooks}`)
|
|
385
|
-
const handlers = `export const { POST } = createKitHandlers({\n ${opts.join(',\n ')},\n})`
|
|
386
|
-
return {
|
|
387
|
-
path: `src/routes/api/${n.route}/+server.ts`,
|
|
388
|
-
description: `Connected API route for ${n.label} (SQL via DATABASE_URL). Runs server-side.`,
|
|
389
|
-
contents: `${driver.imports}
|
|
390
|
-
import { env } from '$env/dynamic/private'
|
|
391
|
-
import { createKitHandlers, createSqlDataSource } from '@svgrid/enterprise'
|
|
392
|
-
import { ${n.schemaVar}, type ${n.type} } from '$lib/schemas'${accessImport}${auditImport}${tenantImport}
|
|
393
|
-
|
|
394
|
-
${driver.setup}
|
|
395
|
-
|
|
396
|
-
const source = createSqlDataSource<${n.type}>({
|
|
397
|
-
schema: ${n.schemaVar},
|
|
398
|
-
table: ${JSON.stringify(table)},${feat.dbSchema ? `\n dbSchema: ${JSON.stringify(feat.dbSchema)},` : ''}${dialectLiteral ? `\n dialect: ${dialectLiteral},` : ''}
|
|
399
|
-
execute: async (text, params) => {
|
|
400
|
-
${driver.exec}
|
|
401
|
-
},
|
|
402
|
-
})
|
|
403
|
-
|
|
404
|
-
${handlers}
|
|
405
|
-
`,
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
/** Map an EntityField type to a Postgres column type for PGlite DDL. */
|
|
410
|
-
const PG_COL_TYPE: Record<string, string> = { number: 'double precision', boolean: 'boolean', date: 'date', datetime: 'timestamptz', json: 'jsonb' }
|
|
411
|
-
/** The shared PGlite client + table DDL + one-time seed, injected into data.ts. */
|
|
412
|
-
function pgliteBootstrap(tables: Array<{ schema: EntitySchema; table: string; seed?: Record<string, unknown>[] }>, seed: Map<string, Array<Record<string, unknown>>>): string {
|
|
413
|
-
const ddls: string[] = []
|
|
414
|
-
const seeds: string[] = []
|
|
415
|
-
for (const { schema, table, seed: curated } of tables) {
|
|
416
|
-
const pk = schema.idField ?? schema.fields.find((f) => f.primaryKey)?.field ?? 'id'
|
|
417
|
-
const cols = schema.fields.map((f) => `"${f.field}" ${PG_COL_TYPE[f.type] ?? 'text'}${f.primaryKey || f.field === pk ? ' primary key' : ''}`).join(', ')
|
|
418
|
-
ddls.push(`CREATE TABLE IF NOT EXISTS "${table}" (${cols});`)
|
|
419
|
-
// Migrate schema additions: fields added after the DB was first created still
|
|
420
|
-
// appear (CREATE TABLE IF NOT EXISTS is a no-op once the table exists).
|
|
421
|
-
for (const f of schema.fields) {
|
|
422
|
-
if (f.primaryKey || f.field === pk) continue
|
|
423
|
-
ddls.push(`ALTER TABLE "${table}" ADD COLUMN IF NOT EXISTS "${f.field}" ${PG_COL_TYPE[f.type] ?? 'text'};`)
|
|
424
|
-
}
|
|
425
|
-
// Prefer rows imported/curated on the source (e.g. a CSV import), else the generator.
|
|
426
|
-
const rows = curated ?? seed.get(schema.name) ?? []
|
|
427
|
-
seeds.push(` await _pgSeed(${JSON.stringify(table)}, ${JSON.stringify(rows)}, ${JSON.stringify(schema.fields.map((f) => f.field))})`)
|
|
428
|
-
}
|
|
429
|
-
return `// Embedded Postgres (PGlite), persisted in the browser (IndexedDB) - a real,
|
|
430
|
-
// persistent database with ZERO backend setup. To go to production, bind these
|
|
431
|
-
// entities to a hosted SQL source (same schema) instead.
|
|
432
|
-
const pg = new PGlite('idb://svgrid-studio')
|
|
433
|
-
const pgReady = (async () => {
|
|
434
|
-
await pg.exec(\`
|
|
435
|
-
${ddls.join('\n ')}
|
|
436
|
-
\`)
|
|
437
|
-
${seeds.join('\n')}
|
|
438
|
-
})()
|
|
439
|
-
async function _pgSeed(table: string, rows: Record<string, unknown>[], cols: string[]): Promise<void> {
|
|
440
|
-
if (!rows.length) return
|
|
441
|
-
const existing = await pg.query(\`select count(*)::int as n from "\${table}"\`)
|
|
442
|
-
if (((existing.rows[0] as { n?: number } | undefined)?.n ?? 0) > 0) return // already seeded
|
|
443
|
-
for (const row of rows) {
|
|
444
|
-
await pg.query(
|
|
445
|
-
\`insert into "\${table}" (\${cols.map((c) => '"' + c + '"').join(', ')}) values (\${cols.map((_, i) => '$' + (i + 1)).join(', ')})\`,
|
|
446
|
-
cols.map((c) => { const v = row[c]; return v != null && typeof v === 'object' ? JSON.stringify(v) : (v ?? null) }),
|
|
447
|
-
)
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
`
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// ---------------------------------------------------------------------------
|
|
454
|
-
// db/schema.sql + db/seed.sql - plain SQL a user runs against their database
|
|
455
|
-
// before first deploy, for SQL entities WITHOUT the Drizzle data layer (which
|
|
456
|
-
// owns DDL via migrations when active). This closes the "tables must already
|
|
457
|
-
// exist" cliff on every dialect - including MSSQL, which Drizzle can't cover.
|
|
458
|
-
// ---------------------------------------------------------------------------
|
|
459
|
-
|
|
460
|
-
type DdlDialect = 'postgres' | 'mysql' | 'sqlite' | 'mssql'
|
|
461
|
-
|
|
462
|
-
/** Normalize a source dialect to a DDL family (supabase = postgres, turso = sqlite). */
|
|
463
|
-
const ddlDialect = (d?: SqlDialectKind): DdlDialect =>
|
|
464
|
-
d === 'mysql' ? 'mysql' : d === 'sqlite' || d === 'turso' ? 'sqlite' : d === 'mssql' ? 'mssql' : 'postgres'
|
|
465
|
-
|
|
466
|
-
/** Column types per dialect (default column type = the `text` entry). */
|
|
467
|
-
const DDL_COL_TYPES: Record<DdlDialect, Record<string, string>> = {
|
|
468
|
-
postgres: { text: 'text', number: 'double precision', boolean: 'boolean', date: 'date', dateString: 'date', datetime: 'timestamptz', json: 'jsonb' },
|
|
469
|
-
mysql: { text: 'varchar(255)', number: 'double', boolean: 'tinyint(1)', date: 'date', dateString: 'date', datetime: 'datetime', json: 'json' },
|
|
470
|
-
sqlite: { text: 'text', number: 'real', boolean: 'integer', date: 'text', dateString: 'text', datetime: 'text', json: 'text' },
|
|
471
|
-
mssql: { text: 'nvarchar(255)', number: 'float', boolean: 'bit', date: 'date', dateString: 'date', datetime: 'datetime2', json: 'nvarchar(max)' },
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/** Identifier quoter per dialect (doubles the quote char to escape, like sql.ts). */
|
|
475
|
-
function ddlQuoter(dialect: DdlDialect): (name: string) => string {
|
|
476
|
-
const q = dialect === 'mysql' ? '`' : '"'
|
|
477
|
-
return (name) => `${q}${name.replaceAll(q, q + q)}${q}`
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/** A SQL literal for a seed value, per dialect. */
|
|
481
|
-
function ddlLiteral(v: unknown, dialect: DdlDialect): string {
|
|
482
|
-
if (v == null || v === '') return 'NULL'
|
|
483
|
-
if (typeof v === 'number') return Number.isFinite(v) ? String(v) : 'NULL'
|
|
484
|
-
if (typeof v === 'boolean') return dialect === 'postgres' ? (v ? 'TRUE' : 'FALSE') : v ? '1' : '0'
|
|
485
|
-
const s = typeof v === 'object' ? JSON.stringify(v) : String(v)
|
|
486
|
-
const escaped = s.replaceAll("'", "''")
|
|
487
|
-
return dialect === 'mssql' ? `N'${escaped}'` : `'${escaped}'`
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/** `db/schema.sql` + `db/seed.sql` for the project's SQL-bound entities. */
|
|
491
|
-
export function sqlDdlFiles(schemas: EntitySchema[], sources: Record<string, EntityDataSource>): GeneratedFile[] {
|
|
492
|
-
const { entries, seed } = prepareEntities(schemas)
|
|
493
|
-
const sqlEntries = entries
|
|
494
|
-
.map((e) => ({ ...e, src: sources[e.schema.name] }))
|
|
495
|
-
.filter((e): e is Prepared & { src: SqlSource } => e.src?.kind === 'sql')
|
|
496
|
-
if (sqlEntries.length === 0) return []
|
|
497
|
-
|
|
498
|
-
const create: string[] = []
|
|
499
|
-
const inserts: string[] = []
|
|
500
|
-
for (const { schema, infos, src } of sqlEntries) {
|
|
501
|
-
const dialect = ddlDialect(src.dialect)
|
|
502
|
-
const q = ddlQuoter(dialect)
|
|
503
|
-
const types = DDL_COL_TYPES[dialect]
|
|
504
|
-
const pk = resolveIdField(schema)
|
|
505
|
-
// Relation display fields are filled at runtime (labels), not stored.
|
|
506
|
-
const display = new Set(infos.map((i) => i.displayField))
|
|
507
|
-
const cols = schema.fields.filter((f) => !display.has(f.field) && !f.formula && !f.computed)
|
|
508
|
-
const colDefs = cols.map((f) => ` ${q(f.field)} ${types[f.type] ?? types.text}${f.field === pk ? ' PRIMARY KEY' : ''}`)
|
|
509
|
-
const body = `CREATE TABLE ${dialect === 'mssql' ? '' : 'IF NOT EXISTS '}${q(src.table)} (\n${colDefs.join(',\n')}\n)`
|
|
510
|
-
create.push(dialect === 'mssql' ? `IF OBJECT_ID(N'${src.table.replaceAll("'", "''")}', N'U') IS NULL\n${body};` : `${body};`)
|
|
511
|
-
|
|
512
|
-
const rows = seed.get(schema.name) ?? []
|
|
513
|
-
if (rows.length) {
|
|
514
|
-
const names = cols.map((f) => q(f.field)).join(', ')
|
|
515
|
-
const values = rows.map((r) => ` (${cols.map((f) => ddlLiteral(r[f.field], dialect)).join(', ')})`).join(',\n')
|
|
516
|
-
inserts.push(`INSERT INTO ${q(src.table)} (${names}) VALUES\n${values};`)
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
const header = (what: string) =>
|
|
521
|
-
`-- ${what} for the app's SQL entities. Generated by SvGrid Studio.\n` +
|
|
522
|
-
`-- Run against the database your DATABASE_URL points at (psql/mysql/sqlite3/sqlcmd,\n` +
|
|
523
|
-
`-- or your provider's SQL console) before first use. Tip: the Drizzle data layer\n` +
|
|
524
|
-
`-- replaces this file with real, versioned migrations.\n\n`
|
|
525
|
-
return [
|
|
526
|
-
{ path: 'db/schema.sql', description: 'CREATE TABLE statements for the SQL entities (run once against your database).', contents: header('Table definitions') + create.join('\n\n') + '\n' },
|
|
527
|
-
...(inserts.length ? [{ path: 'db/seed.sql', description: 'Optional sample rows for the SQL entities.', contents: header('Sample seed rows (optional)') + inserts.join('\n\n') + '\n' }] : []),
|
|
528
|
-
]
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
function dataModule(
|
|
532
|
-
entries: Prepared[],
|
|
533
|
-
seed: Map<string, Array<Record<string, unknown>>>,
|
|
534
|
-
sources?: Record<string, EntityDataSource>,
|
|
535
|
-
supabaseConn?: { url?: string; key?: string },
|
|
536
|
-
): { file: GeneratedFile; needs: DataModuleNeeds } {
|
|
537
|
-
const schemas = entries.map((e) => e.schema)
|
|
538
|
-
// Each type gets its own `type` keyword - SvelteKit enables verbatimModuleSyntax.
|
|
539
|
-
const typeImports = schemas.map((s) => `type ${pascal(s.name)}`).join(', ')
|
|
540
|
-
const schemaImports = schemas.map((s) => namesFor(s).schemaVar).join(', ')
|
|
541
|
-
const hasRelations = entries.some((e) => e.infos.length > 0)
|
|
542
|
-
const entImports = new Set<string>()
|
|
543
|
-
const needs: DataModuleNeeds = { supabase: false, sqlRoutes: [] }
|
|
544
|
-
|
|
545
|
-
// 1. Base store per entity, branched on its bound data source (default: seeded
|
|
546
|
-
// in-memory, so the app runs with no backend).
|
|
547
|
-
const stores = entries
|
|
548
|
-
.map((e) => {
|
|
549
|
-
const T = pascal(e.schema.name)
|
|
550
|
-
const sv = namesFor(e.schema).schemaVar
|
|
551
|
-
const store = storeVar(e.schema)
|
|
552
|
-
const src = sources?.[e.schema.name] ?? { kind: 'memory' as const }
|
|
553
|
-
switch (src.kind) {
|
|
554
|
-
case 'rest':
|
|
555
|
-
entImports.add('createRestDataSource')
|
|
556
|
-
if (src.adapter) entImports.add(restAdapterImport(src.adapter))
|
|
557
|
-
return restStoreExpr(src, store, T, sv)
|
|
558
|
-
case 'sql':
|
|
559
|
-
// SQL runs server-side: the client talks to a generated /api/<entity> route.
|
|
560
|
-
entImports.add('createKitDataSource')
|
|
561
|
-
needs.sqlRoutes.push({ schema: e.schema, table: src.table, dialect: src.dialect, dbSchema: src.schema })
|
|
562
|
-
return `const ${store} = createKitDataSource<${T}>({ endpoint: '/api/${namesFor(e.schema).route}' })`
|
|
563
|
-
case 'supabase':
|
|
564
|
-
entImports.add('createSupabaseDataSource')
|
|
565
|
-
needs.supabase = true
|
|
566
|
-
// Per-entity url/key overrides the project-level connection for the .env hint.
|
|
567
|
-
if (src.url && src.key && !needs.supabaseUrl) { needs.supabaseUrl = src.url; needs.supabaseKey = src.key }
|
|
568
|
-
return `const ${store} = createSupabaseDataSource<${T}>({ client: supabaseClient, table: ${sq(src.table)}, schema: ${sv} })`
|
|
569
|
-
case 'pglite': {
|
|
570
|
-
// Embedded Postgres (PGlite) in the browser - real SQL, persisted to
|
|
571
|
-
// IndexedDB, zero backend setup. Shares one `pg` client + `pgReady`.
|
|
572
|
-
entImports.add('createSqlDataSource')
|
|
573
|
-
needs.pglite = true
|
|
574
|
-
needs.pgliteTables ??= []
|
|
575
|
-
needs.pgliteTables.push({ schema: e.schema, table: src.table, seed: src.seed })
|
|
576
|
-
return `const ${store} = createSqlDataSource<${T}>({ schema: ${sv}, table: ${sq(src.table)}, dialect: { placeholders: '$', ilike: true }, execute: async (text, params) => { await pgReady; return (await pg.query(text, params)).rows as Record<string, unknown>[] } })`
|
|
577
|
-
}
|
|
578
|
-
default: {
|
|
579
|
-
entImports.add('createInMemoryDataSource')
|
|
580
|
-
// Prefer curated seed on the source (a sample app), else the realistic generator.
|
|
581
|
-
const curated = src.kind === 'memory' ? src.seed : undefined
|
|
582
|
-
const rows = JSON.stringify(curated ?? seed.get(e.schema.name) ?? [])
|
|
583
|
-
return `const ${store} = createInMemoryDataSource<${T}>(${rows}, ${sv})`
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
})
|
|
587
|
-
.join('\n')
|
|
588
|
-
|
|
589
|
-
// 2. A searchable picker per relation, reading the related store.
|
|
590
|
-
const lookups: string[] = []
|
|
591
|
-
for (const { schema: s, infos } of entries) {
|
|
592
|
-
for (const info of infos) {
|
|
593
|
-
const rn = namesFor(info.related)
|
|
594
|
-
lookups.push(
|
|
595
|
-
`export const ${lookupVar(s, info.fkField)} = createRelationLookup<${rn.type}>({ source: ${storeVar(info.related)}, schema: ${rn.schemaVar}, labelField: '${(s.fields.find((f) => f.field === info.fkField)?.relation?.labelField) ?? 'name'}' })`,
|
|
596
|
-
)
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
if (hasRelations) { entImports.add('createRelationLookup'); entImports.add('withRelationLabels') }
|
|
600
|
-
|
|
601
|
-
// 3. Exported sources: wrap with withRelationLabels so the grid shows labels.
|
|
602
|
-
const sourceLines = entries
|
|
603
|
-
.map(({ schema: s, infos }) => {
|
|
604
|
-
const n = namesFor(s)
|
|
605
|
-
if (infos.length === 0) return `export const ${n.sourceVar} = ${storeVar(s)}`
|
|
606
|
-
const cfgs = infos.map((i) => `{ fk: '${i.fkField}', lookup: ${lookupVar(s, i.fkField)}, as: '${i.displayField}' }`).join(', ')
|
|
607
|
-
return `export const ${n.sourceVar} = withRelationLabels(${storeVar(s)}, [${cfgs}])`
|
|
608
|
-
})
|
|
609
|
-
.join('\n')
|
|
610
|
-
|
|
611
|
-
// Project-level connection fills the .env hint when no entity carries its own url/key.
|
|
612
|
-
if (needs.supabase && !needs.supabaseUrl && (supabaseConn?.url || supabaseConn?.key)) {
|
|
613
|
-
needs.supabaseUrl = supabaseConn.url
|
|
614
|
-
needs.supabaseKey = supabaseConn.key
|
|
615
|
-
}
|
|
616
|
-
const connImportLine = needs.supabase ? `\nimport { supabaseClient } from './connections'` : ''
|
|
617
|
-
const pgliteImportLine = needs.pglite ? `\nimport { PGlite } from '@electric-sql/pglite'` : ''
|
|
618
|
-
const pgliteBoot = needs.pglite ? pgliteBootstrap(needs.pgliteTables!, seed) + '\n' : ''
|
|
619
|
-
|
|
620
|
-
const file: GeneratedFile = {
|
|
621
|
-
path: 'src/lib/data.ts',
|
|
622
|
-
description: 'Data sources per entity (in-memory / REST / SQL / Supabase / PGlite) + a searchable lookup per relation.',
|
|
623
|
-
contents: `/**
|
|
624
|
-
* Data sources. Each entity binds to its own backend - in-memory (seeded), a
|
|
625
|
-
* REST endpoint, a SQL table, Supabase, or embedded Postgres (PGlite). In-memory
|
|
626
|
-
* + PGlite run with no setup; SQL / Supabase read their connection from \`./connections\`.
|
|
627
|
-
*/
|
|
628
|
-
import { ${[...entImports].join(', ')} } from '@svgrid/enterprise'
|
|
629
|
-
import { ${schemaImports}, ${typeImports} } from './schemas'${connImportLine}${pgliteImportLine}
|
|
630
|
-
|
|
631
|
-
${pgliteBoot}${stores}
|
|
632
|
-
${lookups.length ? `\n// Searchable pickers for relation (foreign-key) fields.\n${lookups.join('\n')}\n` : ''}
|
|
633
|
-
// Grids show the related label (not the raw id) via withRelationLabels.
|
|
634
|
-
${sourceLines}
|
|
635
|
-
|
|
636
|
-
// New-row ids continue after the seeded rows.
|
|
637
|
-
let seq = ${SEED_COUNT}
|
|
638
|
-
export const nextId = (prefix: string) => \`\${prefix}\${++seq}\`
|
|
639
|
-
`,
|
|
640
|
-
}
|
|
641
|
-
return { file, needs }
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
/** The Supabase client wiring. Reads the project URL + anon key from PUBLIC_ env vars
|
|
645
|
-
* (never inlined into source); the anon key is public / browser-safe, so this is a
|
|
646
|
-
* secret-hygiene win and matches SvelteKit convention. The designer-entered values are
|
|
647
|
-
* shown as a comment so they can be pasted straight into `.env` / `.env.example`. */
|
|
648
|
-
function connectionsModule(needs: DataModuleNeeds): GeneratedFile {
|
|
649
|
-
const hint = needs.supabaseUrl || needs.supabaseKey
|
|
650
|
-
? `// From the Studio designer - paste into .env:\n// PUBLIC_SUPABASE_URL=${needs.supabaseUrl ?? ''}\n// PUBLIC_SUPABASE_ANON_KEY=${needs.supabaseKey ?? ''}\n`
|
|
651
|
-
: `// Set PUBLIC_SUPABASE_URL and PUBLIC_SUPABASE_ANON_KEY in .env (see .env.example).\n`
|
|
652
|
-
return {
|
|
653
|
-
path: 'src/lib/connections.ts',
|
|
654
|
-
description: 'Supabase client for Supabase-bound entities (reads PUBLIC_SUPABASE_* env vars).',
|
|
655
|
-
contents: `import { createClient } from '@supabase/supabase-js'
|
|
656
|
-
import { env } from '$env/dynamic/public'
|
|
657
|
-
import type { SupabaseClientLike } from '@svgrid/enterprise'
|
|
658
|
-
|
|
659
|
-
/**
|
|
660
|
-
* Supabase client. The anon key is public (browser-safe) - access is protected by
|
|
661
|
-
* your Row Level Security policies. Requires \`@supabase/supabase-js\`.
|
|
662
|
-
*/
|
|
663
|
-
${hint}export const supabaseClient = createClient(
|
|
664
|
-
env.PUBLIC_SUPABASE_URL ?? '',
|
|
665
|
-
env.PUBLIC_SUPABASE_ANON_KEY ?? '',
|
|
666
|
-
) as unknown as SupabaseClientLike
|
|
667
|
-
`,
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
/**
|
|
672
|
-
* A CRUD screen page (grid + form) for a prepared entity schema. `route` and
|
|
673
|
-
* `title` default to the entity's, but the Studio designer overrides them so a
|
|
674
|
-
* project can have custom routes / titles / multiple screens per entity.
|
|
675
|
-
*/
|
|
676
|
-
export function entityScreenPage(schema: EntitySchema, route?: string, title?: string): GeneratedFile {
|
|
677
|
-
const n = namesFor(schema)
|
|
678
|
-
const r = route ?? n.route
|
|
679
|
-
const t = title ?? n.label
|
|
680
|
-
const relationFields = schema.fields.filter((f) => f.type === 'relation' && f.relation)
|
|
681
|
-
const lookupImports = relationFields.map((f) => lookupVar(schema, f.field))
|
|
682
|
-
const dataImports = [n.sourceVar, ...lookupImports, 'nextId'].join(', ')
|
|
683
|
-
const lookupsProp = relationFields.length
|
|
684
|
-
? `\n lookups={{ ${relationFields.map((f) => `${f.field}: ${lookupVar(schema, f.field)}`).join(', ')} }}`
|
|
685
|
-
: ''
|
|
686
|
-
|
|
687
|
-
return {
|
|
688
|
-
path: `src/routes/${r}/+page.svelte`,
|
|
689
|
-
description: `CRUD screen for ${t}.`,
|
|
690
|
-
contents: `<script lang="ts">
|
|
691
|
-
import EntityScreen from '$lib/EntityScreen.svelte'
|
|
692
|
-
import { ${n.schemaVar} } from '$lib/schemas'
|
|
693
|
-
import { ${dataImports} } from '$lib/data'
|
|
694
|
-
</script>
|
|
695
|
-
|
|
696
|
-
<h1 class="st__title">${t}</h1>
|
|
697
|
-
<EntityScreen schema={${n.schemaVar}} source={${n.sourceVar}}${lookupsProp} newId={() => nextId('${n.idPrefix}')} />
|
|
698
|
-
`,
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
export type NavItem = { href: string; label: string; id?: string }
|
|
703
|
-
|
|
704
|
-
export function layoutFile(nav: NavItem[], opts: { accent?: string; shell?: ShellConfig; title?: string; themeVars?: Record<string, string>; lightVars?: Record<string, string>; darkVars?: Record<string, string>; dark?: boolean; access?: boolean; auth?: boolean; supabaseAuth?: boolean; authRoutes?: string[]; authAccount?: boolean; i18n?: boolean; appClass?: string } = {}): GeneratedFile {
|
|
705
|
-
// Nav is the app's own screens; `/` just redirects to the first one, so no separate
|
|
706
|
-
// "Home" link (it would duplicate the first screen).
|
|
707
|
-
const links = nav
|
|
708
|
-
const shell = opts.shell ?? {}
|
|
709
|
-
const style: ShellStyle = shell.style ?? 'sidebar'
|
|
710
|
-
// App-level styling hook: an extra class on the shell root so custom.css can target the whole app.
|
|
711
|
-
const appCls = sanitizeClassName(opts.appClass)
|
|
712
|
-
const appClsAttr = appCls ? ` ${appCls}` : ''
|
|
713
|
-
const brand = (shell.brand ?? '').trim() || opts.title || 'My Studio App'
|
|
714
|
-
const footer = shell.footer === undefined ? 'Built with SvGrid Studio' : shell.footer
|
|
715
|
-
const right = style === 'sidebar' && shell.navPosition === 'right'
|
|
716
|
-
// Emit the full theme token bundle so the generated app matches the look chosen
|
|
717
|
-
// in the designer. When both light + dark token sets are supplied we ship a
|
|
718
|
-
// built-in light/dark switcher: both sets are scoped by [data-theme] on <html>,
|
|
719
|
-
// and the bare :root falls back to the mode picked in Studio (pre-hydration).
|
|
720
|
-
const defaultMode: 'light' | 'dark' = opts.dark ? 'dark' : 'light'
|
|
721
|
-
const withAccent = (m?: Record<string, string>) => {
|
|
722
|
-
const v = { ...(m ?? {}) }
|
|
723
|
-
if (opts.accent) v['--sg-accent'] = opts.accent
|
|
724
|
-
return v
|
|
725
|
-
}
|
|
726
|
-
const declLines = (m: Record<string, string>) => Object.entries(m).map(([k, v]) => `${k}: ${v};`).join(' ')
|
|
727
|
-
const hasSwitch = !!(opts.lightVars && opts.darkVars)
|
|
728
|
-
let themeHead = ''
|
|
729
|
-
if (hasSwitch) {
|
|
730
|
-
const lv = withAccent(opts.lightVars)
|
|
731
|
-
const dv = withAccent(opts.darkVars)
|
|
732
|
-
const defRule = [declLines(defaultMode === 'dark' ? dv : lv), `color-scheme: ${defaultMode};`].join(' ')
|
|
733
|
-
const lightRule = [declLines(lv), 'color-scheme: light;'].join(' ')
|
|
734
|
-
const darkRule = [declLines(dv), 'color-scheme: dark;'].join(' ')
|
|
735
|
-
themeHead = `\n<svelte:head><style>:root { ${defRule} }\n:root[data-theme="light"] { ${lightRule} }\n:root[data-theme="dark"] { ${darkRule} }</style></svelte:head>\n`
|
|
736
|
-
} else {
|
|
737
|
-
const vars = withAccent(opts.themeVars)
|
|
738
|
-
const varLines = declLines(vars)
|
|
739
|
-
const rootRule = [varLines, opts.dark ? 'color-scheme: dark;' : ''].filter(Boolean).join(' ')
|
|
740
|
-
themeHead = rootRule ? `\n<svelte:head><style>:root { ${rootRule} }</style></svelte:head>\n` : ''
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
// i18n: translate nav labels via `nav.<id>` keys (Home has no id -> literal).
|
|
744
|
-
const navLabel = opts.i18n ? `{item.id ? $t('nav.' + item.id, item.label) : item.label}` : '{item.label}'
|
|
745
|
-
const anchor = `<a class="sv-app__link" class:is-active={page.url.pathname === item.href} href={item.href}>${navLabel}</a>`
|
|
746
|
-
// When RBAC is on, hide nav links the current role can't open.
|
|
747
|
-
const linkGate = opts.access
|
|
748
|
-
? `{#if !item.id || canScreen($currentRole, item.id)}${anchor}{/if}`
|
|
749
|
-
: anchor
|
|
750
|
-
const localeSwitcher = opts.i18n
|
|
751
|
-
? `\n <select class="sv-app__locale" aria-label="Language" onchange={(e) => currentLocale.set(e.currentTarget.value as typeof $currentLocale)}>
|
|
752
|
-
{#each locales as loc (loc)}<option value={loc} selected={loc === $currentLocale}>{loc}</option>{/each}
|
|
753
|
-
</select>`
|
|
754
|
-
: ''
|
|
755
|
-
const toolbarOn = shell.toolbar !== false && nav.length > 0
|
|
756
|
-
// Built-in light/dark switcher (a sun/moon button). Lives in the toolbar's tools
|
|
757
|
-
// cluster when the toolbar is on, else trails the nav links. Only rendered when
|
|
758
|
-
// both token sets are available (i.e. from the full Studio project emitter).
|
|
759
|
-
const themeToggleBtn = `<button type="button" class="sv-app__tool sv-app__theme" aria-label="Toggle color theme" title="Toggle light / dark" onclick={toggleTheme}>
|
|
760
|
-
{#if theme === 'dark'}<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>{:else}<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>{/if}
|
|
761
|
-
</button>`
|
|
762
|
-
// When there's no toolbar, the toggle trails the nav in its own inline slot.
|
|
763
|
-
const navToggleSlot = hasSwitch && !toolbarOn ? `\n <div class="sv-app__navtools">${themeToggleBtn}</div>` : ''
|
|
764
|
-
// Clicking any nav link closes the mobile drawer.
|
|
765
|
-
const linksMarkup = `<nav class="sv-app__links" onclick={() => (navOpen = false)}>
|
|
766
|
-
{#each nav as item (item.href)}
|
|
767
|
-
${linkGate}
|
|
768
|
-
{/each}
|
|
769
|
-
</nav>${localeSwitcher}${navToggleSlot}`
|
|
770
|
-
const footMarkup = footer ? `\n <span class="sv-app__foot">{footer}</span>` : ''
|
|
771
|
-
const footConst = footer ? `\n const footer = ${JSON.stringify(footer)}` : ''
|
|
772
|
-
// Sidebar-only: a collapsible sidebar that docks on wide screens and becomes an
|
|
773
|
-
// off-canvas drawer when collapsed or on tablet/phone (<= 1024px). The collapse
|
|
774
|
-
// choice persists; tablet/phone start collapsed. top-nav and bottom-nav are
|
|
775
|
-
// always-visible bars with no collapse/drawer state.
|
|
776
|
-
const sideState = style !== 'sidebar' ? '' : `
|
|
777
|
-
let collapsed = $state(false)
|
|
778
|
-
let narrow = $state(false)
|
|
779
|
-
const drawer = $derived(collapsed || narrow)
|
|
780
|
-
function toggleNav() { collapsed = !collapsed; try { localStorage.setItem('svapp:nav', collapsed ? 'c' : 'e') } catch (_) { /* storage blocked */ } }
|
|
781
|
-
$effect(() => {
|
|
782
|
-
// Tablet / phone (<= 1024px) collapse to a drawer by default.
|
|
783
|
-
const mq = window.matchMedia('(max-width: 1024px)')
|
|
784
|
-
const sync = () => (narrow = mq.matches)
|
|
785
|
-
sync()
|
|
786
|
-
try { const s = localStorage.getItem('svapp:nav'); if (s) collapsed = s === 'c' } catch (_) { /* storage blocked */ }
|
|
787
|
-
mq.addEventListener('change', sync)
|
|
788
|
-
return () => mq.removeEventListener('change', sync)
|
|
789
|
-
})`
|
|
790
|
-
|
|
791
|
-
// Brand: a company logo image (data URL / URL) when set, else the app name.
|
|
792
|
-
const logoConst = shell.logo ? `\n const logo = ${JSON.stringify(shell.logo)}` : ''
|
|
793
|
-
const brandInner = shell.logo
|
|
794
|
-
? `<img class="sv-app__logo" src={logo} alt={brand} />`
|
|
795
|
-
: `<span class="sv-app__brandtext">{brand}</span>`
|
|
796
|
-
const brandLink = `<a class="sv-app__brand" href="/">${brandInner}</a>`
|
|
797
|
-
// Collapsed bar (tablet / phone, or a collapsed desktop sidebar): a hamburger +
|
|
798
|
-
// brand. The nav slides in as an off-canvas drawer. On desktop the "dock" button
|
|
799
|
-
// pins the sidebar back open.
|
|
800
|
-
const mobileBar = `<header class="sv-app__mobilebar">
|
|
801
|
-
<button type="button" class="sv-app__burger" aria-label="Toggle navigation" aria-expanded={navOpen} onclick={() => (navOpen = !navOpen)}>
|
|
802
|
-
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
|
|
803
|
-
</button>
|
|
804
|
-
${brandLink}
|
|
805
|
-
{#if !narrow}<button type="button" class="sv-app__dock" aria-label="Dock sidebar" title="Dock sidebar" onclick={toggleNav}>
|
|
806
|
-
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6l6 6-6 6"/></svg>
|
|
807
|
-
</button>{/if}
|
|
808
|
-
</header>
|
|
809
|
-
{#if navOpen}<button type="button" class="sv-app__backdrop" aria-label="Close navigation" onclick={() => (navOpen = false)}></button>{/if}`
|
|
810
|
-
|
|
811
|
-
// App-chrome toolbar (docked at the top of the content area for both layouts):
|
|
812
|
-
// a functional quick-search over the app's screens + an account cluster. Reads as
|
|
813
|
-
// a real product header. Opt out with `shell.toolbar === false`.
|
|
814
|
-
const initials = ((brand.match(/\b[A-Za-z0-9]/g) ?? []).slice(0, 2).join('') || 'A').toUpperCase()
|
|
815
|
-
const brandSlug = brand.toLowerCase().replace(/[^a-z0-9]+/g, '') || 'app'
|
|
816
|
-
const resultLabel = opts.i18n ? `{r.id ? $t('nav.' + r.id, r.label) : r.label}` : '{r.label}'
|
|
817
|
-
const toolbarMarkup = toolbarOn
|
|
818
|
-
? `<div class="sv-app__toolbar">
|
|
819
|
-
<div class="sv-app__search">
|
|
820
|
-
<svg class="sv-app__search-ic" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.35-4.35"/></svg>
|
|
821
|
-
<input class="sv-app__search-in" type="search" placeholder="Search {brand}…" bind:value={q} bind:this={searchEl} aria-label="Search screens" />
|
|
822
|
-
<kbd class="sv-app__kbd" aria-hidden="true">⌘K</kbd>
|
|
823
|
-
{#if results.length}
|
|
824
|
-
<div class="sv-app__results">
|
|
825
|
-
{#each results as r (r.href)}
|
|
826
|
-
<a class="sv-app__result" href={r.href} onclick={() => (q = '')}>${resultLabel}</a>
|
|
827
|
-
{/each}
|
|
828
|
-
</div>
|
|
829
|
-
{/if}
|
|
830
|
-
</div>
|
|
831
|
-
<div class="sv-app__tools">
|
|
832
|
-
${hasSwitch ? themeToggleBtn : ''}
|
|
833
|
-
<div class="sv-app__pop">
|
|
834
|
-
<button type="button" class="sv-app__tool" aria-label="Notifications" aria-expanded={bellOpen} onclick={() => { bellOpen = !bellOpen; menuOpen = false }}>
|
|
835
|
-
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></svg>
|
|
836
|
-
<span class="sv-app__dot"></span>
|
|
837
|
-
</button>
|
|
838
|
-
{#if bellOpen}
|
|
839
|
-
<div class="sv-app__menu sv-app__menu--notif" role="menu">
|
|
840
|
-
<div class="sv-app__menu-head">Notifications</div>
|
|
841
|
-
{#each notifications as n (n.title)}
|
|
842
|
-
<div class="sv-app__notif" role="menuitem"><span class="sv-app__notif-dot" style:background={n.color}></span><div class="sv-app__notif-body"><strong>{n.title}</strong><span>{n.time}</span></div></div>
|
|
843
|
-
{/each}
|
|
844
|
-
<button type="button" class="sv-app__menu-foot" onclick={() => (bellOpen = false)}>Mark all as read</button>
|
|
845
|
-
</div>
|
|
846
|
-
{/if}
|
|
847
|
-
</div>
|
|
848
|
-
<div class="sv-app__pop">
|
|
849
|
-
<button type="button" class="sv-app__avatar" title={brand} aria-label="Account" aria-expanded={menuOpen} onclick={() => { menuOpen = !menuOpen; bellOpen = false }}>{initials}</button>
|
|
850
|
-
{#if menuOpen}
|
|
851
|
-
<div class="sv-app__menu sv-app__menu--acct" role="menu">
|
|
852
|
-
<div class="sv-app__acct-head"><span class="sv-app__avatar sv-app__avatar--lg" aria-hidden="true">{initials}</span><div class="sv-app__acct-id"><strong>${opts.auth ? '{data?.user?.name ?? brand}' : '{brand}'}</strong><span>${opts.auth ? '{data?.user?.email ?? acctEmail}' : '{acctEmail}'}</span></div></div>
|
|
853
|
-
<a class="sv-app__menu-item" href="/" role="menuitem" onclick={() => (menuOpen = false)}>Dashboard</a>
|
|
854
|
-
${opts.authAccount
|
|
855
|
-
? `<a class="sv-app__menu-item" href="/account" role="menuitem" onclick={() => (menuOpen = false)}>Account</a>`
|
|
856
|
-
: `<button type="button" class="sv-app__menu-item" role="menuitem" onclick={() => (menuOpen = false)}>Profile</button>
|
|
857
|
-
<button type="button" class="sv-app__menu-item" role="menuitem" onclick={() => (menuOpen = false)}>Settings</button>`}
|
|
858
|
-
${opts.auth
|
|
859
|
-
? `<form method="POST" action="/logout" class="sv-app__signout"><button type="submit" class="sv-app__menu-item sv-app__menu-item--danger" role="menuitem">Sign out</button></form>`
|
|
860
|
-
: opts.supabaseAuth
|
|
861
|
-
? '' // Supabase Auth: the real sign-out lives on SvAuthGate's own bar, so no placeholder here.
|
|
862
|
-
: `<button type="button" class="sv-app__menu-item sv-app__menu-item--danger" role="menuitem" onclick={() => (menuOpen = false)}>Sign out</button>`}
|
|
863
|
-
</div>
|
|
864
|
-
{/if}
|
|
865
|
-
</div>
|
|
866
|
-
</div>
|
|
867
|
-
{#if bellOpen || menuOpen}<button type="button" class="sv-app__scrim" tabindex="-1" aria-label="Close menus" onclick={() => { bellOpen = false; menuOpen = false }}></button>{/if}
|
|
868
|
-
</div>\n `
|
|
869
|
-
: ''
|
|
870
|
-
// Supabase Auth: gate the whole app behind a client-side sign-in. SvAuthGate shows
|
|
871
|
-
// the sign-in form when signed out, and the app + a "signed in as … / Sign out" bar
|
|
872
|
-
// when signed in. Reads the shared client from $lib/connections.
|
|
873
|
-
const content = opts.supabaseAuth
|
|
874
|
-
? `<SvAuthGate client={supabaseClient as unknown as SupabaseAuthClientLike} title={\`Sign in to \${brand}\`}>
|
|
875
|
-
{@render children()}
|
|
876
|
-
</SvAuthGate>`
|
|
877
|
-
: `{@render children()}`
|
|
878
|
-
const mainMarkup = `<main class="sv-app__main">
|
|
879
|
-
${toolbarMarkup}<div class="sv-app__content">
|
|
880
|
-
${content}
|
|
881
|
-
</div>
|
|
882
|
-
</main>`
|
|
883
|
-
|
|
884
|
-
const body = style === 'top-nav'
|
|
885
|
-
? `<div class="sv-app sv-app--top${appClsAttr}">
|
|
886
|
-
<header class="sv-app__bar">
|
|
887
|
-
${brandLink}
|
|
888
|
-
${linksMarkup}
|
|
889
|
-
</header>
|
|
890
|
-
${mainMarkup}${footer ? `\n <footer class="sv-app__footbar">{footer}</footer>` : ''}
|
|
891
|
-
</div>`
|
|
892
|
-
: style === 'bottom-nav'
|
|
893
|
-
? `<div class="sv-app sv-app--bottom${appClsAttr}">
|
|
894
|
-
${mainMarkup}
|
|
895
|
-
<footer class="sv-app__bar sv-app__bar--bottom">
|
|
896
|
-
${brandLink}
|
|
897
|
-
${linksMarkup}
|
|
898
|
-
</footer>
|
|
899
|
-
</div>`
|
|
900
|
-
: `<div class="sv-app sv-app--side${right ? ' sv-app--right' : ''}${appClsAttr}" class:is-drawer={drawer} class:is-navopen={navOpen}>
|
|
901
|
-
${mobileBar}
|
|
902
|
-
<aside class="sv-app__side">
|
|
903
|
-
<div class="sv-app__sidehead">
|
|
904
|
-
${brandLink}
|
|
905
|
-
<button type="button" class="sv-app__collapse" aria-label="Collapse sidebar" title="Collapse sidebar" onclick={toggleNav}>
|
|
906
|
-
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 6l-6 6 6 6"/></svg>
|
|
907
|
-
</button>
|
|
908
|
-
</div>
|
|
909
|
-
${linksMarkup}${footMarkup}
|
|
910
|
-
</aside>
|
|
911
|
-
${mainMarkup}
|
|
912
|
-
</div>`
|
|
913
|
-
|
|
914
|
-
const styles = style === 'top-nav'
|
|
915
|
-
? ` .sv-app--top { display: flex; flex-direction: column; min-height: 100vh; }
|
|
916
|
-
.sv-app__bar { display: flex; align-items: center; gap: 20px; padding: 12px 22px; border-bottom: 1px solid color-mix(in srgb, var(--sg-fg, #0f172a) 16%, var(--sg-border, #e6e8ec)); background: var(--sg-header-bg, #f8fafc); }
|
|
917
|
-
.sv-app__links { display: flex; flex-direction: row; gap: 4px; flex-wrap: wrap; }
|
|
918
|
-
.sv-app__footbar { padding: 12px 22px; border-top: 1px solid var(--sg-border, #e6e8ec); color: var(--sg-muted, #94a3b8); font-size: 13px; }
|
|
919
|
-
/* Mobile: the bar stacks and its links scroll horizontally. */
|
|
920
|
-
@media (max-width: 640px) {
|
|
921
|
-
.sv-app__bar { flex-direction: column; align-items: stretch; gap: 10px; padding: 10px 14px; }
|
|
922
|
-
.sv-app__links { flex-wrap: nowrap; overflow-x: auto; padding-bottom: 2px; -webkit-overflow-scrolling: touch; }
|
|
923
|
-
.sv-app__link { white-space: nowrap; }
|
|
924
|
-
}`
|
|
925
|
-
: style === 'bottom-nav'
|
|
926
|
-
? ` .sv-app--bottom { display: flex; flex-direction: column; min-height: 100vh; }
|
|
927
|
-
.sv-app__bar--bottom {
|
|
928
|
-
position: fixed; left: 0; right: 0; bottom: 0; z-index: 40;
|
|
929
|
-
display: flex; align-items: center; gap: 20px; padding: 12px 22px calc(12px + env(safe-area-inset-bottom, 0px));
|
|
930
|
-
border-top: 1px solid color-mix(in srgb, var(--sg-fg, #0f172a) 16%, var(--sg-border, #e6e8ec));
|
|
931
|
-
background: var(--sg-header-bg, #f8fafc);
|
|
932
|
-
}
|
|
933
|
-
.sv-app__bar--bottom .sv-app__links { display: flex; flex-direction: row; gap: 4px; flex-wrap: wrap; }
|
|
934
|
-
/* Clears the fixed bottom bar so page content never renders underneath it. */
|
|
935
|
-
.sv-app--bottom .sv-app__main { padding-bottom: 70px; }
|
|
936
|
-
/* Mobile: brand drops out so the tab strip stays roomy; ties overflow to a horizontal scroll if there are many screens. */
|
|
937
|
-
@media (max-width: 640px) {
|
|
938
|
-
.sv-app__bar--bottom { gap: 10px; padding: 6px 12px calc(6px + env(safe-area-inset-bottom, 0px)); }
|
|
939
|
-
.sv-app__bar--bottom .sv-app__brand { display: none; }
|
|
940
|
-
.sv-app__bar--bottom .sv-app__links { flex-wrap: nowrap; overflow-x: auto; justify-content: flex-start; -webkit-overflow-scrolling: touch; }
|
|
941
|
-
.sv-app__bar--bottom .sv-app__link { white-space: nowrap; }
|
|
942
|
-
}`
|
|
943
|
-
: ` .sv-app--side { display: grid; grid-template-columns: 240px minmax(0, 1fr); min-height: 100vh; }
|
|
944
|
-
.sv-app--side.sv-app--right { grid-template-columns: minmax(0, 1fr) 240px; }
|
|
945
|
-
.sv-app--right .sv-app__side { order: 2; border-right: 0; border-left: 1px solid color-mix(in srgb, var(--sg-fg, #0f172a) 16%, var(--sg-border, #e6e8ec)); }
|
|
946
|
-
.sv-app__side { display: flex; flex-direction: column; gap: 4px; padding: 20px 14px; border-right: 1px solid color-mix(in srgb, var(--sg-fg, #0f172a) 16%, var(--sg-border, #e6e8ec)); background: var(--sg-header-bg, #f8fafc); }
|
|
947
|
-
.sv-app__sidehead { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 6px; }
|
|
948
|
-
.sv-app__collapse, .sv-app__dock { display: inline-flex; padding: 5px; border: 0; border-radius: 8px; background: transparent; color: var(--sg-muted, #64748b); cursor: pointer; }
|
|
949
|
-
.sv-app__collapse:hover, .sv-app__dock:hover { background: color-mix(in srgb, var(--sg-fg, #0f172a) 8%, transparent); color: var(--sg-fg, #0f172a); }
|
|
950
|
-
.sv-app__mobilebar { display: none; align-items: center; gap: 12px; padding: 10px 14px; border-bottom: 1px solid color-mix(in srgb, var(--sg-fg, #0f172a) 16%, var(--sg-border, #e6e8ec)); background: var(--sg-header-bg, #f8fafc); }
|
|
951
|
-
.sv-app__mobilebar .sv-app__dock { margin-left: auto; }
|
|
952
|
-
.sv-app__burger { display: inline-flex; padding: 6px; border: 0; border-radius: 8px; background: transparent; color: var(--sg-fg, #0f172a); cursor: pointer; }
|
|
953
|
-
.sv-app__backdrop { position: fixed; inset: 0; z-index: 55; border: 0; background: rgba(15, 23, 42, 0.4); cursor: pointer; }
|
|
954
|
-
.sv-app__links { display: flex; flex-direction: column; gap: 2px; }
|
|
955
|
-
.sv-app__foot { margin-top: auto; padding-top: 14px; color: var(--sg-muted, #94a3b8); font-size: 12px; }
|
|
956
|
-
/* Drawer mode: tablet / phone (<= 1024px) or a user-collapsed desktop sidebar.
|
|
957
|
-
Driven by the .is-drawer class (JS matchMedia + the collapse toggle), so the
|
|
958
|
-
sidebar docks on wide screens and slides in as an off-canvas drawer otherwise. */
|
|
959
|
-
.sv-app--side.is-drawer, .sv-app--side.is-drawer.sv-app--right { display: block; }
|
|
960
|
-
.sv-app--side.is-drawer .sv-app__mobilebar { display: flex; }
|
|
961
|
-
.sv-app--side.is-drawer .sv-app__side {
|
|
962
|
-
position: fixed; top: 0; bottom: 0; left: 0; z-index: 60; width: 264px; max-width: 82vw;
|
|
963
|
-
transform: translateX(-100%); transition: transform 0.24s ease; box-shadow: 0 0 40px rgba(15, 23, 42, 0.28);
|
|
964
|
-
}
|
|
965
|
-
.sv-app--side.is-drawer.sv-app--right .sv-app__side { left: auto; right: 0; transform: translateX(100%); border-left: 0; }
|
|
966
|
-
.sv-app--side.is-drawer.is-navopen .sv-app__side { transform: translateX(0); }
|
|
967
|
-
.sv-app--side.is-drawer .sv-app__sidehead .sv-app__brand { display: none; } /* brand already in the top bar */
|
|
968
|
-
.sv-app--side.is-drawer .sv-app__collapse { display: none; } /* dock lives in the top bar when drawered */`
|
|
969
|
-
|
|
970
|
-
return {
|
|
971
|
-
path: 'src/routes/+layout.svelte',
|
|
972
|
-
description: `App shell (${style}): nav linking every screen.`,
|
|
973
|
-
contents: `<script lang="ts">
|
|
974
|
-
import '../app.css'
|
|
975
|
-
import '../custom.css'
|
|
976
|
-
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'` : ''}
|
|
977
|
-
|
|
978
|
-
let { children${opts.auth ? ', data' : ''} }${opts.auth ? ": { children: import('svelte').Snippet; data: LayoutData }" : ''} = $props()
|
|
979
|
-
const nav = ${JSON.stringify(links)}
|
|
980
|
-
const brand = ${JSON.stringify(brand)}${footConst}${logoConst}
|
|
981
|
-
let navOpen = $state(false)${opts.auth && opts.access ? `\n // Seed the client role store from the signed-in session (server-resolved).\n $effect(() => { if (data?.role) currentRole.set(data.role as never) })` : ''}
|
|
982
|
-
// Close the mobile drawer whenever the route changes.
|
|
983
|
-
$effect(() => { void page.url.pathname; navOpen = false })${hasSwitch ? `
|
|
984
|
-
// Light/dark switcher: defaults to the mode picked in Studio, then honours the
|
|
985
|
-
// visitor's saved choice. Applies via [data-theme] on <html> (see the token
|
|
986
|
-
// sets in <svelte:head>), and persists per browser.
|
|
987
|
-
let theme = $state<'light' | 'dark'>('${defaultMode}')
|
|
988
|
-
function applyTheme(t: 'light' | 'dark') {
|
|
989
|
-
theme = t
|
|
990
|
-
try { document.documentElement.dataset.theme = t } catch (_) { /* no DOM */ }
|
|
991
|
-
try { localStorage.setItem('svapp:theme', t) } catch (_) { /* storage blocked */ }
|
|
992
|
-
}
|
|
993
|
-
function toggleTheme() { applyTheme(theme === 'dark' ? 'light' : 'dark') }
|
|
994
|
-
$effect(() => {
|
|
995
|
-
let t: 'light' | 'dark' = '${defaultMode}'
|
|
996
|
-
try { const s = localStorage.getItem('svapp:theme'); if (s === 'light' || s === 'dark') t = s } catch (_) { /* storage blocked */ }
|
|
997
|
-
applyTheme(t)
|
|
998
|
-
})` : ''}${sideState}${toolbarOn ? `
|
|
999
|
-
// App-chrome quick-search: filter the screens by label as you type.
|
|
1000
|
-
const initials = ${JSON.stringify(initials)}
|
|
1001
|
-
const acctEmail = ${JSON.stringify(`admin@${brandSlug}.com`)}
|
|
1002
|
-
let q = $state('')
|
|
1003
|
-
const results = $derived(q.trim() ? nav.filter((n) => n.label.toLowerCase().includes(q.trim().toLowerCase())).slice(0, 6) : [])
|
|
1004
|
-
let bellOpen = $state(false)
|
|
1005
|
-
let menuOpen = $state(false)
|
|
1006
|
-
let searchEl: HTMLInputElement | null = $state(null)
|
|
1007
|
-
const notifications = [
|
|
1008
|
-
{ title: 'Welcome to ' + brand, time: 'just now', color: 'var(--sg-accent, #6366f1)' },
|
|
1009
|
-
{ title: 'Your workspace is ready', time: '2m ago', color: '#10b981' },
|
|
1010
|
-
{ title: 'Sample data loaded', time: '5m ago', color: '#f59e0b' },
|
|
1011
|
-
]
|
|
1012
|
-
// Cmd/Ctrl+K focuses search; Escape closes the menus.
|
|
1013
|
-
$effect(() => {
|
|
1014
|
-
function onKey(e: KeyboardEvent) {
|
|
1015
|
-
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); searchEl?.focus() }
|
|
1016
|
-
else if (e.key === 'Escape') { bellOpen = false; menuOpen = false }
|
|
1017
|
-
}
|
|
1018
|
-
window.addEventListener('keydown', onKey)
|
|
1019
|
-
return () => window.removeEventListener('keydown', onKey)
|
|
1020
|
-
})
|
|
1021
|
-
// Close both menus on route change.
|
|
1022
|
-
$effect(() => { void page.url.pathname; bellOpen = false; menuOpen = false })` : ''}
|
|
1023
|
-
</script>
|
|
1024
|
-
${themeHead}
|
|
1025
|
-
|
|
1026
|
-
${opts.auth ? `{#if ${JSON.stringify(opts.authRoutes ?? ['/login'])}.includes(page.url.pathname)}
|
|
1027
|
-
{@render children()}
|
|
1028
|
-
{:else}
|
|
1029
|
-
${body}
|
|
1030
|
-
{/if}` : body}
|
|
1031
|
-
|
|
1032
|
-
<style>
|
|
1033
|
-
${styles}
|
|
1034
|
-
.sv-app__brand { display: inline-flex; align-items: center; font-weight: 700; font-size: 15px; color: var(--sg-fg, #0f172a); text-decoration: none; padding: 4px 8px; }
|
|
1035
|
-
.sv-app__logo { max-height: 34px; max-width: 160px; width: auto; object-fit: contain; display: block; }
|
|
1036
|
-
.sv-app__link { padding: 7px 10px; border-radius: 8px; color: var(--sg-fg, #334155); text-decoration: none; font-size: 14px; }
|
|
1037
|
-
.sv-app__link:hover { background: color-mix(in srgb, var(--sg-fg, #0f172a) 6%, transparent); }
|
|
1038
|
-
.sv-app__link.is-active { background: color-mix(in srgb, var(--sg-accent, #4f46e5) 14%, transparent); color: var(--sg-accent, #4f46e5); font-weight: 600; }
|
|
1039
|
-
.sv-app__main { display: flex; flex-direction: column; min-width: 0; }
|
|
1040
|
-
.sv-app__content { padding: 24px 28px; min-width: 0; }
|
|
1041
|
-
/* App-chrome toolbar: a sticky product header with quick-search + account cluster. */
|
|
1042
|
-
.sv-app__toolbar { position: sticky; top: 0; z-index: 40; display: flex; align-items: center; gap: 16px; padding: 10px 28px; background: color-mix(in srgb, var(--sg-header-bg, #f8fafc) 86%, transparent); backdrop-filter: blur(8px); border-bottom: 1px solid color-mix(in srgb, var(--sg-fg, #0f172a) 10%, var(--sg-border, #e6e8ec)); }
|
|
1043
|
-
.sv-app__search { position: relative; flex: 1; max-width: 440px; display: flex; align-items: center; }
|
|
1044
|
-
.sv-app__search-ic { position: absolute; left: 11px; color: var(--sg-muted, #94a3b8); pointer-events: none; }
|
|
1045
|
-
.sv-app__search-in { width: 100%; font: inherit; font-size: 13.5px; padding: 7px 46px 7px 34px; border: 1px solid var(--sg-border, #e6e8ec); border-radius: 9px; background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a); }
|
|
1046
|
-
.sv-app__search-in::-webkit-search-cancel-button { -webkit-appearance: none; }
|
|
1047
|
-
.sv-app__search-in:focus { outline: none; border-color: var(--sg-accent, #6366f1); box-shadow: 0 0 0 3px color-mix(in srgb, var(--sg-accent, #6366f1) 18%, transparent); }
|
|
1048
|
-
.sv-app__results { position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 20; display: flex; flex-direction: column; padding: 5px; background: var(--sg-bg, #fff); border: 1px solid var(--sg-border, #e6e8ec); border-radius: 10px; box-shadow: 0 12px 32px -8px rgba(15, 23, 42, 0.25); }
|
|
1049
|
-
.sv-app__result { padding: 7px 10px; border-radius: 7px; font-size: 13.5px; color: var(--sg-fg, #334155); text-decoration: none; }
|
|
1050
|
-
.sv-app__result:hover { background: color-mix(in srgb, var(--sg-accent, #6366f1) 12%, transparent); color: var(--sg-accent, #6366f1); }
|
|
1051
|
-
.sv-app__tools { margin-left: auto; display: flex; align-items: center; gap: 10px; }
|
|
1052
|
-
.sv-app__tool { position: relative; display: inline-flex; padding: 7px; border: 1px solid var(--sg-border, #e6e8ec); border-radius: 9px; background: var(--sg-bg, #fff); color: var(--sg-muted, #64748b); cursor: pointer; }
|
|
1053
|
-
.sv-app__tool:hover { color: var(--sg-fg, #0f172a); border-color: color-mix(in srgb, var(--sg-fg, #0f172a) 20%, var(--sg-border, #e6e8ec)); }
|
|
1054
|
-
.sv-app__dot { position: absolute; top: 5px; right: 5px; width: 6px; height: 6px; border-radius: 50%; background: var(--sg-accent, #6366f1); box-shadow: 0 0 0 2px var(--sg-bg, #fff); }
|
|
1055
|
-
.sv-app__avatar { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0; border: none; border-radius: 50%; font: inherit; font-size: 12px; font-weight: 700; color: #fff; background: var(--sg-accent, #6366f1); letter-spacing: 0.02em; cursor: pointer; }
|
|
1056
|
-
.sv-app__kbd { position: absolute; right: 8px; font-size: 10.5px; font-weight: 600; color: var(--sg-muted, #94a3b8); background: color-mix(in srgb, var(--sg-fg, #0f172a) 6%, transparent); border: 1px solid var(--sg-border, #e6e8ec); border-radius: 5px; padding: 1px 5px; pointer-events: none; }
|
|
1057
|
-
.sv-app__pop { position: relative; }
|
|
1058
|
-
.sv-app__scrim { position: fixed; inset: 0; z-index: 44; border: 0; padding: 0; background: transparent; cursor: default; }
|
|
1059
|
-
.sv-app__menu { position: absolute; top: calc(100% + 8px); right: 0; z-index: 46; min-width: 232px; padding: 6px; background: var(--sg-bg, #fff); border: 1px solid var(--sg-border, #e6e8ec); border-radius: 11px; box-shadow: 0 16px 40px -10px rgba(15, 23, 42, 0.3); }
|
|
1060
|
-
.sv-app__menu-head { padding: 6px 10px; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: var(--sg-muted, #94a3b8); }
|
|
1061
|
-
.sv-app__notif { display: flex; gap: 9px; padding: 8px 10px; border-radius: 8px; }
|
|
1062
|
-
.sv-app__notif:hover { background: color-mix(in srgb, var(--sg-fg, #0f172a) 5%, transparent); }
|
|
1063
|
-
.sv-app__notif-dot { flex: none; width: 8px; height: 8px; margin-top: 5px; border-radius: 50%; }
|
|
1064
|
-
.sv-app__notif-body { display: flex; flex-direction: column; min-width: 0; }
|
|
1065
|
-
.sv-app__notif-body strong { font-size: 13px; font-weight: 600; color: var(--sg-fg, #0f172a); }
|
|
1066
|
-
.sv-app__notif-body span { font-size: 11.5px; color: var(--sg-muted, #94a3b8); }
|
|
1067
|
-
.sv-app__menu-foot { width: 100%; margin-top: 4px; padding: 8px; font: inherit; font-size: 12.5px; font-weight: 600; color: var(--sg-accent, #6366f1); background: none; border: none; border-top: 1px solid var(--sg-border, #eef0f3); cursor: pointer; }
|
|
1068
|
-
.sv-app__acct-head { display: flex; align-items: center; gap: 10px; padding: 8px 10px 10px; border-bottom: 1px solid var(--sg-border, #eef0f3); margin-bottom: 4px; }
|
|
1069
|
-
.sv-app__avatar--lg { width: 38px; height: 38px; font-size: 14px; }
|
|
1070
|
-
.sv-app__acct-id { display: flex; flex-direction: column; min-width: 0; }
|
|
1071
|
-
.sv-app__acct-id strong { font-size: 13.5px; color: var(--sg-fg, #0f172a); }
|
|
1072
|
-
.sv-app__acct-id span { font-size: 12px; color: var(--sg-muted, #94a3b8); overflow: hidden; text-overflow: ellipsis; }
|
|
1073
|
-
.sv-app__menu-item { display: block; width: 100%; text-align: left; padding: 8px 10px; font: inherit; font-size: 13px; color: var(--sg-fg, #334155); background: none; border: none; border-radius: 8px; text-decoration: none; cursor: pointer; }
|
|
1074
|
-
.sv-app__menu-item:hover { background: color-mix(in srgb, var(--sg-accent, #6366f1) 10%, transparent); color: var(--sg-accent, #6366f1); }
|
|
1075
|
-
.sv-app__menu-item--danger:hover { background: color-mix(in srgb, #ef4444 12%, transparent); color: #ef4444; }
|
|
1076
|
-
.sv-app__signout { margin: 0; display: block; }
|
|
1077
|
-
.sv-app__signout .sv-app__menu-item { width: 100%; }
|
|
1078
|
-
.sv-app__locale { margin-top: 10px; padding: 5px 8px; font: inherit; font-size: 12.5px; color: var(--sg-fg, #0f172a); background: var(--sg-bg, #fff); border: 1px solid var(--sg-border, #e6e8ec); border-radius: 8px; }
|
|
1079
|
-
.sv-app__theme { align-items: center; justify-content: center; cursor: pointer; }
|
|
1080
|
-
/* No-toolbar fallback slot: trails the nav, pushed to the far edge in row bars. */
|
|
1081
|
-
.sv-app__navtools { display: flex; align-items: center; }
|
|
1082
|
-
.sv-app--top .sv-app__navtools, .sv-app__bar--bottom .sv-app__navtools { margin-left: auto; }
|
|
1083
|
-
/* Mobile bar + drawer chrome (hidden on desktop; media queries above switch it on) */
|
|
1084
|
-
.sv-app__mobilebar { display: none; align-items: center; gap: 12px; padding: 10px 14px; border-bottom: 1px solid var(--sg-border, #e6e8ec); background: var(--sg-header-bg, #f8fafc); position: sticky; top: 0; z-index: 50; }
|
|
1085
|
-
.sv-app__burger { display: inline-flex; align-items: center; justify-content: center; width: 40px; height: 40px; padding: 0; border: 1px solid var(--sg-border, #e6e8ec); border-radius: 9px; background: var(--sg-bg, #fff); color: var(--sg-fg, #0f172a); cursor: pointer; }
|
|
1086
|
-
.sv-app__backdrop { position: fixed; inset: 0; z-index: 55; border: 0; padding: 0; background: rgba(15, 23, 42, 0.42); cursor: pointer; }
|
|
1087
|
-
@media (max-width: 860px) { .sv-app__content { padding: 18px 16px; } .sv-app__toolbar { padding: 9px 16px; } }
|
|
1088
|
-
@media (max-width: 640px) { .sv-app__content { padding: 14px 12px; } .sv-app__toolbar { padding: 8px 12px; gap: 10px; } .sv-app__tool { display: none; } }
|
|
1089
|
-
</style>
|
|
1090
|
-
`,
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
export function homeFile(nav: NavItem[]): GeneratedFile {
|
|
1095
|
-
// Land on the app's primary screen (the first dashboard) instead of a generic
|
|
1096
|
-
// welcome, so opening the app drops you straight into the real product. A themed
|
|
1097
|
-
// splash + link covers the pre-hydration moment and the no-JS case.
|
|
1098
|
-
const first = nav[0]?.href ?? '/'
|
|
1099
|
-
if (nav.length === 0) {
|
|
1100
|
-
return { path: 'src/routes/+page.svelte', description: 'Home page.', contents: `<h1 class="st__title">Your data app</h1>\n<p class="st__sub">Add a screen to get started.</p>\n` }
|
|
1101
|
-
}
|
|
1102
|
-
return {
|
|
1103
|
-
path: 'src/routes/+page.svelte',
|
|
1104
|
-
description: 'Home: redirects to the primary dashboard.',
|
|
1105
|
-
contents: `<script lang="ts">
|
|
1106
|
-
import { onMount } from 'svelte'
|
|
1107
|
-
import { goto } from '$app/navigation'
|
|
1108
|
-
const home = ${JSON.stringify(first)}
|
|
1109
|
-
onMount(() => { goto(home, { replaceState: true }) })
|
|
1110
|
-
</script>
|
|
1111
|
-
|
|
1112
|
-
<div class="st-home">
|
|
1113
|
-
<div class="st-home__spinner" aria-hidden="true"></div>
|
|
1114
|
-
<p>Opening your workspace… <a href={home}>Continue</a></p>
|
|
1115
|
-
</div>
|
|
1116
|
-
|
|
1117
|
-
<style>
|
|
1118
|
-
.st-home { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px; min-height: 52vh; color: var(--sg-muted, #64748b); font-size: 14px; }
|
|
1119
|
-
.st-home__spinner { width: 26px; height: 26px; border-radius: 50%; border: 3px solid var(--sg-border, #e6e8ec); border-top-color: var(--sg-accent, #6366f1); animation: st-home-spin 0.7s linear infinite; }
|
|
1120
|
-
.st-home a { color: var(--sg-accent, #6366f1); font-weight: 600; }
|
|
1121
|
-
@keyframes st-home-spin { to { transform: rotate(360deg); } }
|
|
1122
|
-
</style>
|
|
1123
|
-
`,
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
/**
|
|
1128
|
-
* Prepare entities for emission: resolve relations, rewrite each schema so the
|
|
1129
|
-
* grid shows a related label column instead of the raw FK, and build the seed.
|
|
1130
|
-
* Shared by `emitStudioApp` and the Studio designer's `emitStudioProject`.
|
|
1131
|
-
*/
|
|
1132
|
-
export function prepareEntities(schemas: EntitySchema[]): { entries: Prepared[]; seed: Map<string, Array<Record<string, unknown>>> } {
|
|
1133
|
-
for (const s of schemas) resolveIdField(s) // fail fast on an unresolvable key
|
|
1134
|
-
const byName = new Map(schemas.map((s) => [s.name, s]))
|
|
1135
|
-
const entries: Prepared[] = schemas.map((s) => {
|
|
1136
|
-
const infos = relationInfos(s, byName)
|
|
1137
|
-
return { schema: prepareSchema(s, infos), infos }
|
|
1138
|
-
})
|
|
1139
|
-
return { entries, seed: generateSeed(entries) }
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
/** Emit the shared entity modules: `src/lib/schemas.ts` + `src/lib/data.ts` (+ `connections.ts`). */
|
|
1143
|
-
export function emitEntityModules(
|
|
1144
|
-
schemas: EntitySchema[],
|
|
1145
|
-
opts: { sources?: Record<string, EntityDataSource>; accessEnabled?: boolean; auditEnabled?: boolean; screensByEntity?: Map<string, string[]>; triggers?: Record<string, EntityTriggers>; supabaseConn?: { url?: string; key?: string }; supabaseAuth?: boolean; tenantField?: string; tenantScoped?: (entity: string) => boolean } = {},
|
|
1146
|
-
): { files: GeneratedFile[]; prepared: EntitySchema[] } {
|
|
1147
|
-
const { entries, seed } = prepareEntities(schemas)
|
|
1148
|
-
const { file: data, needs } = dataModule(entries, seed, opts.sources, opts.supabaseConn)
|
|
1149
|
-
const files: GeneratedFile[] = [schemasModule(entries), data]
|
|
1150
|
-
// Supabase Auth needs the shared client even with no Supabase-bound entity; seed
|
|
1151
|
-
// the .env paste hint from the project connection when nothing else did.
|
|
1152
|
-
if ((needs.supabase || opts.supabaseAuth) && !needs.supabaseUrl && (opts.supabaseConn?.url || opts.supabaseConn?.key)) {
|
|
1153
|
-
needs.supabaseUrl = opts.supabaseConn.url; needs.supabaseKey = opts.supabaseConn.key
|
|
1154
|
-
}
|
|
1155
|
-
if (needs.supabase || opts.supabaseAuth) files.push(connectionsModule(needs))
|
|
1156
|
-
for (const r of needs.sqlRoutes) files.push(sqlRouteFile(r.schema, r.table, r.dialect, {
|
|
1157
|
-
access: opts.accessEnabled,
|
|
1158
|
-
audit: opts.auditEnabled,
|
|
1159
|
-
screenIds: opts.screensByEntity?.get(r.schema.name) ?? [],
|
|
1160
|
-
triggers: opts.triggers?.[r.schema.name],
|
|
1161
|
-
dbSchema: r.dbSchema,
|
|
1162
|
-
// A shared (reference-data) entity stays global - no scope on its route.
|
|
1163
|
-
tenantField: opts.tenantField && opts.tenantScoped?.(r.schema.name) !== false ? opts.tenantField : undefined,
|
|
1164
|
-
}))
|
|
1165
|
-
return { files, prepared: entries.map((e) => e.schema) }
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
/**
|
|
1169
|
-
* Emit every source file of a create-studio app for the given entities:
|
|
1170
|
-
* `schemas.ts`, `data.ts`, one `+page.svelte` per entity, and the nav shell.
|
|
1171
|
-
* The template supplies the rest (EntityScreen, theme, configs).
|
|
1172
|
-
*/
|
|
1173
|
-
export function emitStudioApp(schemas: EntitySchema[]): GeneratedFile[] {
|
|
1174
|
-
if (schemas.length === 0) throw new Error('emitStudioApp: no schemas to emit')
|
|
1175
|
-
const { files, prepared } = emitEntityModules(schemas)
|
|
1176
|
-
const nav: NavItem[] = prepared.map((s) => {
|
|
1177
|
-
const n = namesFor(s)
|
|
1178
|
-
return { href: `/${n.route}`, label: n.label }
|
|
1179
|
-
})
|
|
1180
|
-
return [...files, ...prepared.map((s) => entityScreenPage(s)), layoutFile(nav), homeFile(nav)]
|
|
1181
|
-
}
|