@selvajs/ui 6.0.0-beta.8 → 6.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.
Files changed (94) hide show
  1. package/README.md +5 -7
  2. package/dist/components/compute/AppLayout.svelte +3 -8
  3. package/dist/components/compute/AppLayout.svelte.d.ts +0 -2
  4. package/dist/components/compute/ComputeApp.svelte +30 -43
  5. package/dist/components/compute/ComputeApp.svelte.d.ts +11 -24
  6. package/dist/components/compute/ParameterPresetManager.svelte +4 -9
  7. package/dist/components/compute/ParameterPresetManager.svelte.d.ts +1 -1
  8. package/dist/components/layout/AppShell.svelte +5 -13
  9. package/dist/components/layout/AppShell.svelte.d.ts +1 -2
  10. package/dist/components/layout/EmptyState.svelte +2 -3
  11. package/dist/components/layout/PageFooter.svelte +0 -4
  12. package/dist/components/layout/index.js +0 -1
  13. package/dist/components/preview/Group.svelte +2 -5
  14. package/dist/components/preview/ImageOutput.svelte +5 -4
  15. package/dist/components/preview/InputControl.svelte +27 -28
  16. package/dist/components/preview/InputControl.svelte.d.ts +3 -3
  17. package/dist/components/preview/OutputDisplay.svelte +3 -6
  18. package/dist/components/preview/TabLayout.svelte +3 -3
  19. package/dist/components/preview/inputs/FileInput.svelte +9 -12
  20. package/dist/components/primitives/field/field-error.svelte +1 -3
  21. package/dist/components/primitives/index.js +0 -1
  22. package/dist/components/viewer/SceneManager.svelte +9 -20
  23. package/dist/components/viewer/SceneManager.svelte.d.ts +3 -3
  24. package/dist/components/viewer/Viewer.svelte +42 -63
  25. package/dist/components/viewer/Viewer.svelte.d.ts +8 -19
  26. package/dist/composables/useFooterItem.svelte.d.ts +3 -4
  27. package/dist/composables/useFooterItem.svelte.js +3 -3
  28. package/dist/compute/solving.svelte.d.ts +6 -6
  29. package/dist/compute/solving.svelte.js +6 -6
  30. package/dist/compute/useSolveSession.svelte.d.ts +5 -7
  31. package/dist/compute/useSolveSession.svelte.js +11 -24
  32. package/dist/constants.js +4 -4
  33. package/dist/contexts/clientSlotContext.svelte.d.ts +3 -5
  34. package/dist/contexts/footerContext.svelte.d.ts +6 -8
  35. package/dist/external/storage.js +2 -3
  36. package/dist/i18n/localeContext.svelte.d.ts +5 -7
  37. package/dist/i18n/localeContext.svelte.js +8 -15
  38. package/dist/i18n/messages.d.ts +0 -2
  39. package/dist/i18n/messages.js +3 -8
  40. package/dist/index.js +7 -13
  41. package/dist/public.js +14 -30
  42. package/dist/schema/defaults.js +2 -3
  43. package/dist/schema/dynamic-value-list.js +13 -15
  44. package/dist/schema/param-exporter.d.ts +7 -14
  45. package/dist/schema/param-exporter.js +6 -12
  46. package/dist/schema/traversal.js +3 -4
  47. package/dist/schema/visibility-rules.d.ts +4 -8
  48. package/dist/schema/visibility-rules.js +4 -8
  49. package/dist/types/actionButton.d.ts +1 -25
  50. package/dist/types/presetLabels.d.ts +2 -3
  51. package/dist/utils/color.js +6 -9
  52. package/dist/utils/debounce.d.ts +1 -11
  53. package/dist/utils/debounce.js +1 -11
  54. package/dist/utils/loadScript.d.ts +2 -7
  55. package/dist/utils/loadScript.js +2 -7
  56. package/package.json +17 -17
  57. package/src/lib/components/compute/AppLayout.svelte +3 -8
  58. package/src/lib/components/compute/ComputeApp.svelte +30 -43
  59. package/src/lib/components/compute/ParameterPresetManager.svelte +4 -9
  60. package/src/lib/components/layout/AppShell.svelte +5 -13
  61. package/src/lib/components/layout/EmptyState.svelte +2 -3
  62. package/src/lib/components/layout/PageFooter.svelte +0 -4
  63. package/src/lib/components/layout/index.ts +0 -1
  64. package/src/lib/components/preview/Group.svelte +2 -5
  65. package/src/lib/components/preview/ImageOutput.svelte +5 -4
  66. package/src/lib/components/preview/InputControl.svelte +27 -28
  67. package/src/lib/components/preview/OutputDisplay.svelte +3 -6
  68. package/src/lib/components/preview/TabLayout.svelte +3 -3
  69. package/src/lib/components/preview/inputs/FileInput.svelte +9 -12
  70. package/src/lib/components/primitives/field/field-error.svelte +1 -3
  71. package/src/lib/components/primitives/index.ts +0 -2
  72. package/src/lib/components/viewer/SceneManager.svelte +9 -20
  73. package/src/lib/components/viewer/Viewer.svelte +42 -63
  74. package/src/lib/composables/useFooterItem.svelte.ts +3 -4
  75. package/src/lib/compute/solving.svelte.ts +6 -6
  76. package/src/lib/compute/useSolveSession.svelte.ts +11 -24
  77. package/src/lib/constants.ts +4 -4
  78. package/src/lib/contexts/clientSlotContext.svelte.ts +7 -12
  79. package/src/lib/contexts/footerContext.svelte.ts +6 -8
  80. package/src/lib/external/storage.ts +2 -3
  81. package/src/lib/i18n/localeContext.svelte.ts +8 -17
  82. package/src/lib/i18n/messages.ts +3 -10
  83. package/src/lib/index.ts +8 -13
  84. package/src/lib/public.ts +22 -40
  85. package/src/lib/schema/defaults.ts +2 -3
  86. package/src/lib/schema/dynamic-value-list.ts +13 -15
  87. package/src/lib/schema/param-exporter.ts +9 -16
  88. package/src/lib/schema/traversal.ts +3 -4
  89. package/src/lib/schema/visibility-rules.ts +4 -8
  90. package/src/lib/types/actionButton.ts +1 -25
  91. package/src/lib/types/presetLabels.ts +2 -3
  92. package/src/lib/utils/color.ts +6 -9
  93. package/src/lib/utils/debounce.ts +1 -11
  94. package/src/lib/utils/loadScript.ts +2 -7
