@selvajs/ui 6.6.0-beta.3 → 6.6.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/dist/components/ErrorScreen.svelte +6 -2
  2. package/dist/components/compute/AppLayout.svelte +157 -71
  3. package/dist/components/compute/AppLayout.svelte.d.ts +6 -0
  4. package/dist/components/compute/ComputeApp.svelte +39 -20
  5. package/dist/components/compute/ComputeApp.svelte.d.ts +19 -2
  6. package/dist/components/compute/ComputeMessagesDialog.svelte +30 -40
  7. package/dist/components/compute/ParameterPresetManager.svelte +7 -44
  8. package/dist/components/compute/ParameterPresetManager.svelte.d.ts +4 -5
  9. package/dist/components/compute/SolveMessages.svelte +30 -37
  10. package/dist/components/compute/SolveMessages.svelte.d.ts +0 -12
  11. package/dist/components/layout/PageFooter.svelte +21 -11
  12. package/dist/components/preview/ChartOutput.svelte +12 -7
  13. package/dist/components/preview/Group.svelte +33 -18
  14. package/dist/components/preview/HelpButton.svelte +4 -2
  15. package/dist/components/preview/ImageOutput.svelte +427 -68
  16. package/dist/components/preview/InputControl.svelte +11 -2
  17. package/dist/components/preview/OutputDisplay.svelte +17 -12
  18. package/dist/components/preview/TabContent.svelte +3 -0
  19. package/dist/components/preview/inputs/ColorInput.svelte +3 -1
  20. package/dist/components/preview/inputs/ColorInput.svelte.d.ts +1 -0
  21. package/dist/components/preview/inputs/DropdownInput.svelte +4 -1
  22. package/dist/components/preview/inputs/FileInput.svelte +50 -34
  23. package/dist/components/preview/inputs/NumberInput.svelte +92 -10
  24. package/dist/components/preview/inputs/NumberInput.svelte.d.ts +2 -0
  25. package/dist/components/preview/inputs/TextInput.svelte +8 -3
  26. package/dist/components/primitives/CalculateButton.svelte +49 -23
  27. package/dist/components/primitives/CalculateButton.svelte.d.ts +3 -0
  28. package/dist/components/primitives/SolvingIndicator.svelte +21 -17
  29. package/dist/components/primitives/badge/badge.svelte +2 -2
  30. package/dist/components/primitives/badge/badge.svelte.d.ts +4 -4
  31. package/dist/components/primitives/button/button.svelte +2 -2
  32. package/dist/components/primitives/button/button.svelte.d.ts +4 -4
  33. package/dist/components/primitives/checkbox/checkbox.svelte +1 -1
  34. package/dist/components/primitives/dialog/dialog-content.svelte +6 -2
  35. package/dist/components/primitives/dropdown-menu/dropdown-menu-content.svelte +1 -1
  36. package/dist/components/primitives/dropdown-menu/dropdown-menu-sub-content.svelte +1 -1
  37. package/dist/components/primitives/dropdown-menu/dropdown-menu-sub-trigger.svelte +1 -1
  38. package/dist/components/primitives/input/input.svelte +2 -2
  39. package/dist/components/primitives/input-group/input-group.svelte +1 -1
  40. package/dist/components/primitives/mode-toggle/mode-toggle.svelte +4 -1
  41. package/dist/components/primitives/popover/popover-content.svelte +1 -1
  42. package/dist/components/primitives/scroll-area/scroll-area.svelte +1 -1
  43. package/dist/components/primitives/search/search.svelte +1 -1
  44. package/dist/components/primitives/select/select-trigger.svelte +1 -1
  45. package/dist/components/primitives/slider/slider.svelte +7 -2
  46. package/dist/components/primitives/slider/slider.svelte.d.ts +5 -1
  47. package/dist/components/primitives/switch/switch.svelte +1 -1
  48. package/dist/components/primitives/tabs/tabs-trigger.svelte +1 -1
  49. package/dist/components/primitives/tabs/tabs-trigger.svelte.d.ts +1 -1
  50. package/dist/components/primitives/textarea/textarea.svelte +1 -1
  51. package/dist/components/primitives/toggle/toggle.svelte +1 -1
  52. package/dist/components/primitives/toggle/toggle.svelte.d.ts +1 -1
  53. package/dist/components/primitives/toggle-group/toggle-group-item.svelte +1 -1
  54. package/dist/components/primitives/toggle-group/toggle-group.svelte +1 -1
  55. package/dist/components/viewer/Viewer.svelte +20 -17
  56. package/dist/compute/run-panel-action.d.ts +8 -0
  57. package/dist/compute/run-panel-action.js +12 -0
  58. package/dist/compute/useSolveSession.svelte.js +3 -0
  59. package/dist/i18n/localeContext.svelte.d.ts +10 -3
  60. package/dist/i18n/localeContext.svelte.js +27 -5
  61. package/dist/i18n/messages.d.ts +115 -2
  62. package/dist/i18n/messages.js +199 -11
  63. package/dist/index.d.ts +1 -1
  64. package/dist/public.d.ts +1 -1
  65. package/dist/schema/visibility-rules.js +21 -0
  66. package/dist/styles/base.css +9 -6
  67. package/dist/types/actionButton.d.ts +17 -3
  68. package/dist/types/presetLabels.d.ts +3 -2
  69. package/dist/types/presetLabels.js +9 -8
  70. package/package.json +8 -8
  71. package/src/lib/components/ErrorScreen.svelte +6 -2
  72. package/src/lib/components/compute/AppLayout.svelte +157 -71
  73. package/src/lib/components/compute/ComputeApp.svelte +39 -20
  74. package/src/lib/components/compute/ComputeMessagesDialog.svelte +30 -40
  75. package/src/lib/components/compute/ParameterPresetManager.svelte +7 -44
  76. package/src/lib/components/compute/SolveMessages.svelte +30 -37
  77. package/src/lib/components/layout/PageFooter.svelte +21 -11
  78. package/src/lib/components/preview/ChartOutput.svelte +12 -7
  79. package/src/lib/components/preview/Group.svelte +33 -18
  80. package/src/lib/components/preview/HelpButton.svelte +4 -2
  81. package/src/lib/components/preview/ImageOutput.svelte +427 -68
  82. package/src/lib/components/preview/InputControl.svelte +11 -2
  83. package/src/lib/components/preview/OutputDisplay.svelte +17 -12
  84. package/src/lib/components/preview/TabContent.svelte +3 -0
  85. package/src/lib/components/preview/inputs/ColorInput.svelte +3 -1
  86. package/src/lib/components/preview/inputs/DropdownInput.svelte +4 -1
  87. package/src/lib/components/preview/inputs/FileInput.svelte +50 -34
  88. package/src/lib/components/preview/inputs/NumberInput.svelte +92 -10
  89. package/src/lib/components/preview/inputs/TextInput.svelte +8 -3
  90. package/src/lib/components/primitives/CalculateButton.svelte +49 -23
  91. package/src/lib/components/primitives/SolvingIndicator.svelte +21 -17
  92. package/src/lib/components/primitives/badge/badge.svelte +2 -2
  93. package/src/lib/components/primitives/button/button.svelte +2 -2
  94. package/src/lib/components/primitives/checkbox/checkbox.svelte +1 -1
  95. package/src/lib/components/primitives/dialog/dialog-content.svelte +6 -2
  96. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-content.svelte +1 -1
  97. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-sub-content.svelte +1 -1
  98. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-sub-trigger.svelte +1 -1
  99. package/src/lib/components/primitives/input/input.svelte +2 -2
  100. package/src/lib/components/primitives/input-group/input-group.svelte +1 -1
  101. package/src/lib/components/primitives/mode-toggle/mode-toggle.svelte +4 -1
  102. package/src/lib/components/primitives/popover/popover-content.svelte +1 -1
  103. package/src/lib/components/primitives/scroll-area/scroll-area.svelte +1 -1
  104. package/src/lib/components/primitives/search/search.svelte +1 -1
  105. package/src/lib/components/primitives/select/select-trigger.svelte +1 -1
  106. package/src/lib/components/primitives/slider/slider.svelte +7 -2
  107. package/src/lib/components/primitives/switch/switch.svelte +1 -1
  108. package/src/lib/components/primitives/tabs/tabs-trigger.svelte +1 -1
  109. package/src/lib/components/primitives/textarea/textarea.svelte +1 -1
  110. package/src/lib/components/primitives/toggle/toggle.svelte +1 -1
  111. package/src/lib/components/primitives/toggle-group/toggle-group-item.svelte +1 -1
  112. package/src/lib/components/primitives/toggle-group/toggle-group.svelte +1 -1
  113. package/src/lib/components/viewer/Viewer.svelte +20 -17
  114. package/src/lib/compute/run-panel-action.ts +16 -0
  115. package/src/lib/compute/useSolveSession.svelte.ts +3 -0
  116. package/src/lib/i18n/localeContext.svelte.ts +40 -5
  117. package/src/lib/i18n/messages.ts +351 -13
  118. package/src/lib/index.ts +1 -0
  119. package/src/lib/public.ts +1 -0
  120. package/src/lib/schema/visibility-rules.ts +23 -0
  121. package/src/lib/styles/base.css +9 -6
  122. package/src/lib/types/actionButton.ts +16 -3
  123. package/src/lib/types/presetLabels.ts +13 -11
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { Download, Upload } from '@lucide/svelte';
2
+ import { Upload } from '@lucide/svelte';
3
3
  import type { UISchema, ParameterPreset } from '@selvajs/schemas';
