@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.
@@ -11,10 +11,18 @@
11
11
  errors?: string[];
12
12
  warnings?: string[];
13
13
  copyrightName?: string;
14
+ /** Fully overrides the copyright line. `{name}` and `{year}` are substituted. */
15
+ footerText?: string;
14
16
  children?: Snippet;
15
17
  }
16
18
 
17
- let { errors = [], warnings = [], copyrightName = 'Selva', children }: Props = $props();
19
+ let {
20
+ errors = [],
21
+ warnings = [],
22
+ copyrightName = 'Selva',
23
+ footerText,
24
+ children
25
+ }: Props = $props();
18
26
 
19
27
  let footerStore = (() => {
20
28
  try {
@@ -64,6 +72,12 @@
64
72
 
65
73
  const groupedErrors = $derived(groupMessages(errors));
66
74
  const groupedWarnings = $derived(groupMessages(warnings));
75
+
76
+ const copyrightLine = $derived(
77
+ footerText
78
+ ? footerText.replace('{name}', copyrightName).replace('{year}', String(_currentYear))
79
+ : `by ${copyrightName} © ${_currentYear}`
80
+ );
67
81
  </script>
68
82
 
69
83
  <footer
@@ -218,6 +232,6 @@
218
232
  <FooterItemRenderer {item} />
219
233
  {/each}
220
234
 
221
- <p>by {copyrightName} &copy; {_currentYear}</p>
235
+ <p>{copyrightLine}</p>
222
236
  </div>
223
237
  </footer>
@@ -3,6 +3,8 @@ interface Props {
3
3
  errors?: string[];
4
4
  warnings?: string[];
5
5
  copyrightName?: string;
6
+ /** Fully overrides the copyright line. `{name}` and `{year}` are substituted. */
7
+ footerText?: string;
6
8
  children?: Snippet;
7
9
  }
8
10
  declare const PageFooter: import("svelte").Component<Props, {}, "">;
package/dist/index.d.ts CHANGED
@@ -14,3 +14,4 @@ export * from './utils';
14
14
  export { randomId } from './utils/randomId';
15
15
  export type { ActionButton } from './types/actionButton';
16
16
  export type { SolveFn } from './types/solveFn';
17
+ export { DEFAULT_PRESET_LABELS, type PresetLabels } from './types/presetLabels';
package/dist/index.js CHANGED
@@ -21,3 +21,4 @@ export * from './composables/useFooterItem.svelte';
21
21
  // Utils (cn function)
22
22
  export * from './utils';
23
23
  export { randomId } from './utils/randomId';
24
+ export { DEFAULT_PRESET_LABELS } from './types/presetLabels';
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Overridable UI strings for the parameter-preset manager (Save/Load flow) and
3
+ * footer copyright text. All optional — unset keys fall back to the English
4
+ * defaults in `DEFAULT_PRESET_LABELS`. Pass a partial object to localize.
5
+ */
6
+ export interface PresetLabels {
7
+ saveButton: string;
8
+ loadButton: string;
9
+ saveDialogTitle: string;
10
+ saveDialogDescription: string;
11
+ saveNameLabel: string;
12
+ saveNamePlaceholder: string;
13
+ saveDescriptionLabel: string;
14
+ saveDescriptionPlaceholder: string;
15
+ saveAuthorLabel: string;
16
+ saveAuthorPlaceholder: string;
17
+ saveTagsLabel: string;
18
+ saveTagsPlaceholder: string;
19
+ saveNameRequired: string;
20
+ loadDialogTitle: string;
21
+ loadDialogDescription: string;
22
+ loadFromFileButton: string;
23
+ loadEmptyList: string;
24
+ loadImportError: string;
25
+ validationTitle: string;
26
+ validationValidatingPrefix: string;
27
+ validationNoIssuesTitle: string;
28
+ validationNoIssuesBody: string;
29
+ validationWarningsTitle: string;
30
+ /** `{count}` is replaced with the number of warnings. */
31
+ validationWarningsBody: string;
32
+ validationErrorsTitle: string;
33
+ validationErrorsBody: string;
34
+ validationIssuesHeading: string;
35
+ validationExpected: string;
36
+ validationActual: string;
37
+ cancelButton: string;
38
+ loadAnywayButton: string;
39
+ }
40
+ export declare const DEFAULT_PRESET_LABELS: PresetLabels;
@@ -0,0 +1,33 @@
1
+ export const DEFAULT_PRESET_LABELS = {
2
+ saveButton: 'Save State',
3
+ loadButton: 'Load State',
4
+ saveDialogTitle: 'Save Parameter State',
5
+ saveDialogDescription: 'Save the current parameter values as a .sps file',
6
+ saveNameLabel: 'State Name *',
7
+ saveNamePlaceholder: 'e.g., Design Option A',
8
+ saveDescriptionLabel: 'Description',
9
+ saveDescriptionPlaceholder: 'Optional description of this state',
10
+ saveAuthorLabel: 'Author',
11
+ saveAuthorPlaceholder: 'Your name or email',
12
+ saveTagsLabel: 'Tags',
13
+ saveTagsPlaceholder: 'facade, option-a, client-approved (comma-separated)',
14
+ saveNameRequired: 'Please enter a name for this state',
15
+ loadDialogTitle: 'Load Parameter State',
16
+ loadDialogDescription: 'Select a .sps state file from your drive to load',
17
+ loadFromFileButton: 'Select .sps File',
18
+ loadEmptyList: 'No saved states found',
19
+ loadImportError: 'Failed to import state: ',
20
+ validationTitle: 'State Validation Report',
21
+ validationValidatingPrefix: 'Validating state: ',
22
+ validationNoIssuesTitle: 'No Issues Found',
23
+ validationNoIssuesBody: 'This state can be loaded safely.',
24
+ validationWarningsTitle: 'Warnings Detected',
25
+ validationWarningsBody: '{count} warning(s) found, but state can still be loaded.',
26
+ validationErrorsTitle: 'Critical Errors',
27
+ validationErrorsBody: 'Cannot load this state due to critical incompatibilities.',
28
+ validationIssuesHeading: 'Issues:',
29
+ validationExpected: 'Expected:',
30
+ validationActual: 'Actual:',
31
+ cancelButton: 'Cancel',
32
+ loadAnywayButton: 'Load Anyway'
33
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@selvajs/ui",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "description": "Shared UI components and utilities for Selva applications",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -84,8 +84,8 @@
84
84
  "bits-ui": "^2.18.0",
85
85
  "svelte": "5.55.5",
86
86
  "tailwind-variants": "^3.2.2",
87
- "@selvajs/schemas": "3.0.0",
88
- "@selvajs/config": "0.0.0"
87
+ "@selvajs/config": "0.0.0",
88
+ "@selvajs/schemas": "3.0.0"
89
89
  },
90
90
  "scripts": {
91
91
  "dev": "vite dev",
@@ -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,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={() => {
@@ -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>