@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
@@ -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,15 +5,12 @@ export interface ViewerConfig {
5
5
  showFullscreenButton?: boolean;
6
6
  showSceneManager?: boolean;
7
7
  showToolsMenu?: boolean;
8
- /** Expose the grid show/hide toggle in the tools menu. Grid starts hidden. */
9
8
  showGridToggle?: boolean;
10
- /**
11
- * Expose the "Display" submenu (render style picker + edges toggle) in the tools menu.
12
- * Defaults on. Starts on the 'technical' style with edges hidden.
13
- */
14
9
  showDisplayMenu?: boolean;
15
10
  enableMeshClick?: boolean;
16
11
  backgroundColor?: string;
12
+ /** Initial state of the edge overlay and its display-menu checkmark. Default false. */
13
+ showEdges?: boolean;
17
14
  }
18
15
  interface Props {
19
16
  meshes: any[];
@@ -22,26 +19,18 @@ interface Props {
22
19
  isBlurred?: boolean;
23
20
  drawerOpen?: boolean;
24
21
  viewerConfig?: ViewerConfig;
25
- /**
26
- * Branding logo URL. When set, shown as a small watermark in the viewer's
27
- * bottom-right corner. Omitted/empty renders nothing.
28
- */
22
+ /** Shown as a watermark in the bottom-right corner. */
29
23
  logoUrl?: string;
30
24
  /**
31
25
  * Hands the live three.js viewer to the host once the canvas is up, for apps drawing their
32
- * own content `addUserGeometry` (survives solves), `tools.register` (claim clicks before
33
- * selection), `labelLayer`, `cameraController`.
34
- *
35
- * Return a cleanup function to tear down what you added; it runs before the viewer disposes.
36
- * Anything added outside a solve needs `viewer.invalidate()` to repaint — the render loop is
37
- * on-demand.
26
+ * own content. Return a cleanup function to tear down what you added; it runs before the
27
+ * viewer disposes. Anything added outside a solve needs `viewer.invalidate()` to repaint —
28
+ * the render loop is on-demand.
38
29
  */
39
30
  onViewerReady?: (viewer: ThreeViewer) => void | (() => void);
40
31
  /**
41
- * UI language for the viewer's own chrome (tools menu, panels, dialogs).
42
- * When set, the viewer provides it to its subtree. When omitted, the viewer
43
- * reads the nearest locale context (set by the host app), defaulting to
44
- * English. Does not translate Grasshopper-sourced names/metadata.
32
+ * UI language for the viewer's own chrome. Omitted, the viewer reads the nearest locale
33
+ * context. Does not translate Grasshopper-sourced names/metadata.
45
34
  */
46
35
  lang?: Locale;
47
36
  }
@@ -1,13 +1,12 @@
1
1
  import { type Component } from 'svelte';
2
2
  import { type FooterItem } from '../contexts/footerContext.svelte';
3
- /** Config for a footer-item registration; generic over the component's props. */
4
3
  export type FooterItemConfig<P extends Record<string, unknown> = Record<string, unknown>> = Omit<FooterItem<P>, 'position' | 'priority'> & Partial<Pick<FooterItem<P>, 'position' | 'priority'>>;
5
4
  /**
6
5
  * Register a footer item for the lifetime of the calling component.
7
6
  *
8
- * Registration is deferred to onMount (client-side only) to avoid SSR context errors;
9
- * it's unregistered on destroy. A no-op when no footer context is present (e.g. a
10
- * component rendered outside the root layout) or no component is supplied.
7
+ * Registration waits for onMount so it never runs during SSR, where reading the context
8
+ * throws. A no-op when no footer context is present (a component rendered outside the root
9
+ * layout) or no component is supplied.
11
10
  */
12
11
  export declare function useFooterItem<P extends Record<string, unknown>>(config: FooterItemConfig<P>): void;
13
12
  export type { Component };
@@ -3,9 +3,9 @@ import { FOOTER_CONTEXT_KEY } from '../contexts/footerContext.svelte';
3
3
  /**
4
4
  * Register a footer item for the lifetime of the calling component.
5
5
  *
6
- * Registration is deferred to onMount (client-side only) to avoid SSR context errors;
7
- * it's unregistered on destroy. A no-op when no footer context is present (e.g. a
8
- * component rendered outside the root layout) or no component is supplied.
6
+ * Registration waits for onMount so it never runs during SSR, where reading the context
7
+ * throws. A no-op when no footer context is present (a component rendered outside the root
8
+ * layout) or no component is supplied.
9
9
  */
10
10
  export function useFooterItem(config) {
11
11
  onMount(() => {
@@ -1,11 +1,11 @@
1
1
  /**
2
- * Creates an adaptive solving indicator that measures actual solve durations
3
- * and adjusts its visibility delay accordingly.
2
+ * Adaptive solving indicator: measures solve durations and adjusts its
3
+ * visibility delay from the running average.
4
4
  *
5
- * - Fast solves (<200ms avg): indicator never shown (avoids flicker)
6
- * - Slow solves (>600ms avg): indicator shown immediately
7
- * - In between: indicator shown after a proportional delay
8
- * - First solve: shows immediately (no history yet)
5
+ * - Average under FAST_THRESHOLD_MS: never shown, so quick solves don't flicker
6
+ * - Average over SLOW_THRESHOLD_MS: shown immediately
7
+ * - Between the two: shown after a proportional slice of ANIMATION_DELAY
8
+ * - First solve: shown immediately, there is no history yet
9
9
  */
10
10
  export declare function createSolvingIndicator(isSolving: () => boolean): {
11
11
  readonly show: boolean;
@@ -1,13 +1,13 @@
1
1
  import { untrack } from 'svelte';
2
2
  import { APP_DEFAULTS } from '../constants';
3
3
  /**
4
- * Creates an adaptive solving indicator that measures actual solve durations
5
- * and adjusts its visibility delay accordingly.
4
+ * Adaptive solving indicator: measures solve durations and adjusts its
5
+ * visibility delay from the running average.
6
6
  *
7
- * - Fast solves (<200ms avg): indicator never shown (avoids flicker)
8
- * - Slow solves (>600ms avg): indicator shown immediately
9
- * - In between: indicator shown after a proportional delay
10
- * - First solve: shows immediately (no history yet)
7
+ * - Average under FAST_THRESHOLD_MS: never shown, so quick solves don't flicker
8
+ * - Average over SLOW_THRESHOLD_MS: shown immediately
9
+ * - Between the two: shown after a proportional slice of ANIMATION_DELAY
10
+ * - First solve: shown immediately, there is no history yet
11
11
  */
12
12
  export function createSolvingIndicator(isSolving) {
13
13
  const { FAST_THRESHOLD_MS, SLOW_THRESHOLD_MS, HISTORY_SIZE, ANIMATION_DELAY } = APP_DEFAULTS.SOLVING_INDICATOR;
@@ -1,12 +1,10 @@
1
1
  import type { SolveSession, SolveSessionArgs } from '@selvajs/solve/client';
2
2
  /**
3
- * Wraps a Solve Session so its state reads reactively inside Svelte components.
3
+ * Wraps a Solve Session so its state reads reactively inside Svelte components. Use this in
4
+ * a component, never `createSolveSession` directly: the raw session's getters return correct
5
+ * values but never re-render.
4
6
  *
5
- * Returns the same `SolveSession` surfaceevery method delegates untouched, and every
6
- * getter additionally depends on the version counter. Callers use it exactly like the
7
- * session it wraps.
8
- *
9
- * Must be called during component initialization (it uses `$effect` to manage the
10
- * subscription, so teardown follows the owning component's lifecycle).
7
+ * Must be called during component initialization the subscription is managed by `$effect`,
8
+ * so teardown follows the owning component's lifecycle.
11
9
  */
12
10
  export declare function useSolveSession(args: SolveSessionArgs): SolveSession;
@@ -1,35 +1,22 @@
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
  import { createSolveSession } from '@selvajs/solve/client';
14
5
  /**
15
- * Wraps a Solve Session so its state reads reactively inside Svelte components.
6
+ * Wraps a Solve Session so its state reads reactively inside Svelte components. Use this in
7
+ * a component, never `createSolveSession` directly: the raw session's getters return correct
8
+ * values but never re-render.
16
9
  *
17
- * Returns the same `SolveSession` surfaceevery method delegates untouched, and every
18
- * getter additionally depends on the version counter. Callers use it exactly like the
19
- * session it wraps.
20
- *
21
- * Must be called during component initialization (it uses `$effect` to manage the
22
- * subscription, so teardown follows the owning component's lifecycle).
10
+ * Must be called during component initialization the subscription is managed by `$effect`,
11
+ * so teardown follows the owning component's lifecycle.
23
12
  */
24
13
  export function useSolveSession(args) {
25
14
  const session = createSolveSession(args);
26
- // Bumped on every session notification. Reading it inside a getter is what registers
27
- // the dependency; the value itself is never meaningful.
15
+ // Reading this inside a getter is what registers the dependency; the value never matters.
28
16
  let version = $state(0);
29
17
  $effect(() => {
30
- // Re-read on mount and unsubscribe on teardown. The session outlives no component
31
- // here — it is created alongside this adapter — so dropping the subscription is the
32
- // whole cleanup.
18
+ // The session is created alongside this adapter and outlives no other component, so
19
+ // dropping the subscription is the whole cleanup.
33
20
  return session.subscribe(() => {
34
21
  version += 1;
35
22
  });
package/dist/constants.js CHANGED
@@ -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
@@ -4,13 +4,11 @@ export interface ClientSlotArgs {
4
4
  /** Grasshopper parameter instance GUID (LayoutItem.paramId / SchemaInput.id). */
5
5
  inputId: string;
6
6
  displayName: string;
7
- /** The current value held for this input (e.g. the prefilled JSON), if any. */
8
7
  value: unknown;
9
8
  /**
10
- * Commit a value for this input. Identical channel to a built-in widget's change
11
- * the value lands in the solve session and is sent to Compute on the next solve.
12
- * `forceSolve` requests a solve even in manual-solve mode (system reconciliation).
13
- * Lets a slot be an interactive control (a custom picker), not just a display cell.
9
+ * Commit a value for this input the same channel a built-in widget's change uses, so a
10
+ * slot can be an interactive control rather than just a display cell. `forceSolve`
11
+ * requests a solve even in manual-solve mode.
14
12
  */
15
13
  onValueChange: (value: SupportedTypes, forceSolve?: boolean) => void;
16
14
  }
@@ -1,19 +1,17 @@
1
1
  import { type Component } from 'svelte';
2
2
  import { SvelteMap } from 'svelte/reactivity';
3
3
  /**
4
- * A footer item registered by a descendant of the root layout. `component` + `position`
5
- * are fixed at registration; the renderer re-invokes `getProps` reactively, so return
6
- * reactive state from it (e.g. `() => ({ status: myState.status })`) to keep the footer
7
- * in sync.
8
- *
9
- * Generic over the component's props `P` so registration is type-checked at the call
10
- * site. The store holds a heterogeneous mix of items, so its map widens `P` to
4
+ * Generic over the component's props `P` so registration is type-checked at the call site.
5
+ * The store holds a heterogeneous mix of items, so its map widens `P` to
11
6
  * `Record<string, unknown>` at the boundary.
12
7
  */
13
8
  export interface FooterItem<P extends Record<string, unknown> = Record<string, unknown>> {
14
9
  id: string;
15
10
  component: Component<P>;
16
- /** Called on every render — return reactive state to keep footer in sync. */
11
+ /**
12
+ * Re-invoked on every render. Return reactive state — `() => ({ status: s.status })` —
13
+ * or the footer never updates.
14
+ */
17
15
  getProps: () => P;
18
16
  position: 'left' | 'right';
19
17
  priority: number;
@@ -1,4 +1,3 @@
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
  export { writeExternalValue, readExternalValue, clearExternalValue, getExternalInputs } from '@selvajs/solve/client';
@@ -1,18 +1,16 @@
1
1
  import { type Locale, type ViewerMessages } from './messages';
2
2
  export interface LocaleContext {
3
- /** Current locale. Called reactively — return reactive state to enable live switching. */
4
3
  readonly locale: Locale;
5
- /** Resolved message catalog for the current locale. */
6
4
  readonly messages: ViewerMessages;
7
5
  }
8
6
  /**
9
- * Provide the locale to descendants. Pass a getter so a reactive source (a
10
- * `$state`, a store, the app's Paraglide locale) keeps consumers in sync.
7
+ * Provide the locale to descendants. Pass a getter, not a value: it is re-read on every
8
+ * consumer render, so a reactive source (a `$state`, the app's Paraglide locale) switches
9
+ * the language live.
11
10
  */
12
11
  export declare function setLocaleContext(getLocale: () => Locale | undefined): void;
13
12
  /**
14
- * Read the locale context. Falls back to an English-only context when no
15
- * provider exists (e.g. a primitive used in isolation), so consumers never
16
- * need a null check.
13
+ * Falls back to an English-only context when no provider exists (a primitive used in
14
+ * isolation), so consumers never need a null check.
17
15
  */
18
16
  export declare function getLocaleContext(): LocaleContext;
@@ -1,13 +1,7 @@
1
1
  import { getContext, setContext } from 'svelte';
2
2
  import { messagesFor, DEFAULT_LOCALE } from './messages';
3
- // ============================================================================
4
- // Viewer locale context
5
- // ============================================================================
6
- //
7
- // Carries the current UI locale down to the viewer and its panels without
8
- // threading a `lang` prop through every layer. The value is a getter so the
9
- // host can back it with reactive state — flip the language and the viewer
10
- // re-renders live.
3
+ // Carries the UI locale down to the viewer and its panels without threading a
4
+ // `lang` prop through every layer.
11
5
  //
12
6
  // Resolution order for any consuming component:
13
7
  // explicit `lang` prop → nearest locale context → English default
@@ -15,12 +9,12 @@ import { messagesFor, DEFAULT_LOCALE } from './messages';
15
9
  // Two ways to provide it:
16
10
  // - Standalone: <Viewer lang="de" /> — Viewer provides the context itself.
17
11
  // - In an app: call setLocaleContext(() => app.locale) once at the root; the
18
- // viewer (and anything else) reads it. selva later wires its Paraglide
19
- // locale in here.
12
+ // viewer (and anything else) reads it.
20
13
  const LOCALE_CONTEXT_KEY = Symbol('viewer-locale-context');
21
14
  /**
22
- * Provide the locale to descendants. Pass a getter so a reactive source (a
23
- * `$state`, a store, the app's Paraglide locale) keeps consumers in sync.
15
+ * Provide the locale to descendants. Pass a getter, not a value: it is re-read on every
16
+ * consumer render, so a reactive source (a `$state`, the app's Paraglide locale) switches
17
+ * the language live.
24
18
  */
25
19
  export function setLocaleContext(getLocale) {
26
20
  const ctx = {
@@ -34,9 +28,8 @@ export function setLocaleContext(getLocale) {
34
28
  setContext(LOCALE_CONTEXT_KEY, ctx);
35
29
  }
36
30
  /**
37
- * Read the locale context. Falls back to an English-only context when no
38
- * provider exists (e.g. a primitive used in isolation), so consumers never
39
- * need a null check.
31
+ * Falls back to an English-only context when no provider exists (a primitive used in
32
+ * isolation), so consumers never need a null check.
40
33
  */
41
34
  export function getLocaleContext() {
42
35
  return (getContext(LOCALE_CONTEXT_KEY) ?? {
@@ -7,9 +7,7 @@ export interface ViewerMessages {
7
7
  views: string;
8
8
  measure: string;
9
9
  grid: string;
10
- /** "Display" submenu label — groups render style + edges. */
11
10
  display: string;
12
- /** Edges show/hide toggle label. */
13
11
  edges: string;
14
12
  sceneManager: string;
15
13
  screenshot: string;
@@ -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
  const en = {
14
9
  toolsMenu: 'Viewer tools',
15
10
  switchTo2D: 'Switch to 2D',