4
4
  import {
5
5
  createSavedState,
@@ -9,17 +9,12 @@
9
9
  importStateFromJson
10
10
  } from '../../schema/param-exporter';
11
11
  import { Button, Input, Label, Textarea, Dialog, Callout, IssueList, toast } from '../primitives';
12
-
13
- import type { ActionButton } from '../../types/actionButton';
14
12
  import { DEFAULT_PRESET_LABELS, type PresetLabels } from '../../types/presetLabels';
15
13
 
16
14
  interface Props {
17
15
  schema: UISchema;
18
16
  currentValues: Record<string, unknown>;
19
17
  onLoadValues: (values: Record<string, unknown>) => void;
20
- showSaveButton?: boolean;
21
- showLoadButton?: boolean;
22
- actions?: ActionButton[];
23
18
  /** When set, persist saved states via this callback instead of downloading a .slvp file. */
24
19
  onSaveState?: (state: ParameterPreset) => void | Promise<void>;
25
20
  /** When set, the Load dialog lists these states instead of showing a file input. */
@@ -28,17 +23,7 @@
28
23
  labels?: Partial<PresetLabels>;
29
24
  }
30
25
 
31
- let {
32
- schema,
33
- currentValues,
34
- onLoadValues,
35
- showSaveButton = true,
36
- showLoadButton = true,
37
- actions = [],
38
- onSaveState,
39
- onListStates,
40
- labels
41
- }: Props = $props();
26
+ let { schema, currentValues, onLoadValues, onSaveState, onListStates, labels }: Props = $props();
42
27
 
