@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
@@ -1,190 +1,212 @@
1
- import type { EntitySchema } from '../../schema.js'
2
- import type { FormatRule } from '../project.js'
3
- import { screen, formScreen, boardScreen, schedulerScreen, 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
- schedulerScreen(tasks, { id: 'calendar', title: 'Schedule', order: 7 }, { startField: 'startedAt', endField: 'dueDate', titleField: 'title', colorField: 'priority', resourceField: 'assigneeId', initialView: 'timelineWeek' }),
187
- ],
188
- })
189
- },
190
- }
1
+ import type { EntitySchema } from '../../schema.js'
2
+ import type { FormatRule } from '../project.js'
3
+ import { screen, formScreen, boardScreen, schedulerScreen, detailScreen, project, dashScreen, statusPills, pad, ids, type SampleApp } from './shared.js'
4
+
5
+ // Jira-style chrome: a dark navy issue-tracker sidebar with muted labels and a
6
+ // blue active row (accent bar + tinted fill), the board front and centre.
7
+ const JIRA_CSS = `.sv-app.theme-jira .sv-app__side { background: #1d2125; border-right: 1px solid #2c333a; }
8
+ .sv-app.theme-jira .sv-app__brandtext { color: #fff; font-weight: 700; }
9
+ .sv-app.theme-jira .sv-app__link { color: #c7d1db; border-radius: 4px; padding: 7px 12px; }
10
+ .sv-app.theme-jira .sv-app__link:hover { background: rgba(255, 255, 255, 0.08); color: #fff; }
11
+ .sv-app.theme-jira .sv-app__link.is-active { background: color-mix(in srgb, #0c66e4 30%, #1d2125); color: #fff; box-shadow: inset 3px 0 0 #4c9aff; }
12
+ .sv-app.theme-jira .sv-app__collapse, .sv-app.theme-jira .sv-app__foot { color: #8c9bab; }`
13
+
14
+ const projectsEntity: EntitySchema = {
15
+ name: 'projects',
16
+ label: 'Project',
17
+ idField: 'id',
18
+ fields: [
19
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
20
+ { field: 'name', type: 'text', required: true, minLength: 2 },
21
+ { field: 'status', type: 'enum', required: true, defaultValue: 'planning', options: [
22
+ { value: 'planning', label: 'Planning', color: '#94a3b8' },
23
+ { value: 'active', label: 'Active', color: '#6366f1' },
24
+ { value: 'on_hold', label: 'On hold', color: '#f59e0b' },
25
+ { value: 'done', label: 'Done', color: '#10b981' },
26
+ ] },
27
+ { field: 'owner', type: 'text', label: 'Owner' },
28
+ { field: 'budget', type: 'number', label: 'Budget ($)', min: 0, input: { prefix: '$', help: 'Approved budget' } },
29
+ { field: 'progress', type: 'number', label: 'Progress (%)', min: 0, max: 100, defaultValue: 0, input: { editorType: 'slider', help: 'Overall completion' } },
30
+ { field: 'color', type: 'text', label: 'Label color', defaultValue: '#6366f1', input: { editorType: 'color', help: 'Board / calendar color' } },
31
+ { field: 'tags', type: 'text', label: 'Tags', input: { editorType: 'chips', help: 'Themes / labels' } },
32
+ { field: 'dueDate', type: 'date', label: 'Due date', required: true },
33
+ ],
34
+ }
35
+
36
+ const members: EntitySchema = {
37
+ name: 'members',
38
+ label: 'Member',
39
+ idField: 'id',
40
+ fields: [
41
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
42
+ { field: 'name', type: 'text', required: true, minLength: 2 },
43
+ { field: 'email', type: 'text', format: 'email', required: true },
44
+ { field: 'phone', type: 'text', label: 'Phone', input: { editorType: 'phone', help: 'Direct line' } },
45
+ { field: 'role', type: 'enum', required: true, defaultValue: 'dev', options: [
46
+ { value: 'dev', label: 'Engineer', color: '#6366f1' },
47
+ { value: 'design', label: 'Designer', color: '#ec4899' },
48
+ { value: 'pm', label: 'PM', color: '#f59e0b' },
49
+ { value: 'qa', label: 'QA', color: '#10b981' },
50
+ ] },
51
+ { field: 'skills', type: 'text', label: 'Skills', input: { editorType: 'chips', help: 'Tech / expertise' } },
52
+ { field: 'rating', type: 'number', label: 'Performance', min: 1, max: 5, defaultValue: 3, input: { editorType: 'rating', help: '1-5 review score' } },
53
+ ],
54
+ }
55
+
56
+ const tasks: EntitySchema = {
57
+ name: 'tasks',
58
+ label: 'Task',
59
+ idField: 'id',
60
+ fields: [
61
+ { field: 'id', type: 'text', primaryKey: true, readonly: true },
62
+ { field: 'title', type: 'text', required: true, minLength: 2 },
63
+ { field: 'projectId', type: 'relation', label: 'Project', relation: { entity: 'projects', foreignKey: 'projectId', labelField: 'name' } },
64
+ { field: 'assigneeId', type: 'relation', label: 'Assignee', relation: { entity: 'members', foreignKey: 'assigneeId', labelField: 'name' } },
65
+ { field: 'priority', type: 'enum', required: true, defaultValue: 'medium', options: [
66
+ { value: 'low', label: 'Low', color: '#94a3b8' },
67
+ { value: 'medium', label: 'Medium', color: '#6366f1' },
68
+ { value: 'high', label: 'High', color: '#f59e0b' },
69
+ { value: 'urgent', label: 'Urgent', color: '#ef4444' },
70
+ ] },
71
+ { field: 'status', type: 'enum', required: true, defaultValue: 'todo', options: [
72
+ { value: 'todo', label: 'To do', color: '#94a3b8' },
73
+ { value: 'doing', label: 'In progress', color: '#6366f1' },
74
+ { value: 'review', label: 'In review', color: '#f59e0b' },
75
+ { value: 'done', label: 'Done', color: '#10b981' },
76
+ ] },
77
+ { field: 'estimate', type: 'number', label: 'Estimate (h)', min: 0, defaultValue: 0, input: { suffix: 'h', help: 'Estimated hours' } },
78
+ { field: 'spent', type: 'number', label: 'Spent (h)', min: 0, defaultValue: 0, input: { suffix: 'h', help: 'Hours logged so far' } },
79
+ // Derived: hours left on the task (display-only, never stored).
80
+ { field: 'remaining', type: 'number', label: 'Remaining (h)', readonly: true, formula: 'estimate - spent' },
81
+ { field: 'progress', type: 'number', label: 'Progress (%)', min: 0, max: 100, defaultValue: 0, input: { editorType: 'slider', help: 'Task completion' } },
82
+ { field: 'startedAt', type: 'datetime', label: 'Started', input: { editorType: 'datetime', help: 'When work began' } },
83
+ { field: 'dueDate', type: 'date', label: 'Due' },
84
+ ],
85
+ }
86
+
87
+ // Task grid formatting: status + priority pills, plus numeric thresholds -
88
+ // over-budget tasks (spent past estimate) red + bold, nearly-done tasks green,
89
+ // barely-started tasks red - data-driven color, not just status pills.
90
+ const taskFormats: FormatRule[] = [
91
+ ...statusPills(tasks, 'status'),
92
+ ...statusPills(tasks, 'priority'),
93
+ { field: 'spent', op: 'gt', value: 16, color: '#dc2626', bold: true },
94
+ { field: 'progress', op: 'gte', value: 80, color: '#16a34a', bold: true },
95
+ { field: 'progress', op: 'lt', value: 25, color: '#dc2626' },
96
+ ]
97
+
98
+ const seed = {
99
+ projects: [
100
+ { id: 'pr1', name: 'Website redesign', status: 'active', owner: 'Sam Rivera', budget: 48000, progress: 65, color: '#6366f1', tags: ['Marketing', 'Design'], dueDate: '2026-09-01' },
101
+ { id: 'pr2', name: 'Mobile app v2', status: 'planning', owner: 'Jamie Chen', budget: 120000, progress: 15, color: '#10b981', tags: ['Mobile', 'Growth'], dueDate: '2026-11-15' },
102
+ { id: 'pr3', name: 'Billing migration', status: 'on_hold', owner: 'Priya Patel', budget: 32000, progress: 40, color: '#f59e0b', tags: ['Infra', 'Finance'], dueDate: '2026-08-20' },
103
+ { id: 'pr4', name: 'Onboarding revamp', status: 'done', owner: 'Sam Rivera', budget: 18000, progress: 100, color: '#ef4444', tags: ['Product'], dueDate: '2026-06-10' },
104
+ ],
105
+ members: [
106
+ { id: 'm1', name: 'Ada Lovelace', email: 'ada@team.dev', phone: '+1 (415) 555-0101', role: 'dev', skills: ['TypeScript', 'Svelte'], rating: 5 },
107
+ { id: 'm2', name: 'Grace Hopper', email: 'grace@team.dev', phone: '+1 (415) 555-0102', role: 'dev', skills: ['Go', 'SQL', 'Docker'], rating: 4 },
108
+ { id: 'm3', name: 'Margaret Hamilton', email: 'mh@team.dev', phone: '+1 (415) 555-0103', role: 'pm', skills: ['Product', 'Leadership'], rating: 5 },
109
+ { id: 'm4', name: 'Radia Perlman', email: 'radia@team.dev', phone: '+1 (415) 555-0104', role: 'design', skills: ['Figma', 'UX'], rating: 4 },
110
+ { id: 'm5', name: 'Katherine Johnson', email: 'kj@team.dev', phone: '+1 (415) 555-0105', role: 'qa', skills: ['Python', 'GraphQL'], rating: 3 },
111
+ ],
112
+ tasks: [
113
+ { 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' },
114
+ { 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' },
115
+ { 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' },
116
+ { 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' },
117
+ { 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' },
118
+ { 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' },
119
+ { 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' },
120
+ { 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' },
121
+ ],
122
+ }
123
+
124
+ export const projectTracker: SampleApp = {
125
+ id: 'projects',
126
+ name: 'Project tracker',
127
+ description: 'Projects, tasks and a team roster - a Jira-style issue tracker behind a sign-in, with 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).',
128
+ emoji: '\u{1F4CB}',
129
+ accent: '#0c66e4',
130
+ build: () => {
131
+ const projectRows = pad(projectsEntity, seed.projects, 10)
132
+ const projectPool = ids(projectRows)
133
+ const memberRows = pad(members, seed.members, 12)
134
+ const memberPool = ids(memberRows)
135
+ const taskRows = pad(tasks, seed.tasks, 60, { projectId: projectPool, assigneeId: memberPool })
136
+ return project({
137
+ title: 'Project Tracker',
138
+ brand: 'Tracker',
139
+ accent: '#0c66e4',
140
+ preset: 'linear',
141
+ footer: '',
142
+ // Jira-style issue tracker: dark navy rail, board-forward.
143
+ appClass: 'theme-jira',
144
+ customCss: JIRA_CSS,
145
+ // Sign-in with two roles: developers move issues but cannot delete them.
146
+ auth: { enabled: true, protect: true },
147
+ access: {
148
+ enabled: true,
149
+ defaultRole: 'developer',
150
+ roles: [
151
+ { role: 'admin', screens: '*', actions: '*' },
152
+ { role: 'developer', screens: '*', actions: ['create', 'update'] },
153
+ ],
154
+ },
155
+ entities: [projectsEntity, members, tasks],
156
+ seed: { projects: projectRows, members: memberRows, tasks: taskRows },
157
+ screens: [
158
+ // Portfolio dashboard: budget KPIs (a total-budget card with a sparkline over
159
+ // due dates + a $400k target), an average-progress gauge (0-100), status
160
+ // breakdowns (bar + pie), a budget-by-owner x status pivot, a tabbed view, and
161
+ // a status-pill project grid with inline edit actions.
162
+ dashScreen(projectsEntity, { id: 'portfolio', title: 'Portfolio', order: 0 }, [
163
+ { kpi: 'Total budget', measure: 'budget', reduce: 'sum', format: 'currency', trendField: 'dueDate', trendReduce: 'sum', target: 400000, span: 1 },
164
+ { kpi: 'Projects', reduce: 'count', span: 1 },
165
+ { kpi: 'Largest budget', measure: 'budget', reduce: 'max', format: 'currency', span: 1 },
166
+ { gauge: 'Avg progress', measure: 'progress', reduce: 'avg', min: 0, max: 100, unit: '%', span: 1 },
167
+ { chart: 'status', measure: 'budget', reduce: 'sum', type: 'bar', span: 2 },
168
+ { chart: 'status', reduce: 'count', type: 'pie', span: 1 },
169
+ { pivot: { rows: ['owner'], cols: ['status'], measure: 'budget', aggregate: 'sum' }, span: 3 },
170
+ { tabs: [
171
+ { label: 'Budget by status', tiles: [{ chart: 'status', measure: 'budget', reduce: 'sum', type: 'bar' }] },
172
+ { label: 'Projects by status', tiles: [{ chart: 'status', reduce: 'count', type: 'pie' }] },
173
+ ], span: 3 },
174
+ { grid: true, format: [...statusPills(projectsEntity, 'status'), { field: 'progress', op: 'gte', value: 100, color: '#16a34a', bold: true }], summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
175
+ ]),
176
+ // Task board: task KPIs (an estimate card trending over due dates), a completion
177
+ // gauge, status + priority breakdowns (bar + pie), a spent-over-time area trend,
178
+ // an estimate pivot by status (rows) x priority (columns), a tabbed view, a
179
+ // filter, and a task grid with status + priority pills and inline edit actions.
180
+ // Kanban board: tasks as cards in status columns (drag to change status).
181
+ boardScreen(tasks, { id: 'board', title: 'Kanban', order: 1 }, { groupBy: 'status', titleField: 'title', badgeField: 'estimate', subtitleField: 'priority', filter: ['priority', 'assigneeId'] }),
182
+ // Project detail: header (status pill) + budget / progress tiles with a
183
+ // timeline of the project's tasks (by due date, colored by status).
184
+ detailScreen(projectsEntity, { id: 'project-detail', title: 'Project detail', order: 2 }, {
185
+ titleField: 'name', subtitleField: 'owner', statusField: 'status', metricFields: ['budget', 'progress'],
186
+ related: [{ entity: 'tasks', foreignKey: 'projectId', label: 'Tasks', titleField: 'title', subtitleField: 'priority', dateField: 'dueDate', statusField: 'status' }],
187
+ }),
188
+ dashScreen(tasks, { id: 'tasks-board', title: 'Task board', order: 3 }, [
189
+ { kpi: 'Tasks', reduce: 'count', span: 1 },
190
+ { kpi: 'Logged hours', measure: 'spent', reduce: 'sum', format: 'compact', span: 1 },
191
+ { kpi: 'Estimated hours', measure: 'estimate', reduce: 'sum', format: 'compact', trendField: 'dueDate', trendReduce: 'sum', span: 1 },
192
+ { gauge: 'Avg completion', measure: 'progress', reduce: 'avg', min: 0, max: 100, unit: '%', span: 1 },
193
+ { chart: 'status', reduce: 'count', type: 'bar', span: 2 },
194
+ { chart: 'priority', reduce: 'count', type: 'pie', span: 1 },
195
+ { chart: 'dueDate', measure: 'spent', reduce: 'sum', type: 'area', span: 3 },
196
+ { pivot: { rows: ['status'], cols: ['priority'], measure: 'estimate', aggregate: 'sum' }, span: 3 },
197
+ { tabs: [
198
+ { label: 'Hours by status', tiles: [{ chart: 'status', measure: 'spent', reduce: 'sum', type: 'bar' }] },
199
+ { label: 'Tasks by priority', tiles: [{ chart: 'priority', reduce: 'count', type: 'bar' }] },
200
+ ], span: 3 },
201
+ { filter: ['status', 'priority', 'assigneeId'], span: 3 },
202
+ { grid: true, format: taskFormats, summaries: true, rowActions: [{ kind: 'edit' }], span: 3 },
203
+ ]),
204
+ screen(projectsEntity, 'master-detail', { id: 'projects', title: 'Projects', order: 4, child: tasks, foreignKey: 'projectId', linkScreen: 'project-detail' }),
205
+ formScreen(tasks, { id: 'tasks', title: 'Tasks', order: 5 }, undefined, { format: taskFormats, summaries: true, rowActions: [{ kind: 'edit' }] }, ['status', 'priority', 'assigneeId']),
206
+ formScreen(members, { id: 'members', title: 'Members', order: 6 }, undefined, { format: statusPills(members, 'role'), summaries: true }, ['role']),
207
+ // Schedule: tasks placed on a month calendar by due date.
208
+ schedulerScreen(tasks, { id: 'calendar', title: 'Schedule', order: 7 }, { startField: 'startedAt', endField: 'dueDate', titleField: 'title', colorField: 'priority', resourceField: 'assigneeId', initialView: 'timelineWeek' }),
209
+ ],
210
+ })
211
+ },
212
+ }