@selvajs/ui 4.2.0 → 4.4.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 (85) hide show
  1. package/dist/components/compute/AppLayout.svelte +1 -1
  2. package/dist/components/compute/AppLayout.svelte.d.ts +1 -1
  3. package/dist/components/compute/ComputeApp.svelte +54 -133
  4. package/dist/components/compute/ComputeApp.svelte.d.ts +2 -9
  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/InputControl.svelte +96 -72
  12. package/dist/components/preview/OutputDisplay.svelte +6 -6
  13. package/dist/components/preview/TabLayout.svelte +6 -5
  14. package/dist/components/preview/inputs/FileInput.svelte +10 -19
  15. package/dist/composables/useFooterItem.svelte.d.ts +10 -12
  16. package/dist/composables/useFooterItem.svelte.js +9 -16
  17. package/dist/compute/computeThrottle.svelte.js +5 -2
  18. package/dist/compute/createSolveSession.svelte.d.ts +56 -0
  19. package/dist/compute/createSolveSession.svelte.js +122 -0
  20. package/dist/compute/solve-session-core.d.ts +45 -0
  21. package/dist/compute/solve-session-core.js +69 -0
  22. package/dist/constants.d.ts +0 -2
  23. package/dist/constants.js +0 -2
  24. package/dist/contexts/clientSlotContext.svelte.d.ts +13 -0
  25. package/dist/contexts/clientSlotContext.svelte.js +8 -0
  26. package/dist/contexts/footerContext.svelte.d.ts +17 -5
  27. package/dist/contexts/footerContext.svelte.js +10 -2
  28. package/dist/external/storage.js +7 -23
  29. package/dist/index.d.ts +2 -0
  30. package/dist/index.js +2 -0
  31. package/dist/schema/param-exporter.d.ts +25 -1
  32. package/dist/schema/param-exporter.js +30 -27
  33. package/dist/schema/traversal.d.ts +1 -0
  34. package/dist/schema/traversal.js +5 -0
  35. package/dist/schema/visibility-rules.d.ts +12 -0
  36. package/dist/schema/visibility-rules.js +20 -0
  37. package/dist/types/solveFn.d.ts +1 -2
  38. package/dist/utils/file-download.d.ts +5 -17
  39. package/dist/utils/file-download.js +12 -19
  40. package/dist/utils/randomId.js +3 -19
  41. package/package.json +10 -26
  42. package/src/lib/components/compute/AppLayout.svelte +1 -1
  43. package/src/lib/components/compute/ComputeApp.svelte +54 -133
  44. package/src/lib/components/compute/ComputeMessagesDialog.svelte +125 -0
  45. package/src/lib/components/compute/ParameterPresetManager.svelte +25 -33
  46. package/src/lib/components/layout/PageFooter.svelte +22 -144
  47. package/src/lib/components/preview/Group.svelte +6 -3
  48. package/src/lib/components/preview/ImageOutput.svelte +1 -13
  49. package/src/lib/components/preview/InputControl.svelte +96 -72
  50. package/src/lib/components/preview/OutputDisplay.svelte +6 -6
  51. package/src/lib/components/preview/TabLayout.svelte +6 -5
  52. package/src/lib/components/preview/inputs/FileInput.svelte +10 -19
  53. package/src/lib/composables/useFooterItem.svelte.ts +25 -25
  54. package/src/lib/compute/computeThrottle.svelte.ts +5 -2
  55. package/src/lib/compute/computeThrottle.test.ts +106 -0
  56. package/src/lib/compute/createSolveSession.svelte.ts +188 -0
  57. package/src/lib/compute/solve-session-core.test.ts +153 -0
  58. package/src/lib/compute/solve-session-core.ts +102 -0
  59. package/src/lib/constants.ts +0 -2
  60. package/src/lib/contexts/clientSlotContext.svelte.ts +33 -0
  61. package/src/lib/contexts/footerContext.svelte.ts +29 -14
  62. package/src/lib/external/storage.test.ts +99 -0
  63. package/src/lib/external/storage.ts +6 -22
  64. package/src/lib/index.ts +2 -0
  65. package/src/lib/schema/param-exporter.test.ts +136 -0
  66. package/src/lib/schema/param-exporter.ts +44 -30
  67. package/src/lib/schema/traversal.test.ts +92 -0
  68. package/src/lib/schema/traversal.ts +5 -0
  69. package/src/lib/schema/visibility-rules.test.ts +173 -0
  70. package/src/lib/schema/visibility-rules.ts +25 -0
  71. package/src/lib/types/solveFn.ts +1 -1
  72. package/src/lib/utils/file-download.ts +13 -20
  73. package/src/lib/utils/randomId.ts +3 -19
  74. package/dist/components/compute/ComputeMessages.svelte +0 -169
  75. package/dist/components/compute/ComputeMessages.svelte.d.ts +0 -7
  76. package/dist/components/compute/index.d.ts +0 -5
  77. package/dist/components/compute/index.js +0 -5
  78. package/dist/components/preview/index.d.ts +0 -3
  79. package/dist/components/preview/index.js +0 -4
  80. package/dist/components/viewer/index.d.ts +0 -3
  81. package/dist/components/viewer/index.js +0 -3
  82. package/src/lib/components/compute/ComputeMessages.svelte +0 -169
  83. package/src/lib/components/compute/index.ts +0 -5
  84. package/src/lib/components/preview/index.ts +0 -4
  85. package/src/lib/components/viewer/index.ts +0 -3
