@selvajs/ui 6.6.0-beta.4 → 6.6.0-beta.6

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 (114) hide show
  1. package/dist/components/ErrorScreen.svelte +6 -2
  2. package/dist/components/compute/AppLayout.svelte +163 -74
  3. package/dist/components/compute/AppLayout.svelte.d.ts +11 -0
  4. package/dist/components/compute/ComputeApp.svelte +40 -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 +17 -8
  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/input/input.svelte +2 -2
  36. package/dist/components/primitives/input-group/input-group.svelte +1 -1
  37. package/dist/components/primitives/mode-toggle/mode-toggle.svelte +4 -1
  38. package/dist/components/primitives/scroll-area/scroll-area.svelte +1 -1
  39. package/dist/components/primitives/search/search.svelte +1 -1
  40. package/dist/components/primitives/select/select-trigger.svelte +1 -1
  41. package/dist/components/primitives/slider/slider.svelte +7 -2
  42. package/dist/components/primitives/slider/slider.svelte.d.ts +5 -1
  43. package/dist/components/primitives/switch/switch.svelte +1 -1
  44. package/dist/components/primitives/tabs/tabs-trigger.svelte +1 -1
  45. package/dist/components/primitives/tabs/tabs-trigger.svelte.d.ts +1 -1
  46. package/dist/components/primitives/textarea/textarea.svelte +1 -1
  47. package/dist/components/primitives/toggle/toggle.svelte +1 -1
  48. package/dist/components/primitives/toggle/toggle.svelte.d.ts +1 -1
  49. package/dist/components/viewer/Viewer.svelte +20 -17
  50. package/dist/compute/run-panel-action.d.ts +8 -0
  51. package/dist/compute/run-panel-action.js +12 -0
  52. package/dist/compute/useSolveSession.svelte.js +3 -0
  53. package/dist/i18n/localeContext.svelte.d.ts +10 -3
  54. package/dist/i18n/localeContext.svelte.js +27 -5
  55. package/dist/i18n/messages.d.ts +115 -2
  56. package/dist/i18n/messages.js +199 -11
  57. package/dist/index.d.ts +1 -1
  58. package/dist/public.d.ts +1 -1
  59. package/dist/schema/visibility-rules.js +21 -0
  60. package/dist/styles/base.css +9 -6
  61. package/dist/types/actionButton.d.ts +17 -3
  62. package/dist/types/presetLabels.d.ts +3 -2
  63. package/dist/types/presetLabels.js +9 -8
  64. package/dist/utils/loadScript.d.ts +2 -0
  65. package/dist/utils/loadScript.js +2 -0
  66. package/package.json +8 -8
  67. package/src/lib/components/ErrorScreen.svelte +6 -2
  68. package/src/lib/components/compute/AppLayout.svelte +163 -74
  69. package/src/lib/components/compute/ComputeApp.svelte +40 -20
  70. package/src/lib/components/compute/ComputeMessagesDialog.svelte +30 -40
  71. package/src/lib/components/compute/ParameterPresetManager.svelte +7 -44
  72. package/src/lib/components/compute/SolveMessages.svelte +30 -37
  73. package/src/lib/components/layout/PageFooter.svelte +21 -11
  74. package/src/lib/components/preview/ChartOutput.svelte +17 -8
  75. package/src/lib/components/preview/Group.svelte +33 -18
  76. package/src/lib/components/preview/HelpButton.svelte +4 -2
  77. package/src/lib/components/preview/ImageOutput.svelte +427 -68
  78. package/src/lib/components/preview/InputControl.svelte +11 -2
  79. package/src/lib/components/preview/OutputDisplay.svelte +17 -12
  80. package/src/lib/components/preview/TabContent.svelte +3 -0
  81. package/src/lib/components/preview/inputs/ColorInput.svelte +3 -1
  82. package/src/lib/components/preview/inputs/DropdownInput.svelte +4 -1
  83. package/src/lib/components/preview/inputs/FileInput.svelte +50 -34
  84. package/src/lib/components/preview/inputs/NumberInput.svelte +92 -10
  85. package/src/lib/components/preview/inputs/TextInput.svelte +8 -3
  86. package/src/lib/components/primitives/CalculateButton.svelte +49 -23
  87. package/src/lib/components/primitives/SolvingIndicator.svelte +21 -17
  88. package/src/lib/components/primitives/badge/badge.svelte +2 -2
  89. package/src/lib/components/primitives/button/button.svelte +2 -2
  90. package/src/lib/components/primitives/checkbox/checkbox.svelte +1 -1
  91. package/src/lib/components/primitives/dialog/dialog-content.svelte +6 -2
  92. package/src/lib/components/primitives/input/input.svelte +2 -2
  93. package/src/lib/components/primitives/input-group/input-group.svelte +1 -1
  94. package/src/lib/components/primitives/mode-toggle/mode-toggle.svelte +4 -1
  95. package/src/lib/components/primitives/scroll-area/scroll-area.svelte +1 -1
  96. package/src/lib/components/primitives/search/search.svelte +1 -1
  97. package/src/lib/components/primitives/select/select-trigger.svelte +1 -1
  98. package/src/lib/components/primitives/slider/slider.svelte +7 -2
  99. package/src/lib/components/primitives/switch/switch.svelte +1 -1
  100. package/src/lib/components/primitives/tabs/tabs-trigger.svelte +1 -1
  101. package/src/lib/components/primitives/textarea/textarea.svelte +1 -1
  102. package/src/lib/components/primitives/toggle/toggle.svelte +1 -1
  103. package/src/lib/components/viewer/Viewer.svelte +20 -17
  104. package/src/lib/compute/run-panel-action.ts +16 -0
  105. package/src/lib/compute/useSolveSession.svelte.ts +3 -0
  106. package/src/lib/i18n/localeContext.svelte.ts +40 -5
  107. package/src/lib/i18n/messages.ts +351 -13
  108. package/src/lib/index.ts +1 -0
  109. package/src/lib/public.ts +1 -0
  110. package/src/lib/schema/visibility-rules.ts +23 -0
  111. package/src/lib/styles/base.css +9 -6
  112. package/src/lib/types/actionButton.ts +16 -3
  113. package/src/lib/types/presetLabels.ts +13 -11
  114. package/src/lib/utils/loadScript.ts +3 -0
