@svgrid/enterprise 2.2.0 → 2.3.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 (101) hide show
  1. package/README.md +95 -81
  2. package/dist/cdn/svgrid-enterprise.svelte-external.js +22479 -10466
  3. package/dist/node/studio.js +11590 -2907
  4. package/package.json +31 -10
  5. package/src/SvAlertRuleEditor.svelte +294 -0
  6. package/src/SvAlertsManager.svelte +210 -0
  7. package/src/SvAlertsPanel.svelte +129 -0
  8. package/src/SvBoard.svelte +3 -1
  9. package/src/SvExpressionEditor.svelte +341 -0
  10. package/src/SvGridAlerts.dom.test.ts +113 -0
  11. package/src/SvGridAlerts.svelte +265 -0
  12. package/src/SvGridBoard.svelte +2355 -0
  13. package/src/SvGridEditPanel.svelte +849 -794
  14. package/src/SvGridScheduler.svelte +5334 -4410
  15. package/src/SvPivotDesigner.svelte +3 -3
  16. package/src/SvRecordDetail.svelte +6 -2
  17. package/src/SvSchedule.svelte +3 -1
  18. package/src/{ai-export-pdf.test.ts → ai-export-pdf.dom.test.ts} +4 -1
  19. package/src/{ai-export-xlsx.test.ts → ai-export-xlsx.dom.test.ts} +4 -1
  20. package/src/{ai-export.test.ts → ai-export.dom.test.ts} +5 -1
  21. package/src/alerts/alert-engine-attach.ts +165 -0
  22. package/src/alerts/alert-engine.test.ts +135 -0
  23. package/src/alerts/alert-engine.ts +260 -0
  24. package/src/alerts/alert-formats.test.ts +87 -0
  25. package/src/alerts/alert-formats.ts +77 -0
  26. package/src/alerts/alert-observer.test.ts +189 -0
  27. package/src/alerts/alert-observer.ts +208 -0
  28. package/src/alerts/alert-scheduler.ts +96 -0
  29. package/src/alerts/alert-storage.test.ts +54 -0
  30. package/src/alerts/alert-storage.ts +116 -0
  31. package/src/alerts/alert-store.svelte.ts +80 -0
  32. package/src/alerts/alert-types.ts +94 -0
  33. package/src/alerts.ts +28 -0
  34. package/src/board.dom.test.ts +941 -0
  35. package/src/board.ts +36 -0
  36. package/src/export.ts +13 -1
  37. package/src/expressions/evaluate.test.ts +111 -0
  38. package/src/expressions/evaluate.ts +228 -0
  39. package/src/expressions/expression-columns.ts +133 -0
  40. package/src/expressions/expression-types.ts +84 -0
  41. package/src/expressions/parse.test.ts +106 -0
  42. package/src/expressions/parse.ts +610 -0
  43. package/src/import.ts +2 -0
  44. package/src/index.ts +180 -46
  45. package/src/install.ts +12 -2
  46. package/src/pivot-enable.ts +44 -0
  47. package/src/scheduler-assignments.test.ts +97 -0
  48. package/src/scheduler-assignments.ts +134 -0
  49. package/src/scheduler-axis.test.ts +108 -0
  50. package/src/scheduler-axis.ts +238 -0
  51. package/src/scheduler-booking.test.ts +57 -0
  52. package/src/scheduler-booking.ts +63 -0
  53. package/src/scheduler-config.ts +179 -0
  54. package/src/scheduler-dependencies.test.ts +155 -0
  55. package/src/scheduler-dependencies.ts +223 -0
  56. package/src/scheduler-freebusy.test.ts +41 -0
  57. package/src/scheduler-freebusy.ts +36 -0
  58. package/src/scheduler-heatmap.test.ts +39 -0
  59. package/src/scheduler-heatmap.ts +55 -0
  60. package/src/scheduler-resource-tree.test.ts +84 -0
  61. package/src/scheduler-resource-tree.ts +107 -0
  62. package/src/scheduler-slots.test.ts +53 -0
  63. package/src/scheduler-slots.ts +94 -0
  64. package/src/scheduler-summary.test.ts +47 -0
  65. package/src/scheduler-summary.ts +81 -0
  66. package/src/sources/index.ts +1 -1
  67. package/src/sources/introspect-supabase.test.ts +13 -1
  68. package/src/sources/introspect-supabase.ts +20 -0
  69. package/src/studio/copilot-core.test.ts +45 -0
  70. package/src/studio/copilot-core.ts +65 -0
  71. package/src/studio/deploy-cli.test.ts +56 -0
  72. package/src/studio/deploy-cli.ts +100 -0
  73. package/src/studio/emit-project.test.ts +503 -18
  74. package/src/studio/emit-project.ts +829 -111
  75. package/src/studio/emit-schema.test.ts +17 -0
  76. package/src/studio/emit-schema.ts +166 -35
  77. package/src/studio/index.ts +25 -1
  78. package/src/studio/introspect-openapi.test.ts +84 -0
  79. package/src/studio/introspect-openapi.ts +252 -0
  80. package/src/studio/project.test.ts +57 -0
  81. package/src/studio/project.ts +193 -6
  82. package/src/studio/samples/crm.ts +282 -258
  83. package/src/studio/samples/fleet.ts +213 -186
  84. package/src/studio/samples/insurance.ts +223 -195
  85. package/src/studio/samples/inventory.ts +213 -182
  86. package/src/studio/samples/live-data.test.ts +99 -98
  87. package/src/studio/samples/live-data.ts +8 -10
  88. package/src/studio/samples/projects.ts +212 -190
  89. package/src/studio/samples/samples.test.ts +52 -0
  90. package/src/studio/samples/shared.ts +26 -6
  91. package/src/studio/samples/starter.ts +251 -0
  92. package/src/studio/samples/support.ts +209 -184
  93. package/src/studio/ui-components-surface.test.ts +185 -0
  94. package/src/studio/ui-components.generated.ts +5617 -0
  95. package/src/studio/ui-components.ts +641 -472
  96. package/src/sveltekit/sql-source.test.ts +13 -0
  97. package/src/sveltekit/sql-source.ts +11 -6
  98. package/src/upgrade-prompt.ts +2 -2
  99. package/src/watermark.ts +2 -2
  100. package/src/ai.test.ts +0 -522
  101. package/src/ai.ts +0 -1388