@@ -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 = [],
@@ -22,6 +22,6 @@ interface Props {
22
22
  presetLabels?: Partial<PresetLabels>;
23
23
  viewerConfig?: ViewerConfig;
24
24
  }
25
- declare const AppLayout: import("svelte").Component<Props, {}, "values" | "isViewerFullscreen">;
25
+ declare const AppLayout: import("svelte").Component<Props, {}, "isViewerFullscreen">;
26
26
  type AppLayout = ReturnType<typeof AppLayout>;
27
27
  export default AppLayout;
@@ -5,15 +5,17 @@
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';
18
+ import { setClientSlot, type ClientSlot } from '../../contexts/clientSlotContext.svelte';
17
19
 
18
20
  import type { Snippet } from 'svelte';
19
21
 
@@ -46,17 +48,12 @@
46
48
  footerItemPriority?: number;
47
49
  onReady?: (api: { loadValues: (values: Record<string, unknown>) => void }) => void;
48
50
  headerRight?: Snippet;
49
- /**
50
- * Bring-your-own header. When provided, replaces the built-in header inside
51
- * the standard-height sticky bar (so the fixed layout is unaffected).
52
- * Takes precedence over `headerRight`.
53
- */
51
+ // Replaces the built-in header; takes precedence over `headerRight`.
54
52
  header?: Snippet;
55
- /**
56
- * Stable identifier used to scope sessionStorage entries for external-input
57
- * values. If absent, falls back to definitionKey, then to schema.id.
58
- */
53
+ // Scopes sessionStorage for external-input values; falls back to definitionKey then schema.id.
59
54
  externalScopeKey?: string;
55
+ // Renders client-sourced inputs with presentation === 'slot'; receives { inputId, displayName, slotLabel, value }.
56
+ clientSlot?: ClientSlot;
60
57
  }
61
58
 
62
59
  let {
@@ -83,91 +80,36 @@
83
80
  headerRight,
84
81
  header,
85
82
  onReady,
86
- externalScopeKey
83
+ externalScopeKey,
84
+ clientSlot
87
85
  }: Props = $props();
88
86
 
