@svgrid/enterprise 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/dist/cdn/pdfmake-C6pyKjX0.js +58641 -0
  2. package/dist/cdn/smart.export-CfW2zpyq.js +2029 -0
  3. package/dist/cdn/svgrid-enterprise.svelte-external.js +27129 -0
  4. package/dist/cdn/vfs_fonts-BktEULEs.js +13 -0
  5. package/dist/designer/assets/index-Dp44bTid.js +939 -0
  6. package/dist/designer/assets/index-RJp6x8tw.css +1 -0
  7. package/dist/designer/assets/jszip.min-CjMo-QGg.js +2 -0
  8. package/dist/designer/index.html +13 -0
  9. package/dist/node/studio.js +22554 -0
  10. package/package.json +90 -71
  11. package/src/SvAuthGate.svelte +115 -0
  12. package/src/SvBoard.dom.test.ts +67 -0
  13. package/src/SvBoard.svelte +192 -0
  14. package/src/SvExportMenu.svelte +553 -0
  15. package/src/SvFileInput.svelte +113 -0
  16. package/src/SvGridEditPanel.dom.test.ts +146 -0
  17. package/src/SvGridEditPanel.svelte +793 -0
  18. package/src/SvGridMasterDetail.svelte +95 -0
  19. package/src/SvImportDialog.svelte +1020 -0
  20. package/src/SvLookupInput.svelte +180 -0
  21. package/src/SvRecordDetail.dom.test.ts +137 -0
  22. package/src/SvRecordDetail.svelte +288 -0
  23. package/src/SvSchedule.dom.test.ts +57 -0
  24. package/src/SvSchedule.svelte +156 -0
  25. package/src/SvSchemaChart.svelte +233 -0
  26. package/src/SvSchemaDashboard.svelte +130 -0
  27. package/src/ai-export-pdf.test.ts +74 -0
  28. package/src/ai-export-xlsx.test.ts +87 -0
  29. package/src/ai-export.test.ts +110 -0
  30. package/src/ai.ts +1188 -782
  31. package/src/edit-panel.test.ts +213 -0
  32. package/src/edit-panel.ts +228 -0
  33. package/src/export-conditional.test.ts +60 -0
  34. package/src/export-conditional.ts +94 -0
  35. package/src/export-ooxml.test.ts +152 -0
  36. package/src/export-ooxml.ts +485 -0
  37. package/src/export-pdf.test.ts +138 -0
  38. package/src/export-pdf.ts +245 -0
  39. package/src/export-print.test.ts +66 -0
  40. package/src/export-print.ts +132 -0
  41. package/src/export-serialize.test.ts +56 -0
  42. package/src/export-serialize.ts +24 -0
  43. package/src/export-xls.ts +196 -0
  44. package/src/export-xlsx-roundtrip.test.ts +120 -0
  45. package/src/export-xlsx.test.ts +150 -0
  46. package/src/export.test.ts +349 -0
  47. package/src/export.ts +1452 -512
  48. package/src/import-automap.test.ts +131 -0
  49. package/src/import-hardening.test.ts +158 -0
  50. package/src/import.ts +1042 -648
  51. package/src/index.ts +282 -0
  52. package/src/install.ts +134 -114
  53. package/src/license-core.test.ts +23 -0
  54. package/src/license-core.ts +35 -0
  55. package/src/license.ts +103 -90
  56. package/src/master-detail.test.ts +61 -0
  57. package/src/master-detail.ts +42 -0
  58. package/src/pivot.ts +3 -3
  59. package/src/print.ts +107 -127
  60. package/src/schema-designer.test.ts +121 -0
  61. package/src/schema-designer.ts +142 -0
  62. package/src/schema.test.ts +268 -0
  63. package/src/schema.ts +499 -0
  64. package/src/smart-shim.ts +107 -105
  65. package/src/sources/aggregate.test.ts +79 -0
  66. package/src/sources/aggregate.ts +102 -0
  67. package/src/sources/auth-supabase.test.ts +80 -0
  68. package/src/sources/auth-supabase.ts +87 -0
  69. package/src/sources/dashboard.kpi.test.ts +50 -0
  70. package/src/sources/dashboard.test.ts +61 -0
  71. package/src/sources/dashboard.ts +136 -0
  72. package/src/sources/field-inference.test.ts +60 -0
  73. package/src/sources/field-inference.ts +63 -0
  74. package/src/sources/filters.test.ts +58 -0
  75. package/src/sources/filters.ts +64 -0
  76. package/src/sources/index.ts +67 -0
  77. package/src/sources/introspect-supabase.test.ts +100 -0
  78. package/src/sources/introspect-supabase.ts +119 -0
  79. package/src/sources/realtime-supabase.test.ts +93 -0
  80. package/src/sources/realtime-supabase.ts +99 -0
  81. package/src/sources/relation-lookup.test.ts +99 -0
  82. package/src/sources/relation-lookup.ts +128 -0
  83. package/src/sources/rest-adapters.test.ts +95 -0
  84. package/src/sources/rest-adapters.ts +120 -0
  85. package/src/sources/rest.test.ts +104 -0
  86. package/src/sources/rest.ts +129 -0
  87. package/src/sources/schema-from-columns.test.ts +106 -0
  88. package/src/sources/schema-from-columns.ts +88 -0
  89. package/src/sources/supabase.test.ts +110 -0
  90. package/src/sources/supabase.ts +99 -0
  91. package/src/sources/with-entity-rules.test.ts +104 -0
  92. package/src/sources/with-entity-rules.ts +78 -0
  93. package/src/sources/with-relation-labels.test.ts +75 -0
  94. package/src/sources/with-relation-labels.ts +73 -0
  95. package/src/studio/bug-report.test.ts +101 -0
  96. package/src/studio/bug-report.ts +165 -0
  97. package/src/studio/cli.test.ts +187 -0
  98. package/src/studio/cli.ts +112 -0
  99. package/src/studio/csv.test.ts +90 -0
  100. package/src/studio/csv.ts +165 -0
  101. package/src/studio/db-connect-string.test.ts +94 -0
  102. package/src/studio/db-connect-string.ts +128 -0
  103. package/src/studio/emit-project.test.ts +923 -0
  104. package/src/studio/emit-project.ts +1273 -0
  105. package/src/studio/emit-schema.test.ts +94 -0
  106. package/src/studio/emit-schema.ts +920 -0
  107. package/src/studio/index.ts +195 -0
  108. package/src/studio/introspect-db.test.ts +186 -0
  109. package/src/studio/introspect-db.ts +312 -0
  110. package/src/studio/introspect-prisma.test.ts +99 -0
  111. package/src/studio/introspect-prisma.ts +175 -0
  112. package/src/studio/introspect.test.ts +172 -0
  113. package/src/studio/introspect.ts +310 -0
  114. package/src/studio/pipeline.test.ts +42 -0
  115. package/src/studio/project-robust.test.ts +157 -0
  116. package/src/studio/project.test.ts +473 -0
  117. package/src/studio/project.ts +916 -0
  118. package/src/studio/sample-data.test.ts +58 -0
  119. package/src/studio/sample-data.ts +200 -0
  120. package/src/studio/samples/ats.ts +202 -0
  121. package/src/studio/samples/clinic.ts +177 -0
  122. package/src/studio/samples/crm.ts +250 -0
  123. package/src/studio/samples/ecommerce.ts +187 -0
  124. package/src/studio/samples/events.ts +199 -0
  125. package/src/studio/samples/fleet.ts +184 -0
  126. package/src/studio/samples/gym.ts +213 -0
  127. package/src/studio/samples/hr.ts +193 -0
  128. package/src/studio/samples/index.ts +55 -0
  129. package/src/studio/samples/insurance.ts +195 -0
  130. package/src/studio/samples/inventory.ts +182 -0
  131. package/src/studio/samples/invoicing.ts +162 -0
  132. package/src/studio/samples/library.ts +180 -0
  133. package/src/studio/samples/live-data.test.ts +98 -0
  134. package/src/studio/samples/live-data.ts +308 -0
  135. package/src/studio/samples/projects.ts +190 -0
  136. package/src/studio/samples/realestate.ts +196 -0
  137. package/src/studio/samples/restaurant.ts +187 -0
  138. package/src/studio/samples/samples.test.ts +208 -0
  139. package/src/studio/samples/school.ts +197 -0
  140. package/src/studio/samples/seed-floor.test.ts +25 -0
  141. package/src/studio/samples/shared.ts +305 -0
  142. package/src/studio/samples/subscriptions.ts +183 -0
  143. package/src/studio/samples/support.ts +182 -0
  144. package/src/studio/scaffold-app.test.ts +91 -0
  145. package/src/studio/scaffold-app.ts +161 -0
  146. package/src/studio/scaffold.test.ts +178 -0
  147. package/src/studio/scaffold.ts +374 -0
  148. package/src/studio/themes.ts +172 -0
  149. package/src/studio/verify.test.ts +47 -0
  150. package/src/studio/verify.ts +79 -0
  151. package/src/sveltekit/in-memory.test.ts +104 -0
  152. package/src/sveltekit/in-memory.ts +129 -0
  153. package/src/sveltekit/index.ts +23 -0
  154. package/src/sveltekit/query-plan.test.ts +109 -0
  155. package/src/sveltekit/query-plan.ts +125 -0
  156. package/src/sveltekit/sql-source.test.ts +185 -0
  157. package/src/sveltekit/sql-source.ts +166 -0
  158. package/src/sveltekit/sql.test.ts +80 -0
  159. package/src/sveltekit/sql.ts +131 -0
  160. package/src/sveltekit/transport.test.ts +175 -0
  161. package/src/sveltekit/transport.ts +254 -0
  162. package/src/sveltekit/types.ts +11 -0
  163. package/src/upgrade-prompt.ts +149 -148
