@svgrid/enterprise 2.2.1 → 2.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/README.md +95 -81
  2. package/dist/cdn/svgrid-enterprise.svelte-external.js +26642 -11252
  3. package/dist/designer/assets/GridMenus-BtWVk9Ab.js +7 -0
  4. package/dist/designer/assets/SvGridChartPanel-DWlBO2SD.js +10 -0
  5. package/dist/designer/assets/SvGridChartView-8c8Mb4j8.js +1 -0
  6. package/dist/designer/assets/SvGridChartView-DX9HfkBR.css +1 -0
  7. package/dist/designer/assets/index-DsDgp9Xq.js +78758 -0
  8. package/dist/designer/assets/index-tTY_Dx4P.css +1 -0
  9. package/dist/designer/assets/jszip.min-fkJdmAmj.js +2 -0
  10. package/dist/designer/assets/pdfmake-DeCsnyl9.js +242 -0
  11. package/dist/designer/assets/smart.export-BZlSCE8T.js +35 -0
  12. package/dist/designer/assets/vfs_fonts-eX2NpmfX.js +1 -0
  13. package/dist/designer/index.html +13 -0
  14. package/dist/node/studio.js +15304 -2708
  15. package/package.json +35 -11
  16. package/src/SvAlertRuleEditor.svelte +294 -0
  17. package/src/SvAlertsManager.svelte +210 -0
  18. package/src/SvAlertsPanel.svelte +129 -0
  19. package/src/SvBoard.svelte +3 -1
  20. package/src/SvExpressionEditor.svelte +341 -0
  21. package/src/SvGridAlerts.dom.test.ts +113 -0
  22. package/src/SvGridAlerts.svelte +265 -0
  23. package/src/SvGridBoard.svelte +2358 -0
  24. package/src/SvGridEditPanel.svelte +849 -794
  25. package/src/SvGridScheduler.svelte +5358 -4410
  26. package/src/SvPivotDesigner.svelte +3 -3
  27. package/src/SvRecordDetail.svelte +6 -2
  28. package/src/SvSchedule.svelte +3 -1
  29. package/src/{ai-export-pdf.test.ts → ai-export-pdf.dom.test.ts} +77 -74
  30. package/src/{ai-export-xlsx.test.ts → ai-export-xlsx.dom.test.ts} +90 -87
  31. package/src/{ai-export.test.ts → ai-export.dom.test.ts} +114 -110
  32. package/src/alerts/alert-engine-attach.ts +165 -0
  33. package/src/alerts/alert-engine.test.ts +135 -0
  34. package/src/alerts/alert-engine.ts +260 -0
  35. package/src/alerts/alert-formats.test.ts +87 -0
  36. package/src/alerts/alert-formats.ts +77 -0
  37. package/src/alerts/alert-observer.test.ts +189 -0
  38. package/src/alerts/alert-observer.ts +208 -0
  39. package/src/alerts/alert-scheduler.ts +96 -0
  40. package/src/alerts/alert-storage.test.ts +54 -0
  41. package/src/alerts/alert-storage.ts +116 -0
  42. package/src/alerts/alert-store.svelte.ts +80 -0
  43. package/src/alerts/alert-types.ts +94 -0
  44. package/src/alerts.ts +28 -0
  45. package/src/board.dom.test.ts +941 -0
  46. package/src/board.ts +36 -0
  47. package/src/export-ooxml.ts +4 -0
  48. package/src/export-xls.ts +3 -0
  49. package/src/export.ts +6 -0
  50. package/src/expressions/evaluate.test.ts +111 -0
  51. package/src/expressions/evaluate.ts +237 -0
  52. package/src/expressions/expression-columns.ts +133 -0
  53. package/src/expressions/expression-types.ts +84 -0
  54. package/src/expressions/parse.test.ts +106 -0
  55. package/src/expressions/parse.ts +610 -0
  56. package/src/import.test.ts +1 -1
  57. package/src/import.ts +3 -1
  58. package/src/index.ts +192 -46
  59. package/src/install.ts +12 -2
  60. package/src/pivot-enable.ts +44 -0
  61. package/src/pivot.test.ts +0 -1
  62. package/src/scheduler-assignments.test.ts +97 -0
  63. package/src/scheduler-assignments.ts +134 -0
  64. package/src/scheduler-axis.test.ts +108 -0
  65. package/src/scheduler-axis.ts +238 -0
  66. package/src/scheduler-booking.test.ts +57 -0
  67. package/src/scheduler-booking.ts +63 -0
  68. package/src/scheduler-config.ts +179 -0
  69. package/src/scheduler-dependencies.test.ts +155 -0
  70. package/src/scheduler-dependencies.ts +223 -0
  71. package/src/scheduler-freebusy.test.ts +41 -0
  72. package/src/scheduler-freebusy.ts +36 -0
  73. package/src/scheduler-heatmap.test.ts +39 -0
  74. package/src/scheduler-heatmap.ts +55 -0
  75. package/src/scheduler-resource-tree.test.ts +84 -0
  76. package/src/scheduler-resource-tree.ts +107 -0
  77. package/src/scheduler-slots.test.ts +53 -0
  78. package/src/scheduler-slots.ts +94 -0
  79. package/src/scheduler-summary.test.ts +47 -0
  80. package/src/scheduler-summary.ts +81 -0
  81. package/src/schema-designer.ts +1 -1
  82. package/src/sources/index.ts +1 -1
  83. package/src/sources/introspect-supabase.test.ts +13 -1
  84. package/src/sources/introspect-supabase.ts +20 -0
  85. package/src/studio/copilot-core.test.ts +45 -0
  86. package/src/studio/copilot-core.ts +65 -0
  87. package/src/studio/deploy-cli.test.ts +56 -0
  88. package/src/studio/deploy-cli.ts +100 -0
  89. package/src/studio/emit-project.test.ts +800 -24
  90. package/src/studio/emit-project.ts +1307 -135
  91. package/src/studio/emit-schema.test.ts +17 -0
  92. package/src/studio/emit-schema.ts +182 -41
  93. package/src/studio/index.ts +59 -1
  94. package/src/studio/init-flow.test.ts +239 -0
  95. package/src/studio/init-flow.ts +358 -0
  96. package/src/studio/introspect-openapi.test.ts +84 -0
  97. package/src/studio/introspect-openapi.ts +252 -0
  98. package/src/studio/project.test.ts +57 -1
  99. package/src/studio/project.ts +302 -8
  100. package/src/studio/samples/crm.ts +282 -258
  101. package/src/studio/samples/datasets.test.ts +84 -0
  102. package/src/studio/samples/datasets.ts +340 -0
  103. package/src/studio/samples/fleet.ts +213 -186
  104. package/src/studio/samples/insurance.ts +223 -195
  105. package/src/studio/samples/inventory.ts +213 -182
  106. package/src/studio/samples/live-data.test.ts +99 -98
  107. package/src/studio/samples/live-data.ts +8 -10
  108. package/src/studio/samples/projects.ts +212 -190
  109. package/src/studio/samples/samples.test.ts +268 -216
  110. package/src/studio/samples/shared.ts +22 -145
  111. package/src/studio/samples/starter.ts +251 -0
  112. package/src/studio/samples/support.ts +209 -184
  113. package/src/studio/screen-suites.test.ts +226 -0
  114. package/src/studio/screen-suites.ts +445 -0
  115. package/src/studio/ui-components-surface.test.ts +185 -0
  116. package/src/studio/ui-components.generated.ts +7154 -0
  117. package/src/studio/ui-components.ts +273 -3
  118. package/src/sveltekit/index.ts +1 -0
  119. package/src/sveltekit/sql-source.test.ts +13 -0
  120. package/src/sveltekit/sql-source.ts +12 -7
  121. package/src/sveltekit/transport-scope.test.ts +125 -0
  122. package/src/sveltekit/transport.ts +76 -2
  123. package/src/upgrade-prompt.ts +2 -2
  124. package/src/watermark.ts +2 -2
  125. package/src/ai.test.ts +0 -522
  126. package/src/ai.ts +0 -1388
