@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.
- package/dist/components/compute/AppLayout.svelte +3 -3
- package/dist/components/compute/AppLayout.svelte.d.ts +2 -2
- package/dist/components/compute/ComputeApp.svelte +47 -140
- package/dist/components/compute/ComputeApp.svelte.d.ts +0 -16
- package/dist/components/compute/ComputeMessagesDialog.svelte +125 -0
- package/dist/components/compute/ComputeMessagesDialog.svelte.d.ts +10 -0
- package/dist/components/compute/ParameterPresetManager.svelte +25 -33
- package/dist/components/layout/PageFooter.svelte +22 -144
- package/dist/components/preview/Group.svelte +6 -3
- package/dist/components/preview/ImageOutput.svelte +1 -13
- package/dist/components/preview/OutputDisplay.svelte +6 -6
- package/dist/components/preview/TabLayout.svelte +6 -5
- package/dist/components/preview/inputs/FileInput.svelte +10 -19
- package/dist/composables/useFooterItem.svelte.d.ts +10 -12
- package/dist/composables/useFooterItem.svelte.js +9 -16
- package/dist/compute/computeThrottle.svelte.js +5 -2
- package/dist/compute/createSolveSession.svelte.d.ts +56 -0
- package/dist/compute/createSolveSession.svelte.js +122 -0
- package/dist/compute/solve-session-core.d.ts +45 -0
- package/dist/compute/solve-session-core.js +69 -0
- package/dist/constants.d.ts +0 -2
- package/dist/constants.js +0 -2
- package/dist/contexts/footerContext.svelte.d.ts +17 -5
- package/dist/contexts/footerContext.svelte.js +10 -2
- package/dist/external/storage.js +7 -23
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -0
- package/dist/schema/param-exporter.d.ts +25 -1
- package/dist/schema/param-exporter.js +30 -27
- package/dist/schema/traversal.d.ts +1 -0
- package/dist/schema/traversal.js +5 -0
- package/dist/schema/visibility-rules.d.ts +12 -0
- package/dist/schema/visibility-rules.js +20 -0
- package/dist/types/solveFn.d.ts +1 -2
- package/dist/utils/file-download.d.ts +5 -17
- package/dist/utils/file-download.js +12 -19
- package/dist/utils/randomId.js +3 -19
- package/package.json +10 -26
- package/src/lib/components/compute/AppLayout.svelte +3 -3
- package/src/lib/components/compute/ComputeApp.svelte +47 -140
- package/src/lib/components/compute/ComputeMessagesDialog.svelte +125 -0
- package/src/lib/components/compute/ParameterPresetManager.svelte +25 -33
- package/src/lib/components/layout/PageFooter.svelte +22 -144
- package/src/lib/components/preview/Group.svelte +6 -3
- package/src/lib/components/preview/ImageOutput.svelte +1 -13
- package/src/lib/components/preview/OutputDisplay.svelte +6 -6
- package/src/lib/components/preview/TabLayout.svelte +6 -5
- package/src/lib/components/preview/inputs/FileInput.svelte +10 -19
- package/src/lib/composables/useFooterItem.svelte.ts +25 -25
- package/src/lib/compute/computeThrottle.svelte.ts +5 -2
- package/src/lib/compute/computeThrottle.test.ts +106 -0
- package/src/lib/compute/createSolveSession.svelte.ts +188 -0
- package/src/lib/compute/solve-session-core.test.ts +153 -0
- package/src/lib/compute/solve-session-core.ts +102 -0
- package/src/lib/constants.ts +0 -2
- package/src/lib/contexts/footerContext.svelte.ts +29 -14
- package/src/lib/external/storage.test.ts +99 -0
- package/src/lib/external/storage.ts +6 -22
- package/src/lib/index.ts +14 -1
- package/src/lib/schema/param-exporter.test.ts +136 -0
- package/src/lib/schema/param-exporter.ts +44 -30
- package/src/lib/schema/traversal.test.ts +92 -0
- package/src/lib/schema/traversal.ts +5 -0
- package/src/lib/schema/visibility-rules.test.ts +173 -0
- package/src/lib/schema/visibility-rules.ts +25 -0
- package/src/lib/types/solveFn.ts +1 -1
- package/src/lib/utils/file-download.ts +13 -20
- package/src/lib/utils/randomId.ts +3 -19
- package/dist/components/compute/ComputeMessages.svelte +0 -169
- package/dist/components/compute/ComputeMessages.svelte.d.ts +0 -7
- package/dist/components/compute/index.d.ts +0 -5
- package/dist/components/compute/index.js +0 -5
- package/dist/components/preview/index.d.ts +0 -3
- package/dist/components/preview/index.js +0 -4
- package/dist/components/viewer/index.d.ts +0 -3
- package/dist/components/viewer/index.js +0 -3
- package/src/lib/components/compute/ComputeMessages.svelte +0 -169
- package/src/lib/components/compute/index.ts +0 -5
- package/src/lib/components/preview/index.ts +0 -4
- package/src/lib/components/viewer/index.ts +0 -3
|
@@ -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
|
|
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;
|
|
@@ -13,7 +13,7 @@ interface Props {
|
|
|
13
13
|
oncalculate?: () => void;
|
|
14
14
|
values: Record<string, unknown>;
|
|
15
15
|
onValueChange: (id: string, val: SupportedTypes) => void | Promise<void>;
|
|
16
|
-
onLoadValues?: () => void | Promise<void>;
|
|
16
|
+
onLoadValues?: (values: Record<string, unknown>) => void | Promise<void>;
|
|
17
17
|
panelActions?: ActionButton[];
|
|
18
18
|
showSaveButton?: boolean;
|
|
19
19
|
showLoadButton?: boolean;
|
|
@@ -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, {}, "
|
|
25
|
+
declare const AppLayout: import("svelte").Component<Props, {}, "isViewerFullscreen">;
|
|
26
26
|
type AppLayout = ReturnType<typeof AppLayout>;
|
|
27
27
|
export default AppLayout;
|
|
@@ -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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
|
168
|
-
const solvingIndicator = createSolvingIndicator(() => solving);
|
|
107
|
+
let isViewerFullscreen = $state(false);
|
|
169
108
|
|
|
170
|
-
|
|
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
|
-
|
|
126
|
+
session.solve();
|
|
199
127
|
}
|
|
200
128
|
} else if (previousDefinitionKey !== definitionKey) {
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
215
|
-
|
|
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
|
-
|
|
141
|
+
session.solve();
|
|
228
142
|
}
|
|
229
143
|
|
|
230
|
-
//
|
|
231
|
-
// footerComponentProps is intentionally
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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={
|
|
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
|
-
|
|
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}
|
|
@@ -35,24 +35,8 @@ interface Props {
|
|
|
35
35
|
loadValues: (values: Record<string, unknown>) => void;
|
|
36
36
|
}) => void;
|
|
37
37
|
headerRight?: Snippet;
|
|
38
|
-
/**
|
|
39
|
-
* Bring-your-own header. When provided, replaces the built-in header inside
|
|
40
|
-
* the standard-height sticky bar (so the fixed layout is unaffected).
|
|
41
|
-
* Takes precedence over `headerRight`.
|
|
42
|
-
*/
|
|
43
38
|
header?: Snippet;
|
|
44
|
-
/**
|
|
45
|
-
* Stable identifier used to scope sessionStorage entries for external-input
|
|
46
|
-
* values. If absent, falls back to definitionKey, then to schema.id.
|
|
47
|
-
*/
|
|
48
39
|
externalScopeKey?: string;
|
|
49
|
-
/**
|
|
50
|
-
* Host-rendered element for client-sourced inputs whose schema sets
|
|
51
|
-
* source.client.presentation === 'slot'. Selva reserves the input's cell and
|
|
52
|
-
* invokes this snippet in its place, passing { inputId, displayName, slotLabel,
|
|
53
|
-
* value }. Selva renders nothing itself and never interprets what the host puts
|
|
54
|
-
* here (e.g. an "Edit JSON" button that navigates back to a producer page).
|
|
55
|
-
*/
|
|
56
40
|
clientSlot?: ClientSlot;
|
|
57
41
|
}
|
|
58
42
|
declare const ComputeApp: import("svelte").Component<Props, {}, "">;
|
|
@@ -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
|
-
|
|
7
|
-
|
|
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
|
|
56
|
-
let
|
|
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
|
-
//
|
|
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
|
|
97
|
-
if (
|
|
98
|
-
onLoadValues(
|
|
96
|
+
const result = loadPreset(preset, schema);
|
|
97
|
+
if (result.isValid) {
|
|
98
|
+
onLoadValues(result.values);
|
|
99
99
|
} else {
|
|
100
|
-
|
|
101
|
-
|
|
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 (!
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
|
292
|
-
{t.validationValidatingPrefix}{
|
|
286
|
+
{#if loadingPresetName}
|
|
287
|
+
{t.validationValidatingPrefix}{loadingPresetName}
|
|
293
288
|
{/if}
|
|
294
289
|
</Dialog.Description>
|
|
295
290
|
</Dialog.Header>
|
|
296
291
|
|
|
297
|
-
{#if
|
|
292
|
+
{#if loadResult}
|
|
298
293
|
<div class="gap-4 py-4 grid">
|
|
299
294
|
<!-- Summary Alert -->
|
|
300
|
-
{#if
|
|
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
|
|
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 !
|
|
338
|
+
{#if !loadResult.isValid}
|
|
347
339
|
<div class="space-y-2">
|
|
348
340
|
<h4 class="text-sm font-medium">{t.validationIssuesHeading}</h4>
|
|
349
|
-
{#each
|
|
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
|
|
373
|
+
{#if loadResult?.canLoad}
|
|
382
374
|
<Button onclick={confirmLoad}>{t.loadAnywayButton}</Button>
|
|
383
375
|
{/if}
|
|
384
376
|
</Dialog.Footer>
|