89
- const resolvedScopeKey = $derived(externalScopeKey || definitionKey || schema?.id || '');
90
-
91
- function createInitialValues(s: UISchema, scopeKey: string) {
92
- const externalSet = new Set(getExternalInputs(s).map((e) => e.paramId));
93
- const v: Record<string, unknown> = {};
94
- for (const input of s.inputs) {
95
- if (externalSet.has(input.id)) {
96
- const stored = readExternalValue({ scopeKey, inputId: input.id });
97
- if (stored !== undefined) v[input.id] = stored;
98
- // else: leave undefined so the missing-inputs panel can detect it
99
- continue;
100
- }
101
- v[input.id] = input.default ?? getDefaultValue(input.paramType);
102
- }
103
- for (const output of s.outputs) {
104
- v[output.id] = null;
105
- }
106
- return v;
107
- }
108
-
109
- // svelte-ignore state_referenced_locally
110
- let values = $state<Record<string, unknown>>(
111
- createInitialValues(schema, externalScopeKey || definitionKey || schema?.id || '')
112
- );
113
- let error = $state('');
114
- let computeErrors = $state<string[]>([]);
115
- let computeWarnings = $state<string[]>([]);
116
- let meshes = $state<any[]>([]);
117
- let pendingValues = $state<Record<string, unknown>>({});
87
+ // Make the host's client-input slot available to InputControl deep in the tree.
118
88
  // svelte-ignore state_referenced_locally
119
- let hasPendingChanges = $state(schema?.instanceSolve === false);
120
- // svelte-ignore state_referenced_locally
121
- let hasNeverSolved = $state(schema?.instanceSolve === false);
122
- let isViewerFullscreen = $state(false);
123
-
124
- async function performSolveInternal(solveValues: Record<string, unknown>, signal: AbortSignal) {
125
- try {
126
- error = '';
127
- computeErrors = [];
128
- computeWarnings = [];
129
-
130
- const result = await onSolve(solveValues, signal);
89
+ setClientSlot(clientSlot);
131
90
 
132
- if (signal.aborted) return;
133
-
134
- computeErrors = result.errors ?? [];
135
- computeWarnings = result.warnings ?? [];
136
- meshes = result.meshes ?? [];
137
-
138
- Object.assign(values, result.outputs);
139
- pendingValues = {};
140
- hasPendingChanges = false;
141
- hasNeverSolved = false;
142
- } catch (err) {
143
- if (err instanceof Error && err.name === 'AbortError') return;
144
- error = err instanceof Error ? err.message : String(err);
145
- }
146
- }
91
+ const resolvedScopeKey = $derived(externalScopeKey || definitionKey || schema?.id || '');
147
92
 
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.
148
96
  // svelte-ignore state_referenced_locally
149
- const computeThrottle = createComputeThrottle<Record<string, unknown>>(performSolveInternal, {
97
+ const driver = createRequestResponseDriver(onSolve, () => session, {
150
98
  timeout: solveTimeoutMs
151
99
  });
100
+ // svelte-ignore state_referenced_locally
101
+ const session = createSolveSession({
102
+ schema,
103
+ scopeKey: externalScopeKey || definitionKey || schema?.id || '',
104
+ driver
105
+ });
152
106
 
153
- let solving = $derived(computeThrottle.isComputing);
154
- const solvingIndicator = createSolvingIndicator(() => solving);
107
+ let isViewerFullscreen = $state(false);
155
108
 
156
- function performSolve() {
157
- computeThrottle.trigger($state.snapshot(values));
158
- }
159
-
160
- function loadValues(incoming: Record<string, unknown>) {
161
- Object.assign(values, incoming);
162
- if (schema?.instanceSolve !== false) {
163
- performSolve();
164
- } else {
165
- hasPendingChanges = true;
166
- }
167
- }
109
+ const solvingIndicator = createSolvingIndicator(() => session.isSolving);
168
110
 
169
111
  $effect(() => {
170
- onReady?.({ loadValues });
112
+ onReady?.({ loadValues: (incoming) => session.loadValues(incoming) });
171
113
  });
172
114
 
173
115
  let previousDefinitionKey = $state('');
@@ -181,50 +123,35 @@
181
123
  isInitialLoad = false;
182
124
  previousDefinitionKey = definitionKey;
183
125
  if (schema?.instanceSolve !== false) {
184
- performSolve();
126
+ session.solve();
185
127
  }
186
128
  } else if (previousDefinitionKey !== definitionKey) {
187
- meshes = [];
188
- values = createInitialValues(schema, resolvedScopeKey);
189
- error = '';
190
- computeErrors = [];
191
- computeWarnings = [];
192
- if (schema && Object.keys(values).length > 0) {
193
- performSolve();
194
- }
129
+ // Host owns the WHEN (definitionKey changed); session owns the WHAT.
130
+ session.rebuild(schema, resolvedScopeKey);
195
131
  previousDefinitionKey = definitionKey;
196
132
  }
197
133
  });
