@selvajs/ui 6.6.0-beta.7 → 6.6.0-beta.9

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 (93) hide show
  1. package/dist/components/compute/AppLayout.svelte +37 -7
  2. package/dist/components/compute/AppLayout.svelte.d.ts +5 -0
  3. package/dist/components/compute/ParameterPresetManager.svelte +8 -20
  4. package/dist/components/compute/SolveMessages.svelte +26 -3
  5. package/dist/components/compute/SolveMessages.svelte.d.ts +7 -0
  6. package/dist/components/layout/PageFooter.svelte +3 -1
  7. package/dist/components/layout/SideNav.svelte +55 -14
  8. package/dist/components/preview/ChartOutput.svelte +18 -13
  9. package/dist/components/preview/Group.svelte +6 -1
  10. package/dist/components/preview/ImageOutput.svelte +3 -1
  11. package/dist/components/preview/InputControl.svelte +108 -44
  12. package/dist/components/preview/InputControl.svelte.d.ts +5 -1
  13. package/dist/components/preview/TabContent.svelte +61 -29
  14. package/dist/components/preview/TabContent.svelte.d.ts +5 -1
  15. package/dist/components/preview/TabLayout.svelte +25 -3
  16. package/dist/components/preview/TabLayout.svelte.d.ts +5 -0
  17. package/dist/components/preview/inputs/ChecklistInput.svelte +7 -2
  18. package/dist/components/preview/inputs/ChecklistInput.svelte.d.ts +2 -1
  19. package/dist/components/preview/inputs/DropdownInput.svelte +16 -4
  20. package/dist/components/preview/inputs/DropdownInput.svelte.d.ts +2 -1
  21. package/dist/components/preview/inputs/FileInput.svelte +56 -65
  22. package/dist/components/preview/inputs/GeometryInput.svelte +128 -0
  23. package/dist/components/preview/inputs/GeometryInput.svelte.d.ts +21 -0
  24. package/dist/components/preview/inputs/OptionGlyph.svelte +29 -0
  25. package/dist/components/preview/inputs/OptionGlyph.svelte.d.ts +8 -0
  26. package/dist/components/preview/inputs/SegmentedInput.svelte +44 -0
  27. package/dist/components/preview/inputs/SegmentedInput.svelte.d.ts +12 -0
  28. package/dist/components/preview/inputs/TileInput.svelte +80 -0
  29. package/dist/components/preview/inputs/TileInput.svelte.d.ts +12 -0
  30. package/dist/components/preview/inputs/index.d.ts +3 -0
  31. package/dist/components/preview/inputs/index.js +3 -0
  32. package/dist/components/primitives/SolvingIndicator.svelte +28 -3
  33. package/dist/components/primitives/SolvingIndicator.svelte.d.ts +7 -0
  34. package/dist/components/primitives/button/button.svelte +3 -0
  35. package/dist/components/primitives/button/button.svelte.d.ts +3 -0
  36. package/dist/components/primitives/toggle/toggle.svelte +3 -1
  37. package/dist/components/primitives/toggle/toggle.svelte.d.ts +3 -0
  38. package/dist/components/viewer/Viewer.svelte +11 -11
  39. package/dist/compute/solving.svelte.d.ts +7 -1
  40. package/dist/compute/solving.svelte.js +12 -1
  41. package/dist/contexts/hostContext.svelte.d.ts +39 -0
  42. package/dist/contexts/hostContext.svelte.js +9 -0
  43. package/dist/i18n/messages.d.ts +33 -0
  44. package/dist/i18n/messages.js +58 -0
  45. package/dist/index.d.ts +3 -0
  46. package/dist/index.js +3 -0
  47. package/dist/schema/host-support.d.ts +29 -0
  48. package/dist/schema/host-support.js +97 -0
  49. package/dist/schema/option-display.d.ts +12 -0
  50. package/dist/schema/option-display.js +29 -0
  51. package/dist/schema/param-exporter.d.ts +4 -1
  52. package/dist/schema/param-exporter.js +10 -6
  53. package/dist/styles/base.css +6 -3
  54. package/dist/utils/file-download.js +10 -22
  55. package/dist/utils/host-files.d.ts +12 -0
  56. package/dist/utils/host-files.js +46 -0
  57. package/dist/utils/webp-to-png.d.ts +10 -0
  58. package/dist/utils/webp-to-png.js +41 -0
  59. package/package.json +9 -9
  60. package/src/lib/components/compute/AppLayout.svelte +37 -7
  61. package/src/lib/components/compute/ParameterPresetManager.svelte +8 -20
  62. package/src/lib/components/compute/SolveMessages.svelte +26 -3
  63. package/src/lib/components/layout/PageFooter.svelte +3 -1
  64. package/src/lib/components/layout/SideNav.svelte +55 -14
  65. package/src/lib/components/preview/ChartOutput.svelte +18 -13
  66. package/src/lib/components/preview/Group.svelte +6 -1
  67. package/src/lib/components/preview/ImageOutput.svelte +3 -1
  68. package/src/lib/components/preview/InputControl.svelte +108 -44
  69. package/src/lib/components/preview/TabContent.svelte +61 -29
  70. package/src/lib/components/preview/TabLayout.svelte +25 -3
  71. package/src/lib/components/preview/inputs/ChecklistInput.svelte +7 -2
  72. package/src/lib/components/preview/inputs/DropdownInput.svelte +16 -4
  73. package/src/lib/components/preview/inputs/FileInput.svelte +56 -65
  74. package/src/lib/components/preview/inputs/GeometryInput.svelte +128 -0
  75. package/src/lib/components/preview/inputs/OptionGlyph.svelte +29 -0
  76. package/src/lib/components/preview/inputs/SegmentedInput.svelte +44 -0
  77. package/src/lib/components/preview/inputs/TileInput.svelte +80 -0
  78. package/src/lib/components/preview/inputs/index.ts +3 -0
  79. package/src/lib/components/primitives/SolvingIndicator.svelte +28 -3
  80. package/src/lib/components/primitives/button/button.svelte +3 -0
  81. package/src/lib/components/primitives/toggle/toggle.svelte +3 -1
  82. package/src/lib/components/viewer/Viewer.svelte +11 -11
  83. package/src/lib/compute/solving.svelte.ts +16 -1
  84. package/src/lib/contexts/hostContext.svelte.ts +51 -0
  85. package/src/lib/i18n/messages.ts +96 -0
  86. package/src/lib/index.ts +20 -0
  87. package/src/lib/schema/host-support.ts +129 -0
  88. package/src/lib/schema/option-display.ts +44 -0
  89. package/src/lib/schema/param-exporter.ts +10 -7
  90. package/src/lib/styles/base.css +6 -3
  91. package/src/lib/utils/file-download.ts +11 -21
  92. package/src/lib/utils/host-files.ts +61 -0
  93. package/src/lib/utils/webp-to-png.ts +48 -0