@@ -14,9 +14,9 @@
14
14
 
15
15
  interface Props {
16
16
  /**
17
- * Owned by `Viewer.svelte`, not built here: this panel unmounts when it is closed, and
18
- * hidden objects have to stay hidden — and keep being re-hidden after each solve — while it
19
- * is. Its state is backed by SvelteSets, so mutating one re-renders this list.
17
+ * Owned by `Viewer.svelte`, never built here: this panel unmounts when it closes, and hidden
18
+ * objects must stay hidden — and be re-hidden after each solve — while it is. Backed by
19
+ * SvelteSets, so mutating one re-renders this list.
20
20
  */
21
21
  outliner: SceneOutliner;
22
22
  sceneVersion?: number;
@@ -24,24 +24,20 @@
24
24
 
25
25
  let { outliner, sceneVersion = 0 }: Props = $props();
26
26
 
27
- // All list logic (content filtering, layer grouping, search, visibility, selection) lives in
28
- // @selvajs/visualization/scene. This component only renders it and forwards clicks.
29
27
  // Derived, not destructured: the prop is reassignable, and these must follow it.
30
28
  const hidden = $derived(outliner.visibility.hidden);
31
29
  const selected = $derived(outliner.selection.selected);
32
30
  const collapsed = $derived(outliner.collapsed);
33
31
 
34
- // Mirrored into runes because the outliner holds them as plain fields, not sets. Both start
35
- // fresh: the search box and the shift-anchor are panel state, so reopening the panel clears
36
- // them, while hiding and collapse persist in the outliner that outlives it.
32
+ // Mirrored into runes because the outliner holds these as plain fields, not sets. Both are panel
33
+ // state, so reopening clears them unlike hiding and collapse, which the outliner outlives.
37
34
  let searchQuery = $state('');
38
35
  let anchor = $state<string | null>(null);
39
36
 
40
37
  $effect(() => outliner.onAnchorChange((next) => (anchor = next)));
41
38
 
42
39
  // `scene.children` is a plain array the render layer mutates in place, so a solve bumping
43
- // `sceneVersion` is the only signal that content changed. Both derivations below read it, which
44
- // is what makes one walk per solve serve every template site that needs the list.
40
+ // `sceneVersion` is the only signal that content changed hence the `void` reads below.
45
41
  const sceneObjects = $derived.by(() => {
46
42
  void sceneVersion;
47
43
  return outliner.objects();
@@ -54,12 +50,9 @@
54
50
  return outliner.layerGroups();
55
51
  });
56
52
 
57
- // Hidden state is keyed by Grasshopper identity, not by uuid, so ask the outliner rather than
58
- // looking the object up in the set directly. Touching `hidden` registers the reactive read that
59
- // re-renders this row when the eye is clicked.
60
- // `SvelteSet.has()` is itself the reactive read, so go through the set rather than calling
61
- // `visibility.isHidden` — that reads the set through a plain reference inside the outliner and
62
- // would not re-render this row.
53
+ // `SvelteSet.has()` is the reactive read, so go through the set rather than calling
54
+ // `visibility.isHidden` that reaches the set through a plain reference inside the outliner
55
+ // and returns a correct value that never re-renders this row.
63
56
  const isObjectHidden = (object: THREE.Object3D) => hidden.has(getTrackingKey(object));
64
57
 
65
58
  // Same reason: count through the reactive set so the layer's tri-state eye tracks its objects.
@@ -165,7 +158,6 @@
165
158
  onkeydown={(e) =>
166
159
  e.key === 'Enter' && selectObject(object.uuid, e as unknown as MouseEvent)}
167
160
  >
168
- <!-- Object visibility toggle -->
169
161
  <button
170
162
  class="rounded p-1 shrink-0 transition-colors hover:bg-muted"
171
163
  onclick={(e) => {
@@ -182,7 +174,6 @@
182
174
  {/if}
183
175
  </button>
184
176
 
185
- <!-- Object name -->
186
177
  <span
187
178
  class="min-w-0 text-xs flex-1 truncate {isHidden
188
179
  ? 'text-muted-foreground line-through'
@@ -191,7 +182,6 @@
191
182
  {getObjectLabel(object)}
192
183
  </span>
193
184
 
194
- <!-- Type badge -->
195
185
  <span
196
186
  class="rounded px-1 py-0.5 font-medium shrink-0 bg-muted text-[9px] text-muted-foreground/70"
197
187
  >
@@ -203,7 +193,6 @@
203
193
  {/if}
204
194
  {/each}
205
195
 
206
- <!-- Empty state -->
207
196
  {#if sceneObjects.length === 0}
208
197
  <div class="py-12 flex flex-col items-center justify-center text-center">
209
198
  <EyeOff class="mb-2 h-5 w-5 text-muted-foreground/30" />
@@ -46,15 +46,12 @@
46
46
  showFullscreenButton?: boolean;
47
47
  showSceneManager?: boolean;
48
48
  showToolsMenu?: boolean;
49
- /** Expose the grid show/hide toggle in the tools menu. Grid starts hidden. */
50
49
  showGridToggle?: boolean;
51
- /**
52
- * Expose the "Display" submenu (render style picker + edges toggle) in the tools menu.
53
- * Defaults on. Starts on the 'technical' style with edges hidden.
54
- */
55
50
  showDisplayMenu?: boolean;
56
51
  enableMeshClick?: boolean;
57
52
  backgroundColor?: string;
53
+ /** Initial state of the edge overlay and its display-menu checkmark. Default false. */
54
+ showEdges?: boolean;
58
55
  }
59
56
 
60
57
  interface Props {
@@ -64,26 +61,18 @@
64
61
  isBlurred?: boolean;
65
62
  drawerOpen?: boolean;
66
63
  viewerConfig?: ViewerConfig;
67
- /**
68
- * Branding logo URL. When set, shown as a small watermark in the viewer's
69
- * bottom-right corner. Omitted/empty renders nothing.
70
- */
64
+ /** Shown as a watermark in the bottom-right corner. */
71
65
  logoUrl?: string;
72
66
  /**
73
67
  * Hands the live three.js viewer to the host once the canvas is up, for apps drawing their
74
- * own content `addUserGeometry` (survives solves), `tools.register` (claim clicks before
75
- * selection), `labelLayer`, `cameraController`.
76
- *
77
- * Return a cleanup function to tear down what you added; it runs before the viewer disposes.
78
- * Anything added outside a solve needs `viewer.invalidate()` to repaint — the render loop is
79
- * on-demand.
68
+ * own content. Return a cleanup function to tear down what you added; it runs before the
69
+ * viewer disposes. Anything added outside a solve needs `viewer.invalidate()` to repaint —
70
+ * the render loop is on-demand.
80
71
  */
81
72
  onViewerReady?: (viewer: ThreeViewer) => void | (() => void);
82
73
  /**
83
- * UI language for the viewer's own chrome (tools menu, panels, dialogs).
84
- * When set, the viewer provides it to its subtree. When omitted, the viewer
85
- * reads the nearest locale context (set by the host app), defaulting to
86
- * English. Does not translate Grasshopper-sourced names/metadata.
74
+ * UI language for the viewer's own chrome. Omitted, the viewer reads the nearest locale
75
+ * context. Does not translate Grasshopper-sourced names/metadata.
87
76
  */
88
77
  lang?: Locale;
89
78
  }
@@ -96,7 +85,8 @@
96
85
  showGridToggle: true,
97
86
  showDisplayMenu: true,
98
87
  enableMeshClick: true,
99
- backgroundColor: '#E6E6E6'
88
+ backgroundColor: '#E6E6E6',
89
+ showEdges: false
100
90
  };
101
91
 
102
92
  let {
@@ -113,13 +103,10 @@
113
103
 
114
104
  const config = $derived({ ...defaultViewerConfig, ...viewerConfig });
115
105
 
116
- // Read any host-provided locale before we (maybe) override it for our subtree.
106
+ // Read the host's locale before overriding it for our subtree.
117
107
  const hostLocale = getLocaleContext();
118
108
 
119
- // Resolution order: explicit `lang` prop host locale context → default.
120
- // Provide the resolved value to our subtree so the scene manager and metadata
121
- // dialog read the same locale. The getter is re-read reactively, so switching
122
- // the `lang` prop (or the host's locale) updates the chrome live.
109
+ // A getter, not a value: it is re-read reactively, so changing `lang` updates the chrome live.
123
110
  setLocaleContext(() => lang ?? hostLocale.locale);
124
111
  const locale = getLocaleContext();
125
112
  const t = $derived(locale.messages);
@@ -142,10 +129,8 @@
142
129
  let hideButton = $state(false);
143
130
  let sceneManagerOpen = $state(false);
144
131
 
145
- // The scene outliner lives here rather than inside <SceneManager> because that component is
146
- // mounted only while its panel is open. Hidden objects must stay hidden when the panel is
147
- // closed — and must be re-hidden after each solve, which nothing would do if the state
148
- // unmounted with the panel.
132
+ // The outliner lives here, not in <SceneManager>: that component mounts only while its panel is
133
+ // open, and hidden objects must stay hidden and be re-hidden after each solve — while it is closed.
149
134
  const hiddenObjects = new SvelteSet<string>();
150
135
  const selectedObjects = new SvelteSet<string>();
151
136
  const collapsedLayers = new SvelteSet<string>();
@@ -153,15 +138,13 @@
153
138
  let projection: CameraProjection = $state('perspective');
154
139
  let measureActive = $state(false);
155
140
  let gridVisible = $state(false);
156
- // Render style + edge overlays. 'technical' is the default look; edges (crease lines) start off —
157
- // both are user-switchable via the Display submenu.
158
141
  let renderStyle: Look = $state('technical');
159
- let edgesVisible = $state(false);
142
+ // Seeded once, then owned by the menu toggle.
143
+ let edgesVisible = $state(untrack(() => viewerConfig.showEdges) ?? defaultViewerConfig.showEdges);
160
144
  let selectedMeshMetadata: Record<string, any> | null = $state(null);
161
145
  let selectedMeshName: string | null = $state(null);
162
146
 
163
- // Render-style options for the Display submenu, derived from the library's LOOKS array adding or
164
- // renaming a look in @selvajs/compute updates this automatically. Label is the value capitalized.
147
+ // Derived from LOOKS so adding a look in @selvajs/visualization shows up here with no edit.
165
148
  const STYLE_OPTIONS: { look: Look; label: string }[] = LOOKS.map((look) => ({
166
149
  look,
167
150
  label: look.charAt(0).toUpperCase() + look.slice(1)
@@ -192,16 +175,13 @@
192
175
  onMount(() => {
193
176
  if (!canvas) return;
194
177
 
195
- // Only options that differ from the library defaults. Seed the initial render style (also the
196
- // library default, but stated explicitly since it's user-switchable via the Display menu) and
197
- // switch off the sun/shadows the technical look doesn't need — flat ambient + HDR image-based
198
- // lighting (baseHDR loads by default) carry it. Grid/measure/click are the tools this viewer uses.
178
+ // Only what differs from the library defaults. The sun and shadows are off because the
179
+ // technical look doesn't need them flat ambient plus the HDR environment carry it.
199
180
  const opts: ThreeInitializerOptions = {
200
181
  look: renderStyle,
201
182
  lighting: { enableSunlight: false },
202
183
  render: { enableShadows: false },
203
184
  environment: { backgroundColor: config.backgroundColor },
204
- // Build the grid so it can be toggled at runtime, but start hidden (off by default).
205
185
  grid: { enabled: config.showToolsMenu && config.showGridToggle },
206
186
  measure: { enabled: config.showToolsMenu },
207
187
  events: {
@@ -237,8 +217,7 @@
237
217
 
238
218
  const renderer = init.renderer;
239
219
 
240
- // Untracked: the host's setup runs once against this canvas, and reading `meshes` or config
241
- // inside it must not re-run onMount's teardown.
220
+ // Untracked so the host reading `meshes` or config inside its setup can't re-run onMount's teardown.
242
221
  const hostCleanup = untrack(() => onViewerReady?.(init));
243
222
 
244
223
  return () => {
@@ -280,9 +259,8 @@
280
259
  setLook(look);
281
260
  }
282
261
 
283
- // Add/remove crease-edge overlays on the current scene content. applyEdges is idempotent per mesh
284
- // (and attaches large meshes' overlays async, off the main thread); clearEdges is its inverse
285
- // it also cancels in-flight attaches and stands down the screen-space fallback for capped meshes.
262
+ // `applyEdges` is idempotent per mesh, so the repeated calls after each solve add no duplicate
263
+ // overlays; `clearEdges` is its inverse.
286
264
  function applyEdgeState() {
287
265
  if (!scene) return;
288
266
  if (edgesVisible) applyEdges?.(scene);
@@ -297,18 +275,18 @@
297
275
  $effect(() => {
298
276
  if (scene && camera && controls) {
299
277
  updateScene(scene, meshes, camera, controls, viewerInitialized);
300
- // updateScene clears and re-adds all content each solve, so the previous solve's edge
301
- // overlays are gone re-attach them if edges are currently shown. Read the flag untracked:
302
- // toggling edges is handled directly by toggleEdges(), so it must not re-trigger a full solve.
278
+ // Untracked because toggleEdges() already handles the toggle directly reading
279
+ // `edgesVisible` tracked here would re-trigger a full solve.
303
280
  untrack(() => {
281
+ // updateScene discarded the previous solve's overlays along with its content.
304
282
  if (edgesVisible) applyEdges?.(scene!);
305
- // Rescale the grid to the new content's extent so cells and fade match the part size.
283
+ // Rescale the grid so cells and fade match the new content's extent.
306
284
  updateGridScale?.();
307
- // The rebuild above also un-hid everything the user had hidden. Re-hide it: the outliner
308
- // keys that state on Grasshopper identity, not on the instances just discarded.
285
+ // The rebuild un-hid everything; the outliner keys hidden state on Grasshopper
286
+ // identity, not on the instances just discarded, so it can re-hide it.
309
287
  outliner?.applyTo();
310
288
  sceneVersion++;
311
- // New solve content — repaint now rather than on the render loop's safety interval.
289
+ // Repaint now rather than on the render loop's safety interval.
312
290
  invalidate?.();
313
291
  });
314
292
 
@@ -373,11 +351,19 @@
373
351
  <!-- `defaultSize` must sum to 100 across the live panes. Panes register a frame before the
374
352
  group recomputes its layout, so a sum of 115 renders one frame at the raw flex-grow ratio
375
353
  and is then renormalized — and if the recompute short-circuits on an equal layout, the
376
- scene pane keeps a sliver of its intended width. Hence 85 + 15, and the explicit
377
- id/order so a conditionally-rendered pane keeps its slot. -->
354
+ scene pane keeps a sliver of its intended width. Hence 85 + 15, and the explicit id/order
355
+ so a conditionally-rendered pane keeps its slot. -->
378
356
  <Resizable.Pane id="viewport" order={1} defaultSize={sceneManagerOpen ? 85 : 100} minSize={40}>
379
357
  <div class="relative h-full w-full" style="touch-action: none;">
380
- <canvas class="block h-full w-full" bind:this={canvas}></canvas>
358
+ <!-- Mesh count of the geometry currently in the scene. The canvas itself is opaque to
359
+ the DOM, so this is the only observable proof a solve's geometry decoded and
360
+ rendered; e2e asserts on it. -->
361
+ <canvas
362
+ class="block h-full w-full"
363
+ data-testid="viewer-canvas"
364
+ data-mesh-count={meshes.length}
365
+ bind:this={canvas}
366
+ ></canvas>
381
367
 
382
368
  <div
383
369
  class="inset-0 blur-overlay absolute z-20 {isBlurred
@@ -392,9 +378,8 @@
392
378
  {/if}
393
379
 
394
380
  {#if logoUrl}
395
- <!-- Branding watermark, bottom-right. Matches the tools menu's
396
- bottom offset so it clears the mobile drawer handle, and is
397
- non-interactive so it never intercepts canvas drags. -->
381
+ <!-- Bottom offset matches the tools menu so it clears the mobile drawer handle.
382
+ Always pointer-events:none so it never intercepts canvas drags. -->
398
383
  <div
399
384
  class="right-4 {isFullscreen ? 'bottom-4' : 'bottom-16 sm:bottom-4'} absolute z-20"
400
385
  style={hideButton
@@ -434,7 +419,6 @@
434
419
  align="start"
435
420
  class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 min-w-44 p-1 shadow-md z-10001 rounded-md border bg-popover text-popover-foreground"
436
421
  >
437
- <!-- Camera -->
438
422
  <DropdownMenu.Item class={itemClass} onSelect={toggleProjection}>
439
423
  {#if projection === 'perspective'}
440
424
  <Square class="h-4 w-4" />
@@ -482,7 +466,6 @@
482
466
  sideOffset={4}
483
467
  class="min-w-40 p-1 shadow-md z-10001 rounded-md border bg-popover text-popover-foreground"
484
468
  >
485
- <!-- Render style: single-choice, current one checked. -->
486
469
  {#each STYLE_OPTIONS as { look, label } (look)}
487
470
  <DropdownMenu.Item
488
471
  closeOnSelect={false}
@@ -498,7 +481,6 @@
498
481
 
499
482
  <DropdownMenu.Separator class="my-1 h-px bg-border" />
500
483
 
501
- <!-- Edges overlay toggle. -->
502
484
  <DropdownMenu.Item
503
485
  closeOnSelect={false}
504
486
  class="{itemClass} {edgesVisible ? 'text-primary' : ''}"
@@ -540,7 +522,6 @@
540
522
  </DropdownMenu.Item>
541
523
  {/if}
542
524
 
543
- <!-- Scene tools -->
544
525
  {#if config.showSceneManager || config.showScreenshotButton || config.showFullscreenButton}
545
526
  <DropdownMenu.Separator class="my-1 h-px bg-border" />
546
527
  {/if}
@@ -585,7 +566,6 @@
585
566
  </div>
586
567
  </Resizable.Pane>
587
568
 
588
- <!-- Scene Manager Pane -->
589
569
  {#if sceneManagerOpen && scene && outliner}
590
570
  <Resizable.Handle withHandle />
591
571
  <Resizable.Pane id="scene-manager" order={2} defaultSize={15} minSize={8} maxSize={30}>
@@ -618,7 +598,6 @@
618
598
  border-radius: 0 !important;
619
599
  }
620
600
 
621
- /* Blur overlay animation */
622
601
  .blur-overlay {
623
602
  pointer-events: none;
624
603
  }
@@ -5,7 +5,6 @@ import {
5
5
  FOOTER_CONTEXT_KEY
6
6
  } from '$lib/contexts/footerContext.svelte';
7
7
 
8
- /** Config for a footer-item registration; generic over the component's props. */
9
8
  export type FooterItemConfig<P extends Record<string, unknown> = Record<string, unknown>> = Omit<
10
9
  FooterItem<P>,
11
10
  'position' | 'priority'
@@ -15,9 +14,9 @@ export type FooterItemConfig<P extends Record<string, unknown> = Record<string,
15
14
  /**
16
15
  * Register a footer item for the lifetime of the calling component.
17
16
  *
18
- * Registration is deferred to onMount (client-side only) to avoid SSR context errors;
19
- * it's unregistered on destroy. A no-op when no footer context is present (e.g. a
20
- * component rendered outside the root layout) or no component is supplied.
17
+ * Registration waits for onMount so it never runs during SSR, where reading the context
18
+ * throws. A no-op when no footer context is present (a component rendered outside the root
19
+ * layout) or no component is supplied.
21
20
  */
22
21
  export function useFooterItem<P extends Record<string, unknown>>(config: FooterItemConfig<P>) {
23
22
  onMount(() => {
@@ -2,13 +2,13 @@ import { untrack } from 'svelte';
2
2
  import { APP_DEFAULTS } from '../constants';
3
3
 
4
4
  /**
5
- * Creates an adaptive solving indicator that measures actual solve durations
6
- * and adjusts its visibility delay accordingly.
5
+ * Adaptive solving indicator: measures solve durations and adjusts its
6
+ * visibility delay from the running average.
7
7
  *
8
- * - Fast solves (<200ms avg): indicator never shown (avoids flicker)
9
- * - Slow solves (>600ms avg): indicator shown immediately
10
- * - In between: indicator shown after a proportional delay
11
- * - First solve: shows immediately (no history yet)
8
+ * - Average under FAST_THRESHOLD_MS: never shown, so quick solves don't flicker
9
+ * - Average over SLOW_THRESHOLD_MS: shown immediately
10
+ * - Between the two: shown after a proportional slice of ANIMATION_DELAY
11
+ * - First solve: shown immediately, there is no history yet
12
12
  */
13
13
  export function createSolvingIndicator(isSolving: () => boolean): { readonly show: boolean } {
14
14
  const { FAST_THRESHOLD_MS, SLOW_THRESHOLD_MS, HISTORY_SIZE, ANIMATION_DELAY } =
@@ -1,40 +1,27 @@
1
- // The Svelte binding for a Solve Session.
2
- //
3
- // The session itself lives in `@selvajs/solve/client` and is framework-free: it
4
- // exposes plain getters plus a `subscribe()` seam. That makes it usable headless, but a
5
- // plain getter read inside Svelte markup is NOT reactive — nothing tells the component to
6
- // re-run. This adapter closes that gap: it subscribes once, bumps a `$state` version
7
- // counter on every notification, and reads that counter inside each getter so any
8
- // component touching one re-runs when the session changes.
9
- //
10
- // The counter (rather than mirroring each field into its own `$state`) keeps this a pure
11
- // republish: no field list to keep in sync as the session grows, and no risk of a mirrored
12
- // copy drifting from the source of truth.
1
+ // A version counter, rather than mirroring each field into its own `$state`, keeps this a
2
+ // pure republish: no field list to keep in sync as the session grows, and no mirrored copy
3
+ // that can drift from the source of truth.
13
4
 
14
5
  import type { SolveSession, SolveSessionArgs } from '@selvajs/solve/client';
15
6
  import { createSolveSession } from '@selvajs/solve/client';
16
7
 
17
8
  /**
18
- * Wraps a Solve Session so its state reads reactively inside Svelte components.
9
+ * Wraps a Solve Session so its state reads reactively inside Svelte components. Use this in
10
+ * a component, never `createSolveSession` directly: the raw session's getters return correct
11
+ * values but never re-render.
19
12
  *
20
- * Returns the same `SolveSession` surfaceevery method delegates untouched, and every
21
- * getter additionally depends on the version counter. Callers use it exactly like the
22
- * session it wraps.
23
- *
24
- * Must be called during component initialization (it uses `$effect` to manage the
25
- * subscription, so teardown follows the owning component's lifecycle).
13
+ * Must be called during component initialization the subscription is managed by `$effect`,
14
+ * so teardown follows the owning component's lifecycle.
26
15
  */
27
16
  export function useSolveSession(args: SolveSessionArgs): SolveSession {
28
17
  const session = createSolveSession(args);
29
18
 
30
- // Bumped on every session notification. Reading it inside a getter is what registers
31
- // the dependency; the value itself is never meaningful.
19
+ // Reading this inside a getter is what registers the dependency; the value never matters.
32
20
  let version = $state(0);
33
21
 
34
22
  $effect(() => {
35
- // Re-read on mount and unsubscribe on teardown. The session outlives no component
36
- // here — it is created alongside this adapter — so dropping the subscription is the
37
- // whole cleanup.
23
+ // The session is created alongside this adapter and outlives no other component, so
24
+ // dropping the subscription is the whole cleanup.
38
25
  return session.subscribe(() => {
39
26
  version += 1;
40
27
  });
@@ -1,10 +1,10 @@
1
1
  export const APP_DEFAULTS = {
2
2
  // File upload limits
3
3
  // TEMP (dev): raised 150 MB → 300 MB so large dev file-widget inputs aren't
4
- // blocked client-side. The server request cap (COMPUTE_REQUEST_MAX_BYTES) was
5
- // bumped to 300 MB to match, but base64 inflates a raw file by ~4/3, so a
6
- // full 300 MB upload still won't fit the 300 MB request body. Revert to 150
7
- // before release.
4
+ // blocked client-side. The server request cap (COMPUTE_REQUEST_MAX_BYTES)
5
+ // still defaults to 256 MB, and base64 inflates a raw file by ~4/3, so
6
+ // anything over ~192 MB is rejected with a 413 after the client accepts it.
7
+ // Revert to 150 before release.
8
8
  FILE_UPLOAD: {
9
9
  MAX_SIZE_MB: 300,
10
10
  MAX_SIZE_BYTES: 300 * 1024 * 1024
@@ -3,27 +3,22 @@ import type { Snippet } from 'svelte';
3
3
  import type { SupportedTypes } from '@selvajs/schemas';
4
4
 
5
5
  // Carries the host app's slot renderer down to InputControl without threading a
6
- // prop through every layout layer (ComputeApp → AppLayout → TabLayout →
7
- // TabContent → Group → InputControl).
6
+ // prop through every layout layer.
8
7
  //
9
8
  // An input with source.kind === 'client' and source.client.presentation === 'slot'
10
- // reserves its cell but renders nothing itself. Instead Selva invokes this snippet
11
- // so the host can render its own element (e.g. an "Edit JSON" button, or a custom
12
- // picker). Selva never interprets what the host renders. The host may COMMIT a value
13
- // back via `onValueChange`, which flows into the solve exactly like any built-in
14
- // widget's change.
9
+ // reserves its cell but renders nothing itself. Selva invokes this snippet instead
10
+ // so the host can render its own element (an "Edit JSON" button, a custom picker)
11
+ // and never interprets what comes back.
15
12
 
16
13
  export interface ClientSlotArgs {
17
14
  /** Grasshopper parameter instance GUID (LayoutItem.paramId / SchemaInput.id). */
18
15
  inputId: string;
19
16
  displayName: string;
20
- /** The current value held for this input (e.g. the prefilled JSON), if any. */
21
17
  value: unknown;
22
18
  /**
23
- * Commit a value for this input. Identical channel to a built-in widget's change
24
- * the value lands in the solve session and is sent to Compute on the next solve.
25
- * `forceSolve` requests a solve even in manual-solve mode (system reconciliation).
26
- * Lets a slot be an interactive control (a custom picker), not just a display cell.
19
+ * Commit a value for this input the same channel a built-in widget's change uses, so a
20
+ * slot can be an interactive control rather than just a display cell. `forceSolve`
21
+ * requests a solve even in manual-solve mode.
27
22
  */
28
23
  onValueChange: (value: SupportedTypes, forceSolve?: boolean) => void;
29
24
  }
@@ -2,19 +2,17 @@ import { getContext, setContext, type Component } from 'svelte';
2
2
  import { SvelteMap } from 'svelte/reactivity';
3
3
 
4
4
  /**
5
- * A footer item registered by a descendant of the root layout. `component` + `position`
6
- * are fixed at registration; the renderer re-invokes `getProps` reactively, so return
7
- * reactive state from it (e.g. `() => ({ status: myState.status })`) to keep the footer
8
- * in sync.
9
- *
10
- * Generic over the component's props `P` so registration is type-checked at the call
11
- * site. The store holds a heterogeneous mix of items, so its map widens `P` to
5
+ * Generic over the component's props `P` so registration is type-checked at the call site.
6
+ * The store holds a heterogeneous mix of items, so its map widens `P` to
12
7
  * `Record<string, unknown>` at the boundary.
13
8
  */
14
9
  export interface FooterItem<P extends Record<string, unknown> = Record<string, unknown>> {
15
10
  id: string;
16
11
  component: Component<P>;
17
- /** Called on every render — return reactive state to keep footer in sync. */
12
+ /**
13
+ * Re-invoked on every render. Return reactive state — `() => ({ status: s.status })` —
14
+ * or the footer never updates.
15
+ */
18
16
  getProps: () => P;
19
17
  position: 'left' | 'right';
20
18
  priority: number;
@@ -1,6 +1,5 @@
1
- // Moved to `@selvajs/solve/client`, where the Solve Session that hydrates from it
2
- // now lives. Re-exported here to keep the published `@selvajs/ui/external` sub-path and
3
- // the pre-step producer routes that import it — working unchanged.
1
+ // Moved to @selvajs/solve/client, beside the Solve Session that hydrates from it.
2
+ // Re-exported to keep the published `@selvajs/ui/external` sub-path working unchanged.
4
3
 
5
4
  export {
6
5
  writeExternalValue,
@@ -1,14 +1,8 @@
1
1
  import { getContext, setContext } from 'svelte';
2
2
  import { type Locale, type ViewerMessages, messagesFor, DEFAULT_LOCALE } from './messages';
3
3
 
4
- // ============================================================================
5
- // Viewer locale context
6
- // ============================================================================
7
- //
8
- // Carries the current UI locale down to the viewer and its panels without
9
- // threading a `lang` prop through every layer. The value is a getter so the
10
- // host can back it with reactive state — flip the language and the viewer
11
- // re-renders live.
4
+ // Carries the UI locale down to the viewer and its panels without threading a
5
+ // `lang` prop through every layer.
12
6
  //
13
7
  // Resolution order for any consuming component:
14
8
  // explicit `lang` prop → nearest locale context → English default
@@ -16,21 +10,19 @@ import { type Locale, type ViewerMessages, messagesFor, DEFAULT_LOCALE } from '.
16
10
  // Two ways to provide it:
17
11
  // - Standalone: <Viewer lang="de" /> — Viewer provides the context itself.
18
12
  // - In an app: call setLocaleContext(() => app.locale) once at the root; the
19
- // viewer (and anything else) reads it. selva later wires its Paraglide
20
- // locale in here.
13
+ // viewer (and anything else) reads it.
21
14
 
22
15
  const LOCALE_CONTEXT_KEY = Symbol('viewer-locale-context');
23
16
 
24
17
  export interface LocaleContext {
25
- /** Current locale. Called reactively — return reactive state to enable live switching. */
26
18
  readonly locale: Locale;
27
- /** Resolved message catalog for the current locale. */
28
19
  readonly messages: ViewerMessages;
29
20
  }
30
21
 
31
22
  /**
32
- * Provide the locale to descendants. Pass a getter so a reactive source (a
33
- * `$state`, a store, the app's Paraglide locale) keeps consumers in sync.
23
+ * Provide the locale to descendants. Pass a getter, not a value: it is re-read on every
24
+ * consumer render, so a reactive source (a `$state`, the app's Paraglide locale) switches
25
+ * the language live.
34
26
  */
35
27
  export function setLocaleContext(getLocale: () => Locale | undefined): void {
36
28
  const ctx: LocaleContext = {
@@ -45,9 +37,8 @@ export function setLocaleContext(getLocale: () => Locale | undefined): void {
45
37
  }
46
38
 
47
39
  /**
48
- * Read the locale context. Falls back to an English-only context when no
49
- * provider exists (e.g. a primitive used in isolation), so consumers never
50
- * need a null check.
40
+ * Falls back to an English-only context when no provider exists (a primitive used in
41
+ * isolation), so consumers never need a null check.
51
42
  */
52
43
  export function getLocaleContext(): LocaleContext {
53
44
  return (
@@ -2,14 +2,9 @@
2
2
  // Viewer message catalog (library-owned strings)
3
3
  // ============================================================================
4
4
  //
5
- // These are the strings @selvajs/ui renders itself in the 3D viewer and its
6
- // panelstool menu, view presets, scene manager, metadata dialog. They are
7
- // NOT the strings that come from a Grasshopper definition (mesh/layer/metadata
8
- // names): those live in the user's .gh file and can't be translated here.
9
- //
10
- // The library ships English + German. A host app can switch locale at runtime
11
- // via the locale context (see ./localeContext.svelte.ts) — e.g. selva feeds its
12
- // own Paraglide locale in. With no provider, components fall back to English.
5
+ // Only the strings @selvajs/ui renders itself. Names coming from a Grasshopper
6
+ // definitionmeshes, layers, metadata keys live in the user's .gh file and
7
+ // cannot be translated here.
13
8
 
14
9
  export type Locale = 'en' | 'de';
15
10
 
@@ -22,9 +17,7 @@ export interface ViewerMessages {
22
17
  views: string;
23
18
  measure: string;
24
19
  grid: string;
25
- /** "Display" submenu label — groups render style + edges. */
26
20
  display: string;
27
- /** Edges show/hide toggle label. */
28
21
  edges: string;
29
22
  sceneManager: string;
30
23
  screenshot: string;