@@ -0,0 +1,265 @@
1
+ <script lang="ts" generics="TRow extends Record<string, unknown>">
2
+ /**
3
+ * SvGridAlerts - the runtime overlay. It observes the grid's data, runs the
4
+ * alert engine off the render frame (never blocking paint), and:
5
+ * - raises toasts + records fired events in the shared log (the badge count),
6
+ * - emits `formats` (bindable) that the consumer spreads into the grid's
7
+ * `conditionalFormats` so matching rows light up and fired cells flash,
8
+ * - hosts the alerts badge, the fired-alerts panel, and the rule manager.
9
+ *
10
+ * All evaluation goes through `createAlertObserver`, which defers work to a
11
+ * post-paint frame and coalesces bursts. Two ways to feed it:
12
+ * - WATCH (default) - the overlay reactively `scan`s `data` on change. Deferred,
13
+ * and O(changed) via an index-diff (Svelte 5 replaces changed rows immutably).
14
+ * This covers streaming feeds / a programmatic `data = ...`, which the grid's
15
+ * `onCellValueChange` (edits only) never signals.
16
+ * - PUSH (`watch={false}`) - a consumer that already knows what changed drives
17
+ * `handle.pushChanged(changedRows)` (via `bind:this` or `onReady`), so cost is
18
+ * O(rows that changed) regardless of dataset size - the path for 100k+ feeds.
19
+ */
20
+ import { onMount, untrack } from 'svelte'
21
+ import type { ConditionalFormat } from '@svgrid/grid/format'
22
+ import { createAlertEngine } from './alerts/alert-engine'
23
+ import { createAlertObserver } from './alerts/alert-observer'
24
+ import { applyAlertEvents, type FlashTarget } from './alerts/alert-engine-attach'
25
+ import { rulesToConditionalFormats } from './alerts/alert-formats'
26
+ import { alertStore } from './alerts/alert-store.svelte'
27
+ import {
28
+ createAlertRules,
29
+ localStorageAlertRules,
30
+ memoryAlertRules,
31
+ type AlertRulesManager,
32
+ } from './alerts/alert-storage'
33
+ import SvAlertsPanel from './SvAlertsPanel.svelte'
34
+ import SvAlertsManager from './SvAlertsManager.svelte'
35
+ import type { ExprColumn } from './expressions/expression-columns'
36
+ import type { AlertEvent, AlertRule } from './alerts/alert-types'
37
+
38
+ /** The imperative handle handed to push-mode consumers. */
39
+ type AlertsHandle<T> = {
40
+ /** Feed only the rows that changed (O(changed) evaluation, deferred). */
41
+ pushChanged: (rows: ReadonlyArray<T>) => void
42
+ /** Silently re-arm edges from the full row set (after a data reset). */
43
+ reseed: (rows: ReadonlyArray<T>) => void
44
+ /** Run any pending evaluation immediately. */
45
+ flush: () => void
46
+ }
47
+
48
+ type Props = {
49
+ /** The same rows bound to the grid. In watch mode, observed for changes. */
50
+ data?: ReadonlyArray<TRow>
51
+ /** Columns offered in the rule/expression editors. */
52
+ columns: ReadonlyArray<ExprColumn>
53
+ /** Stable row identity (defaults to `row.id`). */
54
+ getRowId?: (row: TRow) => string
55
+ /** Bindable output: spread into `<SvGrid conditionalFormats={[...]}>`. */
56
+ formats?: ConditionalFormat<TRow>[]
57
+ /** Seed rules when storage is empty. */
58
+ rules?: AlertRule[]
59
+ /** Persist + share rules under this localStorage key (else in-memory). */
60
+ storageKey?: string
61
+ getValue?: (row: TRow, columnId: string) => unknown
62
+ locale?: string | ReadonlyArray<string>
63
+ /**
64
+ * Reactively scan `data` for changes (default). Set `false` when a consumer
65
+ * drives evaluation through `pushChanged` (large / streaming feeds) - the
66
+ * overlay then never reads `data` on the hot path.
67
+ */
68
+ watch?: boolean
69
+ /** Receive the push-mode handle on mount (alternative to `bind:this`). */
70
+ onReady?: (handle: AlertsHandle<TRow>) => void
71
+ /**
72
+ * Rate-limit toasts to at most one per rule per this many ms. Every event is
73
+ * still logged (the badge stays accurate); this only stops a busy live feed
74
+ * from turning into a toast storm. `0` (default) = no throttle.
75
+ */
76
+ toastCooldownMs?: number
77
+ /** Show the built-in badge + manage buttons. */
78
+ controls?: boolean
79
+ panelOpen?: boolean
80
+ managerOpen?: boolean
81
+ /** Called when the user clicks "Go to row" in the panel. */
82
+ onJump?: (event: AlertEvent) => void
83
+ }
84
+
85
+ let {
86
+ data = [],
87
+ columns,
88
+ getRowId = (row) => String((row as { id?: unknown }).id ?? ''),
89
+ formats = $bindable<ConditionalFormat<TRow>[]>([]),
90
+ rules: initialRules = [],
91
+ storageKey,
92
+ getValue,
93
+ locale,
94
+ watch = true,
95
+ onReady,
96
+ toastCooldownMs = 0,
97
+ controls = true,
98
+ panelOpen = $bindable(false),
99
+ managerOpen = $bindable(false),
100
+ onJump,
101
+ }: Props = $props()
102
+
103
+ const manager: AlertRulesManager = createAlertRules(
104
+ storageKey ? localStorageAlertRules(storageKey) : memoryAlertRules(),
105
+ )
106
+ // Seed once if the store is empty.
107
+ if (initialRules.length && manager.list().length === 0) {
108
+ for (const r of initialRules) manager.save(r)
109
+ }
110
+
111
+ let rules = $state<AlertRule[]>(manager.list())
112
+ let rulesVersion = $state(0)
113
+ let flashFormats = $state<ConditionalFormat<TRow>[]>([])
114
+
115
+ const engine = createAlertEngine<TRow>({ rules, getRowId, getValue, locale })
116
+
117
+ // The observer owns all scheduling + change tracking. It runs evaluation on a
118
+ // post-paint frame (never inside the grid's render flush) and only clones prev
119
+ // snapshots when a rule actually reads prev (`relativeChange`). Fired events
120
+ // come back here as a batch to raise toasts, log, and flash cells.
121
+ // Per-rule toast cooldown: suppress the visible toast (not the log) when a rule
122
+ // fired within `toastCooldownMs`. Keeps a busy feed from storming the screen.
123
+ const lastToastAt = new Map<string, number>()
124
+ function allowToast(e: AlertEvent): boolean {
125
+ if (!toastCooldownMs) return true
126
+ const now = Date.now()
127
+ const last = lastToastAt.get(e.ruleId) ?? -Infinity
128
+ if (now - last < toastCooldownMs) return false
129
+ lastToastAt.set(e.ruleId, now)
130
+ return true
131
+ }
132
+
133
+ const observer = createAlertObserver<TRow>({
134
+ engine,
135
+ getRowId,
136
+ schedule: 'raf',
137
+ onEvents: (events) => {
138
+ const { flashes } = applyAlertEvents(events, getRowId, { shouldToast: allowToast })
139
+ for (const f of flashes) addFlash(f)
140
+ },
141
+ })
142
+
143
+ let seededVersion = -1
144
+
145
+ function addFlash(target: FlashTarget) {
146
+ const fmt: ConditionalFormat<TRow> = {
147
+ type: 'rule',
148
+ ...(target.columnId ? { columns: [target.columnId] } : {}),
149
+ when: ({ row }: { value: unknown; row: TRow }) => getRowId(row) === target.rowId,
150
+ background: 'var(--sg-cell-flash, #fde68a)',
151
+ }
152
+ flashFormats = [...flashFormats, fmt]
153
+ setTimeout(() => {
154
+ flashFormats = flashFormats.filter((f) => f !== fmt)
155
+ }, 900)
156
+ }
157
+
158
+ // Seed + rule changes. Depends ONLY on `rulesVersion` so it never re-runs on a
159
+ // data tick; the seed reads `data` untracked. This silently arms edges so
160
+ // pre-existing matches under the (new) rules do not spam toasts.
161
+ $effect(() => {
162
+ const version = rulesVersion
163
+ if (version === seededVersion) return
164
+ observer.setRules(rules)
165
+ observer.seed(untrack(() => data))
166
+ seededVersion = version
167
+ })
168
+
169
+ // Watch mode: reactively scan `data` on every change (deferred + O(changed)).
170
+ // When `watch` is false the effect returns before touching `data`, so it takes
171
+ // NO data dependency and never re-runs on a tick - the push-mode hot path.
172
+ $effect(() => {
173
+ if (!watch) return
174
+ const rows = data
175
+ observer.scan(() => rows)
176
+ })
177
+
178
+ // Persistent (predicate-driven) styling + transient flashes -> the grid.
179
+ $effect(() => {
180
+ formats = [...rulesToConditionalFormats<TRow>(rules, { getValue, locale }), ...flashFormats]
181
+ })
182
+
183
+ // --- Push-mode API (bind:this or onReady) --------------------------------
184
+ export function pushChanged(rows: ReadonlyArray<TRow>) {
185
+ observer.pushChanged(rows)
186
+ }
187
+ export function reseed(rows: ReadonlyArray<TRow>) {
188
+ observer.seed(rows)
189
+ }
190
+ export function flush() {
191
+ observer.flush()
192
+ }
193
+
194
+ onMount(() => {
195
+ onReady?.({ pushChanged, reseed, flush })
196
+ return () => observer.detach()
197
+ })
198
+
199
+ function onRulesChanged(next: AlertRule[]) {
200
+ rules = next
201
+ rulesVersion += 1
202
+ }
203
+ </script>
204
+
205
+ {#if controls}
206
+ <div class="sg-alerts-controls">
207
+ <button
208
+ type="button"
209
+ class="sg-alerts-bell"
210
+ class:sg-active={alertStore.activeCount > 0}
211
+ onclick={() => (panelOpen = true)}
212
+ aria-label={`Open alerts${alertStore.activeCount ? ` (${alertStore.activeCount} unread)` : ''}`}>
213
+ <svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
214
+ <path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9" />
215
+ <path d="M13.7 21a2 2 0 0 1-3.4 0" />
216
+ </svg>
217
+ {#if alertStore.activeCount > 0}
218
+ <span class="sg-alerts-count">{alertStore.activeCount > 99 ? '99+' : alertStore.activeCount}</span>
219
+ {/if}
220
+ </button>
221
+ <button type="button" class="sg-alerts-manage" onclick={() => (managerOpen = true)}>
222
+ <svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
223
+ <path d="M12 3v3m0 12v3M5.6 5.6l2.1 2.1m8.6 8.6l2.1 2.1M3 12h3m12 0h3M5.6 18.4l2.1-2.1m8.6-8.6l2.1-2.1" />
224
+ </svg>
225
+ Manage alerts
226
+ </button>
227
+ </div>
228
+ {/if}
229
+
230
+ <SvAlertsPanel bind:open={panelOpen} onJump={(e) => onJump?.(e)} />
231
+ <SvAlertsManager bind:open={managerOpen} {manager} {columns} rows={data} onChange={onRulesChanged} />
232
+
233
+ <style>
234
+ /* Theme-agnostic: these controls render OUTSIDE the grid, where the full
235
+ --sg-* token set may not be in scope. Derive every surface from
236
+ `currentColor` + `inherit` so text is always the page's readable colour. */
237
+ .sg-alerts-controls { display: inline-flex; align-items: center; gap: 8px; font-family: var(--sg-font, inherit); color: inherit; }
238
+ .sg-alerts-bell,
239
+ .sg-alerts-manage {
240
+ display: inline-flex; align-items: center; justify-content: center; gap: 7px;
241
+ height: 34px; border-radius: 9px; color: inherit; cursor: pointer;
242
+ border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
243
+ background: color-mix(in srgb, currentColor 5%, transparent);
244
+ transition: background 0.14s ease, border-color 0.14s ease, box-shadow 0.14s ease, transform 0.08s ease;
245
+ }
246
+ .sg-alerts-bell:hover,
247
+ .sg-alerts-manage:hover { background: color-mix(in srgb, currentColor 13%, transparent); border-color: color-mix(in srgb, currentColor 36%, transparent); }
248
+ .sg-alerts-bell:active,
249
+ .sg-alerts-manage:active { transform: translateY(0.5px); }
250
+ .sg-alerts-bell:focus-visible,
251
+ .sg-alerts-manage:focus-visible { outline: none; box-shadow: 0 0 0 3px color-mix(in srgb, var(--sg-accent, #4f46e5) 35%, transparent); }
252
+
253
+ .sg-alerts-bell { position: relative; width: 34px; padding: 0; }
254
+ .sg-alerts-bell.sg-active { color: var(--sg-accent, #4f46e5); border-color: color-mix(in srgb, var(--sg-accent, #4f46e5) 50%, transparent); background: color-mix(in srgb, var(--sg-accent, #4f46e5) 12%, transparent); }
255
+ .sg-alerts-count {
256
+ position: absolute; top: -6px; right: -6px; min-width: 17px; height: 17px; padding: 0 4px;
257
+ display: inline-flex; align-items: center; justify-content: center;
258
+ font-size: 10.5px; font-weight: 700; line-height: 1; color: #fff;
259
+ background: var(--sg-danger, #dc2626); border-radius: 999px;
260
+ border: 1.5px solid color-mix(in srgb, currentColor 100%, transparent); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
261
+ }
262
+
263
+ .sg-alerts-manage { padding: 0 13px 0 11px; font-size: 13px; font-weight: 500; }
264
+ .sg-alerts-manage svg { opacity: 0.6; }
265
+ </style>