@@ -3,6 +3,7 @@
3
3
  import * as Card from './primitives/card/index.js';
4
4
  import { Button } from './primitives/button/index.js';
5
5
  import Callout from './primitives/Callout.svelte';
6
+ import { getLocaleContext } from '../i18n/localeContext.svelte';
6
7
 
7
8
  interface Props {
8
9
  status?: number;
@@ -12,6 +13,9 @@
12
13
  }
13
14
 
14
15
  let { status = 500, message, details, homeLink = '/' }: Props = $props();
16
+
17
+ const locale = getLocaleContext();
18
+ const t = $derived(locale.messages);
15
19
  </script>
16
20
 
17
21
  <div class="p-4 flex min-h-screen items-center justify-center bg-background">
@@ -26,7 +30,7 @@
26
30
  </Card.Header>
27
31
  <Card.Content>
28
32
  <div class="gap-4 flex flex-col">
29
- <Callout tone="danger" title="Error">
33
+ <Callout tone="danger" title={t.errorTitle}>
30
34
  {#if message}{message}{/if}
31
35
  </Callout>
32
36
  {#if details}
@@ -37,7 +41,7 @@
37
41
  </div>
38
42
  </Card.Content>
39
43
  <Card.Footer>
40
- <Button variant="outline" href={homeLink} rel="external" class="w-full">Go Home</Button>
44
+ <Button variant="outline" href={homeLink} rel="external" class="w-full">{t.goHome}</Button>
41
45
  </Card.Footer>
42
46
  </Card.Root>
43
47
  </div>
@@ -1,8 +1,9 @@
1
1
  <script lang="ts">
2
2
  import type { UISchema, SupportedTypes, ParameterPreset } from '@selvajs/schemas';
3
+ import type { SolveSettlement } from '@selvajs/solve/client';
3
4
  import type { ActionButton } from '../../types/actionButton';
4
5
  import type { PresetLabels } from '../../types/presetLabels';
5
- import { ChevronUp } from '@lucide/svelte';
6
+ import { ChevronUp, Download, Ellipsis, Upload } from '@lucide/svelte';
6
7
  import type { ThreeViewer } from '@selvajs/visualization/render';
7
8
  import Viewer, { type ViewerConfig } from '../viewer/Viewer.svelte';
8
9
  import CalculateButton from '../primitives/CalculateButton.svelte';
@@ -10,10 +11,13 @@
10
11
  import TabLayout from '../preview/TabLayout.svelte';
11
12
  import CollapsedPanelStrip from './CollapsedPanelStrip.svelte';
12
13
  import * as Resizable from '../primitives/resizable';
14
+ import * as DropdownMenu from '../primitives/dropdown-menu/index.js';
13
15
  import { Button } from '../..';
14
16
  import ParameterPresetManager from './ParameterPresetManager.svelte';
17
+ import { runPanelAction } from '../../compute/run-panel-action';
15
18
  import { getLocaleContext } from '../../i18n/localeContext.svelte';
16
19
  import { buildVisibilityMap, isTabVisible, itemKey } from '../../schema/visibility-rules';
20
+ import { DEFAULT_PRESET_LABELS } from '../../types/presetLabels';
17
21
 
18
22
  const locale = getLocaleContext();
19
23
  const t = $derived(locale.messages);
@@ -22,6 +26,11 @@
22
26
 
23
27
  interface Props {
24
28
  schema: UISchema;
29
+ /**
30
+ * Whether to draw the 3D viewer. Each host reads its own switch: the plugin's preview
31
+ * `viewerOptions.enableLocal`, a Rhino.Compute host `viewerOptions.enableRemote`.
32
+ */
33
+ showViewer: boolean;
25
34
  meshes?: any[];
26
35
  isSolving: boolean;
27
36
  showSolvingIndicator?: boolean;
@@ -29,6 +38,11 @@
29
38
  hasNeverSolved?: boolean;
30
39
  isViewerFullscreen?: boolean;
31
40
  oncalculate?: () => void;
41
+ /**
42
+ * The result for the current inputs, solving if it must (`session.ensureSolved`). Needed
43
+ * by `solvesFirst` actions; without it they run straight away.
44
+ */
45
+ ensureSolved?: () => Promise<SolveSettlement>;
32
46
  values: Record<string, unknown>;
33
47
  onValueChange: (id: string, val: SupportedTypes, forceSolve?: boolean) => void | Promise<void>;
34
48
  onLoadValues?: (values: Record<string, unknown>) => void | Promise<void>;
@@ -45,6 +59,7 @@
45
59
 
46
60
  let {
47
61
  schema,
62
+ showViewer: hasViewer,
48
63
  meshes = [],
49
64
  isSolving,
50
65
  showSolvingIndicator = false,
@@ -52,6 +67,7 @@
52
67
  hasNeverSolved = false,
53
68
  isViewerFullscreen = $bindable(false),
54
69
  oncalculate = () => {},
70
+ ensureSolved,
55
71
  values,
56
72
  onValueChange,
57
73
  onLoadValues,
@@ -66,9 +82,11 @@
66
82
  onViewerReady
67
83
  }: Props = $props();
68
84
 
69
- const hasViewer = $derived(
70
- !!(schema?.viewerOptions?.enableLocal || schema?.viewerOptions?.enableRemote)
71
- );
85
+ const presetText = $derived({ ...DEFAULT_PRESET_LABELS, ...presetLabels });
86
+ let presets = $state<ParameterPresetManager | null>(null);
87
+ const hasMenu = $derived(showSaveButton || showLoadButton || panelActions.length > 0);
88
+ const resultIsStale = $derived(hasPendingChanges || hasNeverSolved);
89
+
72
90
  const shownTabs = $derived(
73
91
  schema.layout.type === 'tabbed'
74
92
  ? schema.layout.tabs.filter((tab) => isTabVisible(tab, values))
@@ -151,54 +169,110 @@
151
169
  }
152
170
  </script>
153
171
 
172
+ <!-- Everything but Calculate: saving and loading inputs, then the host's own actions. -->
173
+ {#snippet actionsMenu()}
174
+ <DropdownMenu.Root>
175
+ <DropdownMenu.Trigger>
176
+ {#snippet child({ props })}
177
+ <Button
178
+ {...props}
179
+ variant="outline"
180
+ size="icon-lg"
181
+ class="shrink-0"
182
+ title={t.moreActions}
183
+ aria-label={t.moreActions}
184
+ >
185
+ <Ellipsis />
186
+ </Button>
187
+ {/snippet}
188
+ </DropdownMenu.Trigger>
189
+ <!-- Above the fullscreen viewer, which sits at z-index 10000. -->
190
+ <DropdownMenu.Content side="top" align="end" sideOffset={6} class="min-w-56 z-10001 w-auto">
191
+ {#if showSaveButton}
192
+ <DropdownMenu.Item onSelect={() => presets?.openSave()}>
193
+ <Download />
194
+ {presetText.saveMenuItem}
195
+ </DropdownMenu.Item>
196
+ {/if}
197
+ {#if showLoadButton}
198
+ <DropdownMenu.Item onSelect={() => presets?.openLoad()}>
199
+ <Upload />
200
+ {presetText.loadMenuItem}
201
+ </DropdownMenu.Item>
202
+ {/if}
203
+ {#if (showSaveButton || showLoadButton) && panelActions.length > 0}
204
+ <DropdownMenu.Separator />
205
+ {/if}
206
+ {#each panelActions as action (action.id)}
207
+ {@const hint =
208
+ action.hint ?? (action.solvesFirst && resultIsStale ? t.calculatesFirst : undefined)}
209
+ <DropdownMenu.Item
210
+ disabled={action.disabled}
211
+ variant={action.variant ?? 'default'}
212
+ onSelect={() => runPanelAction(action, ensureSolved)}
213
+ class="items-start"
214
+ >
215
+ {#if action.icon}
216
+ {@const IconComponent = action.icon}
217
+ <IconComponent class="mt-0.5" />
218
+ {/if}
219
+ <span class="flex flex-col">
220
+ {action.label}
221
+ {#if hint}
222
+ <span class="text-xs leading-snug text-muted-foreground">{hint}</span>
223
+ {/if}
224
+ </span>
225
+ </DropdownMenu.Item>
226
+ {/each}
227
+ </DropdownMenu.Content>
228
+ </DropdownMenu.Root>
229
+ {/snippet}
230
+
231
+ <!-- One primary action and one menu. A definition that solves on every change has no Calculate,
232
+ so the menu sits alone at the end of the row. -->
233
+ {#snippet footer()}
234
+ {#if schema.instanceSolve === false}
235
+ <CalculateButton
236
+ {hasPendingChanges}
237
+ {hasNeverSolved}
238
+ {isSolving}
239
+ {oncalculate}
240
+ trailing={hasMenu ? actionsMenu : undefined}
241
+ />
242
+ {:else if hasMenu}
243
+ <div class="mt-3 pb-3 pt-2 px-2 flex justify-end border-t border-border/50">
244
+ {@render actionsMenu()}
245
+ </div>
246
+ {/if}
247
+ {/snippet}
248
+
249
+ <!-- `hostsFooter` marks the one panel that carries the footer. On a phone the footer moves to the
250
+ drawer's pinned bottom instead. -->
154
251
  {#snippet panelContent(
155
252
  panelFilter: 'left' | 'right' | undefined,
156
253
  requestedTabId: string | null,
157
- showParameterStateManager = true,
158
- showCalculateButton = true
254
+ hostsFooter = true
159
255
  )}
160
256
  <div class="panel-content-wrapper">
161
257
  {#if schema.layout.type === 'tabbed'}
162
258
  <TabLayout {schema} {values} {onValueChange} {panelFilter} {requestedTabId} />
163
259
  {/if}
164
- {#if showParameterStateManager || (!isMobile && showCalculateButton && schema.instanceSolve === false)}
260
+ {#if hostsFooter && !isMobile}
165
261
  <div class="panel-footer px-1">
166
- <div class="gap-2 flex flex-wrap items-center [&>button]:flex-1">
167
- {#if showParameterStateManager}
168
- <ParameterPresetManager
169
- {schema}
170
- currentValues={values}
171
- onLoadValues={handleLoadValues}
172
- {showSaveButton}
173
- {showLoadButton}
174
- {onSaveState}
175
- {onListStates}
176
- labels={presetLabels}
177
- />
178
- {/if}
179
- {#each panelActions as action (action.id)}
180
- <Button
181
- variant={action.variant ?? 'outline'}
182
- size={action.size ?? 'sm'}
183
- onclick={action.onclick}
184
- >
185
- {#if action.icon}
186
- {@const IconComponent = action.icon}
187
- <IconComponent class="mr-2 h-4 w-4" />
188
- {/if}
189
- {action.label}
190
- </Button>
191
- {/each}
192
- </div>
193
- {#if !isMobile && showCalculateButton && schema.instanceSolve === false}
194
- <CalculateButton {hasPendingChanges} {hasNeverSolved} {isSolving} {oncalculate} />
195
- {/if}
262
+ {@render footer()}
196
263
  </div>
197
264
  {/if}
198
265
  </div>
199
266
  {/snippet}
200
267
 
201
- <div
268
+ <!-- Pinned under the scrolling fields, so the primary action never sits at the end of a long list. -->
269
+ {#snippet drawerFooter(isInert: boolean)}
270
+ <div class="drawer-footer" inert={isInert}>
271
+ {@render footer()}
272
+ </div>
273
+ {/snippet}
274
+
275
+ <main
202
276
  data-layout-root
203
277
  class="min-h-0 sm:flex-row sm:py-(--page-py) flex flex-1 flex-col overflow-hidden {leftCollapsed
204
278
  ? 'sm:pl-0'
@@ -230,12 +304,12 @@
230
304
  class:drawer-open={drawerOpen}
231
305
  class:drawer-closed={!drawerOpen}
232
306
  >
307
+ <!-- Named by its visible label; aria-expanded carries the open/closed state. -->
233
308
  <button
234
309
  class="drawer-handle-bar"
235
310
  onclick={() => (drawerOpen = !drawerOpen)}
236
311
  ontouchstart={onDrawerTouchStart}
237
312
  ontouchend={onDrawerTouchEnd}
238
- aria-label={drawerOpen ? t.collapsePanel : t.expandPanel}
239
313
  aria-expanded={drawerOpen}
240
314
  >
241
315
  <div class="drawer-pill"></div>
@@ -248,62 +322,47 @@
248
322
  />
249
323
  </button>
250
324
 
251
- <div class="drawer-content">
325
+ <!-- Inert while closed: the fields are clipped out of view, and tabbing into them
326
+ would scroll the drawer's insides around behind the handle. -->
327
+ <div class="drawer-content" inert={!drawerOpen}>
252
328
  <div class="px-3 pb-6">
253
329
  {#if hasLeftPanel}
254
- {@render panelContent(
255
- hasRightPanel ? 'left' : undefined,
256
- requestedLeftTabId,
257
- !hasRightPanel,
258
- false
259
- )}
330
+ {@render panelContent(hasRightPanel ? 'left' : undefined, requestedLeftTabId, false)}
260
331
  {/if}
261
332
  {#if hasRightPanel}
262
- <div class="my-4 gap-2 flex items-center">
333
+ <!-- Decorative: the section below names itself with a heading. -->
334
+ <div class="my-4 gap-2 flex items-center" aria-hidden="true">
263
335
  <div class="h-px flex-1 bg-border"></div>
264
336
  <span class="text-xs font-medium px-1 text-muted-foreground">
265
337
  {rightTabs[0]?.label ?? t.moreTab}
266
338
  </span>
267
339
  <div class="h-px flex-1 bg-border"></div>
268
340
  </div>
269
- {@render panelContent('right', requestedRightTabId, true, false)}
341
+ {@render panelContent('right', requestedRightTabId, false)}
270
342
  {/if}
271
343
  </div>
272
344
  </div>
273
345
 
274
- {#if schema.instanceSolve === false}
275
- <div class="drawer-footer">
276
- <CalculateButton {hasPendingChanges} {hasNeverSolved} {isSolving} {oncalculate} />
277
- </div>
278
- {/if}
346
+ {@render drawerFooter(!drawerOpen)}
279
347
  </div>
280
348
  {:else}
281
349
  <div class="flex flex-1 flex-col overflow-hidden">
282
350
  <div class="px-3 pt-3 flex-1 overflow-y-auto">
283
351
  {#if hasLeftPanel}
284
- {@render panelContent(
285
- hasRightPanel ? 'left' : undefined,
286
- requestedLeftTabId,
287
- !hasRightPanel,
288
- false
289
- )}
352
+ {@render panelContent(hasRightPanel ? 'left' : undefined, requestedLeftTabId, false)}
290
353
  {/if}
291
354
  {#if hasRightPanel}
292
- <div class="my-4 gap-2 flex items-center">
355
+ <div class="my-4 gap-2 flex items-center" aria-hidden="true">
293
356
  <div class="h-px flex-1 bg-border"></div>
294
357
  <span class="text-xs font-medium px-1 text-muted-foreground">
295
358
  {rightTabs[0]?.label ?? t.moreTab}
296
359
  </span>
297
360
  <div class="h-px flex-1 bg-border"></div>
298
361
  </div>
299
- {@render panelContent('right', requestedRightTabId, true, false)}
362
+ {@render panelContent('right', requestedRightTabId, false)}
300
363
  {/if}
301
364
  </div>
302
- {#if schema.instanceSolve === false}
303
- <div class="drawer-footer">
304
- <CalculateButton {hasPendingChanges} {hasNeverSolved} {isSolving} {oncalculate} />
305
- </div>
306
- {/if}
365
+ {@render drawerFooter(false)}
307
366
  </div>
308
367
  {/if}
309
368
  {:else if !hasViewer && !isTwoPanelMode && hasLeftPanel !== hasRightPanel}
@@ -312,9 +371,7 @@
312
371
  <div class="min-h-0 flex h-full w-4/5 flex-col">
313
372
  {@render panelContent(
314
373
  hasRightPanel ? 'right' : undefined,
315
- hasRightPanel ? requestedRightTabId : requestedLeftTabId,
316
- !hasRightPanel,
317
- !hasRightPanel
374
+ hasRightPanel ? requestedRightTabId : requestedLeftTabId
318
375
  )}
319
376
  </div>
320
377
  </div>
@@ -412,7 +469,7 @@
412
469
  ? 'hidden'
413
470
  : ''}"
414
471
  >
415
- {@render panelContent('right', requestedRightTabId, !hasLeftPanel, !hasLeftPanel)}
472
+ {@render panelContent('right', requestedRightTabId, !hasLeftPanel)}
416
473
  </div>
417
474
  </Resizable.Pane>
418
475
  {/if}
@@ -432,9 +489,20 @@
432
489
  {/if}
433
490
  </div>
434
491
  {/if}
435
- </div>
436
492
 
437
- <SolvingIndicator show={showSolvingIndicator} />
493
+ <SolvingIndicator show={showSolvingIndicator} />
494
+ </main>
495
+
496
+ <!-- The save, load and validation dialogs; the footer's menu opens them. -->
497
+ <ParameterPresetManager
498
+ bind:this={presets}
499
+ {schema}
500
+ currentValues={values}
501
+ onLoadValues={handleLoadValues}
502
+ {onSaveState}
503
+ {onListStates}
504
+ labels={presetLabels}
505
+ />
438
506
 
439
507
  <style>
440
508
  .fullscreen-layout {
@@ -480,8 +548,10 @@
480
548
  .drawer-closed {
481
549
  height: 60px;
482
550
  }
551
+ /* The pinned footer takes a fixed slice, so a short phone opens the drawer further to leave
552
+ room for the fields; a tall one keeps it at 60% of the screen. */
483
553
  .drawer-open {
484
- height: 60svh;
554
+ height: max(60svh, min(30rem, 100svh - 10rem));
485
555
  }
486
556
 
487
557
  .drawer-handle-bar {
@@ -524,6 +594,25 @@
524
594
  .drawer-footer {
525
595
  flex-shrink: 0;
526
596
  padding: 0rem 1rem;
527
- border-top: 1px solid var(--border);
597
+ }
598
+
599
+ /* Touch gets 44px targets on the panel's controls; a fine pointer keeps the denser sizes. */
600
+ @media (pointer: coarse) {
601
+ .panel-content-wrapper
602
+ :global(
603
+ :is(
604
+ [data-slot='button'],
605
+ [data-slot='input'],
606
+ [data-slot='select-trigger'],
607
+ [data-slot='input-group']
608
+ )
609
+ ),
610
+ .drawer-footer :global([data-slot='button']) {
611
+ min-height: 2.75rem;
612
+ }
613
+ .panel-content-wrapper :global([data-slot='input-group'] > [data-slot='input-group-control']) {
614
+ align-self: stretch;
615
+ height: auto;
616
+ }
528
617
  }
529
618
  </style>
@@ -1,10 +1,16 @@
1
1
  import type { UISchema, SupportedTypes, ParameterPreset } from '@selvajs/schemas';
2
+ import type { SolveSettlement } from '@selvajs/solve/client';
2
3
  import type { ActionButton } from '../../types/actionButton';
3
4
  import type { PresetLabels } from '../../types/presetLabels';
4
5
  import type { ThreeViewer } from '@selvajs/visualization/render';
5
6
  import { type ViewerConfig } from '../viewer/Viewer.svelte';
6
7
  interface Props {
7
8
  schema: UISchema;
9
+ /**
10
+ * Whether to draw the 3D viewer. Each host reads its own switch: the plugin's preview
11
+ * `viewerOptions.enableLocal`, a Rhino.Compute host `viewerOptions.enableRemote`.
12
+ */
13
+ showViewer: boolean;
8
14
  meshes?: any[];
9
15
  isSolving: boolean;
10
16
  showSolvingIndicator?: boolean;
@@ -12,6 +18,11 @@ interface Props {
12
18
  hasNeverSolved?: boolean;
13
19
  isViewerFullscreen?: boolean;
14
20
  oncalculate?: () => void;
21
+ /**
22
+ * The result for the current inputs, solving if it must (`session.ensureSolved`). Needed
23
+ * by `solvesFirst` actions; without it they run straight away.
24
+ */
25
+ ensureSolved?: () => Promise<SolveSettlement>;
15
26
  values: Record<string, unknown>;
16
27
  onValueChange: (id: string, val: SupportedTypes, forceSolve?: boolean) => void | Promise<void>;
17
28
  onLoadValues?: (values: Record<string, unknown>) => void | Promise<void>;
@@ -7,7 +7,12 @@
7
7
  import type { PresetLabels } from '../../types/presetLabels';
8
8
  import { createSolvingIndicator } from '../../compute/solving.svelte';
9
9
  import { createRequestResponseDriver } from '@selvajs/solve/client';
10
- import type { RetainedSolveResult, SolveEventSource, SolveSession } from '@selvajs/solve/client';
10
+ import type {
11
+ RetainedSolveResult,
12
+ SolveEventSource,
13
+ SolveSession,
14
+ SolveSettlement
15
+ } from '@selvajs/solve/client';
11
16
  import SolveMessages from './SolveMessages.svelte';
12
17
  import { meshPolicy } from '@selvajs/visualization/parse';
13
18
  import type { ThreeViewer } from '@selvajs/visualization/render';
@@ -19,8 +24,8 @@
19
24
  import { type ViewerConfig } from '../viewer/Viewer.svelte';
20
25
  import StateDisplay from '../primitives/StateDisplay.svelte';
21
26
  import { setClientSlot, type ClientSlot } from '../../contexts/clientSlotContext.svelte';
22
- import type { Locale } from '../../i18n/messages';
23
- import { setLocaleContext, getLocaleContext } from '../../i18n/localeContext.svelte';
27
+ import type { Locale, ViewerMessages } from '../../i18n/messages';
28
+ import { layerLocaleContext } from '../../i18n/localeContext.svelte';
24
29
 
25
30
  import type { Snippet } from 'svelte';
26
31
 
@@ -34,8 +39,11 @@
34
39
  isEmbedded?: boolean;
35
40
  primaryColor?: string;
36
41
  showModeToggle?: boolean;
42
+ /** Host actions, listed in the panel footer's ⋯ menu after saving and loading inputs. */
37
43
  panelActions?: ActionButton[];
44
+ /** Lists "Save inputs…" in the footer menu. */
38
45
  showSaveButton?: boolean;
46
+ /** Lists "Load inputs…" in the footer menu. */
39
47
  showLoadButton?: boolean;
40
48
  /** When set, persist saved states via this callback instead of downloading a .slvp file. */
41
49
  onSaveState?: (state: ParameterPreset) => void | Promise<void>;
@@ -74,6 +82,12 @@
74
82
  */
75
83
  getSession: () => SolveSession;
76
84
  }) => void;
85
+ /**
86
+ * Once per settled solve: applied, discarded, blocked, aborted or failed. A result held
87
+ * for review counts when the user accepts or rejects it, and one a queued solve replaces
88
+ * never does. Auto-solves from input changes count too.
89
+ */
90
+ onSolved?: (settlement: SolveSettlement) => void;
77
91
  /** Hands the live three.js viewer to the host once it mounts. See `Viewer.svelte` for the contract. */
78
92
  onViewerReady?: (viewer: ThreeViewer) => void | (() => void);
79
93
  /** Viewer chrome and defaults. `backgroundColor` and `showSceneManager` are set by the layout. */
@@ -92,6 +106,14 @@
92
106
  // Language for the app's own chrome, English when unset. Does not translate
93
107
  // schema-authored labels or Grasshopper-sourced names/metadata.
94
108
  lang?: Locale;
109
+ /**
110
+ * Replaces individual strings of the app's own chrome, layered over the `lang` catalog
111
+ * (`VIEWER_MESSAGES`): reword a label, or supply every string for a language with no
112
+ * built-in catalog. Reaches every component inside, the viewer included. A `{name}`
113
+ * placeholder in the default is filled the same way in an override. The preset manager's
114
+ * strings come from `presetLabels` instead.
115
+ */
116
+ messages?: Partial<ViewerMessages>;
95
117
  }
96
118
 
97
119
  let {
@@ -123,22 +145,26 @@
123
145
  brandName,
124
146
  header,
125
147
  onReady,
148
+ onSolved,
126
149
  onViewerReady,
127
150
  viewerConfig = {},
128
151
  externalScopeKey,
129
152
  clientSlot,
130
- lang
153
+ lang,
154
+ messages
131
155
  }: Props = $props();
132
156
 
133
157
  // Reaches InputControl deep in the tree.
134
158
  // svelte-ignore state_referenced_locally
135
159
  setClientSlot(clientSlot);
136
160
 
137
- // Resolution: explicit `lang` → host-provided locale → English. Passed as a getter so
138
- // switching `lang` updates the chrome live.
139
- const hostLocale = getLocaleContext();
140
- setLocaleContext(() => lang ?? hostLocale.locale);
141
- const t = $derived(getLocaleContext().messages);
161
+ // Resolution: explicit `lang` → host-provided locale → English, with `messages` on top.
162
+ // Getters, so switching either updates the chrome live.
163
+ const locale = layerLocaleContext(
164
+ () => lang,
165
+ () => messages
166
+ );
167
+ const t = $derived(locale.messages);
142
168
 
143
169
  const resolvedScopeKey = $derived(externalScopeKey || definitionKey || schema?.id || '');
144
170
 
@@ -168,6 +194,9 @@
168
194
 
169
195
  const solvingIndicator = createSolvingIndicator(() => session.isSolving);
170
196
 
197
+ // Read at call time, so a host swapping the callback needs no resubscribe.
198
+ $effect(() => session.onSettled((settlement) => onSolved?.(settlement)));
199
+
171
200
  $effect(() => {
172
201
  onReady?.({
173
202
  loadValues: (incoming) => session.loadValues(incoming),
@@ -254,6 +283,7 @@
254
283
  {#key definitionKey}
255
284
  <AppLayout
256
285
  {schema}
286
+ showViewer={!!schema.viewerOptions?.enableRemote}
257
287
  meshes={session.meshes}
258
288
  isSolving={session.isSolving}
259
289
  showSolvingIndicator={schema.instanceSolve !== false && solvingIndicator.show}
@@ -272,6 +302,7 @@
272
302
  {presetLabels}
273
303
  onValueChange={handleValueChange}
274
304
  oncalculate={handleCalculate}
305
+ ensureSolved={() => session.ensureSolved()}
275
306
  onLoadValues={() => session.loadValues({})}
276
307
  />
277
308
  {/key}
@@ -286,15 +317,4 @@
286
317
  onabort={() => session.abort()}
287
318
  onconfirm={() => session.acknowledge()}
288
319
  ondiscard={() => session.discard()}
289
- labels={{
290
- solving: t.solveSolving,
291
- blockedTitle: t.solveBlockedTitle,
292
- messagesTitle: t.solveMessagesTitle,
293
- blockedDescription: t.solveBlocked,
294
- messagesDescription: t.solveMessagesDescription,
295
- confirm: t.solveMessagesConfirm,
296
- discard: t.solveMessagesDiscard,
297
- abort: t.solveMessagesAbort,
298
- dismiss: t.solveMessagesDismiss
299
- }}
300
320
  />
@@ -2,11 +2,11 @@ import type { UISchema, ParameterPreset } from '@selvajs/schemas';
2
2
  import type { ActionButton } from '../../types/actionButton';
3
3
  import type { SolveFn } from '@selvajs/solve/shared';
4
4
  import type { PresetLabels } from '../../types/presetLabels';
5
- import type { RetainedSolveResult, SolveEventSource, SolveSession } from '@selvajs/solve/client';
5
+ import type { RetainedSolveResult, SolveEventSource, SolveSession, SolveSettlement } from '@selvajs/solve/client';
6
6
  import type { ThreeViewer } from '@selvajs/visualization/render';
7
7
  import { type ViewerConfig } from '../viewer/Viewer.svelte';
8
8
  import { type ClientSlot } from '../../contexts/clientSlotContext.svelte';
9
- import type { Locale } from '../../i18n/messages';
9
+ import type { Locale, ViewerMessages } from '../../i18n/messages';
10
10
  import type { Snippet } from 'svelte';
11
11
  interface Props {
12
12
  schema: UISchema;
@@ -18,8 +18,11 @@ interface Props {
18
18
  isEmbedded?: boolean;
19
19
  primaryColor?: string;
20
20
  showModeToggle?: boolean;
21
+ /** Host actions, listed in the panel footer's ⋯ menu after saving and loading inputs. */
21
22
  panelActions?: ActionButton[];
23
+ /** Lists "Save inputs…" in the footer menu. */
22
24
  showSaveButton?: boolean;
25
+ /** Lists "Load inputs…" in the footer menu. */
23
26
  showLoadButton?: boolean;
24
27
  /** When set, persist saved states via this callback instead of downloading a .slvp file. */
25
28
  onSaveState?: (state: ParameterPreset) => void | Promise<void>;
@@ -57,6 +60,12 @@ interface Props {
57
60
  */
58
61
  getSession: () => SolveSession;
59
62
  }) => void;
63
+ /**
64
+ * Once per settled solve: applied, discarded, blocked, aborted or failed. A result held
65
+ * for review counts when the user accepts or rejects it, and one a queued solve replaces
66
+ * never does. Auto-solves from input changes count too.
67
+ */
68
+ onSolved?: (settlement: SolveSettlement) => void;
60
69
  /** Hands the live three.js viewer to the host once it mounts. See `Viewer.svelte` for the contract. */
61
70
  onViewerReady?: (viewer: ThreeViewer) => void | (() => void);
62
71
  /** Viewer chrome and defaults. `backgroundColor` and `showSceneManager` are set by the layout. */
@@ -69,6 +78,14 @@ interface Props {
69
78
  externalScopeKey?: string;
70
79
  clientSlot?: ClientSlot;
71
80
  lang?: Locale;
81
+ /**
82
+ * Replaces individual strings of the app's own chrome, layered over the `lang` catalog
83
+ * (`VIEWER_MESSAGES`): reword a label, or supply every string for a language with no
84
+ * built-in catalog. Reaches every component inside, the viewer included. A `{name}`
85
+ * placeholder in the default is filled the same way in an override. The preset manager's
86
+ * strings come from `presetLabels` instead.
87
+ */
88
+ messages?: Partial<ViewerMessages>;
72
89
  }
73
90
  declare const ComputeApp: import("svelte").Component<Props, {}, "">;
74
91
  type ComputeApp = ReturnType<typeof ComputeApp>;