@@ -8,9 +8,14 @@
8
8
  * parse/serialize and generates a runnable app with no special-casing.
9
9
  */
10
10
  import type { EntitySchema } from '../../schema.js'
11
- import type { ChartType } from '@svgrid/grid'
12
11
  import { generateValue } from '../sample-data.js'
13
- import { defaultBlockConfig, sanitizeProject, screenFromTemplate, buildDockLayout, type Block, type BlockConfig, type DetailRelated, type FormatRule, type GridConfig, type GridDensity, type KpiFormat, type Reduce, type RowAction, type RowLink, type Screen, type ScreenTemplate, type StudioProject, type EntityDataSource, type ShellStyle, type SchedulerViewConfig, type SchedulerViewMode } from '../project.js'
12
+ import { gridConfig, type GridOpts } from '../screen-suites.js'
13
+ import { sanitizeProject, screenFromTemplate, buildDockLayout, type Block, type GridConfig, type Screen, type ScreenTemplate, type StudioProject, type EntityDataSource, type ShellStyle, type SchedulerViewConfig, type SchedulerViewMode, type AuthConfig, type AccessControl } from '../project.js'
14
+
15
+ // The generic screen factories live in `../screen-suites.js` (they are public API
16
+ // now - the guided CRUD suite builds on them). Re-exported here so every sample
17
+ // app keeps importing its whole toolkit from one place.
18
+ export { dashScreen, detailScreen, formScreen, gridConfig, listScreen, statusPills, type GridOpts, type Tile } from '../screen-suites.js'
14
19
 
15
20
  type Row = Record<string, unknown>
16
21
 
@@ -72,130 +77,6 @@ export type SampleApp = {
72
77
  build: () => StudioProject
73
78
  }
74
79
 