@@ -8,6 +8,7 @@ export declare const toggleVariants: import("tailwind-variants").TVReturnType<{
8
8
  default: "h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2";
9
9
  sm: "h-7 min-w-7 rounded-md px-2.5 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5";
10
10
  lg: "h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2";
11
+ stacked: "min-h-12 min-w-0 flex-col gap-1 px-1 py-1.5 text-xs";
11
12
  };
12
13
  }, undefined, "hover:text-foreground aria-pressed:bg-muted focus-visible:border-ring focus-visible:ring-ring aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[state=on]:bg-muted gap-1 rounded-lg text-sm font-medium transition-all [&_svg:not([class*='size-'])]:size-4 group/toggle inline-flex items-center justify-center whitespace-nowrap outline-none hover:bg-muted focus-visible:ring-3 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", {
13
14
  variant: {
@@ -18,6 +19,7 @@ export declare const toggleVariants: import("tailwind-variants").TVReturnType<{
18
19
  default: "h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2";
19
20
  sm: "h-7 min-w-7 rounded-md px-2.5 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5";
20
21
  lg: "h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2";
22
+ stacked: "min-h-12 min-w-0 flex-col gap-1 px-1 py-1.5 text-xs";
21
23
  };
22
24
  }, undefined, import("tailwind-variants").TVReturnTypeLike<{
23
25
  variant: {
@@ -28,6 +30,7 @@ export declare const toggleVariants: import("tailwind-variants").TVReturnType<{
28
30
  default: "h-8 min-w-8 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2";
29
31
  sm: "h-7 min-w-7 rounded-md px-2.5 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5";
30
32
  lg: "h-9 min-w-9 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2";
33
+ stacked: "min-h-12 min-w-0 flex-col gap-1 px-1 py-1.5 text-xs";
31
34
  };
32
35
  }, undefined>>;
33
36
  export type ToggleVariant = VariantProps<typeof toggleVariants>['variant'];
@@ -40,6 +40,7 @@
40
40
  import { Button } from '../primitives/button/index.js';
41
41
  import type { Locale } from '../../i18n/messages';
42
42
  import { layerLocaleContext } from '../../i18n/localeContext.svelte';
43
+ import { saveBlob } from '../../utils/host-files';
43
44
 
44
45
  export interface ViewerConfig {
45
46
  showScreenshotButton?: boolean;
@@ -433,15 +434,8 @@
433
434
  const blob = await captureImage?.();
434
435
  if (!blob) return;
435
436
 
436
- const url = URL.createObjectURL(blob);
437
- const link = document.createElement('a');
438
437
  const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-');
439
- link.href = url;
440
- link.download = `viewer-${timestamp}.png`;
441
- document.body.appendChild(link);
442
- link.click();
443
- document.body.removeChild(link);
444
- URL.revokeObjectURL(url);
438
+ await saveBlob(blob, `viewer-${timestamp}.png`);
445
439
  }
446
440
  </script>
447
441
 
@@ -462,7 +456,7 @@
462
456
  the DOM, so this is the only observable proof a solve's geometry decoded and
463
457
  rendered; e2e asserts on it. -->
464
458
  <canvas
465
- class="block h-full w-full"
459
+ class="viewer-canvas block h-full w-full select-none"
466
460
  data-testid="viewer-canvas"
467
461
  data-mesh-count={meshes.length}
468
462
  bind:this={canvas}
@@ -513,7 +507,7 @@
513
507
  {#snippet child({ props })}
514
508
  <Button
515
509
  {...props}
516
- variant="outline"
510
+ variant="overlay"
517
511
  size="icon-lg"
518
512
  title={t.toolsMenu}
519
513
  aria-label={t.toolsMenu}
@@ -630,7 +624,7 @@
630
624
  <!-- Touch has no F key, and fit-to-view is the way back from a framed part. -->
631
625
  <div class="hidden pointer-coarse:block">
632
626
  <Button
633
- variant="outline"
627
+ variant="overlay"
634
628
  size="icon-lg"
635
629
  title={t.fitToView}
636
630
  aria-label={t.fitToView}
@@ -666,6 +660,12 @@
666
660
  border-radius: 0 !important;
667
661
  }
668
662
 
663
+ /* A long press on touch otherwise selects the canvas as an image and opens the save/copy callout. */
664
+ .viewer-canvas {
665
+ -webkit-touch-callout: none;
666
+ -webkit-tap-highlight-color: transparent;
667
+ }
668
+
669
669
  .blur-overlay {
670
670
  pointer-events: none;
671
671
  }
@@ -6,7 +6,13 @@
6
6
  * - Average over SLOW_THRESHOLD_MS: shown immediately
7
7
  * - Between the two: shown after a proportional slice of ANIMATION_DELAY
8
8
  * - First solve: shown immediately, there is no history yet
9
+ *
10
+ * `freezesDuringSolve` is for a host that can't paint while it solves (an embedded WebView on
11
+ * the solver's thread). There the indicator has to be on screen before the solve starts, when its
12
+ * length is still unknown, so it follows `isSolving` exactly: no history, no delay.
9
13
  */
10
- export declare function createSolvingIndicator(isSolving: () => boolean): {
14
+ export declare function createSolvingIndicator(isSolving: () => boolean, options?: {
15
+ freezesDuringSolve?: boolean;
16
+ }): {
11
17
  readonly show: boolean;
12
18
  };
@@ -8,8 +8,19 @@ import { APP_DEFAULTS } from '../constants';
8
8
  * - Average over SLOW_THRESHOLD_MS: shown immediately
9
9
  * - Between the two: shown after a proportional slice of ANIMATION_DELAY
10
10
  * - First solve: shown immediately, there is no history yet
11
+ *
12
+ * `freezesDuringSolve` is for a host that can't paint while it solves (an embedded WebView on
13
+ * the solver's thread). There the indicator has to be on screen before the solve starts, when its
14
+ * length is still unknown, so it follows `isSolving` exactly: no history, no delay.
11
15
  */
12
- export function createSolvingIndicator(isSolving) {
16
+ export function createSolvingIndicator(isSolving, options = {}) {
17
+ if (options.freezesDuringSolve) {
18
+ return {
19
+ get show() {
20
+ return isSolving();
21
+ }
22
+ };
23
+ }
13
24
  const { FAST_THRESHOLD_MS, SLOW_THRESHOLD_MS, HISTORY_SIZE, ANIMATION_DELAY } = APP_DEFAULTS.SOLVING_INDICATOR;
14
25
  let show = $state(false);
15
26
  let timeout = null;
@@ -0,0 +1,39 @@
1
+ import type { GeometryKind } from '@selvajs/schemas';
2
+ import type { UIHost } from '../schema/host-support';
3
+ export type GeometryPickResult = {
4
+ outcome: 'picked';
5
+ summary: string;
6
+ } | {
7
+ outcome: 'cancelled';
8
+ } | {
9
+ outcome: 'failed';
10
+ error: string;
11
+ };
12
+ /** What a pick button can ask for: a Get Geometry filter's kinds, or Get Point/Line/Plane's own. */
13
+ export type PickKind = GeometryKind | 'line' | 'plane';
14
+ /** What a geometry input lets the user pick, as the host discovered it on the canvas. */
15
+ export interface GeometryPickTarget {
16
+ /** The kinds its filter allows; absent when it takes anything. */
17
+ kinds?: readonly GeometryKind[];
18
+ /** 1 picks a single object. */
19
+ atMost?: number;
20
+ }
21
+ export interface HostActions {
22
+ host: UIHost;
23
+ /** Prompts in the Rhino viewport for a geometry input. The picked geometry stays in Grasshopper. */
24
+ pickGeometry?: (paramId: string) => Promise<GeometryPickResult>;
25
+ /** Empties a geometry input again, back to the definition's own value. */
26
+ clearGeometry?: (paramId: string) => void;
27
+ /**
28
+ * Reports what the param holds after every pick or clear, including those another page asked
29
+ * for; null when nothing is picked into it. Returns an unsubscribe.
30
+ */
31
+ onGeometrySummary?: (paramId: string, listener: (summary: string | null) => void) => () => void;
32
+ describeGeometry?: (paramId: string) => GeometryPickTarget | undefined;
33
+ /** Presses a button input: one solve with `true`, then it goes back to `false`. */
34
+ trigger?: (paramId: string) => void;
35
+ /** Grasshopper refuses a press while it solves, so the button waits it out disabled. */
36
+ isSolving?: () => boolean;
37
+ }
38
+ export declare function setHostContext(actions: HostActions): void;
39
+ export declare function getHostContext(): HostActions;
@@ -0,0 +1,9 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ const SELVA_HOST = { host: 'selva' };
3
+ const HOST_CONTEXT_KEY = Symbol('host-context');
4
+ export function setHostContext(actions) {
5
+ setContext(HOST_CONTEXT_KEY, actions);
6
+ }
7
+ export function getHostContext() {
8
+ return getContext(HOST_CONTEXT_KEY) ?? SELVA_HOST;
9
+ }
@@ -50,6 +50,8 @@ export interface ViewerMessages {
50
50
  solveMessagesConfirm: string;
51
51
  solveMessagesDiscard: string;
52
52
  solveMessagesAbort: string;
53
+ /** Stands in for Abort where the page can't take a click mid-solve. */
54
+ solveMessagesEscHint: string;
53
55
  solveMessagesDismiss: string;
54
56
  solveSolving: string;
55
57
  parametersTab: string;
@@ -101,6 +103,37 @@ export interface ViewerMessages {
101
103
  maxLength: string;
102
104
  invalidFormat: string;
103
105
  invalidPattern: string;
106
+ /** The pick button when the input takes several kinds of object. */
107
+ geometryPickAnyOne: string;
108
+ geometryPickAnyOther: string;
109
+ /** Shown while Rhino's viewport prompt for a pick is open. */
110
+ geometryPicking: string;
111
+ /** Where this page may not pick, such as a browser tab beside the Rhino panel. */
112
+ geometryPickInPanel: string;
113
+ geometryPickAgain: string;
114
+ geometryClear: string;
115
+ geometryPickPointOne: string;
116
+ geometryPickPointOther: string;
117
+ geometryPickPointCloudOne: string;
118
+ geometryPickPointCloudOther: string;
119
+ geometryPickCurveOne: string;
120
+ geometryPickCurveOther: string;
121
+ geometryPickLineOne: string;
122
+ geometryPickLineOther: string;
123
+ geometryPickPlaneOne: string;
124
+ geometryPickPlaneOther: string;
125
+ geometryPickSurfaceOne: string;
126
+ geometryPickSurfaceOther: string;
127
+ geometryPickPolysurfaceOne: string;
128
+ geometryPickPolysurfaceOther: string;
129
+ geometryPickExtrusionOne: string;
130
+ geometryPickExtrusionOther: string;
131
+ geometryPickMeshOne: string;
132
+ geometryPickMeshOther: string;
133
+ geometryPickSubdOne: string;
134
+ geometryPickSubdOther: string;
135
+ geometryPickBlockOne: string;
136
+ geometryPickBlockOther: string;
104
137
  fileModeUpload: string;
105
138
  fileModeUrl: string;
106
139
  fileUrlLabel: string;
@@ -58,6 +58,7 @@ const en = {
58
58
  solveMessagesConfirm: 'Continue',
59
59
  solveMessagesDiscard: 'Discard',
60
60
  solveMessagesAbort: 'Abort',
61
+ solveMessagesEscHint: 'Esc aborts the solve',
61
62
  solveMessagesDismiss: 'Dismiss',
62
63
  solveSolving: 'Solving…',
63
64
  parametersTab: 'Parameters',
@@ -98,6 +99,34 @@ const en = {
98
99
  maxLength: 'Maximum {max} characters allowed',
99
100
  invalidFormat: 'Invalid format',
100
101
  invalidPattern: 'Invalid validation pattern configured',
102
+ geometryPickAnyOne: 'Pick an object',
103
+ geometryPickAnyOther: 'Pick objects',
104
+ geometryPicking: 'Pick in the viewport, Esc to cancel',
105
+ geometryPickInPanel: 'Pick in the Rhino panel',
106
+ geometryPickAgain: 'Pick again',
107
+ geometryClear: 'Clear',
108
+ geometryPickPointOne: 'Pick a point',
109
+ geometryPickPointOther: 'Pick points',
110
+ geometryPickPointCloudOne: 'Pick a point cloud',
111
+ geometryPickPointCloudOther: 'Pick point clouds',
112
+ geometryPickCurveOne: 'Pick a curve',
113
+ geometryPickCurveOther: 'Pick curves',
114
+ geometryPickLineOne: 'Pick a line',
115
+ geometryPickLineOther: 'Pick lines',
116
+ geometryPickPlaneOne: 'Pick a plane',
117
+ geometryPickPlaneOther: 'Pick planes',
118
+ geometryPickSurfaceOne: 'Pick a surface',
119
+ geometryPickSurfaceOther: 'Pick surfaces',
120
+ geometryPickPolysurfaceOne: 'Pick a polysurface',
121
+ geometryPickPolysurfaceOther: 'Pick polysurfaces',
122
+ geometryPickExtrusionOne: 'Pick an extrusion',
123
+ geometryPickExtrusionOther: 'Pick extrusions',
124
+ geometryPickMeshOne: 'Pick a mesh',
125
+ geometryPickMeshOther: 'Pick meshes',
126
+ geometryPickSubdOne: 'Pick a SubD',
127
+ geometryPickSubdOther: 'Pick SubDs',
128
+ geometryPickBlockOne: 'Pick a block',
129
+ geometryPickBlockOther: 'Pick blocks',
101
130
  fileModeUpload: 'Upload',
102
131
  fileModeUrl: 'URL',
103
132
  fileUrlLabel: 'File URL',
@@ -203,6 +232,7 @@ const de = {
203
232
  solveMessagesConfirm: 'Weiter',
204
233
  solveMessagesDiscard: 'Verwerfen',
205
234
  solveMessagesAbort: 'Abbrechen',
235
+ solveMessagesEscHint: 'Esc bricht die Berechnung ab',
206
236
  solveMessagesDismiss: 'Schliessen',
207
237
  solveSolving: 'Berechnung läuft…',
208
238
  parametersTab: 'Parameter',
@@ -243,6 +273,34 @@ const de = {
243
273
  maxLength: 'Höchstens {max} Zeichen erlaubt',
244
274
  invalidFormat: 'Ungültiges Format',
245
275
  invalidPattern: 'Ungültiges Prüfmuster hinterlegt',
276
+ geometryPickAnyOne: 'Objekt wählen',
277
+ geometryPickAnyOther: 'Objekte wählen',
278
+ geometryPicking: 'Im Ansichtsfenster wählen, Esc bricht ab',
279
+ geometryPickInPanel: 'Im Rhino-Panel wählen',
280
+ geometryPickAgain: 'Neu wählen',
281
+ geometryClear: 'Leeren',
282
+ geometryPickPointOne: 'Punkt wählen',
283
+ geometryPickPointOther: 'Punkte wählen',
284
+ geometryPickPointCloudOne: 'Punktwolke wählen',
285
+ geometryPickPointCloudOther: 'Punktwolken wählen',
286
+ geometryPickCurveOne: 'Kurve wählen',
287
+ geometryPickCurveOther: 'Kurven wählen',
288
+ geometryPickLineOne: 'Linie wählen',
289
+ geometryPickLineOther: 'Linien wählen',
290
+ geometryPickPlaneOne: 'Ebene wählen',
291
+ geometryPickPlaneOther: 'Ebenen wählen',
292
+ geometryPickSurfaceOne: 'Fläche wählen',
293
+ geometryPickSurfaceOther: 'Flächen wählen',
294
+ geometryPickPolysurfaceOne: 'Flächenverband wählen',
295
+ geometryPickPolysurfaceOther: 'Flächenverbände wählen',
296
+ geometryPickExtrusionOne: 'Extrusion wählen',
297
+ geometryPickExtrusionOther: 'Extrusionen wählen',
298
+ geometryPickMeshOne: 'Netz wählen',
299
+ geometryPickMeshOther: 'Netze wählen',
300
+ geometryPickSubdOne: 'SubD wählen',
301
+ geometryPickSubdOther: 'SubDs wählen',
302
+ geometryPickBlockOne: 'Block wählen',
303
+ geometryPickBlockOther: 'Blöcke wählen',
246
304
  fileModeUpload: 'Hochladen',
247
305
  fileModeUrl: 'URL',
248
306
  fileUrlLabel: 'Datei-URL',
package/dist/index.d.ts CHANGED
@@ -9,15 +9,18 @@ export { default as Viewer } from './components/viewer/Viewer.svelte';
9
9
  export * from './schema/defaults';
10
10
  export * from './schema/dynamic-value-list';
11
11
  export * from './schema/traversal';
12
+ export { type UIHost, type ElementKind, HOST_LABELS, isGeometryParamType, layoutItemKind, hostsFor, isSupportedOn, valuelessInputIds, unsupportedItems } from './schema/host-support';
12
13
  export * from './compute/solving.svelte';
13
14
  export { useSolveSession } from './compute/useSolveSession.svelte';
14
15
  export { createSolveSession, createRequestResponseDriver, type SolveSession, type SolveSessionArgs, type SolveSettlement, type SolveDriver, type SolveReporter, type LiveSolveState, type SolvePhase, EMPTY_LIVE_SOLVE, decodeOutcome, finalizeResult, type SolveOutcome } from '@selvajs/solve/client';
15
16
  export { writeExternalValue, readExternalValue, clearExternalValue, getExternalInputs, type ExternalValueRef, type ExternalInput } from '@selvajs/solve/client';
16
17
  export * from './contexts/footerContext.svelte';
17
18
  export * from './contexts/clientSlotContext.svelte';
19
+ export * from './contexts/hostContext.svelte';
18
20
  export * from './composables/useFooterItem.svelte';
19
21
  export * from './utils';
20
22
  export { randomId } from './utils/randomId';
23
+ export { saveBlob, pickFile, setBlobSaver, setFilePicker, type BlobSaver, type FilePicker } from './utils/host-files';
21
24
  export type { ActionButton } from './types/actionButton';
22
25
  export type { SolveFn, SolveResult, SolveDiagnostic } from '@selvajs/solve/shared';
23
26
  export { DEFAULT_PRESET_LABELS, type PresetLabels } from './types/presetLabels';
package/dist/index.js CHANGED
@@ -13,6 +13,7 @@ export { default as Viewer } from './components/viewer/Viewer.svelte';
13
13
  export * from './schema/defaults';
14
14
  export * from './schema/dynamic-value-list';
15
15
  export * from './schema/traversal';
16
+ export { HOST_LABELS, isGeometryParamType, layoutItemKind, hostsFor, isSupportedOn, valuelessInputIds, unsupportedItems } from './schema/host-support';
16
17
  export * from './compute/solving.svelte';
17
18
  // Solve Session seam. Re-exported so transports outside this package — e.g.
18
19
  // plugin-ui's WebSocket driver — can satisfy SolveDriver and drive a session.
@@ -25,7 +26,9 @@ EMPTY_LIVE_SOLVE, decodeOutcome, finalizeResult } from '@selvajs/solve/client';
25
26
  export { writeExternalValue, readExternalValue, clearExternalValue, getExternalInputs } from '@selvajs/solve/client';
26
27
  export * from './contexts/footerContext.svelte';
27
28
  export * from './contexts/clientSlotContext.svelte';
29
+ export * from './contexts/hostContext.svelte';
28
30
  export * from './composables/useFooterItem.svelte';
29
31
  export * from './utils';
30
32
  export { randomId } from './utils/randomId';
33
+ export { saveBlob, pickFile, setBlobSaver, setFilePicker } from './utils/host-files';
31
34
  export { DEFAULT_PRESET_LABELS } from './types/presetLabels';
@@ -0,0 +1,29 @@
1
+ import type { InputLayoutItem, LayoutItem, SchemaInput, UISchema } from '@selvajs/schemas';
2
+ /**
3
+ * Where a UI runs: `rhino` is the plugin's page inside Rhino, solving in the user's own
4
+ * Grasshopper; `selva` is the Selva app, solving headless on Rhino.Compute.
5
+ */
6
+ export type UIHost = 'rhino' | 'selva';
7
+ /**
8
+ * - `value`: an input or output every host renders.
9
+ * - `geometry`: a Rhino Get Geometry/Point/Line/Plane input, filled by picking in the viewport.
10
+ * - `trigger`: a boolean input shown as a button; a press solves once with `true`.
11
+ * - `unknown`: a layout item from a newer schema than this build knows.
12
+ */
13
+ export type ElementKind = 'value' | 'geometry' | 'trigger' | 'unknown';
14
+ export declare const HOST_LABELS: Record<UIHost, string>;
15
+ /** The control comes from this, whatever the saved widgetType says: saved ones hold `text`. */
16
+ export declare function isGeometryParamType(paramType: string | undefined): boolean;
17
+ export declare function isTriggerItem(item: LayoutItem): boolean;
18
+ export declare function layoutItemKind(item: LayoutItem, inputs: readonly SchemaInput[]): ElementKind;
19
+ export declare function hostsFor(kind: ElementKind): readonly UIHost[];
20
+ export declare function isSupportedOn(host: UIHost, kind: ElementKind): boolean;
21
+ /**
22
+ * Inputs the form keeps no value for: geometry lives on the Grasshopper param after a pick, and a
23
+ * button only sends a press. Value updates and presets skip them; holding a button's `true` would
24
+ * fire its action again on every solve.
25
+ */
26
+ export declare function isValuelessInput(item: InputLayoutItem, input: SchemaInput | undefined): boolean;
27
+ export declare function valuelessInputIds(schema: UISchema): Set<string>;
28
+ /** The placed items a host won't render, for the builder's summary of where the UI runs. */
29
+ export declare function unsupportedItems(schema: UISchema, host: UIHost): LayoutItem[];
@@ -0,0 +1,97 @@
1
+ // Which elements of a schema each host renders. Worked out from the schema on every read and
2
+ // never stored in it: a stored copy goes stale the way a saved paramType does. Making an element
3
+ // work on Selva later is a change to SUPPORT, with no schema change and no migration.
4
+ import { getGroups } from '@selvajs/schemas';
5
+ const SUPPORT = {
6
+ value: ['rhino', 'selva'],
7
+ geometry: ['rhino'],
8
+ trigger: ['rhino'],
9
+ unknown: []
10
+ };
11
+ export const HOST_LABELS = { rhino: 'Rhino', selva: 'Selva' };
12
+ const GEOMETRY_PARAM_TYPES = new Set(['geometry', 'point', 'line', 'plane']);
13
+ // Keyed by the schema's own widget unions, so a widget added to the schema fails to compile here
14
+ // until someone decides which hosts render it.
15
+ const INPUT_WIDGETS = {
16
+ number: true,
17
+ text: true,
18
+ dropdown: true,
19
+ dynamicValueList: true,
20
+ checkbox: true,
21
+ file: true,
22
+ color: true
23
+ };
24
+ const OUTPUT_WIDGETS = {
25
+ text: true,
26
+ number: true,
27
+ file: true,
28
+ chart: true,
29
+ image: true,
30
+ dynamicValueList: true
31
+ };
32
+ const KNOWN_WIDGETS = {
33
+ input: new Set(Object.keys(INPUT_WIDGETS)),
34
+ output: new Set(Object.keys(OUTPUT_WIDGETS))
35
+ };
36
+ /** The control comes from this, whatever the saved widgetType says: saved ones hold `text`. */
37
+ export function isGeometryParamType(paramType) {
38
+ return paramType !== undefined && GEOMETRY_PARAM_TYPES.has(paramType);
39
+ }
40
+ export function isTriggerItem(item) {
41
+ return (item.type === 'input' &&
42
+ item.widgetType === 'checkbox' &&
43
+ item.config?.displayAs === 'button');
44
+ }
45
+ export function layoutItemKind(item, inputs) {
46
+ if (item.type === 'linebreak')
47
+ return 'value';
48
+ if (item.type !== 'input' && item.type !== 'output')
49
+ return 'unknown';
50
+ if (item.type === 'input') {
51
+ const paramType = inputs.find((i) => i.id === item.paramId)?.paramType;
52
+ if (isGeometryParamType(paramType))
53
+ return 'geometry';
54
+ }
55
+ if (!KNOWN_WIDGETS[item.type].has(item.widgetType))
56
+ return 'unknown';
57
+ return isTriggerItem(item) ? 'trigger' : 'value';
58
+ }
59
+ export function hostsFor(kind) {
60
+ return SUPPORT[kind];
61
+ }
62
+ export function isSupportedOn(host, kind) {
63
+ return SUPPORT[kind].includes(host);
64
+ }
65
+ /**
66
+ * Inputs the form keeps no value for: geometry lives on the Grasshopper param after a pick, and a
67
+ * button only sends a press. Value updates and presets skip them; holding a button's `true` would
68
+ * fire its action again on every solve.
69
+ */
70
+ export function isValuelessInput(item, input) {
71
+ return isGeometryParamType(input?.paramType) || isTriggerItem(item);
72
+ }
73
+ export function valuelessInputIds(schema) {
74
+ const ids = new Set();
75
+ for (const input of schema.inputs) {
76
+ if (isGeometryParamType(input.paramType))
77
+ ids.add(input.id);
78
+ }
79
+ for (const group of getGroups(schema)) {
80
+ for (const item of group.items ?? []) {
81
+ if (isTriggerItem(item))
82
+ ids.add(item.paramId);
83
+ }
84
+ }
85
+ return ids;
86
+ }
87
+ /** The placed items a host won't render, for the builder's summary of where the UI runs. */
88
+ export function unsupportedItems(schema, host) {
89
+ const items = [];
90
+ for (const group of getGroups(schema)) {
91
+ for (const item of group.items ?? []) {
92
+ if (!isSupportedOn(host, layoutItemKind(item, schema.inputs)))
93
+ items.push(item);
94
+ }
95
+ }
96
+ return items;
97
+ }
@@ -0,0 +1,12 @@
1
+ import type { DropdownWidgetConfig, OptionMedia, UISchema } from '@selvajs/schemas';
2
+ export type OptionDisplay = NonNullable<DropdownWidgetConfig['displayAs']>;
3
+ export declare function resolveOptionDisplay(displayAs: string | undefined, optionCount: number): OptionDisplay;
4
+ export type GlyphSource = {
5
+ kind: 'icon';
6
+ d: string;
7
+ } | {
8
+ kind: 'image';
9
+ src: string;
10
+ };
11
+ /** What to draw beside an option: its thumbnail, else its icon. */
12
+ export declare function optionGlyph(media: OptionMedia | undefined, assets: UISchema['assets'] | undefined): GlyphSource | undefined;
@@ -0,0 +1,29 @@
1
+ // Past this a button row no longer fits a panel column. A computed list can grow past it at
2
+ // runtime, so the row turns back into a dropdown instead of wrapping.
3
+ const MAX_SEGMENTED_OPTIONS = 6;
4
+ export function resolveOptionDisplay(displayAs, optionCount) {
5
+ switch (displayAs) {
6
+ case 'checklist':
7
+ case 'tiles':
8
+ return displayAs;
9
+ case 'segmented':
10
+ return optionCount <= MAX_SEGMENTED_OPTIONS ? 'segmented' : 'dropdown';
11
+ default:
12
+ return 'dropdown';
13
+ }
14
+ }
15
+ // The schema can come from any .gh, so an image reaches <img> only as an inline raster data URI.
16
+ // Anything else could be a URL that makes every viewer's browser call wherever the file points.
17
+ const RASTER_DATA_URI = /^data:image\/(webp|jpeg|png);base64,[A-Za-z0-9+/=]+$/;
18
+ /** What to draw beside an option: its thumbnail, else its icon. */
19
+ export function optionGlyph(media, assets) {
20
+ if (!media || !assets)
21
+ return undefined;
22
+ if (media.image) {
23
+ const src = assets[media.image]?.img;
24
+ if (src && RASTER_DATA_URI.test(src))
25
+ return { kind: 'image', src };
26
+ }
27
+ const d = media.icon ? assets[media.icon]?.d : undefined;
28
+ return d ? { kind: 'icon', d } : undefined;
29
+ }
@@ -14,6 +14,8 @@ export declare function validateSavedState(savedState: ParameterPreset, currentS
14
14
  * Params the current schema no longer has are dropped. That's the same condition
15
15
  * `validateSavedState` flags as an error, but the rule is re-derived from the schema here
16
16
  * rather than read back off the issue list — nothing couples to how sentinel ids are encoded.
17
+ * Valueless inputs are dropped too: a preset saved before an input became a button or a
18
+ * geometry input still holds a value for it.
17
19
  */
18
20
  export declare function extractLoadableValues(savedState: ParameterPreset, currentSchema: UISchema): Record<string, unknown>;
19
21
  export interface PresetLoadResult {
@@ -28,5 +30,6 @@ export interface PresetLoadResult {
28
30
  }
29
31
  /** Validates and computes the loadable values in one pass, so callers thread one object. */
30
32
  export declare function loadPreset(savedState: ParameterPreset, currentSchema: UISchema): PresetLoadResult;
31
- export declare function exportStateAsJson(savedState: ParameterPreset): void;
33
+ /** Resolves false when the user cancelled the save. */
34
+ export declare function exportStateAsJson(savedState: ParameterPreset): Promise<boolean>;
32
35
  export declare function importStateFromJson(file: File): Promise<ParameterPreset>;
@@ -1,5 +1,7 @@
1
1
  import { randomId } from '../utils/randomId';
2
+ import { saveBlob } from '../utils/host-files';
2
3
  import { getGroups } from '@selvajs/schemas';
4
+ import { isValuelessInput, valuelessInputIds } from './host-support';
3
5
  export function createSavedState(schema, currentValues, metadata) {
4
6
  const parameters = [];
5
7
  for (const group of getGroups(schema)) {
@@ -8,6 +10,8 @@ export function createSavedState(schema, currentValues, metadata) {
8
10
  continue;
9
11
  const value = currentValues[item.paramId];
10
12
  const inputDef = schema.inputs.find((i) => i.id === item.paramId);
13
+ if (isValuelessInput(item, inputDef))
14
+ continue;
11
15
  if (value !== undefined && inputDef) {
12
16
  parameters.push({
13
17
  paramId: item.paramId,
@@ -78,12 +82,15 @@ export function validateSavedState(savedState, currentSchema) {
78
82
  * Params the current schema no longer has are dropped. That's the same condition
79
83
  * `validateSavedState` flags as an error, but the rule is re-derived from the schema here
80
84
  * rather than read back off the issue list — nothing couples to how sentinel ids are encoded.
85
+ * Valueless inputs are dropped too: a preset saved before an input became a button or a
86
+ * geometry input still holds a value for it.
81
87
  */
82
88
  export function extractLoadableValues(savedState, currentSchema) {
83
89
  const values = {};
90
+ const valueless = valuelessInputIds(currentSchema);
84
91
  for (const paramState of savedState.parameters) {
85
92
  const exists = currentSchema.inputs.some((i) => i.id === paramState.paramId);
86
- if (exists) {
93
+ if (exists && !valueless.has(paramState.paramId)) {
87
94
  values[paramState.paramId] = paramState.value;
88
95
  }
89
96
  }
@@ -99,17 +106,14 @@ export function loadPreset(savedState, currentSchema) {
99
106
  canLoad: validation.canLoad
100
107
  };
101
108
  }
109
+ /** Resolves false when the user cancelled the save. */
102
110
  export function exportStateAsJson(savedState) {
103
111
  const blob = new Blob([JSON.stringify(savedState, null, 2)], { type: 'application/json' });
104
112
  const safeName = savedState.name.replace(/[^a-z0-9]/gi, '_');
105
113
  const date = savedState.timestamp.split('T')[0].replace(/-/g, '_');
106
- const link = document.createElement('a');
107
- link.href = URL.createObjectURL(blob);
108
114
  // .slvp = Selva parameter preset. Import still accepts the pre-rename .sps — same JSON,
109
115
  // only the writer's extension changed.
110
- link.download = `${safeName}_${date}.slvp`;
111
- link.click();
112
- URL.revokeObjectURL(link.href);
116
+ return saveBlob(blob, `${safeName}_${date}.slvp`);
113
117
  }
114
118
  export async function importStateFromJson(file) {
115
119
  const state = JSON.parse(await file.text());
@@ -7,11 +7,14 @@
7
7
  --page-py: 1.75rem;
8
8
  --background: oklch(0.99 0 0);
9
9
  --foreground: oklch(0.22 0 0);
10
- --card: oklch(0.985 0 0);
10
+ /* White, so a card stands apart from background and muted; Group adds a border, since
11
+ white on background alone is too close to see. */
12
+ --card: oklch(1 0 0);
11
13
  --card-foreground: oklch(0.22 0 0);
12
14
  --popover: oklch(0.99 0 0);
13
15
  --popover-foreground: oklch(0.22 0 0);
14
- --primary: oklch(0.57 0.07 135);
16
+ /* 4.5:1 as text on background, card and muted, and under white text. */
17
+ --primary: oklch(0.52 0.07 135);
15
18
  --primary-foreground: oklch(0.985 0 0);
16
19
  --secondary: oklch(0.94 0 0);
17
20
  --secondary-foreground: oklch(0.28 0 0);
@@ -40,7 +43,7 @@
40
43
  --chart-5: oklch(0.68 0.15 30);
41
44
  --sidebar: oklch(0.96 0 0);
42
45
  --sidebar-foreground: oklch(0.25 0 0);
43
- --sidebar-primary: oklch(0.57 0.07 135);
46
+ --sidebar-primary: oklch(0.52 0.07 135);
44
47
  --sidebar-primary-foreground: oklch(0.985 0 0);
45
48
  --sidebar-accent: oklch(0.93 0 0);
46
49
  --sidebar-accent-foreground: oklch(0.3 0 0);