@@ -0,0 +1,190 @@
1
+ import type { EntitySchema } from '../../schema.js'
2
+ import type { FormatRule } from '../project.js'
3
+ import { screen, formScreen, boardScreen, calendarScreen, detailScreen, project, dashScreen, statusPills, pad, ids, type SampleApp } from './shared.js'
4
+
5
+ const projectsEntity: EntitySchema = {
6
+ name: 'projects',
7
+ label: 'Project',
8
+ idField: 'id',
9
+ fields: [
10
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
11
+ { field: 'name', type: 'text', required: true, minLength: 2 },
12
+ { field: 'status', type: 'enum', required: true, defaultValue: 'planning', options: [
13
+ { value: 'planning', label: 'Planning', color: '#94a3b8' },
14
+ { value: 'active', label: 'Active', color: '#6366f1' },
15
+ { value: 'on_hold', label: 'On hold', color: '#f59e0b' },
16
+ { value: 'done', label: 'Done', color: '#10b981' },
17
+ ] },
18
+ { field: 'owner', type: 'text', label: 'Owner' },
19
+ { field: 'budget', type: 'number', label: 'Budget ($)', min: 0, input: { prefix: '$', help: 'Approved budget' } },
20
+ { field: 'progress', type: 'number', label: 'Progress (%)', min: 0, max: 100, defaultValue: 0, input: { editorType: 'slider', help: 'Overall completion' } },
21
+ { field: 'color', type: 'text', label: 'Label color', defaultValue: '#6366f1', input: { editorType: 'color', help: 'Board / calendar color' } },
22
+ { field: 'tags', type: 'text', label: 'Tags', input: { editorType: 'chips', help: 'Themes / labels' } },
23
+ { field: 'dueDate', type: 'date', label: 'Due date', required: true },
24
+ ],
25
+ }
26
+
27
+ const members: EntitySchema = {
28
+ name: 'members',
29
+ label: 'Member',
30
+ idField: 'id',
31
+ fields: [
32
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
33
+ { field: 'name', type: 'text', required: true, minLength: 2 },
34
+ { field: 'email', type: 'text', format: 'email', required: true },
35
+ { field: 'phone', type: 'text', label: 'Phone', input: { editorType: 'phone', help: 'Direct line' } },
36
+ { field: 'role', type: 'enum', required: true, defaultValue: 'dev', options: [
37
+ { value: 'dev', label: 'Engineer', color: '#6366f1' },
38
+ { value: 'design', label: 'Designer', color: '#ec4899' },
39
+ { value: 'pm', label: 'PM', color: '#f59e0b' },
40
+ { value: 'qa', label: 'QA', color: '#10b981' },
41
+ ] },
42
+ { field: 'skills', type: 'text', label: 'Skills', input: { editorType: 'chips', help: 'Tech / expertise' } },
43
+ { field: 'rating', type: 'number', label: 'Performance', min: 1, max: 5, defaultValue: 3, input: { editorType: 'rating', help: '1-5 review score' } },
44
+ ],
45
+ }
46
+
47
+ const tasks: EntitySchema = {
48
+ name: 'tasks',
49
+ label: 'Task',
50
+ idField: 'id',
51
+ fields: [
52
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
53
+ { field: 'title', type: 'text', required: true, minLength: 2 },
54
+ { field: 'projectId', type: 'relation', label: 'Project', relation: { entity: 'projects', foreignKey: 'projectId', labelField: 'name' } },
55
+ { field: 'assigneeId', type: 'relation', label: 'Assignee', relation: { entity: 'members', foreignKey: 'assigneeId', labelField: 'name' } },
56
+ { field: 'priority', type: 'enum', required: true, defaultValue: 'medium', options: [
57
+ { value: 'low', label: 'Low', color: '#94a3b8' },
58
+ { value: 'medium', label: 'Medium', color: '#6366f1' },
59
+ { value: 'high', label: 'High', color: '#f59e0b' },
60
+ { value: 'urgent', label: 'Urgent', color: '#ef4444' },
61
+ ] },
62
+ { field: 'status', type: 'enum', required: true, defaultValue: 'todo', options: [
63
+ { value: 'todo', label: 'To do', color: '#94a3b8' },
64
+ { value: 'doing', label: 'In progress', color: '#6366f1' },
65
+ { value: 'review', label: 'In review', color: '#f59e0b' },
66
+ { value: 'done', label: 'Done', color: '#10b981' },
67
+ ] },
68
+ { field: 'estimate', type: 'number', label: 'Estimate (h)', min: 0, defaultValue: 0, input: { suffix: 'h', help: 'Estimated hours' } },
69
+ { field: 'spent', type: 'number', label: 'Spent (h)', min: 0, defaultValue: 0, input: { suffix: 'h', help: 'Hours logged so far' } },
70
+ // Derived: hours left on the task (display-only, never stored).
71
+ { field: 'remaining', type: 'number', label: 'Remaining (h)', readonly: true, formula: 'estimate - spent' },
72
+ { field: 'progress', type: 'number', label: 'Progress (%)', min: 0, max: 100, defaultValue: 0, input: { editorType: 'slider', help: 'Task completion' } },
73
+ { field: 'startedAt', type: 'datetime', label: 'Started', input: { editorType: 'datetime', help: 'When work began' } },
74
+ { field: 'dueDate', type: 'date', label: 'Due' },
75
+ ],
76
+ }
77
+
78
+ // Task grid formatting: status + priority pills, plus numeric thresholds -
79
+ // over-budget tasks (spent past estimate) red + bold, nearly-done tasks green,
80
+ // barely-started tasks red - data-driven color, not just status pills.
81
+ const taskFormats: FormatRule[] = [
82
+ ...statusPills(tasks, 'status'),
83
+ ...statusPills(tasks, 'priority'),
84
+ { field: 'spent', op: 'gt', value: 16, color: '#dc2626', bold: true },
85
+ { field: 'progress', op: 'gte', value: 80, color: '#16a34a', bold: true },
86
+ { field: 'progress', op: 'lt', value: 25, color: '#dc2626' },
87
+ ]
88
+
89
+ const seed = {
90
+ projects: [
91
+ { id: 'pr1', name: 'Website redesign', status: 'active', owner: 'Sam Rivera', budget: 48000, progress: 65, color: '#6366f1', tags: ['Marketing', 'Design'], dueDate: '2026-09-01' },
92
+ { id: 'pr2', name: 'Mobile app v2', status: 'planning', owner: 'Jamie Chen', budget: 120000, progress: 15, color: '#10b981', tags: ['Mobile', 'Growth'], dueDate: '2026-11-15' },
93
+ { id: 'pr3', name: 'Billing migration', status: 'on_hold', owner: 'Priya Patel', budget: 32000, progress: 40, color: '#f59e0b', tags: ['Infra', 'Finance'], dueDate: '2026-08-20' },
94
+ { id: 'pr4', name: 'Onboarding revamp', status: 'done', owner: 'Sam Rivera', budget: 18000, progress: 100, color: '#ef4444', tags: ['Product'], dueDate: '2026-06-10' },
95
+ ],
96
+ members: [
97
+ { id: 'm1', name: 'Ada Lovelace', email: 'ada@team.dev', phone: '+1 (415) 555-0101', role: 'dev', skills: ['TypeScript', 'Svelte'], rating: 5 },
98
+ { id: 'm2', name: 'Grace Hopper', email: 'grace@team.dev', phone: '+1 (415) 555-0102', role: 'dev', skills: ['Go', 'SQL', 'Docker'], rating: 4 },
99
+ { id: 'm3', name: 'Margaret Hamilton', email: 'mh@team.dev', phone: '+1 (415) 555-0103', role: 'pm', skills: ['Product', 'Leadership'], rating: 5 },
100
+ { id: 'm4', name: 'Radia Perlman', email: 'radia@team.dev', phone: '+1 (415) 555-0104', role: 'design', skills: ['Figma', 'UX'], rating: 4 },
101
+ { id: 'm5', name: 'Katherine Johnson', email: 'kj@team.dev', phone: '+1 (415) 555-0105', role: 'qa', skills: ['Python', 'GraphQL'], rating: 3 },
102
+ ],
103
+ tasks: [
104
+ { id: 't1', title: 'Design new homepage', projectId: 'pr1', assigneeId: 'm4', priority: 'high', status: 'doing', estimate: 16, spent: 6, progress: 40, startedAt: '2026-07-02T09:15:00Z', dueDate: '2026-07-20' },
105
+ { id: 't2', title: 'Set up CI pipeline', projectId: 'pr1', assigneeId: 'm1', priority: 'medium', status: 'done', estimate: 8, spent: 8, progress: 100, startedAt: '2026-06-24T10:00:00Z', dueDate: '2026-07-05' },
106
+ { id: 't3', title: 'API rate limiting', projectId: 'pr2', assigneeId: 'm2', priority: 'urgent', status: 'todo', estimate: 12, spent: 0, progress: 0, startedAt: '2026-07-18T08:30:00Z', dueDate: '2026-07-25' },
107
+ { id: 't4', title: 'Migrate invoices', projectId: 'pr3', assigneeId: 'm1', priority: 'high', status: 'review', estimate: 24, spent: 18, progress: 80, startedAt: '2026-07-06T13:45:00Z', dueDate: '2026-08-01' },
108
+ { id: 't5', title: 'QA onboarding flow', projectId: 'pr4', assigneeId: 'm5', priority: 'medium', status: 'done', estimate: 6, spent: 6, progress: 100, startedAt: '2026-06-02T11:20:00Z', dueDate: '2026-06-08' },
109
+ { id: 't6', title: 'Push notifications', projectId: 'pr2', assigneeId: 'm2', priority: 'low', status: 'todo', estimate: 10, spent: 0, progress: 0, startedAt: '2026-07-15T09:00:00Z', dueDate: '2026-08-15' },
110
+ { id: 't7', title: 'Accessibility audit', projectId: 'pr1', assigneeId: 'm5', priority: 'high', status: 'doing', estimate: 14, spent: 5, progress: 35, startedAt: '2026-07-10T14:00:00Z', dueDate: '2026-07-30' },
111
+ { id: 't8', title: 'Pricing page copy', projectId: 'pr1', assigneeId: 'm3', priority: 'low', status: 'review', estimate: 4, spent: 3, progress: 75, startedAt: '2026-07-11T15:30:00Z', dueDate: '2026-07-18' },
112
+ ],
113
+ }
114
+
115
+ export const projectTracker: SampleApp = {
116
+ id: 'projects',
117
+ name: 'Project tracker',
118
+ description: 'Projects, tasks and a team roster - portfolio + task-board dashboards (budget KPI sparkline + target, progress gauge, status/priority breakdowns with pie + area trend, status x priority pivot, tabbed views), filtered status-pill grids with row actions, master/detail, and rich edit forms (progress slider, color labels, tag chips, phone, rating, datetime).',
119
+ emoji: '\u{1F4CB}',
120
+ accent: '#8b5cf6',
121
+ build: () => {
122
+ const projectRows = pad(projectsEntity, seed.projects, 10)
123
+ const projectPool = ids(projectRows)
124
+ const memberRows = pad(members, seed.members, 12)
125
+ const memberPool = ids(memberRows)
126
+ const taskRows = pad(tasks, seed.tasks, 60, { projectId: projectPool, assigneeId: memberPool })
127
+ return project({
128
+ title: 'Project Tracker',
129
+ brand: 'Tracker',
130
+ accent: '#8b5cf6',
131
+ preset: 'linear',
132
+ footer: '',
133
+ entities: [projectsEntity, members, tasks],
134
+ seed: { projects: projectRows, members: memberRows, tasks: taskRows },
135
+ screens: [
136
+ // Portfolio dashboard: budget KPIs (a total-budget card with a sparkline over
137
+ // due dates + a $400k target), an average-progress gauge (0-100), status
138
+ // breakdowns (bar + pie), a budget-by-owner x status pivot, a tabbed view, and
139
+ // a status-pill project grid with inline edit actions.
140
+ dashScreen(projectsEntity, { id: 'portfolio', title: 'Portfolio', order: 0 }, [
141
+ { kpi: 'Total budget', measure: 'budget', reduce: 'sum', format: 'currency', trendField: 'dueDate', trendReduce: 'sum', target: 400000, span: 1 },
142
+ { kpi: 'Projects', reduce: 'count', span: 1 },
143
+ { kpi: 'Largest budget', measure: 'budget', reduce: 'max', format: 'currency', span: 1 },
144
+ { gauge: 'Avg progress', measure: 'progress', reduce: 'avg', min: 0, max: 100, unit: '%', span: 1 },
145
+ { chart: 'status', measure: 'budget', reduce: 'sum', type: 'bar', span: 2 },
146
+ { chart: 'status', reduce: 'count', type: 'pie', span: 1 },
147
+ { pivot: { rows: ['owner'], cols: ['status'], measure: 'budget', aggregate: 'sum' }, span: 3 },
148
+ { tabs: [
149
+ { label: 'Budget by status', tiles: [{ chart: 'status', measure: 'budget', reduce: 'sum', type: 'bar' }] },
150
+ { label: 'Projects by status', tiles: [{ chart: 'status', reduce: 'count', type: 'pie' }] },
151
+ ], span: 3 },
152
+ { grid: true, format: [...statusPills(projectsEntity, 'status'), { field: 'progress', op: 'gte', value: 100, color: '#16a34a', bold: true }], summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
153
+ ]),
154
+ // Task board: task KPIs (an estimate card trending over due dates), a completion
155
+ // gauge, status + priority breakdowns (bar + pie), a spent-over-time area trend,
156
+ // an estimate pivot by status (rows) x priority (columns), a tabbed view, a
157
+ // filter, and a task grid with status + priority pills and inline edit actions.
158
+ // Kanban board: tasks as cards in status columns (drag to change status).
159
+ boardScreen(tasks, { id: 'board', title: 'Kanban', order: 1 }, { groupBy: 'status', titleField: 'title', badgeField: 'estimate', subtitleField: 'priority', filter: ['priority', 'assigneeId'] }),
160
+ // Project detail: header (status pill) + budget / progress tiles with a
161
+ // timeline of the project's tasks (by due date, colored by status).
162
+ detailScreen(projectsEntity, { id: 'project-detail', title: 'Project detail', order: 2 }, {
163
+ titleField: 'name', subtitleField: 'owner', statusField: 'status', metricFields: ['budget', 'progress'],
164
+ related: [{ entity: 'tasks', foreignKey: 'projectId', label: 'Tasks', titleField: 'title', subtitleField: 'priority', dateField: 'dueDate', statusField: 'status' }],
165
+ }),
166
+ dashScreen(tasks, { id: 'tasks-board', title: 'Task board', order: 3 }, [
167
+ { kpi: 'Tasks', reduce: 'count', span: 1 },
168
+ { kpi: 'Logged hours', measure: 'spent', reduce: 'sum', format: 'compact', span: 1 },
169
+ { kpi: 'Estimated hours', measure: 'estimate', reduce: 'sum', format: 'compact', trendField: 'dueDate', trendReduce: 'sum', span: 1 },
170
+ { gauge: 'Avg completion', measure: 'progress', reduce: 'avg', min: 0, max: 100, unit: '%', span: 1 },
171
+ { chart: 'status', reduce: 'count', type: 'bar', span: 2 },
172
+ { chart: 'priority', reduce: 'count', type: 'pie', span: 1 },
173
+ { chart: 'dueDate', measure: 'spent', reduce: 'sum', type: 'area', span: 3 },
174
+ { pivot: { rows: ['status'], cols: ['priority'], measure: 'estimate', aggregate: 'sum' }, span: 3 },
175
+ { tabs: [
176
+ { label: 'Hours by status', tiles: [{ chart: 'status', measure: 'spent', reduce: 'sum', type: 'bar' }] },
177
+ { label: 'Tasks by priority', tiles: [{ chart: 'priority', reduce: 'count', type: 'bar' }] },
178
+ ], span: 3 },
179
+ { filter: ['status', 'priority', 'assigneeId'], span: 3 },
180
+ { grid: true, format: taskFormats, summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
181
+ ]),
182
+ screen(projectsEntity, 'master-detail', { id: 'projects', title: 'Projects', order: 4, child: tasks, foreignKey: 'projectId', linkScreen: 'project-detail' }),
183
+ formScreen(tasks, { id: 'tasks', title: 'Tasks', order: 5 }, undefined, { format: taskFormats, summaries: true, rowActions: [{ kind: 'edit' }] }, ['status', 'priority', 'assigneeId']),
184
+ formScreen(members, { id: 'members', title: 'Members', order: 6 }, undefined, { format: statusPills(members, 'role'), summaries: true }, ['role']),
185
+ // Schedule: tasks placed on a month calendar by due date.
186
+ calendarScreen(tasks, { id: 'calendar', title: 'Schedule', order: 7 }, { dateField: 'dueDate', titleField: 'title', filter: ['status', 'priority', 'assigneeId'] }),
187
+ ],
188
+ })
189
+ },
190
+ }
@@ -0,0 +1,196 @@
1
+ import type { EntitySchema } from '../../schema.js'
2
+ import type { FormatRule } from '../project.js'
3
+ import { screen, formScreen, boardScreen, project, dashScreen, detailScreen, statusPills, pad, ids, type SampleApp } from './shared.js'
4
+
5
+ const agents: EntitySchema = {
6
+ name: 'agents',
7
+ label: 'Agent',
8
+ idField: 'id',
9
+ fields: [
10
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
11
+ { field: 'name', type: 'text', required: true, minLength: 2 },
12
+ { field: 'email', type: 'text', format: 'email', required: true },
13
+ { field: 'phone', type: 'text', input: { editorType: 'phone' } },
14
+ { field: 'license', type: 'text', label: 'License #', input: { editorType: 'mask', mask: 'AA-######', help: 'State license number' } },
15
+ { field: 'rating', type: 'number', label: 'Rating', min: 1, max: 5, defaultValue: 4, input: { editorType: 'rating', help: '1-5 client satisfaction' } },
16
+ { field: 'active', type: 'boolean', label: 'Active', defaultValue: true },
17
+ ],
18
+ }
19
+
20
+ const properties: EntitySchema = {
21
+ name: 'properties',
22
+ label: 'Property',
23
+ idField: 'id',
24
+ fields: [
25
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
26
+ { field: 'title', type: 'text', required: true, minLength: 3 },
27
+ { field: 'type', type: 'enum', required: true, options: [
28
+ { value: 'house', label: 'House', color: '#6366f1' },
29
+ { value: 'condo', label: 'Condo', color: '#0ea5e9' },
30
+ { value: 'land', label: 'Land', color: '#f59e0b' },
31
+ { value: 'commercial', label: 'Commercial', color: '#ec4899' },
32
+ ] },
33
+ { field: 'status', type: 'enum', required: true, options: [
34
+ { value: 'available', label: 'Available', color: '#10b981' },
35
+ { value: 'pending', label: 'Pending', color: '#f59e0b' },
36
+ { value: 'sold', label: 'Sold', color: '#94a3b8' },
37
+ ] },
38
+ { field: 'price', type: 'number', label: 'Price ($)', min: 0 },
39
+ { field: 'beds', type: 'number', label: 'Beds', min: 0 },
40
+ { field: 'baths', type: 'number', label: 'Baths', min: 0 },
41
+ { field: 'area', type: 'number', label: 'Area (sqft)', min: 0 },
42
+ // Derived: list price per square foot (display-only).
43
+ { field: 'pricePerSqft', type: 'number', label: 'Price / sqft ($)', readonly: true, formula: 'price / area' },
44
+ { field: 'yearBuilt', type: 'number', label: 'Year built', min: 1800, max: 2100 },
45
+ { field: 'zip', type: 'text', label: 'ZIP', input: { editorType: 'mask', mask: '#####' } },
46
+ { field: 'city', type: 'text', label: 'City' },
47
+ { field: 'amenities', type: 'text', label: 'Amenities', input: { editorType: 'chips', help: 'Pool, Garage, ...' } },
48
+ { field: 'listingUrl', type: 'text', label: 'Listing', format: 'url', input: { placeholder: 'https://…' } },
49
+ ],
50
+ }
51
+
52
+ const leads: EntitySchema = {
53
+ name: 'leads',
54
+ label: 'Lead',
55
+ idField: 'id',
56
+ fields: [
57
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
58
+ { field: 'name', type: 'text', label: 'Lead name', required: true, minLength: 2 },
59
+ { field: 'phone', type: 'text', input: { editorType: 'phone' } },
60
+ { field: 'propertyId', type: 'relation', label: 'Property', relation: { entity: 'properties', foreignKey: 'propertyId', labelField: 'title' } },
61
+ { field: 'agentId', type: 'relation', label: 'Agent', relation: { entity: 'agents', foreignKey: 'agentId', labelField: 'name' } },
62
+ { field: 'stage', type: 'enum', required: true, options: [
63
+ { value: 'new', label: 'New', color: '#94a3b8' },
64
+ { value: 'touring', label: 'Touring', color: '#0ea5e9' },
65
+ { value: 'offer', label: 'Offer', color: '#f59e0b' },
66
+ { value: 'closed', label: 'Closed', color: '#10b981' },
67
+ { value: 'lost', label: 'Lost', color: '#ef4444' },
68
+ ] },
69
+ { field: 'budget', type: 'number', label: 'Budget ($)', min: 0 },
70
+ { field: 'score', type: 'number', label: 'Lead score', min: 0, max: 100, defaultValue: 50, input: { editorType: 'slider', help: 'Fit / intent score' } },
71
+ { field: 'interests', type: 'text', label: 'Interests', input: { editorType: 'chips', help: 'Neighborhoods / features' } },
72
+ ],
73
+ }
74
+
75
+ // Property grid formatting: type + status pills, plus price thresholds (entry-level
76
+ // listings red, luxury listings green + bold) - data-driven color, not just pills.
77
+ const propertyFormats: FormatRule[] = [
78
+ ...statusPills(properties, 'type'),
79
+ ...statusPills(properties, 'status'),
80
+ { field: 'price', op: 'lt', value: 300000, color: '#dc2626' },
81
+ { field: 'price', op: 'gte', value: 1000000, color: '#16a34a', bold: true },
82
+ ]
83
+
84
+ // Lead grid formatting: stage pills, plus score thresholds (cold leads red, hot
85
+ // leads green + bold) and big-budget rows in bold.
86
+ const leadFormats: FormatRule[] = [
87
+ ...statusPills(leads, 'stage'),
88
+ { field: 'score', op: 'lt', value: 40, color: '#dc2626' },
89
+ { field: 'score', op: 'gte', value: 80, color: '#16a34a', bold: true },
90
+ { field: 'budget', op: 'gte', value: 1000000, bold: true },
91
+ ]
92
+
93
+ const seed = {
94
+ agents: [
95
+ { id: 'ag1', name: 'Sam Rivera', email: 'sam@realty.com', phone: '+1 (415) 555-0201', license: 'CA-104822', rating: 5, active: true },
96
+ { id: 'ag2', name: 'Jamie Chen', email: 'jamie@realty.com', phone: '+1 (650) 555-0202', license: 'CA-118934', rating: 4, active: true },
97
+ { id: 'ag3', name: 'Priya Patel', email: 'priya@realty.com', phone: '+1 (212) 555-0203', license: 'NY-207651', rating: 4, active: true },
98
+ { id: 'ag4', name: 'Diego Morales', email: 'diego@realty.com', phone: '+1 (305) 555-0204', license: 'FL-330487', rating: 3, active: true },
99
+ { id: 'ag5', name: 'Nina Kovac', email: 'nina@realty.com', phone: '+1 (312) 555-0205', license: 'IL-415229', rating: 5, active: true },
100
+ ],
101
+ properties: [
102
+ { id: 'pt1', title: 'Sunny Hill Villa', type: 'house', status: 'available', price: 875000, beds: 4, baths: 3, area: 3200, yearBuilt: 2015, zip: '78701', city: 'Austin', amenities: ['Pool', 'Garage', 'Garden'], listingUrl: 'https://realty.com/listings/pt1' },
103
+ { id: 'pt2', title: 'Downtown Loft', type: 'condo', status: 'pending', price: 420000, beds: 2, baths: 2, area: 1150, yearBuilt: 2019, zip: '60601', city: 'Chicago', amenities: ['Gym', 'Doorman'], listingUrl: 'https://realty.com/listings/pt2' },
104
+ { id: 'pt3', title: 'Riverside Acreage', type: 'land', status: 'available', price: 260000, beds: 0, baths: 0, area: 87120, yearBuilt: 2000, zip: '83702', city: 'Boise', amenities: ['Waterfront'], listingUrl: 'https://realty.com/listings/pt3' },
105
+ { id: 'pt4', title: 'Market Street Office', type: 'commercial', status: 'sold', price: 1950000, beds: 0, baths: 4, area: 12000, yearBuilt: 1998, zip: '94103', city: 'San Francisco', amenities: ['Parking', 'Elevator'], listingUrl: 'https://realty.com/listings/pt4' },
106
+ { id: 'pt5', title: 'Maple Court House', type: 'house', status: 'sold', price: 640000, beds: 3, baths: 2, area: 2100, yearBuilt: 2008, zip: '80203', city: 'Denver', amenities: ['Garage', 'Fireplace'], listingUrl: 'https://realty.com/listings/pt5' },
107
+ { id: 'pt6', title: 'Harbor View Condo', type: 'condo', status: 'available', price: 510000, beds: 2, baths: 2, area: 1340, yearBuilt: 2021, zip: '98101', city: 'Seattle', amenities: ['Balcony', 'Gym', 'View'], listingUrl: 'https://realty.com/listings/pt6' },
108
+ { id: 'pt7', title: 'Old Town Retail', type: 'commercial', status: 'pending', price: 1180000, beds: 0, baths: 2, area: 6800, yearBuilt: 1985, zip: '97205', city: 'Portland', amenities: ['Storefront', 'Parking'], listingUrl: 'https://realty.com/listings/pt7' },
109
+ ],
110
+ leads: [
111
+ { id: 'ld1', name: 'Olivia Brooks', phone: '+1 (415) 555-0301', propertyId: 'pt1', agentId: 'ag1', stage: 'touring', budget: 900000, score: 78, interests: ['Downtown', 'Pool'] },
112
+ { id: 'ld2', name: 'Marcus Reed', phone: '+1 (650) 555-0302', propertyId: 'pt2', agentId: 'ag2', stage: 'offer', budget: 430000, score: 88, interests: ['Loft', 'Gym'] },
113
+ { id: 'ld3', name: 'Hannah Lee', phone: '+1 (208) 555-0303', propertyId: 'pt3', agentId: 'ag3', stage: 'new', budget: 250000, score: 42, interests: ['Land', 'Waterfront'] },
114
+ { id: 'ld4', name: 'Trevor Nash', phone: '+1 (415) 555-0304', propertyId: 'pt4', agentId: 'ag1', stage: 'closed', budget: 1950000, score: 95, interests: ['Commercial'] },
115
+ { id: 'ld5', name: 'Sofia Marin', phone: '+1 (305) 555-0305', propertyId: 'pt5', agentId: 'ag4', stage: 'lost', budget: 600000, score: 30, interests: ['Suburbs', 'Garage'] },
116
+ { id: 'ld6', name: 'Ethan Walsh', phone: '+1 (206) 555-0306', propertyId: 'pt6', agentId: 'ag5', stage: 'touring', budget: 520000, score: 66, interests: ['View', 'Balcony'] },
117
+ { id: 'ld7', name: 'Grace Kim', phone: '+1 (503) 555-0307', propertyId: 'pt7', agentId: 'ag2', stage: 'offer', budget: 1150000, score: 81, interests: ['Retail', 'Parking'] },
118
+ ],
119
+ }
120
+
121
+ export const realEstate: SampleApp = {
122
+ id: 'realestate',
123
+ name: 'Real estate',
124
+ description: 'Properties, agents and leads - sales + pipeline + agent dashboards (price KPI target, rating and lead-score gauges, type x status pivot, tabbed breakdown), filtered status-pill grids with row actions, master/detail, and rich edit forms (phone, license mask, rating, amenity chips, lead-score slider).',
125
+ emoji: '\u{1F3E0}',
126
+ accent: '#10b981',
127
+ build: () => {
128
+ const agentRows = pad(agents, seed.agents, 10)
129
+ const agentPool = ids(agentRows)
130
+ const propertyRows = pad(properties, seed.properties, 18)
131
+ const propertyPool = ids(propertyRows)
132
+ const leadRows = pad(leads, seed.leads, 60, { propertyId: propertyPool, agentId: agentPool })
133
+ return project({
134
+ title: 'Realty',
135
+ brand: 'Realty',
136
+ accent: '#10b981',
137
+ preset: 'nord',
138
+ footer: '© Realty Group',
139
+ entities: [agents, properties, leads],
140
+ seed: { agents: agentRows, properties: propertyRows, leads: leadRows },
141
+ screens: [
142
+ // Sales dashboard: listing KPIs (a total-value card with a $10M target), an
143
+ // average-price gauge, a value-by-type bar, a status pie, a tabbed breakdown,
144
+ // a type x status pivot, and a status-pill property grid with edit actions.
145
+ dashScreen(properties, { id: 'overview', title: 'Sales', order: 0 }, [
146
+ { kpi: 'Listings', reduce: 'count', span: 1 },
147
+ { kpi: 'Total value', measure: 'price', reduce: 'sum', format: 'currency', target: 10000000, span: 1 },
148
+ { kpi: 'Avg price', measure: 'price', reduce: 'avg', format: 'currency', span: 1 },
149
+ { gauge: 'Avg price vs $1M', measure: 'price', reduce: 'avg', min: 0, max: 1000000, unit: '$', span: 1 },
150
+ { chart: 'type', measure: 'price', reduce: 'sum', type: 'bar', span: 2 },
151
+ { chart: 'status', reduce: 'count', type: 'pie', span: 1 },
152
+ { chart: 'type', reduce: 'count', type: 'bar', span: 2 },
153
+ { tabs: [
154
+ { label: 'Value by type', tiles: [{ chart: 'type', measure: 'price', reduce: 'sum', type: 'bar' }] },
155
+ { label: 'Listings by status', tiles: [{ chart: 'status', reduce: 'count', type: 'bar' }] },
156
+ ], span: 3 },
157
+ { pivot: { rows: ['type'], cols: ['status'], measure: 'price', aggregate: 'sum' }, span: 3 },
158
+ { grid: true, format: propertyFormats, summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
159
+ ]),
160
+ // Pipeline dashboard: budget + score KPIs, a lead-score gauge, stage
161
+ // breakdowns, a stage filter, and a score-formatted status-pill lead grid.
162
+ // Listings board: properties as cards in status columns (available/pending/sold).
163
+ boardScreen(properties, { id: 'board', title: 'Listings', order: 1 }, { groupBy: 'status', titleField: 'title', badgeField: 'price', subtitleField: 'city', filter: ['type', 'status'], openScreen: 'property-detail' }),
164
+ dashScreen(leads, { id: 'pipeline', title: 'Pipeline', order: 2 }, [
165
+ { kpi: 'Leads', reduce: 'count', span: 1 },
166
+ { kpi: 'Pipeline budget', measure: 'budget', reduce: 'sum', format: 'currency', span: 1 },
167
+ { kpi: 'Avg score', measure: 'score', reduce: 'avg', span: 1 },
168
+ { gauge: 'Avg lead score', measure: 'score', reduce: 'avg', min: 0, max: 100, span: 1 },
169
+ { chart: 'stage', reduce: 'count', type: 'bar', span: 2 },
170
+ { chart: 'stage', measure: 'budget', reduce: 'sum', type: 'pie', span: 1 },
171
+ { filter: ['stage'], span: 3 },
172
+ { grid: true, format: leadFormats, summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
173
+ ]),
174
+ // Agents dashboard: roster KPIs, an average client-rating gauge (0-5), and a
175
+ // per-agent rating leaderboard.
176
+ dashScreen(agents, { id: 'ratings', title: 'Agents', order: 3 }, [
177
+ { kpi: 'Agents', reduce: 'count', span: 1 },
178
+ { kpi: 'Avg rating', measure: 'rating', reduce: 'avg', span: 1 },
179
+ { gauge: 'Avg client rating', measure: 'rating', reduce: 'avg', min: 0, max: 5, span: 1 },
180
+ { chart: 'name', measure: 'rating', reduce: 'avg', type: 'bar', span: 3 },
181
+ { grid: true, summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
182
+ ]),
183
+ // Property detail: header (title + city + status pill + price/area tiles),
184
+ // tabbed overview, and an Inquiries timeline of related leads.
185
+ detailScreen(properties, { id: 'property-detail', title: 'Property detail', order: 4 }, {
186
+ titleField: 'title', subtitleField: 'city', statusField: 'status',
187
+ metricFields: ['price', 'area'],
188
+ related: [{ entity: 'leads', foreignKey: 'propertyId', label: 'Inquiries', titleField: 'name', subtitleField: 'budget', statusField: 'stage' }],
189
+ }),
190
+ screen(agents, 'master-detail', { id: 'agent-book', title: 'Agent book', order: 5, child: leads, foreignKey: 'agentId' }),
191
+ formScreen(properties, { id: 'properties', title: 'Properties', order: 6 }, undefined, { format: propertyFormats, summaries: true, rowActions: [{ kind: 'edit' }], rowLink: { screen: 'property-detail', sourceField: 'id', targetField: 'id' } }, ['type', 'status', 'city']),
192
+ formScreen(leads, { id: 'leads', title: 'Leads', order: 7 }, undefined, { format: leadFormats, summaries: true, rowActions: [{ kind: 'edit' }] }, ['stage']),
193
+ ],
194
+ })
195
+ },
196
+ }
@@ -0,0 +1,187 @@
1
+ import type { EntitySchema } from '../../schema.js'
2
+ import type { FormatRule } from '../project.js'
3
+ import { screen, formScreen, calendarScreen, detailScreen, project, dashScreen, statusPills, pad, ids, type SampleApp } from './shared.js'
4
+
5
+ const menuItems: EntitySchema = {
6
+ name: 'menuItems',
7
+ label: 'Menu item',
8
+ idField: 'id',
9
+ fields: [
10
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
11
+ { field: 'name', type: 'text', required: true, minLength: 2 },
12
+ { field: 'category', type: 'enum', required: true, options: [
13
+ { value: 'starter', label: 'Starter', color: '#6366f1' },
14
+ { value: 'main', label: 'Main', color: '#10b981' },
15
+ { value: 'dessert', label: 'Dessert', color: '#ec4899' },
16
+ { value: 'drink', label: 'Drink', color: '#0ea5e9' },
17
+ ] },
18
+ { field: 'price', type: 'number', label: 'Price ($)', required: true, min: 0, input: { prefix: '$' } },
19
+ { field: 'dietaryTags', type: 'text', label: 'Dietary tags', input: { editorType: 'chips', help: 'Dietary labels, e.g. Vegan / Gluten-free' } },
20
+ { field: 'spiceLevel', type: 'number', label: 'Spice level', min: 0, max: 5, defaultValue: 0, input: { editorType: 'slider', help: '0 mild to 5 fiery' } },
21
+ { field: 'prepMinutes', type: 'number', label: 'Prep (min)', min: 0 },
22
+ { field: 'rating', type: 'number', label: 'Rating', min: 0, max: 5, input: { editorType: 'rating' } },
23
+ { field: 'available', type: 'boolean', label: 'Available', defaultValue: true },
24
+ ],
25
+ }
26
+
27
+ const tables: EntitySchema = {
28
+ name: 'tables',
29
+ label: 'Table',
30
+ idField: 'id',
31
+ fields: [
32
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
33
+ { field: 'name', type: 'text', required: true, minLength: 2 },
34
+ { field: 'seats', type: 'number', label: 'Seats', required: true, min: 1, max: 20 },
35
+ { field: 'section', type: 'enum', required: true, options: [
36
+ { value: 'indoor', label: 'Indoor', color: '#6366f1' },
37
+ { value: 'patio', label: 'Patio', color: '#10b981' },
38
+ { value: 'bar', label: 'Bar', color: '#f59e0b' },
39
+ { value: 'private', label: 'Private', color: '#a855f7' },
40
+ ] },
41
+ { field: 'color', type: 'text', label: 'Map color', defaultValue: '#6366f1', input: { editorType: 'color', help: 'Swatch used on the floor plan' } },
42
+ ],
43
+ }
44
+
45
+ const orders: EntitySchema = {
46
+ name: 'orders',
47
+ label: 'Order',
48
+ idField: 'id',
49
+ fields: [
50
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
51
+ { field: 'tableId', type: 'relation', label: 'Table', relation: { entity: 'tables', foreignKey: 'tableId', labelField: 'name' } },
52
+ { field: 'status', type: 'enum', required: true, options: [
53
+ { value: 'open', label: 'Open', color: '#0ea5e9' },
54
+ { value: 'served', label: 'Served', color: '#f59e0b' },
55
+ { value: 'paid', label: 'Paid', color: '#10b981' },
56
+ { value: 'void', label: 'Void', color: '#ef4444' },
57
+ ] },
58
+ { field: 'total', type: 'number', label: 'Total ($)', required: true, min: 0, input: { prefix: '$' } },
59
+ { field: 'partySize', type: 'number', label: 'Party size', min: 1, max: 20 },
60
+ { field: 'tipPercent', type: 'number', label: 'Tip (%)', min: 0, max: 30, defaultValue: 15, input: { editorType: 'slider', help: 'Gratuity applied to the total' } },
61
+ // Derived: total plus gratuity (display-only, never stored).
62
+ { field: 'grandTotal', type: 'number', label: 'Grand total ($)', readonly: true, formula: 'total + total * tipPercent / 100' },
63
+ { field: 'customerPhone', type: 'text', label: 'Customer phone', input: { editorType: 'phone' } },
64
+ { field: 'reservationTime', type: 'datetime', label: 'Reservation' },
65
+ { field: 'placedAt', type: 'date', label: 'Placed at' },
66
+ ],
67
+ }
68
+
69
+ const seed = {
70
+ menuItems: [
71
+ { id: 'mi1', name: 'Bruschetta', category: 'starter', price: 9, dietaryTags: ['Vegetarian'], spiceLevel: 0, prepMinutes: 8, rating: 4, available: true },
72
+ { id: 'mi2', name: 'Caesar Salad', category: 'starter', price: 11, dietaryTags: ['Vegetarian', 'Gluten-free'], spiceLevel: 0, prepMinutes: 6, rating: 4, available: true },
73
+ { id: 'mi3', name: 'Margherita Pizza', category: 'main', price: 16, dietaryTags: ['Vegetarian'], spiceLevel: 1, prepMinutes: 14, rating: 5, available: true },
74
+ { id: 'mi4', name: 'Ribeye Steak', category: 'main', price: 32, dietaryTags: ['Gluten-free', 'High-protein'], spiceLevel: 2, prepMinutes: 22, rating: 5, available: true },
75
+ { id: 'mi5', name: 'Spicy Arrabbiata', category: 'main', price: 18, dietaryTags: ['Vegan', 'Spicy'], spiceLevel: 4, prepMinutes: 16, rating: 4, available: true },
76
+ { id: 'mi6', name: 'Tiramisu', category: 'dessert', price: 8, dietaryTags: ['Vegetarian'], spiceLevel: 0, prepMinutes: 5, rating: 5, available: true },
77
+ { id: 'mi7', name: 'Espresso', category: 'drink', price: 4, dietaryTags: ['Vegan'], spiceLevel: 0, prepMinutes: 3, rating: 4, available: true },
78
+ { id: 'mi8', name: 'House Red', category: 'drink', price: 12, dietaryTags: ['Vegan'], spiceLevel: 0, prepMinutes: 2, rating: 3, available: false },
79
+ ],
80
+ tables: [
81
+ { id: 'tb1', name: 'Table 1', seats: 2, section: 'indoor', color: '#6366f1' },
82
+ { id: 'tb2', name: 'Table 2', seats: 4, section: 'indoor', color: '#6366f1' },
83
+ { id: 'tb3', name: 'Table 5', seats: 6, section: 'patio', color: '#10b981' },
84
+ { id: 'tb4', name: 'Table 8', seats: 4, section: 'patio', color: '#10b981' },
85
+ { id: 'tb5', name: 'Bar 1', seats: 1, section: 'bar', color: '#f59e0b' },
86
+ { id: 'tb6', name: 'Bar 2', seats: 1, section: 'bar', color: '#f59e0b' },
87
+ { id: 'tb7', name: 'Salon', seats: 10, section: 'private', color: '#a855f7' },
88
+ ],
89
+ orders: [
90
+ { id: 'or1', tableId: 'tb1', status: 'paid', total: 54, partySize: 2, tipPercent: 18, customerPhone: '+1 (415) 555-0142', reservationTime: '2026-07-10T19:30:00', placedAt: '2026-07-10' },
91
+ { id: 'or2', tableId: 'tb2', status: 'served', total: 88, partySize: 4, tipPercent: 15, customerPhone: '+1 (415) 555-0187', reservationTime: '2026-07-11T20:00:00', placedAt: '2026-07-11' },
92
+ { id: 'or3', tableId: 'tb3', status: 'open', total: 132, partySize: 6, tipPercent: 20, customerPhone: '+1 (628) 555-0110', reservationTime: '2026-07-12T18:45:00', placedAt: '2026-07-12' },
93
+ { id: 'or4', tableId: 'tb4', status: 'paid', total: 76, partySize: 4, tipPercent: 15, customerPhone: '+1 (415) 555-0163', reservationTime: '2026-07-12T21:15:00', placedAt: '2026-07-12' },
94
+ { id: 'or5', tableId: 'tb5', status: 'void', total: 18, partySize: 1, tipPercent: 0, customerPhone: '+1 (510) 555-0199', reservationTime: '2026-07-13T17:30:00', placedAt: '2026-07-13' },
95
+ { id: 'or6', tableId: 'tb1', status: 'open', total: 42, partySize: 2, tipPercent: 18, customerPhone: '+1 (415) 555-0125', reservationTime: '2026-07-14T19:00:00', placedAt: '2026-07-14' },
96
+ { id: 'or7', tableId: 'tb7', status: 'served', total: 265, partySize: 10, tipPercent: 22, customerPhone: '+1 (628) 555-0148', reservationTime: '2026-07-14T20:30:00', placedAt: '2026-07-14' },
97
+ ],
98
+ }
99
+
100
+ // Order grid formatting: status pills, plus numeric thresholds (big tickets bold,
101
+ // tiny tickets red, generous tips green + bold) - data-driven color, not just
102
+ // status pills.
103
+ const orderFormats: FormatRule[] = [
104
+ ...statusPills(orders, 'status'),
105
+ { field: 'total', op: 'gte', value: 150, bold: true },
106
+ { field: 'total', op: 'lt', value: 30, color: '#dc2626' },
107
+ { field: 'tipPercent', op: 'gte', value: 20, color: '#16a34a', bold: true },
108
+ ]
109
+
110
+ // Menu grid formatting: category pills, plus spice / rating / price thresholds
111
+ // (fiery dishes red + bold, five-star dishes green + bold, premium plates bold).
112
+ const menuFormats: FormatRule[] = [
113
+ ...statusPills(menuItems, 'category'),
114
+ { field: 'spiceLevel', op: 'gte', value: 4, color: '#dc2626', bold: true },
115
+ { field: 'rating', op: 'gte', value: 5, color: '#16a34a', bold: true },
116
+ { field: 'price', op: 'gte', value: 25, bold: true },
117
+ ]
118
+
119
+ export const restaurant: SampleApp = {
120
+ id: 'restaurant',
121
+ name: 'Restaurant',
122
+ description: 'Menu, tables and orders - sales + menu dashboards (KPI sparkline + target, sales-over-time area trend, order-status pie, day x status pivot, rating gauge, tabs), filtered status-pill grids with row actions, tables/orders master-detail, and rich edit forms (chips, spice slider, rating, color swatch, phone, reservation datetime).',
123
+ emoji: '\u{1F37D}',
124
+ accent: '#f97316',
125
+ build: () => {
126
+ const menuRows = pad(menuItems, seed.menuItems, 40)
127
+ const tableRows = pad(tables, seed.tables, 16)
128
+ const orderRows = pad(orders, seed.orders, 90, { tableId: ids(tableRows) })
129
+ return project({
130
+ title: 'Bistro',
131
+ brand: 'Bistro',
132
+ accent: '#f97316',
133
+ preset: 'bootstrap',
134
+ navStyle: 'top-nav',
135
+ footer: '',
136
+ entities: [menuItems, tables, orders],
137
+ seed: { menuItems: menuRows, tables: tableRows, orders: orderRows },
138
+ screens: [
139
+ // Sales dashboard: KPI cards (a total-sales card with a sparkline over
140
+ // placed dates + a $2k target), a sales-vs-target gauge, a sales-over-time
141
+ // area trend, an order-status pie, a day x status value pivot, a tabbed
142
+ // breakdown, and a status-pill order grid with inline edit actions.
143
+ dashScreen(orders, { id: 'overview', title: 'Overview', order: 0 }, [
144
+ { kpi: 'Total sales', measure: 'total', reduce: 'sum', format: 'currency', trendField: 'placedAt', trendReduce: 'sum', target: 2000, span: 1 },
145
+ { kpi: 'Orders', reduce: 'count', span: 1 },
146
+ { kpi: 'Avg ticket', measure: 'total', reduce: 'avg', format: 'currency', span: 1 },
147
+ { gauge: 'Sales vs $3k target', measure: 'total', reduce: 'sum', min: 0, max: 3000, unit: '$', span: 1 },
148
+ { chart: 'placedAt', measure: 'total', reduce: 'sum', type: 'area', span: 2 },
149
+ { chart: 'status', reduce: 'count', type: 'pie', span: 1 },
150
+ { chart: 'status', measure: 'total', reduce: 'sum', type: 'bar', span: 3 },
151
+ { pivot: { rows: ['placedAt'], cols: ['status'], measure: 'total', aggregate: 'sum' }, span: 3 },
152
+ { tabs: [
153
+ { label: 'Sales by status', tiles: [{ chart: 'status', measure: 'total', reduce: 'sum', type: 'bar' }] },
154
+ { label: 'Orders by status', tiles: [{ chart: 'status', reduce: 'count', type: 'bar' }] },
155
+ ], span: 3 },
156
+ { grid: true, format: orderFormats, summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
157
+ ]),
158
+ // Menu dashboard: price / spice / rating KPIs, an average-rating gauge, a
159
+ // price-by-category bar + a category pie, a category x availability price
160
+ // pivot, a tabbed breakdown, a faceted filter, and a category-pill menu grid.
161
+ dashScreen(menuItems, { id: 'menu', title: 'Menu', order: 1 }, [
162
+ { kpi: 'Menu items', reduce: 'count', span: 1 },
163
+ { kpi: 'Avg price', measure: 'price', reduce: 'avg', format: 'currency', span: 1 },
164
+ { kpi: 'Avg spice', measure: 'spiceLevel', reduce: 'avg', span: 1 },
165
+ { gauge: 'Avg rating', measure: 'rating', reduce: 'avg', min: 0, max: 5, span: 1 },
166
+ { chart: 'category', measure: 'price', reduce: 'avg', type: 'bar', span: 2 },
167
+ { chart: 'category', reduce: 'count', type: 'pie', span: 1 },
168
+ { pivot: { rows: ['category'], cols: ['available'], measure: 'price', aggregate: 'avg' }, span: 3 },
169
+ { tabs: [
170
+ { label: 'Price by category', tiles: [{ chart: 'category', measure: 'price', reduce: 'avg', type: 'bar' }] },
171
+ { label: 'Rating by category', tiles: [{ chart: 'category', measure: 'rating', reduce: 'avg', type: 'bar' }] },
172
+ ], span: 3 },
173
+ { filter: ['category', 'available'], span: 3 },
174
+ { grid: true, format: menuFormats, summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
175
+ ]),
176
+ calendarScreen(orders, { id: 'reservations', title: 'Reservations', order: 2 }, { dateField: 'reservationTime', titleField: 'tableId', colorField: 'status', filter: ['status', 'tableId'] }),
177
+ detailScreen(tables, { id: 'table-detail', title: 'Table detail', order: 3 }, {
178
+ titleField: 'name', statusField: 'section', metricFields: ['seats'],
179
+ related: [{ entity: 'orders', foreignKey: 'tableId', label: 'Orders', titleField: 'customerPhone', subtitleField: 'total', dateField: 'reservationTime', statusField: 'status' }],
180
+ }),
181
+ screen(tables, 'master-detail', { id: 'tables', title: 'Tables', order: 4, child: orders, foreignKey: 'tableId', linkScreen: 'table-detail' }),
182
+ formScreen(orders, { id: 'orders', title: 'Orders', order: 5 }, undefined, { format: orderFormats, summaries: true, rowActions: [{ kind: 'edit' }] }, ['tableId', 'status']),
183
+ formScreen(menuItems, { id: 'menuItems', title: 'Menu items', order: 6 }, undefined, { format: menuFormats, summaries: true, rowActions: [{ kind: 'edit' }] }, ['category', 'dietaryTags', 'available']),
184
+ ],
185
+ })
186
+ },
187
+ }