@selvajs/ui 3.0.0 → 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.
@@ -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('Please enter a name for this state');
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((t) => t.trim())
68
- .filter((t) => t.length > 0)
84
+ .map((tag) => tag.trim())
85
+ .filter((tag) => tag.length > 0)
69
86
  });
70
87
 
71
- exportStateAsJson(state);
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 file = input.files[0];
81
- const imported = await importStateFromJson(file);
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('Failed to import state: ' + (error as Error).message);
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
- Save State
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
- Load State
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>Save Parameter State</Dialog.Title>
172
- <Dialog.Description>Save the current parameter values as a .sps file</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">State Name *</Label>
178
- <Input id="export-name" bind:value={exportName} placeholder="e.g., Design Option A" />
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">Description</Label>
207
+ <Label for="export-description">{t.saveDescriptionLabel}</Label>
183
208
  <Textarea
184
209
  id="export-description"
185
210
  bind:value={exportDescription}
186
- placeholder="Optional description of this state"
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">Author</Label>
193
- <Input id="export-author" bind:value={exportAuthor} placeholder="Your name or email" />
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">Tags</Label>
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)}>Cancel</Button>
208
- <Button onclick={handleExport}>Save State</Button>
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>Load Parameter State</Dialog.Title>
218
- <Dialog.Description>Select a .sps state file from your drive to load</Dialog.Description>
238
+ <Dialog.Title>{t.loadDialogTitle}</Dialog.Title>
239
+ <Dialog.Description>{t.loadDialogDescription}</Dialog.Description>
219
240
  </Dialog.Header>
220
241
 
221
- <div class="py-8">
222
- <Button onclick={handleLoadClick} class="w-full" size="lg">
223
- <Upload class="mr-2 h-4 w-4" />
224
- Select .sps File
225
- </Button>
226
- </div>
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)}>Cancel</Button>
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>State Validation Report</Dialog.Title>
289
+ <Dialog.Title>{t.validationTitle}</Dialog.Title>
239
290
  <Dialog.Description>
240
291
  {#if importedState}
241
- Validating state: {importedState.name}
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">No Issues Found</h4>
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
- This state can be loaded safely.
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">Warnings Detected</h4>
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
- {validationResult.issues.length} warning(s) found, but state can still be loaded.
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">Critical Errors</h4>
336
+ <h4 class="text-sm font-semibold text-destructive">{t.validationErrorsTitle}</h4>
279
337
  <p class="text-sm mt-1 text-destructive/80">
280
- Cannot load this state due to critical incompatibilities.
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">Issues:</h4>
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
- Expected: {issue.details.expected} → Actual: {issue.details.actual}
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}>Cancel</Button>
380
+ <Button variant="outline" onclick={cancelImport}>{t.cancelButton}</Button>
321
381
  {#if validationResult?.canLoad}
322
- <Button onclick={confirmLoad}>Load Anyway</Button>
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
- <PageHeader
87
- {homeUrl}
88
- {title}
89
- {logo}
90
- {brandName}
91
- {showModeToggle}
92
- {navItems}
93
- {rightContent}
94
- {subnav}
95
- class={headerClass}
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;