198
134
  });
199
135
 
200
- async function handleValueChange(id: string, val: unknown) {
201
- values[id] = val;
202
-
203
- if (schema?.instanceSolve === false) {
204
- pendingValues[id] = val;
205
- hasPendingChanges = true;
206
- return;
207
- }
208
-
209
- performSolve();
136
+ function handleValueChange(id: string, val: unknown) {
137
+ session.setValue(id, val);
210
138
  }
211
139
 
212
140
  function handleCalculate() {
213
- performSolve();
141
+ session.solve();
214
142
  }
215
143
 
216
- // Use untrack to read these static props without creating reactive dependencies.
217
- // footerComponentProps is intentionally NOT untracked — it's a getter called every render.
218
- const _footerItemId = untrack(() => footerItemId);
219
- const _footerComponent = untrack(() => footerComponent);
220
- const _footerItemPriority = untrack(() => footerItemPriority);
221
- useFooterItem(
222
- _footerItemId,
223
- _footerComponent,
224
- () => (_footerComponent ? (footerComponentProps?.() ?? {}) : {}),
225
- 'left',
226
- _footerItemPriority
227
- );
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
+ });
228
155
 
229
156
  let resolvedIsEmbedded = $derived(isEmbedded ?? page.url.searchParams.get('embed') === 'true');
230
157
  let resolvedPrimaryColor = $derived(primaryColor ?? page.url.searchParams.get('primary'));
@@ -246,12 +173,12 @@
246
173
  {footerText}
247
174
  {header}
248
175
  rightContent={headerRight}
249
- errors={computeErrors}
250
- warnings={computeWarnings}
176
+ errors={session.computeErrors}
177
+ warnings={session.computeWarnings}
251
178
  >
