@selvajs/ui 4.3.0 → 4.5.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 (80) hide show
  1. package/dist/components/compute/AppLayout.svelte +3 -3
  2. package/dist/components/compute/AppLayout.svelte.d.ts +2 -2
  3. package/dist/components/compute/ComputeApp.svelte +47 -140
  4. package/dist/components/compute/ComputeApp.svelte.d.ts +0 -16
  5. package/dist/components/compute/ComputeMessagesDialog.svelte +125 -0
  6. package/dist/components/compute/ComputeMessagesDialog.svelte.d.ts +10 -0
  7. package/dist/components/compute/ParameterPresetManager.svelte +25 -33
  8. package/dist/components/layout/PageFooter.svelte +22 -144
  9. package/dist/components/preview/Group.svelte +6 -3
  10. package/dist/components/preview/ImageOutput.svelte +1 -13
  11. package/dist/components/preview/OutputDisplay.svelte +6 -6
  12. package/dist/components/preview/TabLayout.svelte +6 -5
  13. package/dist/components/preview/inputs/FileInput.svelte +10 -19
  14. package/dist/composables/useFooterItem.svelte.d.ts +10 -12
  15. package/dist/composables/useFooterItem.svelte.js +9 -16
  16. package/dist/compute/computeThrottle.svelte.js +5 -2
  17. package/dist/compute/createSolveSession.svelte.d.ts +56 -0
  18. package/dist/compute/createSolveSession.svelte.js +122 -0
  19. package/dist/compute/solve-session-core.d.ts +45 -0
  20. package/dist/compute/solve-session-core.js +69 -0
  21. package/dist/constants.d.ts +0 -2
  22. package/dist/constants.js +0 -2
  23. package/dist/contexts/footerContext.svelte.d.ts +17 -5
  24. package/dist/contexts/footerContext.svelte.js +10 -2
  25. package/dist/external/storage.js +7 -23
  26. package/dist/index.d.ts +3 -1
  27. package/dist/index.js +5 -0
  28. package/dist/schema/param-exporter.d.ts +25 -1
  29. package/dist/schema/param-exporter.js +30 -27
  30. package/dist/schema/traversal.d.ts +1 -0
  31. package/dist/schema/traversal.js +5 -0
  32. package/dist/schema/visibility-rules.d.ts +12 -0
  33. package/dist/schema/visibility-rules.js +20 -0
  34. package/dist/types/solveFn.d.ts +1 -2
  35. package/dist/utils/file-download.d.ts +5 -17
  36. package/dist/utils/file-download.js +12 -19
  37. package/dist/utils/randomId.js +3 -19
  38. package/package.json +10 -26
  39. package/src/lib/components/compute/AppLayout.svelte +3 -3
  40. package/src/lib/components/compute/ComputeApp.svelte +47 -140
  41. package/src/lib/components/compute/ComputeMessagesDialog.svelte +125 -0
  42. package/src/lib/components/compute/ParameterPresetManager.svelte +25 -33
  43. package/src/lib/components/layout/PageFooter.svelte +22 -144
  44. package/src/lib/components/preview/Group.svelte +6 -3
  45. package/src/lib/components/preview/ImageOutput.svelte +1 -13
  46. package/src/lib/components/preview/OutputDisplay.svelte +6 -6
  47. package/src/lib/components/preview/TabLayout.svelte +6 -5
  48. package/src/lib/components/preview/inputs/FileInput.svelte +10 -19
  49. package/src/lib/composables/useFooterItem.svelte.ts +25 -25
  50. package/src/lib/compute/computeThrottle.svelte.ts +5 -2
  51. package/src/lib/compute/computeThrottle.test.ts +106 -0
  52. package/src/lib/compute/createSolveSession.svelte.ts +188 -0
  53. package/src/lib/compute/solve-session-core.test.ts +153 -0
  54. package/src/lib/compute/solve-session-core.ts +102 -0
  55. package/src/lib/constants.ts +0 -2
  56. package/src/lib/contexts/footerContext.svelte.ts +29 -14
  57. package/src/lib/external/storage.test.ts +99 -0
  58. package/src/lib/external/storage.ts +6 -22
  59. package/src/lib/index.ts +14 -1
  60. package/src/lib/schema/param-exporter.test.ts +136 -0
  61. package/src/lib/schema/param-exporter.ts +44 -30
  62. package/src/lib/schema/traversal.test.ts +92 -0
  63. package/src/lib/schema/traversal.ts +5 -0
  64. package/src/lib/schema/visibility-rules.test.ts +173 -0
  65. package/src/lib/schema/visibility-rules.ts +25 -0
  66. package/src/lib/types/solveFn.ts +1 -1
  67. package/src/lib/utils/file-download.ts +13 -20
  68. package/src/lib/utils/randomId.ts +3 -19
  69. package/dist/components/compute/ComputeMessages.svelte +0 -169
  70. package/dist/components/compute/ComputeMessages.svelte.d.ts +0 -7
  71. package/dist/components/compute/index.d.ts +0 -5
  72. package/dist/components/compute/index.js +0 -5
  73. package/dist/components/preview/index.d.ts +0 -3
  74. package/dist/components/preview/index.js +0 -4
  75. package/dist/components/viewer/index.d.ts +0 -3
  76. package/dist/components/viewer/index.js +0 -3
  77. package/src/lib/components/compute/ComputeMessages.svelte +0 -169
  78. package/src/lib/components/compute/index.ts +0 -5
  79. package/src/lib/components/preview/index.ts +0 -4
  80. package/src/lib/components/viewer/index.ts +0 -3
