@selvajs/ui 2.0.11 → 3.1.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 +11 -1
- package/dist/components/compute/AppLayout.svelte.d.ts +5 -1
- package/dist/components/compute/ComputeApp.svelte +30 -1
- package/dist/components/compute/ComputeApp.svelte.d.ts +18 -1
- package/dist/components/compute/ParameterPresetManager.svelte +132 -72
- package/dist/components/compute/ParameterPresetManager.svelte.d.ts +8 -1
- package/dist/components/layout/AppShell.svelte +32 -12
- package/dist/components/layout/AppShell.svelte.d.ts +8 -0
- package/dist/components/layout/PageFooter.svelte +16 -2
- package/dist/components/layout/PageFooter.svelte.d.ts +2 -0
- package/dist/external/storage.js +3 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/types/presetLabels.d.ts +40 -0
- package/dist/types/presetLabels.js +33 -0
- package/package.json +3 -3
- package/src/lib/components/compute/AppLayout.svelte +11 -1
- package/src/lib/components/compute/ComputeApp.svelte +30 -1
- package/src/lib/components/compute/ParameterPresetManager.svelte +132 -72
- package/src/lib/components/layout/AppShell.svelte +32 -12
- package/src/lib/components/layout/PageFooter.svelte +16 -2
- package/src/lib/external/storage.ts +3 -3
- package/src/lib/index.ts +1 -0
- package/src/lib/types/presetLabels.ts +86 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { UISchema, SupportedTypes } from '@selvajs/schemas';
|
|
2
|
+
import type { UISchema, SupportedTypes, ParameterPreset } from '@selvajs/schemas';
|
|
3
3
|
import type { ActionButton } from '../../types/actionButton';
|
|
4
|
+
import type { PresetLabels } from '../../types/presetLabels';
|
|
4
5
|
import { ChevronUp } from '@lucide/svelte';
|
|
5
6
|
import Viewer, { type ViewerConfig } from '../viewer/Viewer.svelte';
|
|
6
7
|
import CalculateButton from '../primitives/CalculateButton.svelte';
|
|
@@ -28,6 +29,9 @@
|
|
|
28
29
|
panelActions?: ActionButton[];
|
|
29
30
|
showSaveButton?: boolean;
|
|
30
31
|
showLoadButton?: boolean;
|
|
32
|
+
onSaveState?: (state: ParameterPreset) => void | Promise<void>;
|
|
33
|
+
onListStates?: () => ParameterPreset[] | Promise<ParameterPreset[]>;
|
|
34
|
+
presetLabels?: Partial<PresetLabels>;
|
|
31
35
|
viewerConfig?: ViewerConfig;
|
|
32
36
|
}
|
|
33
37
|
|
|
@@ -46,6 +50,9 @@
|
|
|
46
50
|
panelActions = [],
|
|
47
51
|
showSaveButton = true,
|
|
48
52
|
showLoadButton = true,
|
|
53
|
+
onSaveState,
|
|
54
|
+
onListStates,
|
|
55
|
+
presetLabels,
|
|
49
56
|
viewerConfig = {}
|
|
50
57
|
}: Props = $props();
|
|
51
58
|
|
|
@@ -130,6 +137,9 @@
|
|
|
130
137
|
onLoadValues={handleLoadValues}
|
|
131
138
|
{showSaveButton}
|
|
132
139
|
{showLoadButton}
|
|
140
|
+
{onSaveState}
|
|
141
|
+
{onListStates}
|
|
142
|
+
labels={presetLabels}
|
|
133
143
|
/>
|
|
134
144
|
{/if}
|
|
135
145
|
{#each panelActions as action (action.id)}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { UISchema, SupportedTypes } from '@selvajs/schemas';
|
|
1
|
+
import type { UISchema, SupportedTypes, ParameterPreset } from '@selvajs/schemas';
|
|
2
2
|
import type { ActionButton } from '../../types/actionButton';
|
|
3
|
+
import type { PresetLabels } from '../../types/presetLabels';
|
|
3
4
|
import { type ViewerConfig } from '../viewer/Viewer.svelte';
|
|
4
5
|
interface Props {
|
|
5
6
|
schema: UISchema;
|
|
@@ -16,6 +17,9 @@ interface Props {
|
|
|
16
17
|
panelActions?: ActionButton[];
|
|
17
18
|
showSaveButton?: boolean;
|
|
18
19
|
showLoadButton?: boolean;
|
|
20
|
+
onSaveState?: (state: ParameterPreset) => void | Promise<void>;
|
|
21
|
+
onListStates?: () => ParameterPreset[] | Promise<ParameterPreset[]>;
|
|
22
|
+
presetLabels?: Partial<PresetLabels>;
|
|
19
23
|
viewerConfig?: ViewerConfig;
|
|
20
24
|
}
|
|
21
25
|
declare const AppLayout: import("svelte").Component<Props, {}, "values" | "isViewerFullscreen">;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { untrack } from 'svelte';
|
|
3
3
|
import { page } from '$app/state';
|
|
4
|
-
import type { UISchema } from '@selvajs/schemas';
|
|
4
|
+
import type { UISchema, ParameterPreset } from '@selvajs/schemas';
|
|
5
5
|
import type { ActionButton } from '../../types/actionButton';
|
|
6
6
|
import type { SolveFn } from '../../types/solveFn';
|
|
7
|
+
import type { PresetLabels } from '../../types/presetLabels';
|
|
7
8
|
import { getDefaultValue } from '../../schema/defaults';
|
|
8
9
|
import { createComputeThrottle } from '../../compute/computeThrottle.svelte';
|
|
9
10
|
import { createSolvingIndicator } from '../../compute/solving.svelte';
|
|
@@ -27,6 +28,16 @@
|
|
|
27
28
|
panelActions?: ActionButton[];
|
|
28
29
|
showSaveButton?: boolean;
|
|
29
30
|
showLoadButton?: boolean;
|
|
31
|
+
/** When set, persist saved states via this callback instead of downloading a .sps file. */
|
|
32
|
+
onSaveState?: (state: ParameterPreset) => void | Promise<void>;
|
|
33
|
+
/** When set, the Load dialog lists these states instead of showing a file input. */
|
|
34
|
+
onListStates?: () => ParameterPreset[] | Promise<ParameterPreset[]>;
|
|
35
|
+
/** Partial overrides for the preset-manager UI strings (e.g. for localization). */
|
|
36
|
+
presetLabels?: Partial<PresetLabels>;
|
|
37
|
+
/** Name shown in the footer copyright line. Defaults to the brand name ("Selva"). */
|
|
38
|
+
copyrightName?: string;
|
|
39
|
+
/** Fully overrides the footer copyright line. `{name}` and `{year}` are substituted. */
|
|
40
|
+
footerText?: string;
|
|
30
41
|
/** Per-solve abort timeout (ms). Falls back to createComputeThrottle's default. */
|
|
31
42
|
solveTimeoutMs?: number;
|
|
32
43
|
footerComponent?: any;
|
|
@@ -35,6 +46,12 @@
|
|
|
35
46
|
footerItemPriority?: number;
|
|
36
47
|
onReady?: (api: { loadValues: (values: Record<string, unknown>) => void }) => void;
|
|
37
48
|
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
|
+
*/
|
|
54
|
+
header?: Snippet;
|
|
38
55
|
/**
|
|
39
56
|
* Stable identifier used to scope sessionStorage entries for external-input
|
|
40
57
|
* values. If absent, falls back to definitionKey, then to schema.id.
|
|
@@ -53,12 +70,18 @@
|
|
|
53
70
|
panelActions = [],
|
|
54
71
|
showSaveButton = true,
|
|
55
72
|
showLoadButton = true,
|
|
73
|
+
onSaveState,
|
|
74
|
+
onListStates,
|
|
75
|
+
presetLabels,
|
|
76
|
+
copyrightName,
|
|
77
|
+
footerText,
|
|
56
78
|
solveTimeoutMs,
|
|
57
79
|
footerComponent,
|
|
58
80
|
footerComponentProps,
|
|
59
81
|
footerItemId = 'footer-item',
|
|
60
82
|
footerItemPriority = 0,
|
|
61
83
|
headerRight,
|
|
84
|
+
header,
|
|
62
85
|
onReady,
|
|
63
86
|
externalScopeKey
|
|
64
87
|
}: Props = $props();
|
|
@@ -219,6 +242,9 @@
|
|
|
219
242
|
showFooter
|
|
220
243
|
title={pageTitle}
|
|
221
244
|
{showModeToggle}
|
|
245
|
+
{copyrightName}
|
|
246
|
+
{footerText}
|
|
247
|
+
{header}
|
|
222
248
|
rightContent={headerRight}
|
|
223
249
|
errors={computeErrors}
|
|
224
250
|
warnings={computeWarnings}
|
|
@@ -245,6 +271,9 @@
|
|
|
245
271
|
{panelActions}
|
|
246
272
|
{showSaveButton}
|
|
247
273
|
{showLoadButton}
|
|
274
|
+
{onSaveState}
|
|
275
|
+
{onListStates}
|
|
276
|
+
{presetLabels}
|
|
248
277
|
onValueChange={handleValueChange}
|
|
249
278
|
oncalculate={handleCalculate}
|
|
250
279
|
onLoadValues={() => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { UISchema } from '@selvajs/schemas';
|
|
1
|
+
import type { UISchema, ParameterPreset } from '@selvajs/schemas';
|
|
2
2
|
import type { ActionButton } from '../../types/actionButton';
|
|
3
3
|
import type { SolveFn } from '../../types/solveFn';
|
|
4
|
+
import type { PresetLabels } from '../../types/presetLabels';
|
|
4
5
|
import type { Snippet } from 'svelte';
|
|
5
6
|
interface Props {
|
|
6
7
|
schema: UISchema;
|
|
@@ -13,6 +14,16 @@ interface Props {
|
|
|
13
14
|
panelActions?: ActionButton[];
|
|
14
15
|
showSaveButton?: boolean;
|
|
15
16
|
showLoadButton?: boolean;
|
|
17
|
+
/** When set, persist saved states via this callback instead of downloading a .sps file. */
|
|
18
|
+
onSaveState?: (state: ParameterPreset) => void | Promise<void>;
|
|
19
|
+
/** When set, the Load dialog lists these states instead of showing a file input. */
|
|
20
|
+
onListStates?: () => ParameterPreset[] | Promise<ParameterPreset[]>;
|
|
21
|
+
/** Partial overrides for the preset-manager UI strings (e.g. for localization). */
|
|
22
|
+
presetLabels?: Partial<PresetLabels>;
|
|
23
|
+
/** Name shown in the footer copyright line. Defaults to the brand name ("Selva"). */
|
|
24
|
+
copyrightName?: string;
|
|
25
|
+
/** Fully overrides the footer copyright line. `{name}` and `{year}` are substituted. */
|
|
26
|
+
footerText?: string;
|
|
16
27
|
/** Per-solve abort timeout (ms). Falls back to createComputeThrottle's default. */
|
|
17
28
|
solveTimeoutMs?: number;
|
|
18
29
|
footerComponent?: any;
|
|
@@ -23,6 +34,12 @@ interface Props {
|
|
|
23
34
|
loadValues: (values: Record<string, unknown>) => void;
|
|
24
35
|
}) => void;
|
|
25
36
|
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
|
+
header?: Snippet;
|
|
26
43
|
/**
|
|
27
44
|
* Stable identifier used to scope sessionStorage entries for external-input
|
|
28
45
|
* values. If absent, falls back to definitionKey, then to schema.id.
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { Button, Input, Label, Textarea, Dialog, Card } from '../primitives';
|
|
12
12
|
|
|
13
13
|
import type { ActionButton } from '../../types/actionButton';
|
|
14
|
+
import { DEFAULT_PRESET_LABELS, type PresetLabels } from '../../types/presetLabels';
|
|
14
15
|
|
|
15
16
|
interface Props {
|
|
16
17
|
schema: UISchema;
|
|
@@ -19,6 +20,12 @@
|
|
|
19
20
|
showSaveButton?: boolean;
|
|
20
21
|
showLoadButton?: boolean;
|
|
21
22
|
actions?: ActionButton[];
|
|
23
|
+
/** When set, persist saved states via this callback instead of downloading a .sps file. */
|
|
24
|
+
onSaveState?: (state: ParameterPreset) => void | Promise<void>;
|
|
25
|
+
/** When set, the Load dialog lists these states instead of showing a file input. */
|
|
26
|
+
onListStates?: () => ParameterPreset[] | Promise<ParameterPreset[]>;
|
|
27
|
+
/** Partial overrides for UI strings (e.g. for localization). */
|
|
28
|
+
labels?: Partial<PresetLabels>;
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
let {
|
|
@@ -27,9 +34,14 @@
|
|
|
27
34
|
onLoadValues,
|
|
28
35
|
showSaveButton = true,
|
|
29
36
|
showLoadButton = true,
|
|
30
|
-
actions = []
|
|
37
|
+
actions = [],
|
|
38
|
+
onSaveState,
|
|
39
|
+
onListStates,
|
|
40
|
+
labels
|
|
31
41
|
}: Props = $props();
|
|
32
42
|
|
|
43
|
+
const t = $derived({ ...DEFAULT_PRESET_LABELS, ...labels });
|
|
44
|
+
|
|
33
45
|
// Save dialog state
|
|
34
46
|
let showExportDialog = $state(false);
|
|
35
47
|
let exportName = $state('');
|
|
@@ -44,6 +56,11 @@
|
|
|
44
56
|
let validationResult = $state<ReturnType<typeof validateSavedState> | null>(null);
|
|
45
57
|
let fileInputRef = $state<HTMLInputElement | null>(null);
|
|
46
58
|
|
|
59
|
+
// Listed states (when onListStates is provided)
|
|
60
|
+
let listedStates = $state<ParameterPreset[]>([]);
|
|
61
|
+
let isLoadingList = $state(false);
|
|
62
|
+
let listError = $state('');
|
|
63
|
+
|
|
47
64
|
function openExportDialog() {
|
|
48
65
|
exportName = `State ${new Date().toLocaleDateString()}`;
|
|
49
66
|
exportDescription = '';
|
|
@@ -52,9 +69,9 @@
|
|
|
52
69
|
showExportDialog = true;
|
|
53
70
|
}
|
|
54
71
|
|
|
55
|
-
function handleExport() {
|
|
72
|
+
async function handleExport() {
|
|
56
73
|
if (!exportName.trim()) {
|
|
57
|
-
alert(
|
|
74
|
+
alert(t.saveNameRequired);
|
|
58
75
|
return;
|
|
59
76
|
}
|
|
60
77
|
|
|
@@ -64,46 +81,37 @@
|
|
|
64
81
|
author: exportAuthor.trim() || undefined,
|
|
65
82
|
tags: exportTags
|
|
66
83
|
.split(',')
|
|
67
|
-
.map((
|
|
68
|
-
.filter((
|
|
84
|
+
.map((tag) => tag.trim())
|
|
85
|
+
.filter((tag) => tag.length > 0)
|
|
69
86
|
});
|
|
70
87
|
|
|
71
|
-
|
|
88
|
+
if (onSaveState) await onSaveState(state);
|
|
89
|
+
else exportStateAsJson(state);
|
|
72
90
|
showExportDialog = false;
|
|
73
91
|
}
|
|
74
92
|
|
|
93
|
+
// Validate a preset, then either load it directly (no issues) or open the
|
|
94
|
+
// validation dialog (any errors or warnings). Shared by every load path.
|
|
95
|
+
function tryLoad(preset: ParameterPreset) {
|
|
96
|
+
const validation = validateSavedState(preset, schema);
|
|
97
|
+
if (validation.isValid) {
|
|
98
|
+
onLoadValues(extractLoadableValues(preset, schema, validation));
|
|
99
|
+
} else {
|
|
100
|
+
importedState = preset;
|
|
101
|
+
validationResult = validation;
|
|
102
|
+
showValidationDialog = true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
75
106
|
async function handleImport(event: Event) {
|
|
76
107
|
const input = event.target as HTMLInputElement;
|
|
77
108
|
if (!input.files || input.files.length === 0) return;
|
|
78
109
|
|
|
79
110
|
try {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
// Validate before loading
|
|
84
|
-
const validation = validateSavedState(imported, schema);
|
|
85
|
-
|
|
86
|
-
if (!validation.canLoad) {
|
|
87
|
-
// Show validation errors
|
|
88
|
-
importedState = imported;
|
|
89
|
-
validationResult = validation;
|
|
90
|
-
showValidationDialog = true;
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Warnings only - show validation but allow load
|
|
95
|
-
if (!validation.isValid) {
|
|
96
|
-
importedState = imported;
|
|
97
|
-
validationResult = validation;
|
|
98
|
-
showValidationDialog = true;
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// No issues - load immediately
|
|
103
|
-
const values = extractLoadableValues(imported, schema, validation);
|
|
104
|
-
onLoadValues(values);
|
|
111
|
+
const imported = await importStateFromJson(input.files[0]);
|
|
112
|
+
tryLoad(imported);
|
|
105
113
|
} catch (error) {
|
|
106
|
-
alert(
|
|
114
|
+
alert(t.loadImportError + (error as Error).message);
|
|
107
115
|
}
|
|
108
116
|
|
|
109
117
|
// Reset input
|
|
@@ -128,27 +136,44 @@
|
|
|
128
136
|
validationResult = null;
|
|
129
137
|
}
|
|
130
138
|
|
|
131
|
-
function openLoadDialog() {
|
|
139
|
+
async function openLoadDialog() {
|
|
132
140
|
showLoadDialog = true;
|
|
141
|
+
if (!onListStates) return;
|
|
142
|
+
|
|
143
|
+
isLoadingList = true;
|
|
144
|
+
listError = '';
|
|
145
|
+
listedStates = [];
|
|
146
|
+
try {
|
|
147
|
+
listedStates = await onListStates();
|
|
148
|
+
} catch (error) {
|
|
149
|
+
listError = (error as Error).message;
|
|
150
|
+
} finally {
|
|
151
|
+
isLoadingList = false;
|
|
152
|
+
}
|
|
133
153
|
}
|
|
134
154
|
|
|
135
155
|
function handleLoadClick() {
|
|
136
156
|
fileInputRef?.click();
|
|
137
157
|
showLoadDialog = false;
|
|
138
158
|
}
|
|
159
|
+
|
|
160
|
+
function selectListedState(preset: ParameterPreset) {
|
|
161
|
+
tryLoad(preset);
|
|
162
|
+
showLoadDialog = false;
|
|
163
|
+
}
|
|
139
164
|
</script>
|
|
140
165
|
|
|
141
166
|
{#if showSaveButton}
|
|
142
167
|
<Button variant="default" size="sm" onclick={openExportDialog}>
|
|
143
168
|
<Download class="mr-2 h-4 w-4" />
|
|
144
|
-
|
|
169
|
+
{t.saveButton}
|
|
145
170
|
</Button>
|
|
146
171
|
{/if}
|
|
147
172
|
|
|
148
173
|
{#if showLoadButton}
|
|
149
174
|
<Button variant="outline" size="sm" onclick={openLoadDialog}>
|
|
150
175
|
<Upload class="mr-2 h-4 w-4" />
|
|
151
|
-
|
|
176
|
+
{t.loadButton}
|
|
152
177
|
</Button>
|
|
153
178
|
{/if}
|
|
154
179
|
|
|
@@ -168,44 +193,40 @@
|
|
|
168
193
|
<Dialog.Root bind:open={showExportDialog}>
|
|
169
194
|
<Dialog.Content>
|
|
170
195
|
<Dialog.Header>
|
|
171
|
-
<Dialog.Title>
|
|
172
|
-
<Dialog.Description>
|
|
196
|
+
<Dialog.Title>{t.saveDialogTitle}</Dialog.Title>
|
|
197
|
+
<Dialog.Description>{t.saveDialogDescription}</Dialog.Description>
|
|
173
198
|
</Dialog.Header>
|
|
174
199
|
|
|
175
200
|
<div class="gap-4 py-4 grid">
|
|
176
201
|
<div class="gap-2 grid">
|
|
177
|
-
<Label for="export-name">
|
|
178
|
-
<Input id="export-name" bind:value={exportName} placeholder=
|
|
202
|
+
<Label for="export-name">{t.saveNameLabel}</Label>
|
|
203
|
+
<Input id="export-name" bind:value={exportName} placeholder={t.saveNamePlaceholder} />
|
|
179
204
|
</div>
|
|
180
205
|
|
|
181
206
|
<div class="gap-2 grid">
|
|
182
|
-
<Label for="export-description">
|
|
207
|
+
<Label for="export-description">{t.saveDescriptionLabel}</Label>
|
|
183
208
|
<Textarea
|
|
184
209
|
id="export-description"
|
|
185
210
|
bind:value={exportDescription}
|
|
186
|
-
placeholder=
|
|
211
|
+
placeholder={t.saveDescriptionPlaceholder}
|
|
187
212
|
rows={3}
|
|
188
213
|
/>
|
|
189
214
|
</div>
|
|
190
215
|
|
|
191
216
|
<div class="gap-2 grid">
|
|
192
|
-
<Label for="export-author">
|
|
193
|
-
<Input id="export-author" bind:value={exportAuthor} placeholder=
|
|
217
|
+
<Label for="export-author">{t.saveAuthorLabel}</Label>
|
|
218
|
+
<Input id="export-author" bind:value={exportAuthor} placeholder={t.saveAuthorPlaceholder} />
|
|
194
219
|
</div>
|
|
195
220
|
|
|
196
221
|
<div class="gap-2 grid">
|
|
197
|
-
<Label for="export-tags">
|
|
198
|
-
<Input
|
|
199
|
-
id="export-tags"
|
|
200
|
-
bind:value={exportTags}
|
|
201
|
-
placeholder="facade, option-a, client-approved (comma-separated)"
|
|
202
|
-
/>
|
|
222
|
+
<Label for="export-tags">{t.saveTagsLabel}</Label>
|
|
223
|
+
<Input id="export-tags" bind:value={exportTags} placeholder={t.saveTagsPlaceholder} />
|
|
203
224
|
</div>
|
|
204
225
|
</div>
|
|
205
226
|
|
|
206
227
|
<Dialog.Footer>
|
|
207
|
-
<Button variant="outline" onclick={() => (showExportDialog = false)}>
|
|
208
|
-
<Button onclick={handleExport}>
|
|
228
|
+
<Button variant="outline" onclick={() => (showExportDialog = false)}>{t.cancelButton}</Button>
|
|
229
|
+
<Button onclick={handleExport}>{t.saveButton}</Button>
|
|
209
230
|
</Dialog.Footer>
|
|
210
231
|
</Dialog.Content>
|
|
211
232
|
</Dialog.Root>
|
|
@@ -214,19 +235,49 @@
|
|
|
214
235
|
<Dialog.Root bind:open={showLoadDialog}>
|
|
215
236
|
<Dialog.Content>
|
|
216
237
|
<Dialog.Header>
|
|
217
|
-
<Dialog.Title>
|
|
218
|
-
<Dialog.Description>
|
|
238
|
+
<Dialog.Title>{t.loadDialogTitle}</Dialog.Title>
|
|
239
|
+
<Dialog.Description>{t.loadDialogDescription}</Dialog.Description>
|
|
219
240
|
</Dialog.Header>
|
|
220
241
|
|
|
221
|
-
|
|
222
|
-
<
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
242
|
+
{#if onListStates}
|
|
243
|
+
<div class="py-4 max-h-[60vh] overflow-y-auto">
|
|
244
|
+
{#if isLoadingList}
|
|
245
|
+
<p class="text-sm py-8 text-center text-muted-foreground">…</p>
|
|
246
|
+
{:else if listError}
|
|
247
|
+
<p class="text-sm py-8 text-center text-destructive">{listError}</p>
|
|
248
|
+
{:else if listedStates.length === 0}
|
|
249
|
+
<p class="text-sm py-8 text-center text-muted-foreground">{t.loadEmptyList}</p>
|
|
250
|
+
{:else}
|
|
251
|
+
<div class="space-y-2">
|
|
252
|
+
{#each listedStates as preset (preset.id)}
|
|
253
|
+
<button
|
|
254
|
+
type="button"
|
|
255
|
+
onclick={() => selectListedState(preset)}
|
|
256
|
+
class="p-3 w-full rounded-lg border text-left transition-colors hover:bg-muted"
|
|
257
|
+
>
|
|
258
|
+
<div class="text-sm font-medium">{preset.name}</div>
|
|
259
|
+
{#if preset.description}
|
|
260
|
+
<div class="text-xs mt-0.5 text-muted-foreground">{preset.description}</div>
|
|
261
|
+
{/if}
|
|
262
|
+
<div class="text-xs mt-1 text-muted-foreground">
|
|
263
|
+
{new Date(preset.timestamp).toLocaleString()}
|
|
264
|
+
</div>
|
|
265
|
+
</button>
|
|
266
|
+
{/each}
|
|
267
|
+
</div>
|
|
268
|
+
{/if}
|
|
269
|
+
</div>
|
|
270
|
+
{:else}
|
|
271
|
+
<div class="py-8">
|
|
272
|
+
<Button onclick={handleLoadClick} class="w-full" size="lg">
|
|
273
|
+
<Upload class="mr-2 h-4 w-4" />
|
|
274
|
+
{t.loadFromFileButton}
|
|
275
|
+
</Button>
|
|
276
|
+
</div>
|
|
277
|
+
{/if}
|
|
227
278
|
|
|
228
279
|
<Dialog.Footer>
|
|
229
|
-
<Button variant="outline" onclick={() => (showLoadDialog = false)}>
|
|
280
|
+
<Button variant="outline" onclick={() => (showLoadDialog = false)}>{t.cancelButton}</Button>
|
|
230
281
|
</Dialog.Footer>
|
|
231
282
|
</Dialog.Content>
|
|
232
283
|
</Dialog.Root>
|
|
@@ -235,10 +286,10 @@
|
|
|
235
286
|
<Dialog.Root bind:open={showValidationDialog}>
|
|
236
287
|
<Dialog.Content class="max-w-2xl max-h-[80vh] overflow-y-auto">
|
|
237
288
|
<Dialog.Header>
|
|
238
|
-
<Dialog.Title>
|
|
289
|
+
<Dialog.Title>{t.validationTitle}</Dialog.Title>
|
|
239
290
|
<Dialog.Description>
|
|
240
291
|
{#if importedState}
|
|
241
|
-
|
|
292
|
+
{t.validationValidatingPrefix}{importedState.name}
|
|
242
293
|
{/if}
|
|
243
294
|
</Dialog.Description>
|
|
244
295
|
</Dialog.Header>
|
|
@@ -251,9 +302,11 @@
|
|
|
251
302
|
<div class="gap-3 flex items-start">
|
|
252
303
|
<CheckCircle class="h-5 w-5 text-success" />
|
|
253
304
|
<div>
|
|
254
|
-
<h4 class="text-sm font-semibold text-success-foreground">
|
|
305
|
+
<h4 class="text-sm font-semibold text-success-foreground">
|
|
306
|
+
{t.validationNoIssuesTitle}
|
|
307
|
+
</h4>
|
|
255
308
|
<p class="text-sm mt-1 text-success-foreground/80">
|
|
256
|
-
|
|
309
|
+
{t.validationNoIssuesBody}
|
|
257
310
|
</p>
|
|
258
311
|
</div>
|
|
259
312
|
</div>
|
|
@@ -263,9 +316,14 @@
|
|
|
263
316
|
<div class="gap-3 flex items-start">
|
|
264
317
|
<AlertTriangle class="h-5 w-5 text-warning" />
|
|
265
318
|
<div>
|
|
266
|
-
<h4 class="text-sm font-semibold text-warning-foreground">
|
|
319
|
+
<h4 class="text-sm font-semibold text-warning-foreground">
|
|
320
|
+
{t.validationWarningsTitle}
|
|
321
|
+
</h4>
|
|
267
322
|
<p class="text-sm mt-1 text-warning-foreground/80">
|
|
268
|
-
{
|
|
323
|
+
{t.validationWarningsBody.replace(
|
|
324
|
+
'{count}',
|
|
325
|
+
String(validationResult.issues.length)
|
|
326
|
+
)}
|
|
269
327
|
</p>
|
|
270
328
|
</div>
|
|
271
329
|
</div>
|
|
@@ -275,9 +333,9 @@
|
|
|
275
333
|
<div class="gap-3 flex items-start">
|
|
276
334
|
<AlertTriangle class="h-5 w-5 text-destructive" />
|
|
277
335
|
<div>
|
|
278
|
-
<h4 class="text-sm font-semibold text-destructive">
|
|
336
|
+
<h4 class="text-sm font-semibold text-destructive">{t.validationErrorsTitle}</h4>
|
|
279
337
|
<p class="text-sm mt-1 text-destructive/80">
|
|
280
|
-
|
|
338
|
+
{t.validationErrorsBody}
|
|
281
339
|
</p>
|
|
282
340
|
</div>
|
|
283
341
|
</div>
|
|
@@ -287,7 +345,7 @@
|
|
|
287
345
|
<!-- Issues List -->
|
|
288
346
|
{#if !validationResult.isValid}
|
|
289
347
|
<div class="space-y-2">
|
|
290
|
-
<h4 class="text-sm font-medium">
|
|
348
|
+
<h4 class="text-sm font-medium">{t.validationIssuesHeading}</h4>
|
|
291
349
|
{#each validationResult.issues as issue (issue.message)}
|
|
292
350
|
<div
|
|
293
351
|
class="p-3 rounded-lg border {issue.severity === 'error'
|
|
@@ -304,7 +362,9 @@
|
|
|
304
362
|
<p class="text-sm font-medium">{issue.message}</p>
|
|
305
363
|
{#if issue.details}
|
|
306
364
|
<p class="text-xs mt-1 text-muted-foreground">
|
|
307
|
-
|
|
365
|
+
{t.validationExpected}
|
|
366
|
+
{issue.details.expected} → {t.validationActual}
|
|
367
|
+
{issue.details.actual}
|
|
308
368
|
</p>
|
|
309
369
|
{/if}
|
|
310
370
|
</div>
|
|
@@ -317,9 +377,9 @@
|
|
|
317
377
|
{/if}
|
|
318
378
|
|
|
319
379
|
<Dialog.Footer>
|
|
320
|
-
<Button variant="outline" onclick={cancelImport}>
|
|
380
|
+
<Button variant="outline" onclick={cancelImport}>{t.cancelButton}</Button>
|
|
321
381
|
{#if validationResult?.canLoad}
|
|
322
|
-
<Button onclick={confirmLoad}>
|
|
382
|
+
<Button onclick={confirmLoad}>{t.loadAnywayButton}</Button>
|
|
323
383
|
{/if}
|
|
324
384
|
</Dialog.Footer>
|
|
325
385
|
</Dialog.Content>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { UISchema } from '@selvajs/schemas';
|
|
1
|
+
import type { UISchema, ParameterPreset } from '@selvajs/schemas';
|
|
2
2
|
import type { ActionButton } from '../../types/actionButton';
|
|
3
|
+
import { type PresetLabels } from '../../types/presetLabels';
|
|
3
4
|
interface Props {
|
|
4
5
|
schema: UISchema;
|
|
5
6
|
currentValues: Record<string, unknown>;
|
|
@@ -7,6 +8,12 @@ interface Props {
|
|
|
7
8
|
showSaveButton?: boolean;
|
|
8
9
|
showLoadButton?: boolean;
|
|
9
10
|
actions?: ActionButton[];
|
|
11
|
+
/** When set, persist saved states via this callback instead of downloading a .sps file. */
|
|
12
|
+
onSaveState?: (state: ParameterPreset) => void | Promise<void>;
|
|
13
|
+
/** When set, the Load dialog lists these states instead of showing a file input. */
|
|
14
|
+
onListStates?: () => ParameterPreset[] | Promise<ParameterPreset[]>;
|
|
15
|
+
/** Partial overrides for UI strings (e.g. for localization). */
|
|
16
|
+
labels?: Partial<PresetLabels>;
|
|
10
17
|
}
|
|
11
18
|
declare const ParameterPresetManager: import("svelte").Component<Props, {}, "">;
|
|
12
19
|
type ParameterPresetManager = ReturnType<typeof ParameterPresetManager>;
|
|
@@ -16,6 +16,12 @@
|
|
|
16
16
|
rightContent?: Snippet;
|
|
17
17
|
subnav?: Snippet;
|
|
18
18
|
headerClass?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Bring-your-own header. When provided, this renders inside the sticky
|
|
21
|
+
* header bar instead of the built-in PageHeader. The bar keeps the
|
|
22
|
+
* standard `--header-h` height so the fixed-mode layout math is unaffected.
|
|
23
|
+
*/
|
|
24
|
+
header?: Snippet;
|
|
19
25
|
|
|
20
26
|
// Body layout
|
|
21
27
|
// 'fixed' — full viewport, header + body + footer; body owns its own scroll. For app-like pages (builder, preview, library/[guid]).
|
|
@@ -29,6 +35,8 @@
|
|
|
29
35
|
showFooter?: boolean;
|
|
30
36
|
errors?: string[];
|
|
31
37
|
warnings?: string[];
|
|
38
|
+
/** Fully overrides the footer copyright line. `{name}` and `{year}` are substituted. */
|
|
39
|
+
footerText?: string;
|
|
32
40
|
footerChildren?: Snippet;
|
|
33
41
|
|
|
34
42
|
// Body content
|
|
@@ -49,11 +57,13 @@
|
|
|
49
57
|
rightContent,
|
|
50
58
|
subnav,
|
|
51
59
|
headerClass = '',
|
|
60
|
+
header,
|
|
52
61
|
mode: _mode = 'scroll',
|
|
53
62
|
sidenav,
|
|
54
63
|
showFooter = false,
|
|
55
64
|
errors = [],
|
|
56
65
|
warnings = [],
|
|
66
|
+
footerText,
|
|
57
67
|
footerChildren,
|
|
58
68
|
class: className = '',
|
|
59
69
|
bodyClass = '',
|
|
@@ -83,17 +93,27 @@
|
|
|
83
93
|
|
|
84
94
|
<div class={rootClass}>
|
|
85
95
|
{#if showHeader}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
{#if header}
|
|
97
|
+
<header
|
|
98
|
+
class={`top-0 backdrop-blur-sm sticky z-40 border-b border-border bg-background/90 ${headerClass}`}
|
|
99
|
+
>
|
|
100
|
+
<div class="flex h-(--header-h) items-center px-(--page-px)">
|
|
101
|
+
{@render header()}
|
|
102
|
+
</div>
|
|
103
|
+
</header>
|
|
104
|
+
{:else}
|
|
105
|
+
<PageHeader
|
|
106
|
+
{homeUrl}
|
|
107
|
+
{title}
|
|
108
|
+
{logo}
|
|
109
|
+
{brandName}
|
|
110
|
+
{showModeToggle}
|
|
111
|
+
{navItems}
|
|
112
|
+
{rightContent}
|
|
113
|
+
{subnav}
|
|
114
|
+
class={headerClass}
|
|
115
|
+
/>
|
|
116
|
+
{/if}
|
|
97
117
|
{/if}
|
|
98
118
|
|
|
99
119
|
<div class={bodyShellClass} style={bodyShellStyle}>
|
|
@@ -109,7 +129,7 @@
|
|
|
109
129
|
|
|
110
130
|
{#if showFooter}
|
|
111
131
|
<div class="bottom-0 sticky z-10 shrink-0">
|
|
112
|
-
<PageFooter {errors} {warnings} copyrightName={_copyright}>
|
|
132
|
+
<PageFooter {errors} {warnings} copyrightName={_copyright} {footerText}>
|
|
113
133
|
{#if footerChildren}
|
|
114
134
|
{@render footerChildren()}
|
|
115
135
|
{/if}
|
|
@@ -11,11 +11,19 @@ interface AppShellProps {
|
|
|
11
11
|
rightContent?: Snippet;
|
|
12
12
|
subnav?: Snippet;
|
|
13
13
|
headerClass?: string;
|
|
14
|
+
/**
|
|
15
|
+
* Bring-your-own header. When provided, this renders inside the sticky
|
|
16
|
+
* header bar instead of the built-in PageHeader. The bar keeps the
|
|
17
|
+
* standard `--header-h` height so the fixed-mode layout math is unaffected.
|
|
18
|
+
*/
|
|
19
|
+
header?: Snippet;
|
|
14
20
|
mode?: 'fixed' | 'scroll';
|
|
15
21
|
sidenav?: Snippet;
|
|
16
22
|
showFooter?: boolean;
|
|
17
23
|
errors?: string[];
|
|
18
24
|
warnings?: string[];
|
|
25
|
+
/** Fully overrides the footer copyright line. `{name}` and `{year}` are substituted. */
|
|
26
|
+
footerText?: string;
|
|
19
27
|
footerChildren?: Snippet;
|
|
20
28
|
class?: string;
|
|
21
29
|
bodyClass?: string;
|