75
- /** Rich-grid options an enterprise screen layers onto the default grid: status
76
- * pills / thresholds (formatRules), a totals row, row actions, row-click drill,
77
- * and density. */
78
- export type GridOpts = {
79
- format?: FormatRule[]
80
- rowActions?: RowAction[]
81
- rowLink?: RowLink
82
- summaries?: boolean
83
- density?: GridDensity
84
- pageSize?: number
85
- }
86
-
87
- /** A dashboard tile: KPI card (with trend/target/format), chart, gauge, pivot,
88
- * tree, a rich grid, or a tabbed group of display tiles. */
89
- export type Tile =
90
- | { kpi: string; measure?: string; reduce: Reduce; format?: KpiFormat; trendField?: string; trendReduce?: Reduce; target?: number; span?: 1 | 2 | 3 }
91
- | { chart: string; measure?: string; reduce?: Reduce; type?: ChartType; span?: 1 | 2 | 3 }
92
- | { gauge: string; measure?: string; reduce: Reduce; min?: number; max?: number; unit?: string; span?: 1 | 2 | 3 }
93
- | { pivot: { rows: string[]; cols: string[]; measure?: string; aggregate?: Reduce }; span?: 1 | 2 | 3 }
94
- | { tree: { labelField: string; parentField: string }; span?: 1 | 2 | 3 }
95
- | ({ grid: true; span?: 1 | 2 | 3 } & GridOpts)
96
- | { filter: string[]; span?: 1 | 2 | 3 }
97
- | { tabs: { label: string; tiles: Tile[] }[]; span?: 1 | 2 | 3 }
98
-
99
- /** The default grid config with enterprise options merged on. */
100
- function gridConfig(entity: EntitySchema, opts: GridOpts): BlockConfig {
101
- const base = defaultBlockConfig('grid', entity) as GridConfig
102
- return {
103
- ...base,
104
- ...(opts.format ? { formatRules: opts.format } : {}),
105
- ...(opts.rowActions ? { rowActions: opts.rowActions } : {}),
106
- ...(opts.rowLink ? { rowLink: opts.rowLink } : {}),
107
- ...(opts.summaries ? { rowSummaries: true } : {}),
108
- ...(opts.density ? { density: opts.density } : {}),
109
- ...(opts.pageSize ? { pageSize: opts.pageSize } : {}),
110
- }
111
- }
112
-
113
- /** One dashboard tile -> a Block. Recurses for `tabs` (whose children must be
114
- * display blocks: chart / kpi / gauge / pivot / tree). */
115
- function tileBlock(entity: EntitySchema, t: Tile, i: number): Block {
116
- const id = `blk-${i + 1}`
117
- if ('kpi' in t) {
118
- const config: BlockConfig = {
119
- kind: 'kpi', label: t.kpi, ...(t.measure ? { measure: t.measure } : {}), reduce: t.reduce,
120
- ...(t.format ? { format: t.format } : {}), ...(t.trendField ? { trendField: t.trendField } : {}),
121
- ...(t.trendReduce ? { trendReduce: t.trendReduce } : {}), ...(t.target != null ? { target: t.target } : {}),
122
- }
123
- return { id, span: t.span ?? 1, config }
124
- }
125
- if ('chart' in t) {
126
- return { id, span: t.span ?? 2, config: { kind: 'chart', dimension: t.chart, ...(t.measure ? { measure: t.measure } : {}), reduce: t.reduce ?? 'sum', type: t.type ?? 'bar' } }
127
- }
128
- if ('gauge' in t) {
129
- return { id, span: t.span ?? 1, config: { kind: 'gauge', label: t.gauge, ...(t.measure ? { measure: t.measure } : {}), reduce: t.reduce, min: t.min ?? 0, max: t.max ?? 100, ...(t.unit ? { unit: t.unit } : {}) } }
130
- }
131
- if ('pivot' in t) {
132
- return { id, span: t.span ?? 3, config: { kind: 'pivot', rows: t.pivot.rows, cols: t.pivot.cols, ...(t.pivot.measure ? { measure: t.pivot.measure } : {}), aggregate: t.pivot.aggregate ?? 'sum' } }
133
- }
134
- if ('tree' in t) {
135
- return { id, span: t.span ?? 1, config: { kind: 'tree', labelField: t.tree.labelField, parentField: t.tree.parentField } }
136
- }
137
- if ('filter' in t) {
138
- return { id, span: t.span ?? 3, config: { kind: 'filter', fields: t.filter } }
139
- }
140
- if ('tabs' in t) {
141
- return { id, span: t.span ?? 3, config: { kind: 'tabs', tabs: t.tabs.map((tab) => ({ label: tab.label, blocks: tab.tiles.map((tt, j) => tileBlock(entity, tt, j)) })) } }
142
- }
143
- return { id, span: t.span ?? 3, config: gridConfig(entity, t) }
144
- }
145
-
146
- /** Compose a dashboard screen from explicit tiles (KPI cards + gauges + charts +
147
- * pivots + trees + a rich grid). */
148
- export function dashScreen(
149
- entity: EntitySchema,
150
- meta: { id: string; title: string; order: number },
151
- tiles: Tile[],
152
- ): Screen {
153
- const blocks = tiles.map((t, i) => tileBlock(entity, t, i))
154
- return { id: meta.id, entity: entity.name, title: meta.title, route: meta.id, blocks, nav: { show: true, label: meta.title, order: meta.order } }
155
- }
156
-
157
- /** FormatRule[] that color-code an enum field's cells by its option colors -
158
- * turns a plain status column into enterprise status pills. */
159
- export function statusPills(entity: EntitySchema, field: string): FormatRule[] {
160
- const f = entity.fields.find((x) => x.field === field)
161
- if (!f?.options) return []
162
- return f.options
163
- .filter((o) => o.color)
164
- .map((o) => ({ field, op: 'eq' as const, value: o.value as string | number, background: `color-mix(in srgb, ${o.color} 18%, transparent)`, color: o.color, bold: true }))
165
- }
166
-
167
- /** A list screen: an optional faceted filter panel + a rich grid (status pills,
168
- * totals, row actions, drill-through). */
169
- export function listScreen(
170
- entity: EntitySchema,
171
- meta: { id: string; title: string; order: number },
172
- opts: { filter?: string[]; grid?: GridOpts } = {},
173
- ): Screen {
174
- const blocks: Block[] = []
175
- if (opts.filter?.length) blocks.push({ id: 'filter-1', span: 3, config: { kind: 'filter', fields: opts.filter } })
176
- blocks.push({ id: 'grid-1', span: 3, config: gridConfig(entity, opts.grid ?? {}) })
177
- return { id: meta.id, entity: entity.name, title: meta.title, route: meta.id, blocks, nav: { show: true, label: meta.title, order: meta.order } }
178
- }
179
-
180
- /**
181
- * A form-showcase screen: a grid to pick a row + an editable record panel that
182
- * renders the full field form (phone / rating / tags / mask / slider editors)
183
- * inline. Lets a user see and try the rich editors on load, not just on edit.
184
- */
185
- export function formScreen(
186
- entity: EntitySchema,
187
- meta: { id: string; title: string; order: number },
188
- fields?: string[],
189
- grid?: GridOpts,
190
- filter?: string[],
191
- ): Screen {
192
- const blocks: Block[] = []
193
- if (filter?.length) blocks.push({ id: 'filter-1', span: 3, config: { kind: 'filter', fields: filter } })
194
- blocks.push({ id: 'grid-1', span: 3, config: gridConfig(entity, grid ?? {}) })
195
- blocks.push({ id: 'record-1', span: 3, config: { kind: 'record', editable: true, ...(fields ? { fields } : {}) } })
196
- return { id: meta.id, entity: entity.name, title: meta.title, route: meta.id, blocks, nav: { show: true, label: meta.title, order: meta.order } }
197
- }
198
-
199
80
  /** A Kanban board screen: an optional filter panel + a board grouping rows by an
200
81
  * enum field into draggable columns (the signature "pipeline" view). */