@@ -1,10 +1,7 @@
1
- /**
2
- * Wrapper utilities for file downloads from Grasshopper outputs
3
- * Uses the core package implementation for file handling
4
- */
5
1
  import { downloadFileData } from '@selvajs/compute';
2
+ import { SvelteMap } from 'svelte/reactivity';
6
3
  import { APP_DEFAULTS } from '../constants';
7
- const MIME_BY_EXT = {
4
+ export const MIME_BY_EXT = {
8
5
  '.png': 'image/png',
9
6
  '.jpg': 'image/jpeg',
10
7
  '.jpeg': 'image/jpeg',
@@ -41,10 +38,6 @@ function saveSingleFile(file) {
41
38
  a.click();
42
39
  URL.revokeObjectURL(a.href);
43
40
  }
44
- /**
45
- * Download file(s) from Grasshopper outputs
46
- * Single files are downloaded directly, multiple files are packaged as ZIP
47
- */
48
41
  export async function downloadFiles(fileData, fileName = 'grasshopper-output') {
49
42
  try {
50
43
  const filesArray = Array.isArray(fileData) ? fileData : [fileData];
@@ -63,9 +56,6 @@ export async function downloadFiles(fileData, fileName = 'grasshopper-output') {
63
56
  throw error;
64
57
  }
65
58
  }
66
- /**
67
- * Check if data is FileData
68
- */
69
59
  export function isFileData(data) {
70
60
  return (typeof data === 'object' &&
71
61
  data !== null &&
@@ -73,9 +63,16 @@ export function isFileData(data) {
73
63
  'data' in data &&
74
64
  'isBase64Encoded' in data);
75
65
  }
76
- /**
77
- * Format file size for display
78
- */
66
+ export function groupMessages(messages) {
67
+ const grouped = new SvelteMap();
68
+ for (const msg of messages) {
69
+ const baseMsg = msg.replace(/\([a-f0-9-]{36}\)$/i, '(...)').trim();
70
+ grouped.set(baseMsg, (grouped.get(baseMsg) ?? 0) + 1);
71
+ }
72
+ return Array.from(grouped.entries())
73
+ .map(([message, count]) => ({ message, count }))
74
+ .sort((a, b) => b.count - a.count);
75
+ }
79
76
  export function formatFileSize(bytes) {
80
77
  if (bytes === 0)
81
78
  return '0 Bytes';
@@ -84,10 +81,6 @@ export function formatFileSize(bytes) {
84
81
  const i = Math.floor(Math.log(bytes) / Math.log(k));
85
82
  return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
86
83
  }
87
- /**
88
- * Get approximate file size from base64 string
89
- */
90
84
  export function getBase64FileSize(base64) {
91
- // Base64 encoded string is approximately 33% larger than original
92
85
  return Math.ceil((base64.length * 3) / 4);
93
86
  }
@@ -1,22 +1,6 @@
1
- // Generate a UUIDv4-shaped string in any browser context.
2
- //
3
- // `crypto.randomUUID()` requires a secure context (HTTPS or localhost).
4
- // Plain-HTTP deployments — common for first-time self-hosters trying things
5
- // out — get `crypto.randomUUID is not a function`, which is a confusing
6
- // browser-level error nobody can diagnose without devtools open.
7
- //
8
- // We try in order:
9
- // 1. crypto.randomUUID() — fastest path, available in secure contexts
10
- // 2. crypto.getRandomValues() — Web Crypto's lower-level entropy, works
11
- // in any context that ships Web Crypto
12
- // 3. Math.random() fallback — last resort; non-cryptographic, but the
13
- // IDs we generate here are only used as
14
- // collision-resistant keys for client-side
15
- // form rows, not as security tokens.
16
- //
17
- // Callers that need cryptographic randomness (tokens, secrets) must NOT use
18
- // this — they should fail loudly in insecure contexts instead of silently
19
- // downgrading.
1
+ // Falls back through crypto.randomUUID crypto.getRandomValues Math.random so
2
+ // plain-HTTP deployments don't crash. Math.random IDs are non-cryptographic —
3
+ // fine for collision-resistant UI keys, not for tokens or secrets.
20
4
  export function randomId() {
21
5
  const c = typeof crypto !== 'undefined' ? crypto : undefined;
22
6
  if (c?.randomUUID)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selvajs/ui",
3
- "version": "4.3.0",
3
+ "version": "4.5.0",
4
4
  "description": "Shared UI components and utilities for Selva applications",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -14,29 +14,9 @@
14
14
  "types": "./dist/index.d.ts",
15
15
  "svelte": "./dist/index.js"
16
16
  },
17
- "./primitives": {
18
- "types": "./dist/components/primitives/index.d.ts",
19
- "svelte": "./dist/components/primitives/index.js"
20
- },
21
- "./layout": {
22
- "types": "./dist/components/layout/index.d.ts",
23
- "svelte": "./dist/components/layout/index.js"
24
- },
25
- "./compute": {
26
- "types": "./dist/components/compute/index.d.ts",
27
- "svelte": "./dist/components/compute/index.js"
28
- },
29
- "./viewer": {
30
- "types": "./dist/components/viewer/index.d.ts",
31
- "svelte": "./dist/components/viewer/index.js"
32
- },
33
- "./footer": {
34
- "types": "./dist/contexts/footerContext.svelte.d.ts",
35
- "svelte": "./dist/contexts/footerContext.svelte.js"
36
- },
37
- "./utils": {
38
- "types": "./dist/utils.d.ts",
39
- "import": "./dist/utils.js"
17
+ "./external": {
18
+ "types": "./dist/external/storage.d.ts",
19
+ "svelte": "./dist/external/storage.js"
40
20
  },
41
21
  "./styles/base.css": "./src/lib/styles/base.css",
42
22
  "./styles/themes/*": "./src/lib/styles/themes/*"
@@ -59,7 +39,7 @@
59
39
  "svelte": "^5",
60
40
  "tailwind-variants": "^3.2.2",
61
41
  "three": "^0.184.0",
62
- "@selvajs/schemas": "^4.3.0"
42
+ "@selvajs/schemas": "^4.4.0"
63
43
  },
64
44
  "peerDependenciesMeta": {
65
45
  "three": {
@@ -80,12 +60,14 @@
80
60
  "devDependencies": {
81
61
  "@internationalized/date": "^3.12.1",
82
62
  "@sveltejs/kit": "2.58.0",
63
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
83
64
  "@types/three": "^0.184.0",
84
65
  "bits-ui": "^2.18.0",
85
66
  "svelte": "5.55.5",
86
67
  "tailwind-variants": "^3.2.2",
68
+ "vitest": "^3.2.4",
87
69
  "@selvajs/config": "0.0.0",
88
- "@selvajs/schemas": "4.3.0"
70
+ "@selvajs/schemas": "4.4.0"
89
71
  },
90
72
  "scripts": {
91
73
  "dev": "vite dev",
@@ -95,6 +77,8 @@
95
77
  "preview": "vite preview",
96
78
  "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
97
79
  "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
80
+ "test": "vitest run",
81
+ "test:watch": "vitest",
98
82
  "format": "prettier --write .",
99
83
  "lint": "prettier --check . && eslint ."
100
84
  }
@@ -25,7 +25,7 @@
25
25
  oncalculate?: () => void;
26
26
  values: Record<string, unknown>;
27
27
  onValueChange: (id: string, val: SupportedTypes) => void | Promise<void>;
28
- onLoadValues?: () => void | Promise<void>;
28
+ onLoadValues?: (values: Record<string, unknown>) => void | Promise<void>;
29
29
  panelActions?: ActionButton[];
30
30
  showSaveButton?: boolean;
31
31
  showLoadButton?: boolean;
@@ -44,7 +44,7 @@
44
44
  hasNeverSolved = false,
45
45
  isViewerFullscreen = $bindable(false),
46
46
  oncalculate = () => {},
47
- values = $bindable({}),
47
+ values,
48
48
  onValueChange,
49
49
  onLoadValues,
50
50
  panelActions = [],
@@ -99,7 +99,7 @@
99
99
 
100
100
  async function handleLoadValues(loadedValues: Record<string, unknown>) {
101
101
  Object.assign(values, loadedValues);
102
- await onLoadValues?.();
102
+ await onLoadValues?.(loadedValues);
103
103
  }
104
104
 
105
105
  let _touchStartY = 0;
@@ -5,15 +5,16 @@
5
5
  import type { ActionButton } from '../../types/actionButton';
6
6
  import type { SolveFn } from '../../types/solveFn';
7
7
  import type { PresetLabels } from '../../types/presetLabels';
8
- import { getDefaultValue } from '../../schema/defaults';
9
- import { createComputeThrottle } from '../../compute/computeThrottle.svelte';
10
8
  import { createSolvingIndicator } from '../../compute/solving.svelte';
9
+ import {
10
+ createSolveSession,
11
+ createRequestResponseDriver
12
+ } from '../../compute/createSolveSession.svelte';
11
13
  import { useFooterItem } from '../../composables/useFooterItem.svelte';
12
14
  import { hexToOklch } from '../../utils/color';
13
15
  import AppShell from '../layout/AppShell.svelte';
14
16
  import AppLayout from './AppLayout.svelte';
15
17
  import StateDisplay from '../primitives/StateDisplay.svelte';
16
- import { getExternalInputs, readExternalValue } from '../../external/storage';
17
18
  import { setClientSlot, type ClientSlot } from '../../contexts/clientSlotContext.svelte';
18
19
 
19
20
  import type { Snippet } from 'svelte';
@@ -47,24 +48,11 @@
47
48
  footerItemPriority?: number;
48
49
  onReady?: (api: { loadValues: (values: Record<string, unknown>) => void }) => void;
49
50
  headerRight?: Snippet;
50
- /**
51
- * Bring-your-own header. When provided, replaces the built-in header inside
52
- * the standard-height sticky bar (so the fixed layout is unaffected).
53
- * Takes precedence over `headerRight`.
54
- */
51
+ // Replaces the built-in header; takes precedence over `headerRight`.
55
52
  header?: Snippet;
56
- /**
57
- * Stable identifier used to scope sessionStorage entries for external-input
58
- * values. If absent, falls back to definitionKey, then to schema.id.
59
- */
53
+ // Scopes sessionStorage for external-input values; falls back to definitionKey then schema.id.
60
54
  externalScopeKey?: string;
61
- /**
62
- * Host-rendered element for client-sourced inputs whose schema sets
63
- * source.client.presentation === 'slot'. Selva reserves the input's cell and
64
- * invokes this snippet in its place, passing { inputId, displayName, slotLabel,
65
- * value }. Selva renders nothing itself and never interprets what the host puts
66
- * here (e.g. an "Edit JSON" button that navigates back to a producer page).
67
- */
55
+ // Renders client-sourced inputs with presentation === 'slot'; receives { inputId, displayName, slotLabel, value }.
68
56
  clientSlot?: ClientSlot;
69
57
  }
70
58
 
@@ -102,86 +90,26 @@
102
90
 
103
91
  const resolvedScopeKey = $derived(externalScopeKey || definitionKey || schema?.id || '');
104
92
 
105
- function createInitialValues(s: UISchema, scopeKey: string) {
106
- const externalSet = new Set(getExternalInputs(s).map((e) => e.paramId));
107
- const v: Record<string, unknown> = {};
108
- for (const input of s.inputs) {
109
- if (externalSet.has(input.id)) {
110
- const stored = readExternalValue({ scopeKey, inputId: input.id });
111
- if (stored !== undefined) v[input.id] = stored;
112
- // else: leave undefined so the missing-inputs panel can detect it
113
- continue;
114
- }
115
- v[input.id] = input.default ?? getDefaultValue(input.paramType);
116
- }
117
- for (const output of s.outputs) {
118
- v[output.id] = null;
119
- }
120
- return v;
121
- }
122
-
93
+ // Solve Session owns the value/lifecycle state machine; the request/response driver
94
+ // gives it its transport (Rhino.Compute over HTTP via onSolve, throttled). The driver
95
+ // reads the reporter lazily so it can capture the session it's wired into.
123
96
  // svelte-ignore state_referenced_locally
124
- let values = $state<Record<string, unknown>>(
125
- createInitialValues(schema, externalScopeKey || definitionKey || schema?.id || '')
126
- );
127
- let error = $state('');
128
- let computeErrors = $state<string[]>([]);
129
- let computeWarnings = $state<string[]>([]);
130
- let meshes = $state<any[]>([]);
131
- let pendingValues = $state<Record<string, unknown>>({});
132
- // svelte-ignore state_referenced_locally
133
- let hasPendingChanges = $state(schema?.instanceSolve === false);
134
- // svelte-ignore state_referenced_locally
135
- let hasNeverSolved = $state(schema?.instanceSolve === false);
136
- let isViewerFullscreen = $state(false);
137
-
138
- async function performSolveInternal(solveValues: Record<string, unknown>, signal: AbortSignal) {
139
- try {
140
- error = '';
141
- computeErrors = [];
142
- computeWarnings = [];
143
-
144
- const result = await onSolve(solveValues, signal);
145
-
146
- if (signal.aborted) return;
147
-
148
- computeErrors = result.errors ?? [];
149
- computeWarnings = result.warnings ?? [];
150
- meshes = result.meshes ?? [];
151
-
152
- Object.assign(values, result.outputs);
153
- pendingValues = {};
154
- hasPendingChanges = false;
155
- hasNeverSolved = false;
156
- } catch (err) {
157
- if (err instanceof Error && err.name === 'AbortError') return;
158
- error = err instanceof Error ? err.message : String(err);
159
- }
160
- }
161
-
162
- // svelte-ignore state_referenced_locally
163
- const computeThrottle = createComputeThrottle<Record<string, unknown>>(performSolveInternal, {
97
+ const driver = createRequestResponseDriver(onSolve, () => session, {
164
98
  timeout: solveTimeoutMs
165
99
  });
100
+ // svelte-ignore state_referenced_locally
101
+ const session = createSolveSession({
102
+ schema,
103
+ scopeKey: externalScopeKey || definitionKey || schema?.id || '',
104
+ driver
105
+ });
166
106
 
167
- let solving = $derived(computeThrottle.isComputing);
168
- const solvingIndicator = createSolvingIndicator(() => solving);
107
+ let isViewerFullscreen = $state(false);
169
108
 
170
- function performSolve() {
171
- computeThrottle.trigger($state.snapshot(values));
172
- }
173
-
174
- function loadValues(incoming: Record<string, unknown>) {
175
- Object.assign(values, incoming);
176
- if (schema?.instanceSolve !== false) {
177
- performSolve();
178
- } else {
179
- hasPendingChanges = true;
180
- }
181
- }
109
+ const solvingIndicator = createSolvingIndicator(() => session.isSolving);
182
110
 
183
111
  $effect(() => {
184
- onReady?.({ loadValues });
112
+ onReady?.({ loadValues: (incoming) => session.loadValues(incoming) });
185
113
  });
186
114
 
187
115
  let previousDefinitionKey = $state('');
@@ -195,50 +123,35 @@
195
123
  isInitialLoad = false;
196
124
  previousDefinitionKey = definitionKey;
197
125
  if (schema?.instanceSolve !== false) {
198
- performSolve();
126
+ session.solve();
199
127
  }
200
128
  } else if (previousDefinitionKey !== definitionKey) {
201
- meshes = [];
202
- values = createInitialValues(schema, resolvedScopeKey);
203
- error = '';
204
- computeErrors = [];
205
- computeWarnings = [];
206
- if (schema && Object.keys(values).length > 0) {
207
- performSolve();
208
- }
129
+ // Host owns the WHEN (definitionKey changed); session owns the WHAT.
130
+ session.rebuild(schema, resolvedScopeKey);
209
131
  previousDefinitionKey = definitionKey;
210
132
  }
211
133
  });
212
134
  });
213
135
 
214
- async function handleValueChange(id: string, val: unknown) {
215
- values[id] = val;
216
-
217
- if (schema?.instanceSolve === false) {
218
- pendingValues[id] = val;
219
- hasPendingChanges = true;
220
- return;
221
- }
222
-
223
- performSolve();
136
+ function handleValueChange(id: string, val: unknown) {
137
+ session.setValue(id, val);
224
138
  }
225
139
 
226
140
  function handleCalculate() {
227
- performSolve();
141
+ session.solve();
228
142
  }
229
143
 
230
- // Use untrack to read these static props without creating reactive dependencies.
231
- // footerComponentProps is intentionally NOT untracked — it's a getter called every render.
232
- const _footerItemId = untrack(() => footerItemId);
233
- const _footerComponent = untrack(() => footerComponent);
234
- const _footerItemPriority = untrack(() => footerItemPriority);
235
- useFooterItem(
236
- _footerItemId,
237
- _footerComponent,
238
- () => (_footerComponent ? (footerComponentProps?.() ?? {}) : {}),
239
- 'left',
240
- _footerItemPriority
241
- );
144
+ // Read static props without creating reactive dependencies (registration is fixed at
145
+ // mount). footerComponentProps is intentionally read live — it's a getter the renderer
146
+ // calls every render to keep the footer in sync. The composable no-ops when component
147
+ // is absent, so the hook itself stays unconditional.
148
+ useFooterItem({
149
+ id: untrack(() => footerItemId),
150
+ component: untrack(() => footerComponent),
151
+ getProps: () => footerComponentProps?.() ?? {},
152
+ position: 'left',
153
+ priority: untrack(() => footerItemPriority)
154
+ });
242
155
 
243
156
  let resolvedIsEmbedded = $derived(isEmbedded ?? page.url.searchParams.get('embed') === 'true');
244
157
  let resolvedPrimaryColor = $derived(primaryColor ?? page.url.searchParams.get('primary'));
@@ -260,12 +173,12 @@
260
173
  {footerText}
261
174
  {header}
262
175
  rightContent={headerRight}
263
- errors={computeErrors}
264
- warnings={computeWarnings}
176
+ errors={session.computeErrors}
177
+ warnings={session.computeWarnings}
265
178
  >
266
- {#if error}
179
+ {#if session.error}
267
180
  <div class="min-h-100 p-8 flex items-center justify-center">
268
- <StateDisplay type="error" size="medium" message={error} />
181
+ <StateDisplay type="error" size="medium" message={session.error} />
269
182
  </div>
270
183
  {:else if !schema}
271
184
  <div class="min-h-100 flex items-center justify-center">
@@ -275,13 +188,13 @@
275
188
  {#key definitionKey}
276
189
  <AppLayout
277
190
  {schema}
278
- {meshes}
279
- isSolving={solving}
191
+ meshes={session.meshes}
192
+ isSolving={session.isSolving}
280
193
  showSolvingIndicator={schema.instanceSolve !== false && solvingIndicator.show}
281
- {hasPendingChanges}
282
- {hasNeverSolved}
194
+ hasPendingChanges={session.hasPendingChanges}
195
+ hasNeverSolved={session.hasNeverSolved}
283
196
  bind:isViewerFullscreen
284
- bind:values
197
+ values={session.values}
285
198
  {panelActions}
286
199
  {showSaveButton}
287
200
  {showLoadButton}
@@ -290,13 +203,7 @@
290
203
  {presetLabels}
291
204
  onValueChange={handleValueChange}
292
205
  oncalculate={handleCalculate}
293
- onLoadValues={() => {
294
- if (schema?.instanceSolve !== false) {
295
- performSolve();
296
- } else {
297
- hasPendingChanges = true;
298
- }
299
- }}
206
+ onLoadValues={() => session.loadValues({})}
300
207
  />
301
208
  {/key}
302
209
  {/if}
@@ -0,0 +1,125 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { CircleAlert, TriangleAlert, ChevronDown, ChevronRight } from '@lucide/svelte';
4
+ import * as Collapsible from '$lib/components/primitives/collapsible';
5
+ import * as Dialog from '$lib/components/primitives/dialog';
6
+ import { groupMessages } from '$lib/utils/file-download';
7
+
8
+ interface Props {
9
+ errors?: string[];
10
+ warnings?: string[];
11
+ /** Renders inside Dialog.Trigger. */
12
+ trigger: Snippet;
13
+ }
14
+
15
+ let { errors = [], warnings = [], trigger }: Props = $props();
16
+
17
+ let open = $state(false);
18
+ let showErrors = $state(true);
19
+ let showWarnings = $state(false);
20
+
21
+ const groupedErrors = $derived(groupMessages(errors));
22
+ const groupedWarnings = $derived(groupMessages(warnings));
23
+ const totalCount = $derived(errors.length + warnings.length);
24
+ </script>
25
+
26
+ <Dialog.Root bind:open>
27
+ <Dialog.Trigger class="focus:outline-none">
28
+ {@render trigger()}
29
+ </Dialog.Trigger>
30
+
31
+ <Dialog.Content class="max-w-2xl max-h-[80vh]">
32
+ <Dialog.Header>
33
+ <Dialog.Title class="gap-2 flex items-center">
34
+ <CircleAlert class="h-5 w-5" />
35
+ Compute Messages
36
+ </Dialog.Title>
37
+ <Dialog.Description>
38
+ {totalCount}
39
+ {totalCount === 1 ? 'issue' : 'issues'} detected during solve
40
+ </Dialog.Description>
41
+ </Dialog.Header>
42
+
43
+ <div class="space-y-3 pr-2 overflow-y-auto" style="max-height: calc(80vh - 180px);">
44
+ {#if errors.length > 0}
45
+ <Collapsible.Root bind:open={showErrors}>
46
+ <div class="overflow-hidden rounded-lg border border-destructive bg-card">
47
+ <div class="px-4 py-3 flex items-center">
48
+ <Collapsible.Trigger
49
+ class="-mx-4 -my-3 gap-3 px-4 py-3 flex flex-1 items-center text-left transition-colors hover:bg-destructive/5"
50
+ >
51
+ {#if showErrors}
52
+ <ChevronDown class="h-4 w-4 shrink-0 text-destructive" />
53
+ {:else}
54
+ <ChevronRight class="h-4 w-4 shrink-0 text-destructive" />
55
+ {/if}
56
+ <CircleAlert class="h-4 w-4 shrink-0 text-destructive" />
57
+ <span class="text-sm font-medium text-destructive">
58
+ {errors.length === 1 ? '1 Error' : `${errors.length} Errors`}
59
+ </span>
60
+ </Collapsible.Trigger>
61
+ </div>
62
+
63
+ <Collapsible.Content class="space-y-0">
64
+ <div class="max-h-60 px-4 py-3 overflow-y-auto border-t border-destructive bg-card">
65
+ <ul class="space-y-2">
66
+ {#each groupedErrors as { message, count } (message)}
67
+ <li class="gap-2 text-sm flex text-destructive/90">
68
+ <span class="shrink-0">•</span>
69
+ <span class="flex-1">
70
+ {message}
71
+ {#if count > 1}
72
+ <span class="ml-1 font-medium text-destructive/70">×{count} </span>
73
+ {/if}
74
+ </span>
75
+ </li>
76
+ {/each}
77
+ </ul>
78
+ </div>
79
+ </Collapsible.Content>
80
+ </div>
81
+ </Collapsible.Root>
82
+ {/if}
83
+
84
+ {#if warnings.length > 0}
85
+ <Collapsible.Root bind:open={showWarnings}>
86
+ <div class="overflow-hidden rounded-lg border border-warning/50 bg-card">
87
+ <div class="px-4 py-3 flex items-center">
88
+ <Collapsible.Trigger
89
+ class="-mx-4 -my-3 gap-3 px-4 py-3 flex flex-1 items-center text-left transition-colors hover:bg-warning/5"
90
+ >
91
+ {#if showWarnings}
92
+ <ChevronDown class="h-4 w-4 shrink-0 text-warning" />
93
+ {:else}
94
+ <ChevronRight class="h-4 w-4 shrink-0 text-warning" />
95
+ {/if}
96
+ <TriangleAlert class="h-4 w-4 shrink-0 text-warning" />
97
+ <span class="text-sm font-medium text-warning">
98
+ {warnings.length === 1 ? '1 Warning' : `${warnings.length} Warnings`}
99
+ </span>
100
+ </Collapsible.Trigger>
101
+ </div>
102
+
103
+ <Collapsible.Content class="space-y-0">
104
+ <div class="max-h-60 px-4 py-3 overflow-y-auto border-t border-warning/50 bg-card">
105
+ <ul class="space-y-2">
106
+ {#each groupedWarnings as { message, count } (message)}
107
+ <li class="gap-2 text-sm flex text-muted-foreground">
108
+ <span class="shrink-0">•</span>
109
+ <span class="flex-1">
110
+ {message}
111
+ {#if count > 1}
112
+ <span class="ml-1 font-medium text-warning/70">×{count}</span>
113
+ {/if}
114
+ </span>
115
+ </li>
116
+ {/each}
117
+ </ul>
118
+ </div>
119
+ </Collapsible.Content>
120
+ </div>
121
+ </Collapsible.Root>
122
+ {/if}
123
+ </div>
124
+ </Dialog.Content>
125
+ </Dialog.Root>