43
28
  const t = $derived({ ...DEFAULT_PRESET_LABELS, ...labels });
44
29
 
@@ -58,7 +43,8 @@
58
43
  let isLoadingList = $state(false);
59
44
  let listError = $state('');
60
45
 
61
- function openExportDialog() {
46
+ /** Opens the save dialog. The panel footer's menu calls it. */
47
+ export function openSave() {
62
48
  exportName = `State ${new Date().toLocaleDateString()}`;
63
49
  exportDescription = '';
64
50
  exportAuthor = schema.author || '';
@@ -128,7 +114,8 @@
128
114
  loadResult = null;
129
115
  }
130
116
 
131
- async function openLoadDialog() {
117
+ /** Opens the load dialog. The panel footer's menu calls it. */
118
+ export async function openLoad() {
132
119
  showLoadDialog = true;
133
120
  if (!onListStates) return;
134
121
 
@@ -155,30 +142,6 @@
155
142
  }
156
143
  </script>
157
144
 
158
- {#if showSaveButton}
159
- <Button variant="outline" size="sm" onclick={openExportDialog}>
160
- <Download class="mr-2 h-4 w-4" />
161
- {t.saveButton}
162
- </Button>
163
- {/if}
164
-
165
- {#if showLoadButton}
166
- <Button variant="outline" size="sm" onclick={openLoadDialog}>
167
- <Upload class="mr-2 h-4 w-4" />
168
- {t.loadButton}
169
- </Button>
170
- {/if}
171
-
172
- {#each actions as action (action.id)}
173
- <Button variant={action.variant ?? 'outline'} size={action.size ?? 'sm'} onclick={action.onclick}>
174
- {#if action.icon}
175
- {@const IconComponent = action.icon}
176
- <IconComponent class="mr-2 h-4 w-4" />
177
- {/if}
178
- {action.label}
179
- </Button>
180
- {/each}
181
-
182
145
  <!-- .sps is the pre-rename extension; files with it exist on users' drives, so keep accepting it. -->
183
146
  <input
184
147
  bind:this={fileInputRef}
@@ -225,7 +188,7 @@
225
188
 
226
189
  <Dialog.Footer>
227
190
  <Button variant="outline" onclick={() => (showExportDialog = false)}>{t.cancelButton}</Button>
228
- <Button onclick={handleExport}>{t.saveButton}</Button>
191
+ <Button onclick={handleExport}>{t.saveConfirmButton}</Button>
229
192
  </Dialog.Footer>
230
193
  </Dialog.Content>
231
194
  </Dialog.Root>
@@ -19,6 +19,7 @@
19
19
  import { Button } from '$lib/components/primitives/button';
20
20
  import IssueList, { type Issue } from '$lib/components/primitives/IssueList.svelte';
21
21
  import SeverityIcon from '$lib/components/primitives/SeverityIcon.svelte';
22
+ import { getLocaleContext } from '$lib/i18n/localeContext.svelte';
22
23
 
23
24
  interface Props {
24
25
  /** What the running (or most recent) solve has said over the live channel. */
@@ -30,43 +31,20 @@
30
31
  onabort: () => void;
31
32
  onconfirm: () => void;
32
33
  ondiscard: () => void;
33
- labels?: Labels;
34
34
  }
35
35
 
36
- interface Labels {
37
- solving: string;
38
- blockedTitle: string;
39
- messagesTitle: string;
40
- blockedDescription: string;
41
- messagesDescription: string;
42
- confirm: string;
43
- discard: string;
44
- abort: string;
45
- dismiss: string;
46
- }
47
-
48
- const DEFAULT_LABELS: Labels = {
49
- solving: 'Solving…',
50
- blockedTitle: 'Solve stopped',
51
- messagesTitle: 'Solve messages',
52
- blockedDescription: 'The definition stopped this solve. No results were produced.',
53
- messagesDescription: 'This solve reported the following. Continue to see the result.',
54
- confirm: 'Continue',
55
- discard: 'Discard',
56
- abort: 'Abort',
57
- dismiss: 'Dismiss'
58
- };
59
-
60
36
  let {
61
37
  live,
62
38
  phase: sessionPhase,
63
39
  diagnostics = [],
64
40
  onabort,
65
41
  onconfirm,
66
- ondiscard,
67
- labels = DEFAULT_LABELS
42
+ ondiscard
68
43
  }: Props = $props();
69
44
 
45
+ const locale = getLocaleContext();
46
+ const t = $derived(locale.messages);
47
+
70
48
  const REMARK_LINGER_MS = 6000;
71
49
 
72
50
  const progress = $derived([...live.progress.values()]);
@@ -239,6 +217,19 @@
239
217
  : rows.length > 0 && lingering
240
218
  );
241
219
 
220
+ // What a screen reader hears of the passive views, through a region that outlives the panel: one
221
+ // created together with its text is not announced. The step name, not its counter, so a long
222
+ // solve announces each step rather than every tick. Review and blocked are alerts of their own.
223
+ const announcement = $derived(
224
+ !visible
225
+ ? ''
226
+ : view === 'live'
227
+ ? ((headline ?? progress[0])?.label ?? t.solveSolving)
228
+ : view === 'idle'
229
+ ? rows.map((d) => d.message).join(' ')
230
+ : ''
231
+ );
232
+
242
233
  function dismiss() {
243
234
  dismissedFor = diagnostics;
244
235
  }
@@ -254,13 +245,15 @@
254
245
 
255
246
  <svelte:window onkeydown={handleKeydown} />
256
247
 
248
+ <div class="sr-only" role="status" aria-live="polite">{announcement}</div>
249
+
257
250
  {#if visible}
258
251
  <div
259
252
  class="bottom-4 p-3 shadow-lg w-lg fixed left-1/2 z-50 max-w-screen-inset -translate-x-1/2 rounded-lg border bg-background {view ===
260
253
  'blocked'
261
254
  ? 'border-destructive/50'
262
255
  : ''}"
263
- role={view === 'live' || view === 'idle' ? 'status' : 'alert'}
256
+ role={view === 'review' || view === 'blocked' ? 'alert' : undefined}
264
257
  data-testid="solve-messages"
265
258
  data-view={view}
266
259
  >
@@ -270,7 +263,7 @@
270
263
  <div>
271
264
  <div class="gap-2 flex items-center">
272
265
  <LoaderCircle class="h-4 w-4 animate-spin shrink-0 text-muted-foreground" />
273
- <span class="min-w-0 text-sm flex-1 truncate">{head?.label ?? labels.solving}</span>
266
+ <span class="min-w-0 text-sm flex-1 truncate">{head?.label ?? t.solveSolving}</span>
274
267
  {#if head?.done !== undefined && head?.total !== undefined}
275
268
  <span class="text-xs shrink-0 text-muted-foreground tabular-nums">
276
269
  {head.done} / {head.total}
@@ -283,7 +276,7 @@
283
276
  disabled={finishing}
284
277
  onclick={onabort}
285
278
  >
286
- {labels.abort}
279
+ {t.solveMessagesAbort}
287
280
  </Button>
288
281
  </div>
289
282
  {#each progress as p, i (p.source ?? '')}
@@ -300,7 +293,7 @@
300
293
  <div
301
294
  class="h-1 overflow-hidden rounded-full bg-muted"
302
295
  role="progressbar"
303
- aria-label={p.label ?? p.source ?? labels.solving}
296
+ aria-label={p.label ?? p.source ?? t.solveSolving}
304
297
  aria-valuemin={0}
305
298
  aria-valuemax={100}
306
299
  aria-valuenow={p.fraction !== undefined ? Math.round(p.fraction * 100) : undefined}
@@ -327,14 +320,14 @@
327
320
  <div class="gap-2 text-sm font-medium flex items-center">
328
321
  {#if view === 'blocked'}
329
322
  <SeverityIcon severity="error" />
330
- {labels.blockedTitle}
323
+ {t.solveBlockedTitle}
331
324
  {:else}
332
325
  <SeverityIcon severity="warning" />
333
- {labels.messagesTitle}
326
+ {t.solveMessagesTitle}
334
327
  {/if}
335
328
  </div>
336
329
  <p class="text-xs text-muted-foreground">
337
- {view === 'blocked' ? labels.blockedDescription : labels.messagesDescription}
330
+ {view === 'blocked' ? t.solveBlocked : t.solveMessagesDescription}
338
331
  </p>
339
332
  </div>
340
333
  {/if}
@@ -348,10 +341,10 @@
348
341
  {#if view === 'review' || view === 'blocked'}
349
342
  <div class="mt-3 gap-2 flex justify-end">
350
343
  {#if view === 'blocked'}
351
- <Button variant="outline" size="sm" onclick={dismiss}>{labels.dismiss}</Button>
344
+ <Button variant="outline" size="sm" onclick={dismiss}>{t.solveMessagesDismiss}</Button>
352
345
  {:else}
353
- <Button variant="outline" size="sm" onclick={ondiscard}>{labels.discard}</Button>
354
- <Button size="sm" onclick={onconfirm}>{labels.confirm}</Button>
346
+ <Button variant="outline" size="sm" onclick={ondiscard}>{t.solveMessagesDiscard}</Button>
347
+ <Button size="sm" onclick={onconfirm}>{t.solveMessagesConfirm}</Button>
355
348
  {/if}
356
349
  </div>
357
350
  {/if}
@@ -5,6 +5,8 @@
5
5
  import { useFooter } from '$lib/contexts/footerContext.svelte';
6
6
  import ComputeMessagesDialog from '../compute/ComputeMessagesDialog.svelte';
7
7
  import FooterItemRenderer from './FooterItemRenderer.svelte';
8
+ import { getLocaleContext } from '$lib/i18n/localeContext.svelte';
9
+ import { formatCount, formatMessage } from '$lib/i18n/messages';
8
10
 
9
11
  interface Props {
10
12
  errors?: string[];
@@ -26,6 +28,9 @@
26
28
  children
27
29
  }: Props = $props();
28
30
 
31
+ const locale = getLocaleContext();
32
+ const t = $derived(locale.messages);
33
+
29
34
  let footerStore = (() => {
30
35
  try {
31
36
  return useFooter();
@@ -66,16 +71,15 @@
66
71
  const hasMessages = $derived(totalCount > 0);
67
72
 
68
73
  const copyrightLine = $derived(
69
- footerText
70
- ? footerText.replace('{name}', copyrightName).replace('{year}', String(_currentYear))
71
- : `by ${copyrightName} © ${_currentYear}`
74
+ formatMessage(footerText ?? t.copyright, { name: copyrightName, year: _currentYear })
72
75
  );
73
76
  </script>
74
77
 
78
+ <!-- One line at any width: the message counts keep their size and the copyright truncates. -->
75
79
  <footer
76
- class="px-4 gap-4 text-xs h-7 flex shrink-0 items-center justify-between border-t border-border bg-background text-muted-foreground select-none"
80
+ class="px-4 gap-4 text-xs h-7 flex shrink-0 items-center justify-between border-t border-border bg-background whitespace-nowrap text-muted-foreground select-none"
77
81
  >
78
- <div class="gap-4 flex items-center">
82
+ <div class="gap-4 flex shrink-0 items-center">
79
83
  {#each leftItems as item (item.id)}
80
84
  <FooterItemRenderer {item} />
81
85
  {/each}
@@ -90,11 +94,13 @@
90
94
  : warningCount > 0
91
95
  ? 'text-warning hover:bg-warning/10'
92
96
  : 'hover:bg-muted'}"
93
- title={`${totalCount} ${totalCount === 1 ? 'message' : 'messages'} from the last solve`}
97
+ title={formatCount(totalCount, t.lastSolveMessagesOne, t.lastSolveMessagesOther)}
94
98
  >
95
99
  {#if errorCount > 0}
96
100
  <CircleAlert class="h-3.5 w-3.5" />
97
- <span class="font-medium">{errorCount} Error{errorCount !== 1 ? 's' : ''}</span>
101
+ <span class="font-medium"
102
+ >{formatCount(errorCount, t.errorCountOne, t.errorCountOther)}</span
103
+ >
98
104
  {/if}
99
105
 
100
106
  {#if warningCount > 0}
@@ -102,12 +108,16 @@
102
108
  <span class="text-border">•</span>
103
109
  {/if}
104
110
  <TriangleAlert class="h-3.5 w-3.5" />
105
- <span class="font-medium">{warningCount} Warning{warningCount !== 1 ? 's' : ''}</span>
111
+ <span class="font-medium"
112
+ >{formatCount(warningCount, t.warningCountOne, t.warningCountOther)}</span
113
+ >
106
114
  {/if}
107
115
 
108
116
  {#if errorCount === 0 && warningCount === 0}
109
117
  <Info class="h-3.5 w-3.5" />
110
- <span class="font-medium">{remarkCount} Note{remarkCount !== 1 ? 's' : ''}</span>
118
+ <span class="font-medium"
119
+ >{formatCount(remarkCount, t.noteCountOne, t.noteCountOther)}</span
120
+ >
111
121
  {/if}
112
122
  </div>
113
123
  {/snippet}
@@ -121,11 +131,11 @@
121
131
  {/if}
122
132
  </div>
123
133
 
124
- <div class="gap-4 ml-auto flex items-center">
134
+ <div class="gap-4 min-w-0 ml-auto flex items-center">
125
135
  {#each rightItems as item (item.id)}
126
136
  <FooterItemRenderer {item} />
127
137
  {/each}
128
138
 
129
- <p>{copyrightLine}</p>
139
+ <p class="truncate">{copyrightLine}</p>
130
140
  </div>
131
141
  </footer>
@@ -3,6 +3,8 @@
3
3
  import { onMount } from 'svelte';
4
4
  import { Loader, Maximize, Minimize } from '@lucide/svelte';
5
5
  import { loadScript } from '$lib/utils/loadScript';
6
+ import { getLocaleContext } from '$lib/i18n/localeContext.svelte';
7
+ import { formatMessage } from '$lib/i18n/messages';
6
8
 
7
9
  interface Props {
8
10
  item: OutputChartLayoutItem;
@@ -11,6 +13,9 @@
11
13
 
12
14
  let { item: _item, value }: Props = $props();
13
15
 
16
+ const locale = getLocaleContext();
17
+ const t = $derived(locale.messages);
18
+
14
19
  const MAP_TYPE_SET = new Set(['scattermap', 'scattermapbox', 'choropleth', 'scattergeo']);
15
20
 
16
21
  type PlotlyTrace = Record<string, unknown>;
@@ -49,11 +54,11 @@
49
54
  if (!wrapperEl) return;
50
55
  if (!document.fullscreenElement) {
51
56
  wrapperEl.requestFullscreen().catch((err) => {
52
- error = `Fullscreen unavailable: ${err.message}`;
57
+ error = formatMessage(t.fullscreenUnavailable, { error: err.message });
53
58
  });
54
59
  } else {
55
60
  document.exitFullscreen().catch((err) => {
56
- error = `Exit fullscreen failed: ${err.message}`;
61
+ error = formatMessage(t.exitFullscreenFailed, { error: err.message });
57
62
  });
58
63
  }
59
64
  }
@@ -129,7 +134,7 @@
129
134
  ...figData.config
130
135
  });
131
136
  } catch (e) {
132
- error = e instanceof Error ? e.message : 'Failed to render chart';
137
+ error = e instanceof Error ? e.message : t.chartRenderFailed;
133
138
  } finally {
134
139
  loading = false;
135
140
  }
@@ -183,10 +188,9 @@
183
188
  >
184
189
  {#if error}
185
190
  <div class="px-4 py-3 text-sm text-destructive">{error}</div>
186
- {:else if !figData && value}
187
- <div class="px-4 py-8 text-sm text-center text-muted-foreground">Receiving chart data...</div>
188
191
  {:else if !figData}
189
- <div class="px-4 py-8 text-sm text-center text-muted-foreground">Waiting for chart data...</div>
192
+ <!-- The layout hides this output while it has no data, so this is data that doesn't parse yet. -->
193
+ <div class="px-4 py-8 text-sm text-center text-muted-foreground">{t.chartReceiving}</div>
190
194
  {:else}
191
195
  <div bind:this={containerEl} class="w-full {isFullscreen ? 'h-screen' : 'h-95'}"></div>
192
196
 
@@ -206,7 +210,8 @@
206
210
  <button
207
211
  onclick={toggleFullscreen}
208
212
  disabled={loading}
209
- title={isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
213
+ title={isFullscreen ? t.exitFullscreen : t.fullscreen}
214
+ aria-label={isFullscreen ? t.exitFullscreen : t.fullscreen}
210
215
  class="rounded p-1.5 backdrop-blur-sm flex items-center border border-border bg-background/80 text-foreground transition-colors hover:bg-background disabled:cursor-not-allowed disabled:opacity-50"
211
216
  >
212
217
  {#if isFullscreen}
@@ -38,6 +38,9 @@
38
38
 
39
39
  const visibilityMap = $derived(buildVisibilityMap(items, values));
40
40
 
41
+ const uid = $props.id();
42
+ const descriptionId = `${uid}-description`;
43
+
41
44
  // Below this a label and its control stop fitting side by side, so a wide group
42
45
  // sheds columns as its panel narrows instead of squeezing every input.
43
46
  const MIN_COLUMN_WIDTH = 160;
@@ -72,24 +75,36 @@
72
75
  {@render grid()}
73
76
  </div>
74
77
  {:else}
75
- <button
76
- type="button"
77
- class="gap-3 px-4 py-3.5 flex w-full items-start text-left outline-none select-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset"
78
- aria-expanded={!collapsed}
79
- onclick={onToggle}
80
- >
81
- <span class="min-w-0 flex-1">
82
- <span class="text-base font-semibold leading-snug block text-foreground">{label}</span>
83
- {#if description}
84
- <span class="text-xs mt-0.5 leading-snug block text-muted-foreground">{description}</span>
85
- {/if}
86
- </span>
87
- <ChevronDown
88
- class="mt-1 h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200 {collapsed
89
- ? '-rotate-90'
90
- : ''}"
91
- />
92
- </button>
78
+ <!-- The host's page owns the h1 and the panel holds the h2. The description sits outside the
79
+ heading's button, so the heading and the button are named by the label alone. -->
80
+ <h3>
81
+ <button
82
+ type="button"
83
+ class="gap-3 px-4 pt-3.5 flex w-full items-start text-left outline-none select-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset {description
84
+ ? 'pb-0'
85
+ : 'pb-3.5'}"
86
+ aria-expanded={!collapsed}
87
+ aria-describedby={description ? descriptionId : undefined}
88
+ onclick={onToggle}
89
+ >
90
+ <span class="min-w-0 text-base font-semibold leading-snug flex-1 text-foreground"
91
+ >{label}</span
92
+ >
93
+ <ChevronDown
94
+ class="mt-1 h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200 {collapsed
95
+ ? '-rotate-90'
96
+ : ''}"
97
+ />
98
+ </button>
99
+ </h3>
100
+ {#if description}
101
+ <p
102
+ id={descriptionId}
103
+ class="pl-4 pr-11 pt-0.5 pb-3.5 text-xs leading-snug text-muted-foreground"
104
+ >
105
+ {description}
106
+ </p>
107
+ {/if}
93
108
  <div class="content-wrapper" class:collapsed inert={collapsed}>
94
109
  <div class="content-inner">
95
110
  <div class="px-4 pt-1 pb-5">
@@ -12,13 +12,15 @@
12
12
  </script>
13
13
 
14
14
  <!-- Every field has one, so a column of them is noise. Where a fine pointer can hover, it shows
15
- only while the pointer or focus is inside the enclosing `group/field`; touch keeps it visible. -->
15
+ only while the pointer or focus is inside the enclosing `group/field`; touch keeps it visible.
16
+ The box is 24px, touch gets a 44px hit area, and the negative margin keeps the label row from
17
+ growing. -->
16
18
  <Dialog.Root>
17
19
  <Dialog.Trigger>
18
20
  {#snippet child({ props })}
19
21
  <button
20
22
  {...props}
21
- class="-m-1 p-1 shrink-0 cursor-help rounded-sm text-muted-foreground transition hover:text-foreground pointer-fine:opacity-0 pointer-fine:group-focus-within/field:opacity-100 pointer-fine:group-hover/field:opacity-100 pointer-fine:focus-visible:opacity-100"
23
+ class="-m-1 size-6 pointer-coarse:after:-inset-2.5 relative flex shrink-0 cursor-help items-center justify-center rounded-sm text-muted-foreground transition hover:text-foreground pointer-coarse:after:absolute pointer-fine:opacity-0 pointer-fine:group-focus-within/field:opacity-100 pointer-fine:group-hover/field:opacity-100 pointer-fine:focus-visible:opacity-100"
22
24
  aria-label={label}
23
25
  >
24
26
  <HelpCircle class="size-3.5" />