201
82
  export function boardScreen(
@@ -272,25 +153,6 @@ export function workspaceScreen(
272
153
  return { ...base, dock: buildDockLayout(base) }
273
154
  }
274
155
 
275
- /** A record detail-page screen: a full record view (header + status pill + metric
276
- * tiles + tabbed Overview / related timelines) - the signature view for
277
- * relation-heavy records (a customer / deal / patient / trip page). */
278
- export function detailScreen(
279
- entity: EntitySchema,
280
- meta: { id: string; title: string; order: number },
281
- opts: { titleField: string; subtitleField?: string; statusField?: string; metricFields?: string[]; sections?: { label: string; fields: string[] }[]; related?: DetailRelated[] },
282
- ): Screen {
283
- const blocks: Block[] = [{ id: 'detail-1', span: 3, config: {
284
- kind: 'detail', titleField: opts.titleField,
285
- ...(opts.subtitleField ? { subtitleField: opts.subtitleField } : {}),
286
- ...(opts.statusField ? { statusField: opts.statusField } : {}),
287
- ...(opts.metricFields?.length ? { metricFields: opts.metricFields } : {}),
288
- ...(opts.sections?.length ? { sections: opts.sections } : {}),
289
- ...(opts.related?.length ? { related: opts.related } : {}),
290
- } }]
291
- return { id: meta.id, entity: entity.name, title: meta.title, route: meta.id, blocks, nav: { show: true, label: meta.title, order: meta.order } }
292
- }
293
-
294
156
  /** Build a screen from a template, then override its id / route / title / nav.
295
157
  * `linkScreen` wires a master-detail parent row to drill into a detail screen. */
296
158
  export function screen(
@@ -324,6 +186,17 @@ export function project(opts: {
324
186
  * what makes each app read as a distinct product (Salesforce vs Excel vs Linear). */
325
187
  preset?: string
326
188
  mode?: 'light' | 'dark'
189
+ /** Bespoke app chrome: raw CSS written to the generated app's `custom.css` (imported
190
+ * after `app.css`, so it overrides). Scope rules under `.sv-app.<appClass>` so an
191
+ * app can look like its real-world archetype, not a recolored sibling. */
192
+ customCss?: string
193
+ /** Extra class on the shell root (the hook `customCss` targets). */
194
+ appClass?: string
195
+ /** A sign-in starter: `{ enabled: true }` gives the generated app a `/login` page,
196
+ * a signed-cookie session, and route guards (dependency-free, no DB). */
197
+ auth?: AuthConfig
198
+ /** Role-based access. With `auth`, seeds one demo login per role. */
199
+ access?: AccessControl
327
200
  entities: EntitySchema[]
328
201
  screens: Screen[]
329
202
  seed: Record<string, Record<string, unknown>[]>
@@ -340,7 +213,11 @@ export function project(opts: {
340
213
  accent: opts.accent,
341
214
  ...(opts.preset ? { preset: opts.preset } : {}),
342
215
  ...(opts.mode ? { mode: opts.mode } : {}),
216
+ ...(opts.customCss ? { customCss: opts.customCss } : {}),
217
+ ...(opts.appClass ? { appClass: opts.appClass } : {}),
343
218
  shell: { style: opts.navStyle ?? 'sidebar', brand: opts.brand, footer: opts.footer ?? '', navPosition: opts.navPosition ?? 'left' },
344
219
  },
220
+ ...(opts.auth?.enabled ? { auth: opts.auth } : {}),
221
+ ...(opts.access?.enabled ? { access: opts.access } : {}),
345
222
  })
346
223
  }
@@ -0,0 +1,251 @@
1
+ /**
2
+ * The project a fresh `svgrid-studio designer` session opens with: not a bare stub,
3
+ * but a complete, enterprise-ready multi-page CRM that shows off the grid's range on
4
+ * first paint - a KPI/chart **Overview**, the rich **Customers** CRUD grid (status
5
+ * pills, health bars, export, popup form), a drag-and-drop **Pipeline** board, a
6
+ * **Deals** grid, a **Calendar** of close dates, and a **Forecast** analytics page.
7
+ * Every generated Studio app already wires `@svgrid/enterprise` (data layer, columns,
8
+ * validation); the rich grids + export make that enterprise surface visible.
9
+ */
10
+ import { project, dashScreen, boardScreen, calendarScreen } from './shared.js'
11
+ import { gridColumns, defaultBlockConfig, buildDockLayout, type StudioProject, type Screen, type Block, type GridConfig, type GridColumnConfig } from '../project.js'
12
+ import type { EntitySchema } from '../../schema.js'
13
+
14
+ /** A realistic B2B customer/account: company + contact, region, industry, plan,
15
+ * seats, MRR, a health score, and lifecycle dates. */
16
+ const customers: EntitySchema = {
17
+ name: 'customers',
18
+ label: 'Customer',
19
+ idField: 'id',
20
+ fields: [
21
+ { field: 'id', label: 'ID', type: 'number', primaryKey: true, readonly: true },
22
+ { field: 'company', label: 'Company', type: 'text', required: true },
23
+ { field: 'contact', label: 'Contact', type: 'text', required: true },
24
+ { field: 'email', label: 'Email', type: 'text', format: 'email' },
25
+ { field: 'country', label: 'Country', type: 'text' },
26
+ { field: 'industry', label: 'Industry', type: 'enum', input: { editorType: 'list' }, options: [
27
+ { value: 'saas', label: 'SaaS', color: '#6366f1' },
28
+ { value: 'fintech', label: 'Fintech', color: '#0ea5e9' },
29
+ { value: 'healthcare', label: 'Healthcare', color: '#10b981' },
30
+ { value: 'retail', label: 'Retail', color: '#f59e0b' },
31
+ { value: 'manufacturing', label: 'Manufacturing', color: '#64748b' },
32
+ { value: 'media', label: 'Media', color: '#ec4899' },
33
+ { value: 'education', label: 'Education', color: '#14b8a6' },
34
+ { value: 'logistics', label: 'Logistics', color: '#a855f7' },
35
+ ] },
36
+ { field: 'status', label: 'Status', type: 'enum', required: true, input: { editorType: 'list' }, options: [
37
+ { value: 'active', label: 'Active', color: '#10b981' },
38
+ { value: 'trial', label: 'Trial', color: '#f59e0b' },
39
+ { value: 'paused', label: 'Paused', color: '#64748b' },
40
+ { value: 'churned', label: 'Churned', color: '#ef4444' },
41
+ ] },
42
+ { field: 'plan', label: 'Plan', type: 'enum', input: { editorType: 'list' }, options: [
43
+ { value: 'starter', label: 'Starter', color: '#94a3b8' },
44
+ { value: 'growth', label: 'Growth', color: '#6366f1' },
45
+ { value: 'scale', label: 'Scale', color: '#0ea5e9' },
46
+ { value: 'enterprise', label: 'Enterprise', color: '#8b5cf6' },
47
+ ] },
48
+ { field: 'seats', label: 'Seats', type: 'number' },
49
+ { field: 'mrr', label: 'MRR', type: 'number' },
50
+ { field: 'health', label: 'Health', type: 'number', min: 0, max: 100 },
51
+ { field: 'signupDate', label: 'Signed up', type: 'date' },
52
+ { field: 'lastContact', label: 'Last contact', type: 'date' },
53
+ ],
54
+ }
55
+
56
+ /** Sales opportunities against accounts - the Pipeline board / Calendar / Forecast. */
57
+ const deals: EntitySchema = {
58
+ name: 'deals',
59
+ label: 'Deal',
60
+ idField: 'id',
61
+ fields: [
62
+ { field: 'id', label: 'ID', type: 'number', primaryKey: true, readonly: true },
63
+ { field: 'name', label: 'Deal', type: 'text', required: true },
64
+ { field: 'customerId', label: 'Account', type: 'relation', relation: { entity: 'customers', labelField: 'company' } },
65
+ { field: 'stage', label: 'Stage', type: 'enum', required: true, input: { editorType: 'list' }, options: [
66
+ { value: 'lead', label: 'Lead', color: '#94a3b8' },
67
+ { value: 'qualified', label: 'Qualified', color: '#38bdf8' },
68
+ { value: 'proposal', label: 'Proposal', color: '#6366f1' },
69
+ { value: 'negotiation', label: 'Negotiation', color: '#f59e0b' },
70
+ { value: 'won', label: 'Won', color: '#10b981' },
71
+ { value: 'lost', label: 'Lost', color: '#ef4444' },
72
+ ] },
73
+ { field: 'owner', label: 'Owner', type: 'enum', input: { editorType: 'list' }, options: [
74
+ { value: 'Sarah Chen', label: 'Sarah Chen', color: '#6366f1' },
75
+ { value: 'Marcus Lee', label: 'Marcus Lee', color: '#10b981' },
76
+ { value: 'Elena Vidal', label: 'Elena Vidal', color: '#f59e0b' },
77
+ { value: 'Tom Baker', label: 'Tom Baker', color: '#ec4899' },
78
+ ] },
79
+ { field: 'amount', label: 'Amount', type: 'number' },
80
+ { field: 'probability', label: 'Probability', type: 'number', min: 0, max: 100 },
81
+ { field: 'closeDate', label: 'Close date', type: 'date' },
82
+ ],
83
+ }
84
+
85
+ /** ~20 seeded accounts with a realistic spread of industries, regions, plans and
86
+ * lifecycle states (active/paused carry MRR; trials + churned sit at 0). */
87
+ const customerSeed: Record<string, unknown>[] = [
88
+ { id: 1, company: 'Meridian Software', contact: 'Sarah Chen', email: 'sarah.chen@meridiansoft.com', country: 'United States', industry: 'saas', status: 'active', plan: 'scale', seats: 42, mrr: 1490, health: 88, signupDate: '2024-03-12', lastContact: '2026-07-28' },
89
+ { id: 2, company: 'Cedar Analytics', contact: 'Michael Rodriguez', email: 'm.rodriguez@cedaranalytics.io', country: 'United States', industry: 'fintech', status: 'active', plan: 'enterprise', seats: 180, mrr: 4200, health: 94, signupDate: '2023-11-05', lastContact: '2026-08-04' },
90
+ { id: 3, company: 'BlueSky Labs', contact: 'Emma Thompson', email: 'emma@blueskylabs.co.uk', country: 'United Kingdom', industry: 'saas', status: 'trial', plan: 'starter', seats: 3, mrr: 0, health: 65, signupDate: '2026-06-20', lastContact: '2026-08-06' },
91
+ { id: 4, company: 'Northgate Systems', contact: 'David Kim', email: 'dkim@northgate.com', country: 'Canada', industry: 'manufacturing', status: 'active', plan: 'growth', seats: 15, mrr: 499, health: 79, signupDate: '2025-01-18', lastContact: '2026-07-15' },
92
+ { id: 5, company: 'Vantage Digital', contact: 'Olivia Martins', email: 'olivia.martins@vantage.digital', country: 'Australia', industry: 'media', status: 'active', plan: 'growth', seats: 12, mrr: 399, health: 82, signupDate: '2024-09-30', lastContact: '2026-07-22' },
93
+ { id: 6, company: 'Harbor Health', contact: "James O'Brien", email: 'j.obrien@harborhealth.org', country: 'United States', industry: 'healthcare', status: 'active', plan: 'enterprise', seats: 240, mrr: 5400, health: 91, signupDate: '2023-06-14', lastContact: '2026-08-01' },
94
+ { id: 7, company: 'Riverstone Media', contact: 'Priya Sharma', email: 'priya@riverstone.media', country: 'India', industry: 'media', status: 'churned', plan: 'scale', seats: 0, mrr: 0, health: 22, signupDate: '2024-02-08', lastContact: '2026-03-19' },
95
+ { id: 8, company: 'Lumen Retail', contact: 'Lucas Müller', email: 'lucas.mueller@lumenretail.de', country: 'Germany', industry: 'retail', status: 'active', plan: 'scale', seats: 55, mrr: 1890, health: 85, signupDate: '2024-12-02', lastContact: '2026-07-30' },
96
+ { id: 9, company: 'Pinewood Financial', contact: 'Aisha Bello', email: 'aisha.bello@pinewoodfin.com', country: 'United States', industry: 'fintech', status: 'paused', plan: 'growth', seats: 8, mrr: 0, health: 48, signupDate: '2025-04-27', lastContact: '2026-05-11' },
97
+ { id: 10, company: 'Orchard Foods', contact: 'Carlos Mendez', email: 'carlos@orchardfoods.mx', country: 'Brazil', industry: 'retail', status: 'active', plan: 'starter', seats: 4, mrr: 99, health: 73, signupDate: '2025-08-09', lastContact: '2026-07-19' },
98
+ { id: 11, company: 'Ironclad Security', contact: 'Nina Petrova', email: 'nina.petrova@ironcladsec.com', country: 'Netherlands', industry: 'saas', status: 'active', plan: 'enterprise', seats: 130, mrr: 3600, health: 96, signupDate: '2023-09-21', lastContact: '2026-08-05' },
99
+ { id: 12, company: 'Brightpath Education', contact: 'Tomás Silva', email: 'tomas.silva@brightpath.edu', country: 'Portugal', industry: 'education', status: 'trial', plan: 'starter', seats: 2, mrr: 0, health: 61, signupDate: '2026-07-01', lastContact: '2026-08-07' },
100
+ { id: 13, company: 'Quantum Ledger', contact: 'Grace Okafor', email: 'grace@quantumledger.io', country: 'United Kingdom', industry: 'fintech', status: 'active', plan: 'scale', seats: 38, mrr: 1290, health: 87, signupDate: '2024-05-16', lastContact: '2026-07-26' },
101
+ { id: 14, company: 'Evergreen Energy', contact: 'Hiroshi Tanaka', email: 'h.tanaka@evergreen-energy.jp', country: 'Japan', industry: 'manufacturing', status: 'active', plan: 'growth', seats: 20, mrr: 549, health: 80, signupDate: '2025-02-11', lastContact: '2026-06-28' },
102
+ { id: 15, company: 'Summit Logistics', contact: 'Fatima Al-Farsi', email: 'fatima@summitlogistics.com', country: 'United States', industry: 'logistics', status: 'active', plan: 'scale', seats: 47, mrr: 1590, health: 89, signupDate: '2024-07-03', lastContact: '2026-08-02' },
103
+ { id: 16, company: 'Fabrikam Retail', contact: 'Erik Larsson', email: 'erik.larsson@fabrikam.se', country: 'Sweden', industry: 'retail', status: 'churned', plan: 'starter', seats: 0, mrr: 0, health: 18, signupDate: '2025-03-22', lastContact: '2026-01-30' },
104
+ { id: 17, company: 'Beacon Analytics', contact: 'Sofia Rossi', email: 'sofia.rossi@beaconanalytics.com', country: 'Italy', industry: 'saas', status: 'active', plan: 'growth', seats: 11, mrr: 449, health: 76, signupDate: '2025-05-19', lastContact: '2026-07-24' },
105
+ { id: 18, company: 'Cobalt Cloud', contact: 'Daniel Okoye', email: 'daniel@cobaltcloud.com', country: 'United States', industry: 'saas', status: 'trial', plan: 'growth', seats: 6, mrr: 0, health: 68, signupDate: '2026-06-28', lastContact: '2026-08-08' },
106
+ { id: 19, company: 'Willow Health', contact: 'Mei Lin', email: 'mei.lin@willowhealth.sg', country: 'Singapore', industry: 'healthcare', status: 'active', plan: 'enterprise', seats: 95, mrr: 2800, health: 92, signupDate: '2023-12-08', lastContact: '2026-07-31' },
107
+ { id: 20, company: 'Atlas Manufacturing', contact: 'Pierre Dubois', email: 'p.dubois@atlasmfg.fr', country: 'France', industry: 'manufacturing', status: 'paused', plan: 'scale', seats: 30, mrr: 0, health: 44, signupDate: '2024-10-15', lastContact: '2026-04-22' },
108
+ ]
109
+
110
+ /** ~24 open + closed opportunities across the accounts, spread over stages, owners
111
+ * and close dates - so the board, calendar, and forecast are all alive. */
112
+ const dealSeed: Record<string, unknown>[] = [
113
+ { id: 1, name: 'Enterprise rollout', customerId: 2, stage: 'negotiation', owner: 'Sarah Chen', amount: 84000, probability: 70, closeDate: '2026-09-15' },
114
+ { id: 2, name: 'Platform upgrade', customerId: 6, stage: 'proposal', owner: 'Marcus Lee', amount: 62000, probability: 55, closeDate: '2026-10-02' },
115
+ { id: 3, name: 'Seat expansion', customerId: 1, stage: 'won', owner: 'Elena Vidal', amount: 24000, probability: 100, closeDate: '2026-07-20' },
116
+ { id: 4, name: 'Pilot program', customerId: 3, stage: 'qualified', owner: 'Tom Baker', amount: 12000, probability: 35, closeDate: '2026-11-10' },
117
+ { id: 5, name: 'Annual renewal', customerId: 11, stage: 'negotiation', owner: 'Sarah Chen', amount: 58000, probability: 65, closeDate: '2026-09-30' },
118
+ { id: 6, name: 'Security add-on', customerId: 19, stage: 'proposal', owner: 'Marcus Lee', amount: 33000, probability: 45, closeDate: '2026-10-18' },
119
+ { id: 7, name: 'Data migration', customerId: 8, stage: 'lead', owner: 'Elena Vidal', amount: 21000, probability: 20, closeDate: '2026-12-05' },
120
+ { id: 8, name: 'Multi-region', customerId: 13, stage: 'won', owner: 'Tom Baker', amount: 47000, probability: 100, closeDate: '2026-06-28' },
121
+ { id: 9, name: 'Onboarding package', customerId: 5, stage: 'qualified', owner: 'Sarah Chen', amount: 9000, probability: 40, closeDate: '2026-11-22' },
122
+ { id: 10, name: 'Analytics tier', customerId: 15, stage: 'proposal', owner: 'Marcus Lee', amount: 38000, probability: 50, closeDate: '2026-10-08' },
123
+ { id: 11, name: 'Compliance module', customerId: 6, stage: 'negotiation', owner: 'Elena Vidal', amount: 72000, probability: 75, closeDate: '2026-09-12' },
124
+ { id: 12, name: 'Trial conversion', customerId: 3, stage: 'lead', owner: 'Tom Baker', amount: 6000, probability: 15, closeDate: '2026-12-20' },
125
+ { id: 13, name: 'Enterprise SSO', customerId: 2, stage: 'won', owner: 'Sarah Chen', amount: 30000, probability: 100, closeDate: '2026-07-05' },
126
+ { id: 14, name: 'Capacity boost', customerId: 14, stage: 'qualified', owner: 'Marcus Lee', amount: 15000, probability: 30, closeDate: '2026-11-15' },
127
+ { id: 15, name: 'Premium support', customerId: 17, stage: 'proposal', owner: 'Elena Vidal', amount: 18000, probability: 45, closeDate: '2026-10-25' },
128
+ { id: 16, name: 'Global expansion', customerId: 11, stage: 'lost', owner: 'Tom Baker', amount: 90000, probability: 0, closeDate: '2026-05-30' },
129
+ { id: 17, name: 'API integration', customerId: 10, stage: 'qualified', owner: 'Sarah Chen', amount: 8000, probability: 35, closeDate: '2026-11-28' },
130
+ { id: 18, name: 'Renewal + upsell', customerId: 19, stage: 'negotiation', owner: 'Marcus Lee', amount: 66000, probability: 70, closeDate: '2026-09-22' },
131
+ { id: 19, name: 'Sandbox pilot', customerId: 18, stage: 'lead', owner: 'Elena Vidal', amount: 7000, probability: 20, closeDate: '2026-12-12' },
132
+ { id: 20, name: 'Custom reporting', customerId: 15, stage: 'won', owner: 'Tom Baker', amount: 26000, probability: 100, closeDate: '2026-06-18' },
133
+ { id: 21, name: 'Migration service', customerId: 4, stage: 'proposal', owner: 'Sarah Chen', amount: 22000, probability: 50, closeDate: '2026-10-14' },
134
+ { id: 22, name: 'Team plan', customerId: 5, stage: 'lost', owner: 'Marcus Lee', amount: 11000, probability: 0, closeDate: '2026-04-16' },
135
+ { id: 23, name: 'Platform renewal', customerId: 1, stage: 'negotiation', owner: 'Elena Vidal', amount: 44000, probability: 60, closeDate: '2026-09-08' },
136
+ { id: 24, name: 'Advanced tier', customerId: 13, stage: 'qualified', owner: 'Tom Baker', amount: 19000, probability: 40, closeDate: '2026-11-05' },
137
+ ]
138
+
139
+ /** The hero grid: the no-code default plus the enterprise surface (filtering, export
140
+ * toolbar, totals, status pills, row actions) and custom cells - MRR as currency,
141
+ * health as a progress bar, email as a link, dates formatted, larger comfortable rows. */
142
+ function crudGrid(): Block {
143
+ const base = defaultBlockConfig('grid', customers) as GridConfig
144
+ const columns: GridColumnConfig[] = gridColumns(customers).map((c) =>
145
+ c.field === 'mrr' ? { ...c, header: 'MRR', format: { type: 'currency', currency: 'USD' }, align: 'right', aggregate: 'sum' }
146
+ : c.field === 'seats' ? { ...c, align: 'right', aggregate: 'sum' }
147
+ : c.field === 'health' ? { ...c, header: 'Health', width: 140, cellType: { kind: 'progress', max: 100 } }
148
+ : c.field === 'signupDate' || c.field === 'lastContact' ? { ...c, format: { type: 'date' } }
149
+ : c.field === 'email' ? { ...c, cellType: { kind: 'link', as: 'email' } }
150
+ : c,
151
+ )
152
+ const config: GridConfig = {
153
+ ...base,
154
+ columns,
155
+ filterable: true,
156
+ filterUi: { global: true, row: false, menu: true },
157
+ export: { csv: true, json: true, copy: true },
158
+ rowSummaries: true,
159
+ pageSize: 10,
160
+ density: 'comfortable',
161
+ rowActions: [{ kind: 'edit' }, { kind: 'delete' }],
162
+ props: { headerHeight: 48 },
163
+ }
164
+ return { id: 'grid-1', span: 3, height: 440, config }
165
+ }
166
+
167
+ /** The Deals grid: stage/owner chips, currency amount (summed), a probability bar. */
168
+ function dealsGrid(): Block {
169
+ const base = defaultBlockConfig('grid', deals) as GridConfig
170
+ const columns: GridColumnConfig[] = gridColumns(deals).map((c) =>
171
+ c.field === 'amount' ? { ...c, format: { type: 'currency', currency: 'USD' }, align: 'right', aggregate: 'sum' }
172
+ : c.field === 'probability' ? { ...c, header: 'Win %', width: 130, cellType: { kind: 'progress', max: 100 } }
173
+ : c.field === 'closeDate' ? { ...c, format: { type: 'date' } }
174
+ : c,
175
+ )
176
+ const config: GridConfig = {
177
+ ...base,
178
+ columns,
179
+ filterable: true,
180
+ filterUi: { global: true, row: false, menu: true },
181
+ export: { csv: true, json: true, copy: true },
182
+ rowSummaries: true,
183
+ pageSize: 10,
184
+ density: 'comfortable',
185
+ rowActions: [{ kind: 'edit' }, { kind: 'delete' }],
186
+ props: { headerHeight: 48 },
187
+ }
188
+ return { id: 'grid-1', span: 3, height: 440, config }
189
+ }
190
+
191
+ /** A docking-workspace console: a facet filter, the rich grid, and a record panel laid
192
+ * out in a live SvDockManager (drag to float / pin / re-dock) - the signature "wow"
193
+ * layout. `buildDockLayout` seeds filter-left, grid-centre, record-right. */
194
+ function dockConsole(entity: EntitySchema, id: string, title: string, order: number, gridBlock: Block, filterFields: string[]): Screen {
195
+ const blocks: Block[] = [
196
+ { id: 'filter-1', span: 3, config: { kind: 'filter', fields: filterFields } },
197
+ gridBlock,
198
+ { id: 'record-1', span: 3, config: { kind: 'record', editable: true } },
199
+ ]
200
+ const base: Screen = { id, entity: entity.name, title, route: id, blocks, nav: { show: true, label: title, order }, layout: 'dock' }
201
+ return { ...base, dock: buildDockLayout(base) }
202
+ }
203
+
204
+ /** The Overview: the hero Customers grid FIRST (so opening the app lands on a real
205
+ * data grid, not a chart), with KPI cards + charts below it as supporting context. */
206
+ function overviewScreen(): Screen {
207
+ const dash = dashScreen(customers, { id: 'overview', title: 'Overview', order: 0 }, [
208
+ { kpi: 'Recurring revenue', measure: 'mrr', reduce: 'sum', format: 'currency', span: 1 },
209
+ { kpi: 'Accounts', reduce: 'count', span: 1 },
210
+ { kpi: 'Seats', measure: 'seats', reduce: 'sum', format: 'compact', span: 1 },
211
+ { gauge: 'Avg health', measure: 'health', reduce: 'avg', min: 0, max: 100, span: 1 },
212
+ { chart: 'industry', measure: 'mrr', reduce: 'sum', type: 'bar', span: 2 },
213
+ { chart: 'status', reduce: 'count', type: 'pie', span: 1 },
214
+ ])
215
+ return { ...dash, blocks: [crudGrid(), ...dash.blocks] }
216
+ }
217
+
218
+ /** The default first-run project: a complete, enterprise-ready multi-page CRM whose
219
+ * hero is the CRUD grid (Overview + two SvDockManager consoles). */
220
+ export function starterProject(): StudioProject {
221
+ return project({
222
+ title: 'Acme CRM',
223
+ brand: 'Acme CRM',
224
+ accent: '#6366f1',
225
+ footer: 'Built with SvGrid Studio',
226
+ entities: [customers, deals],
227
+ screens: [
228
+ // Overview: KPIs + charts ABOVE the hero Customers grid.
229
+ overviewScreen(),
230
+ // Customers: a dockable console - filter + hero grid + record panel (drag to float / pin).
231
+ dockConsole(customers, 'customers', 'Customers', 1, crudGrid(), ['status', 'industry', 'plan']),
232
+ // Pipeline: deals as draggable cards in stage columns (drag to change stage).
233
+ boardScreen(deals, { id: 'pipeline', title: 'Pipeline', order: 2 }, { groupBy: 'stage', titleField: 'name', badgeField: 'amount', subtitleField: 'owner', filter: ['owner', 'stage'] }),
234
+ // Deals: a second docking console over the opportunity grid.
235
+ dockConsole(deals, 'deals', 'Deals', 3, dealsGrid(), ['stage', 'owner']),
236
+ // Calendar: deals placed on a month calendar by their close date, tinted by stage.
237
+ calendarScreen(deals, { id: 'calendar', title: 'Calendar', order: 4 }, { dateField: 'closeDate', titleField: 'name', colorField: 'stage', filter: ['owner'] }),
238
+ // Forecast: pipeline KPIs, value by stage/owner, and a pivot of amount by owner x stage.
239
+ dashScreen(deals, { id: 'forecast', title: 'Forecast', order: 5 }, [
240
+ { kpi: 'Pipeline value', measure: 'amount', reduce: 'sum', format: 'currency', span: 1 },
241
+ { kpi: 'Open deals', reduce: 'count', span: 1 },
242
+ { kpi: 'Avg deal size', measure: 'amount', reduce: 'avg', format: 'currency', span: 1 },
243
+ { kpi: 'Largest deal', measure: 'amount', reduce: 'max', format: 'currency', span: 1 },
244
+ { chart: 'stage', measure: 'amount', reduce: 'sum', type: 'bar', span: 2 },
245
+ { chart: 'owner', measure: 'amount', reduce: 'sum', type: 'bar', span: 1 },
246
+ { pivot: { rows: ['owner'], cols: ['stage'], measure: 'amount', aggregate: 'sum' }, span: 3 },
247
+ ]),
248
+ ],
249
+ seed: { customers: customerSeed, deals: dealSeed },
250
+ })
251
+ }