252
- {#if error}
179
+ {#if session.error}
253
180
  <div class="min-h-100 p-8 flex items-center justify-center">
254
- <StateDisplay type="error" size="medium" message={error} />
181
+ <StateDisplay type="error" size="medium" message={session.error} />
255
182
  </div>
256
183
  {:else if !schema}
257
184
  <div class="min-h-100 flex items-center justify-center">
@@ -261,13 +188,13 @@
261
188
  {#key definitionKey}
262
189
  <AppLayout
263
190
  {schema}
264
- {meshes}
265
- isSolving={solving}
191
+ meshes={session.meshes}
192
+ isSolving={session.isSolving}
266
193
  showSolvingIndicator={schema.instanceSolve !== false && solvingIndicator.show}
267
- {hasPendingChanges}
268
- {hasNeverSolved}
194
+ hasPendingChanges={session.hasPendingChanges}
195
+ hasNeverSolved={session.hasNeverSolved}
269
196
  bind:isViewerFullscreen
270
- bind:values
197
+ values={session.values}
271
198
  {panelActions}
272
199
  {showSaveButton}
273
200
  {showLoadButton}
@@ -276,13 +203,7 @@
276
203
  {presetLabels}
277
204
  onValueChange={handleValueChange}
278
205
  oncalculate={handleCalculate}
279
- onLoadValues={() => {
280
- if (schema?.instanceSolve !== false) {
281
- performSolve();
282
- } else {
283
- hasPendingChanges = true;
284
- }
285
- }}
206
+ onLoadValues={() => session.loadValues({})}
286
207
  />
287
208
  {/key}
288
209
  {/if}
@@ -2,6 +2,7 @@ import type { UISchema, ParameterPreset } from '@selvajs/schemas';
2
2
  import type { ActionButton } from '../../types/actionButton';
3
3
  import type { SolveFn } from '../../types/solveFn';
4
4
  import type { PresetLabels } from '../../types/presetLabels';
5
+ import { type ClientSlot } from '../../contexts/clientSlotContext.svelte';
5
6
  import type { Snippet } from 'svelte';
6
7
  interface Props {
7
8
  schema: UISchema;
@@ -34,17 +35,9 @@ interface Props {
34
35
  loadValues: (values: Record<string, unknown>) => void;
35
36
  }) => void;
36
37
  headerRight?: Snippet;
37
- /**
38
- * Bring-your-own header. When provided, replaces the built-in header inside
39
- * the standard-height sticky bar (so the fixed layout is unaffected).
40
- * Takes precedence over `headerRight`.
41
- */
42
38
  header?: Snippet;
43
- /**
44
- * Stable identifier used to scope sessionStorage entries for external-input
45
- * values. If absent, falls back to definitionKey, then to schema.id.
46
- */
47
39
  externalScopeKey?: string;
40
+ clientSlot?: ClientSlot;
48
41
  }
49
42
  declare const ComputeApp: import("svelte").Component<Props, {}, "">;
50
43
  type ComputeApp = ReturnType<typeof ComputeApp>;
@@ -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 '../primitives/collapsible';
5
+ import * as Dialog from '../primitives/dialog';
6
+ import { groupMessages } from '../../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>
@@ -0,0 +1,10 @@
1
+ import type { Snippet } from 'svelte';
2
+ interface Props {
3
+ errors?: string[];
4
+ warnings?: string[];
5
+ /** Renders inside Dialog.Trigger. */
6
+ trigger: Snippet;
7
+ }
8
+ declare const ComputeMessagesDialog: import("svelte").Component<Props, {}, "">;
9
+ type ComputeMessagesDialog = ReturnType<typeof ComputeMessagesDialog>;
10
+ export default ComputeMessagesDialog;
@@ -3,8 +3,8 @@
3
3
  import type { UISchema, ParameterPreset } from '@selvajs/schemas';
4
4
  import {
5
5
  createSavedState,
6
- validateSavedState,
7
- extractLoadableValues,
6
+ loadPreset,
7
+ type PresetLoadResult,
8
8
  exportStateAsJson,
9
9
  importStateFromJson
10
10
  } from '../../schema/param-exporter';
@@ -52,8 +52,8 @@
52
52
  // Import/validation state
53
53
  let showValidationDialog = $state(false);
54
54
  let showLoadDialog = $state(false);
55
- let importedState = $state<ParameterPreset | null>(null);
56
- let validationResult = $state<ReturnType<typeof validateSavedState> | null>(null);
55
+ let loadResult = $state<PresetLoadResult | null>(null);
56
+ let loadingPresetName = $state('');
57
57
  let fileInputRef = $state<HTMLInputElement | null>(null);
58
58
 
59
59
  // Listed states (when onListStates is provided)
@@ -90,15 +90,15 @@
90
90
  showExportDialog = false;
91
91
  }
92
92
 
93
- // Validate a preset, then either load it directly (no issues) or open the
93
+ // Load a preset in one pass, then either apply it directly (no issues) or open the
94
94
  // validation dialog (any errors or warnings). Shared by every load path.
95
95
  function tryLoad(preset: ParameterPreset) {
96
- const validation = validateSavedState(preset, schema);
97
- if (validation.isValid) {
98
- onLoadValues(extractLoadableValues(preset, schema, validation));
96
+ const result = loadPreset(preset, schema);
97
+ if (result.isValid) {
98
+ onLoadValues(result.values);
99
99
  } else {
100
- importedState = preset;
101
- validationResult = validation;
100
+ loadResult = result;
101
+ loadingPresetName = preset.name;
102
102
  showValidationDialog = true;
103
103
  }
104
104
  }
@@ -119,21 +119,16 @@
119
119
  }
120
120
 
121
121
  function confirmLoad() {
122
- if (!importedState || !validationResult) return;
123
-
124
- if (validationResult.canLoad) {
125
- const values = extractLoadableValues(importedState, schema, validationResult);
126
- onLoadValues(values);
127
- showValidationDialog = false;
128
- importedState = null;
129
- validationResult = null;
130
- }
122
+ if (!loadResult || !loadResult.canLoad) return;
123
+
124
+ onLoadValues(loadResult.values);
125
+ showValidationDialog = false;
126
+ loadResult = null;
131
127
  }
132
128
 
133
129
  function cancelImport() {
134
130
  showValidationDialog = false;
135
- importedState = null;
136
- validationResult = null;
131
+ loadResult = null;
137
132
  }
138
133
 
139
134
  async function openLoadDialog() {
@@ -288,16 +283,16 @@
288
283
  <Dialog.Header>
289
284
  <Dialog.Title>{t.validationTitle}</Dialog.Title>
290
285
  <Dialog.Description>
291
- {#if importedState}
292
- {t.validationValidatingPrefix}{importedState.name}
286
+ {#if loadingPresetName}
287
+ {t.validationValidatingPrefix}{loadingPresetName}
293
288
  {/if}
294
289
  </Dialog.Description>
295
290
  </Dialog.Header>
296
291
 
297
- {#if validationResult}
292
+ {#if loadResult}
298
293
  <div class="gap-4 py-4 grid">
299
294
  <!-- Summary Alert -->
300
- {#if validationResult.isValid}
295
+ {#if loadResult.isValid}
301
296
  <Card.Root class="p-4 border-success/30 bg-success/5">
302
297
  <div class="gap-3 flex items-start">
303
298
  <CheckCircle class="h-5 w-5 text-success" />
@@ -311,7 +306,7 @@
311
306
  </div>
312
307
  </div>
313
308
  </Card.Root>
314
- {:else if validationResult.canLoad}
309
+ {:else if loadResult.canLoad}
315
310
  <Card.Root class="p-4 border-warning/30 bg-warning/5">
316
311
  <div class="gap-3 flex items-start">
317
312
  <AlertTriangle class="h-5 w-5 text-warning" />
@@ -320,10 +315,7 @@
320
315
  {t.validationWarningsTitle}
321
316
  </h4>
322
317
  <p class="text-sm mt-1 text-warning-foreground/80">
323
- {t.validationWarningsBody.replace(
324
- '{count}',
325
- String(validationResult.issues.length)
326
- )}
318
+ {t.validationWarningsBody.replace('{count}', String(loadResult.issues.length))}
327
319
  </p>
328
320
  </div>
329
321
  </div>
@@ -343,10 +335,10 @@
343
335
  {/if}
344
336
 
345
337
  <!-- Issues List -->
346
- {#if !validationResult.isValid}
338
+ {#if !loadResult.isValid}
347
339
  <div class="space-y-2">
348
340
  <h4 class="text-sm font-medium">{t.validationIssuesHeading}</h4>
349
- {#each validationResult.issues as issue (issue.message)}
341
+ {#each loadResult.issues as issue (issue.message)}
350
342
  <div
351
343
  class="p-3 rounded-lg border {issue.severity === 'error'
352
344
  ? 'border-destructive/30 bg-destructive/5'
@@ -378,7 +370,7 @@
378
370
 
379
371
  <Dialog.Footer>
380
372
  <Button variant="outline" onclick={cancelImport}>{t.cancelButton}</Button>
381
- {#if validationResult?.canLoad}
373
+ {#if loadResult?.canLoad}
382
374
  <Button onclick={confirmLoad}>{t.loadAnywayButton}</Button>
383
375
  {/if}
384
376
  </Dialog.Footer>