@selvajs/ui 6.6.0-beta.15 → 6.6.0-beta.17

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 (213) hide show
  1. package/README.md +0 -3
  2. package/dist/components/compute/AppLayout.svelte +5 -3
  3. package/dist/components/compute/CollapsedPanelStrip.svelte +1 -1
  4. package/dist/components/compute/ParameterPresetManager.svelte +1 -1
  5. package/dist/components/compute/SolveMessages.svelte +2 -2
  6. package/dist/components/layout/FooterItemRenderer.svelte +1 -1
  7. package/dist/components/layout/PageFooter.svelte +2 -2
  8. package/dist/components/layout/PageHeader.svelte +1 -1
  9. package/dist/components/layout/SideNav.svelte +3 -3
  10. package/dist/components/layout/SubNav.svelte +2 -2
  11. package/dist/components/preview/ChartOutput.svelte +3 -3
  12. package/dist/components/preview/Group.svelte +7 -7
  13. package/dist/components/preview/ImageOutput.svelte +6 -6
  14. package/dist/components/preview/InputControl.svelte +1 -0
  15. package/dist/components/preview/OutputDisplay.svelte +8 -6
  16. package/dist/components/preview/TabBar.svelte +6 -4
  17. package/dist/components/preview/TabBar.svelte.d.ts +2 -0
  18. package/dist/components/preview/TabContent.svelte +1 -1
  19. package/dist/components/preview/TabLayout.svelte +3 -4
  20. package/dist/components/preview/inputs/ChecklistInput.svelte +3 -3
  21. package/dist/components/preview/inputs/ColorInput.svelte +356 -14
  22. package/dist/components/preview/inputs/ColorInput.svelte.d.ts +1 -0
  23. package/dist/components/preview/inputs/FileInput.svelte +3 -3
  24. package/dist/components/preview/inputs/SegmentedInput.svelte +2 -1
  25. package/dist/components/preview/inputs/TileInput.svelte +1 -1
  26. package/dist/components/primitives/CalculateButton.svelte +2 -2
  27. package/dist/components/primitives/Callout.svelte +8 -6
  28. package/dist/components/primitives/Callout.svelte.d.ts +4 -4
  29. package/dist/components/primitives/DataTable.svelte +1 -1
  30. package/dist/components/primitives/ImageUploadField.svelte +1 -1
  31. package/dist/components/primitives/alert-dialog/alert-dialog-content.svelte +1 -1
  32. package/dist/components/primitives/badge/badge.svelte +2 -2
  33. package/dist/components/primitives/badge/badge.svelte.d.ts +6 -6
  34. package/dist/components/primitives/button/button.svelte +6 -7
  35. package/dist/components/primitives/button/button.svelte.d.ts +18 -18
  36. package/dist/components/primitives/card/card.svelte +1 -1
  37. package/dist/components/primitives/card/card.svelte.d.ts +1 -1
  38. package/dist/components/primitives/checkbox/checkbox.svelte +1 -1
  39. package/dist/components/primitives/command/command-input.svelte +1 -1
  40. package/dist/components/primitives/command/command-item.svelte +1 -1
  41. package/dist/components/primitives/command/command-link-item.svelte +1 -1
  42. package/dist/components/primitives/command/command.svelte +1 -1
  43. package/dist/components/primitives/context-menu/context-menu-checkbox-item.svelte +1 -1
  44. package/dist/components/primitives/context-menu/context-menu-content.svelte +1 -1
  45. package/dist/components/primitives/context-menu/context-menu-item.svelte +1 -1
  46. package/dist/components/primitives/context-menu/context-menu-radio-item.svelte +1 -1
  47. package/dist/components/primitives/context-menu/context-menu-sub-content.svelte +1 -1
  48. package/dist/components/primitives/context-menu/context-menu-sub-trigger.svelte +1 -1
  49. package/dist/components/primitives/dialog/dialog-content.svelte +2 -2
  50. package/dist/components/primitives/drawer/Drawer.svelte +1 -1
  51. package/dist/components/primitives/dropdown-menu/dropdown-menu-checkbox-item.svelte +31 -11
  52. package/dist/components/primitives/dropdown-menu/dropdown-menu-checkbox-item.svelte.d.ts +2 -0
  53. package/dist/components/primitives/dropdown-menu/dropdown-menu-content.svelte +5 -1
  54. package/dist/components/primitives/dropdown-menu/dropdown-menu-content.svelte.d.ts +2 -0
  55. package/dist/components/primitives/dropdown-menu/dropdown-menu-item.svelte +4 -2
  56. package/dist/components/primitives/dropdown-menu/dropdown-menu-item.svelte.d.ts +2 -1
  57. package/dist/components/primitives/dropdown-menu/dropdown-menu-radio-item.svelte +1 -1
  58. package/dist/components/primitives/dropdown-menu/dropdown-menu-shortcut.svelte +1 -1
  59. package/dist/components/primitives/dropdown-menu/dropdown-menu-sub-content.svelte +1 -1
  60. package/dist/components/primitives/dropdown-menu/dropdown-menu-sub-trigger.svelte +1 -1
  61. package/dist/components/primitives/index.d.ts +1 -1
  62. package/dist/components/primitives/index.js +1 -1
  63. package/dist/components/primitives/input/input.svelte +2 -2
  64. package/dist/components/primitives/input-group/input-group.svelte +1 -1
  65. package/dist/components/primitives/popover/popover-content.svelte +1 -1
  66. package/dist/components/primitives/resizable/resizable-handle.svelte +2 -2
  67. package/dist/components/primitives/search/search.svelte +1 -1
  68. package/dist/components/primitives/select/select-content.svelte +1 -1
  69. package/dist/components/primitives/select/select-item.svelte +1 -1
  70. package/dist/components/primitives/select/select-trigger.svelte +1 -1
  71. package/dist/components/primitives/slider/slider.svelte +1 -1
  72. package/dist/components/primitives/switch/switch.svelte +1 -1
  73. package/dist/components/primitives/tabs/tabs-list.svelte +1 -1
  74. package/dist/components/primitives/tabs/tabs-list.svelte.d.ts +3 -3
  75. package/dist/components/primitives/tabs/tabs-trigger.svelte +1 -1
  76. package/dist/components/primitives/tabs/tabs-trigger.svelte.d.ts +3 -3
  77. package/dist/components/primitives/textarea/textarea.svelte +1 -1
  78. package/dist/components/primitives/toggle/toggle.svelte +6 -3
  79. package/dist/components/primitives/toggle/toggle.svelte.d.ts +10 -7
  80. package/dist/components/primitives/toggle-group/toggle-group-item.svelte +1 -1
  81. package/dist/components/primitives/toggle-group/toggle-group.svelte +1 -1
  82. package/dist/components/primitives/tooltip/index.d.ts +6 -0
  83. package/dist/components/primitives/tooltip/index.js +8 -0
  84. package/dist/components/primitives/tooltip/tooltip-content.svelte +34 -0
  85. package/dist/components/primitives/tooltip/tooltip-content.svelte.d.ts +10 -0
  86. package/dist/components/primitives/tooltip/tooltip-portal.svelte +7 -0
  87. package/dist/components/primitives/tooltip/tooltip-portal.svelte.d.ts +4 -0
  88. package/dist/components/primitives/tooltip/tooltip-provider.svelte +7 -0
  89. package/dist/components/primitives/tooltip/tooltip-provider.svelte.d.ts +4 -0
  90. package/dist/components/primitives/tooltip/tooltip-trigger.svelte +7 -0
  91. package/dist/components/primitives/tooltip/tooltip-trigger.svelte.d.ts +4 -0
  92. package/dist/components/primitives/tooltip/tooltip.svelte +7 -0
  93. package/dist/components/primitives/tooltip/tooltip.svelte.d.ts +4 -0
  94. package/dist/components/viewer/MeshMetadataPanel.svelte +1 -1
  95. package/dist/components/viewer/SceneManager.svelte +19 -19
  96. package/dist/components/viewer/Viewer.svelte +68 -157
  97. package/dist/components/viewer/ViewerDock.svelte +404 -0
  98. package/dist/components/viewer/ViewerDock.svelte.d.ts +42 -0
  99. package/dist/i18n/messages.d.ts +11 -0
  100. package/dist/i18n/messages.js +18 -0
  101. package/dist/index.d.ts +1 -0
  102. package/dist/index.js +1 -0
  103. package/dist/public.d.ts +1 -0
  104. package/dist/public.js +1 -0
  105. package/dist/schema/visibility-rules.d.ts +7 -1
  106. package/dist/schema/visibility-rules.js +17 -0
  107. package/dist/styles/base.css +81 -80
  108. package/dist/utils/color-space.d.ts +21 -0
  109. package/dist/utils/color-space.js +90 -0
  110. package/package.json +8 -9
  111. package/src/lib/components/compute/AppLayout.svelte +5 -3
  112. package/src/lib/components/compute/CollapsedPanelStrip.svelte +1 -1
  113. package/src/lib/components/compute/ParameterPresetManager.svelte +1 -1
  114. package/src/lib/components/compute/SolveMessages.svelte +2 -2
  115. package/src/lib/components/layout/FooterItemRenderer.svelte +1 -1
  116. package/src/lib/components/layout/PageFooter.svelte +2 -2
  117. package/src/lib/components/layout/PageHeader.svelte +1 -1
  118. package/src/lib/components/layout/SideNav.svelte +3 -3
  119. package/src/lib/components/layout/SubNav.svelte +2 -2
  120. package/src/lib/components/preview/ChartOutput.svelte +3 -3
  121. package/src/lib/components/preview/Group.svelte +7 -7
  122. package/src/lib/components/preview/ImageOutput.svelte +6 -6
  123. package/src/lib/components/preview/InputControl.svelte +1 -0
  124. package/src/lib/components/preview/OutputDisplay.svelte +8 -6
  125. package/src/lib/components/preview/TabBar.svelte +6 -4
  126. package/src/lib/components/preview/TabContent.svelte +1 -1
  127. package/src/lib/components/preview/TabLayout.svelte +3 -4
  128. package/src/lib/components/preview/inputs/ChecklistInput.svelte +3 -3
  129. package/src/lib/components/preview/inputs/ColorInput.svelte +356 -14
  130. package/src/lib/components/preview/inputs/FileInput.svelte +3 -3
  131. package/src/lib/components/preview/inputs/SegmentedInput.svelte +2 -1
  132. package/src/lib/components/preview/inputs/TileInput.svelte +1 -1
  133. package/src/lib/components/primitives/CalculateButton.svelte +2 -2
  134. package/src/lib/components/primitives/Callout.svelte +8 -6
  135. package/src/lib/components/primitives/DataTable.svelte +1 -1
  136. package/src/lib/components/primitives/ImageUploadField.svelte +1 -1
  137. package/src/lib/components/primitives/alert-dialog/alert-dialog-content.svelte +1 -1
  138. package/src/lib/components/primitives/badge/badge.svelte +2 -2
  139. package/src/lib/components/primitives/button/button.svelte +6 -7
  140. package/src/lib/components/primitives/card/card.svelte +1 -1
  141. package/src/lib/components/primitives/checkbox/checkbox.svelte +1 -1
  142. package/src/lib/components/primitives/command/command-input.svelte +1 -1
  143. package/src/lib/components/primitives/command/command-item.svelte +1 -1
  144. package/src/lib/components/primitives/command/command-link-item.svelte +1 -1
  145. package/src/lib/components/primitives/command/command.svelte +1 -1
  146. package/src/lib/components/primitives/context-menu/context-menu-checkbox-item.svelte +1 -1
  147. package/src/lib/components/primitives/context-menu/context-menu-content.svelte +1 -1
  148. package/src/lib/components/primitives/context-menu/context-menu-item.svelte +1 -1
  149. package/src/lib/components/primitives/context-menu/context-menu-radio-item.svelte +1 -1
  150. package/src/lib/components/primitives/context-menu/context-menu-sub-content.svelte +1 -1
  151. package/src/lib/components/primitives/context-menu/context-menu-sub-trigger.svelte +1 -1
  152. package/src/lib/components/primitives/dialog/dialog-content.svelte +2 -2
  153. package/src/lib/components/primitives/drawer/Drawer.svelte +1 -1
  154. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-checkbox-item.svelte +31 -11
  155. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-content.svelte +5 -1
  156. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-item.svelte +4 -2
  157. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-radio-item.svelte +1 -1
  158. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-shortcut.svelte +1 -1
  159. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-sub-content.svelte +1 -1
  160. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-sub-trigger.svelte +1 -1
  161. package/src/lib/components/primitives/index.ts +1 -1
  162. package/src/lib/components/primitives/input/input.svelte +2 -2
  163. package/src/lib/components/primitives/input-group/input-group.svelte +1 -1
  164. package/src/lib/components/primitives/popover/popover-content.svelte +1 -1
  165. package/src/lib/components/primitives/resizable/resizable-handle.svelte +2 -2
  166. package/src/lib/components/primitives/search/search.svelte +1 -1
  167. package/src/lib/components/primitives/select/select-content.svelte +1 -1
  168. package/src/lib/components/primitives/select/select-item.svelte +1 -1
  169. package/src/lib/components/primitives/select/select-trigger.svelte +1 -1
  170. package/src/lib/components/primitives/slider/slider.svelte +1 -1
  171. package/src/lib/components/primitives/switch/switch.svelte +1 -1
  172. package/src/lib/components/primitives/tabs/tabs-list.svelte +1 -1
  173. package/src/lib/components/primitives/tabs/tabs-trigger.svelte +1 -1
  174. package/src/lib/components/primitives/textarea/textarea.svelte +1 -1
  175. package/src/lib/components/primitives/toggle/toggle.svelte +6 -3
  176. package/src/lib/components/primitives/toggle-group/toggle-group-item.svelte +1 -1
  177. package/src/lib/components/primitives/toggle-group/toggle-group.svelte +1 -1
  178. package/src/lib/components/primitives/tooltip/index.ts +19 -0
  179. package/src/lib/components/primitives/tooltip/tooltip-content.svelte +34 -0
  180. package/src/lib/components/primitives/tooltip/tooltip-portal.svelte +7 -0
  181. package/src/lib/components/primitives/tooltip/tooltip-provider.svelte +7 -0
  182. package/src/lib/components/primitives/tooltip/tooltip-trigger.svelte +7 -0
  183. package/src/lib/components/primitives/tooltip/tooltip.svelte +7 -0
  184. package/src/lib/components/viewer/MeshMetadataPanel.svelte +1 -1
  185. package/src/lib/components/viewer/SceneManager.svelte +19 -19
  186. package/src/lib/components/viewer/Viewer.svelte +68 -157
  187. package/src/lib/components/viewer/ViewerDock.svelte +404 -0
  188. package/src/lib/i18n/messages.ts +29 -0
  189. package/src/lib/index.ts +1 -0
  190. package/src/lib/public.ts +1 -0
  191. package/src/lib/schema/visibility-rules.ts +27 -1
  192. package/src/lib/styles/base.css +81 -80
  193. package/src/lib/utils/color-space.ts +107 -0
  194. package/dist/components/primitives/theme-switcher/ThemeSwitcher.svelte +0 -39
  195. package/dist/components/primitives/theme-switcher/ThemeSwitcher.svelte.d.ts +0 -3
  196. package/dist/components/primitives/theme-switcher/index.d.ts +0 -1
  197. package/dist/components/primitives/theme-switcher/index.js +0 -1
  198. package/dist/styles/themes/cyberpunk.css +0 -98
  199. package/dist/styles/themes/neutral.css +0 -72
  200. package/dist/styles/themes/ocean.css +0 -75
  201. package/dist/styles/themes/selva.css +0 -105
  202. package/dist/theme/themeStore.svelte.d.ts +0 -12
  203. package/dist/theme/themeStore.svelte.js +0 -40
  204. package/dist/theme/themes.d.ts +0 -5
  205. package/dist/theme/themes.js +0 -13
  206. package/src/lib/components/primitives/theme-switcher/ThemeSwitcher.svelte +0 -39
  207. package/src/lib/components/primitives/theme-switcher/index.ts +0 -1
  208. package/src/lib/styles/themes/cyberpunk.css +0 -98
  209. package/src/lib/styles/themes/neutral.css +0 -72
  210. package/src/lib/styles/themes/ocean.css +0 -75
  211. package/src/lib/styles/themes/selva.css +0 -105
  212. package/src/lib/theme/themeStore.svelte.ts +0 -50
  213. package/src/lib/theme/themes.ts +0 -18
@@ -0,0 +1,404 @@
1
+ <script lang="ts">
2
+ import { MediaQuery } from 'svelte/reactivity';
3
+ import {
4
+ Camera,
5
+ Grid3x3,
6
+ Layers,
7
+ LocateFixed,
8
+ Maximize,
9
+ Minimize,
10
+ Moon,
11
+ Ellipsis,
12
+ Palette,
13
+ Ruler,
14
+ Settings2,
15
+ Spline,
16
+ Cuboid
17
+ } from '@lucide/svelte';
18
+ import type { CameraProjection, Look, ViewPreset } from '@selvajs/visualization/render';
19
+ import * as DropdownMenu from '$lib/components/primitives/dropdown-menu/index.js';
20
+ import type { ViewerMessages } from '$lib/i18n/messages';
21
+
22
+ interface Props {
23
+ t: ViewerMessages;
24
+ /** Fades the dock out while a host drawer covers the viewer. */
25
+ hidden?: boolean;
26
+ showDisplay: boolean;
27
+ showGrid: boolean;
28
+ showSceneManager: boolean;
29
+ showScreenshot: boolean;
30
+ showFullscreen: boolean;
31
+ styleOptions: { look: Look; label: string }[];
32
+ viewPresets: { preset: ViewPreset; label: string }[];
33
+ renderStyle: Look;
34
+ nightOn: boolean;
35
+ edgesVisible: boolean;
36
+ measureActive: boolean;
37
+ gridVisible: boolean;
38
+ sceneManagerOpen: boolean;
39
+ projection: CameraProjection;
40
+ isFullscreen: boolean;
41
+ onSetLook: (look: Look) => void;
42
+ onToggleNight: () => void;
43
+ onToggleEdges: () => void;
44
+ onToggleMeasure: () => void;
45
+ onToggleGrid: () => void;
46
+ onToggleSceneManager: () => void;
47
+ onToggleProjection: () => void;
48
+ onSetView: (preset: ViewPreset) => void;
49
+ onFit: () => void;
50
+ onScreenshot: () => void;
51
+ onToggleFullscreen: () => void;
52
+ }
53
+
54
+ let {
55
+ t,
56
+ hidden = false,
57
+ showDisplay,
58
+ showGrid,
59
+ showSceneManager,
60
+ showScreenshot,
61
+ showFullscreen,
62
+ styleOptions,
63
+ viewPresets,
64
+ renderStyle,
65
+ nightOn,
66
+ edgesVisible,
67
+ measureActive,
68
+ gridVisible,
69
+ sceneManagerOpen,
70
+ projection,
71
+ isFullscreen,
72
+ onSetLook,
73
+ onToggleNight,
74
+ onToggleEdges,
75
+ onToggleMeasure,
76
+ onToggleGrid,
77
+ onToggleSceneManager,
78
+ onToggleProjection,
79
+ onSetView,
80
+ onFit,
81
+ onScreenshot,
82
+ onToggleFullscreen
83
+ }: Props = $props();
84
+
85
+ // A hairline pill at the bottom edge is the iOS home indicator on a phone, and swiping it leaves
86
+ // the app. Touch gets a visible round button instead.
87
+ const touch = new MediaQuery('(pointer: coarse)');
88
+ const reducedMotion = new MediaQuery('(prefers-reduced-motion: reduce)');
89
+
90
+ const COLLAPSED = $derived(touch.current ? { w: 44, h: 44 } : { w: 72, h: 8 });
91
+ const EXPANDED_H = $derived(touch.current ? 54 : 46);
92
+ const PAD = 4;
93
+
94
+ let dockEl: HTMLDivElement | undefined = $state();
95
+ let rowWidth = $state(0);
96
+ let engaged = $state(false);
97
+ // Set by an outside press: the dock was dismissed, so it shrinks faster.
98
+ let dismissed = $state(false);
99
+ let menusOpen = $state(0);
100
+ let focusWithin = $state(false);
101
+ let idleTimer: ReturnType<typeof setTimeout> | undefined;
102
+
103
+ const wantOpen = $derived(engaged || menusOpen > 0 || focusWithin);
104
+
105
+ // Opening leads: the pill grows first and the buttons pop in behind it. Closing mirrors it: the
106
+ // buttons fade first, then the pill shrinks. Without the delay on close the row is clipped mid-fade.
107
+ const EASE = 'cubic-bezier(0.32, 0.72, 0, 1)';
108
+ const containerStyle = $derived.by(() => {
109
+ const bottom = touch.current ? 'max(1rem,env(safe-area-inset-bottom))' : '0.75rem';
110
+ const geometry = wantOpen
111
+ ? `width:${rowWidth + PAD * 2 + 2}px;height:${EXPANDED_H}px;border-radius:14px;padding:${PAD}px`
112
+ : `width:${COLLAPSED.w}px;height:${COLLAPSED.h}px;border-radius:${touch.current ? 22 : 99}px;padding:0px`;
113
+ const size = `bottom:${bottom};${geometry}`;
114
+ if (reducedMotion.current) return size;
115
+ const ms = wantOpen ? 420 : dismissed ? 130 : 170;
116
+ const delay = wantOpen || dismissed ? 0 : 20;
117
+ const props = ['width', 'height', 'border-radius', 'padding'];
118
+ return `${size};transition:${props.map((p) => `${p} ${ms}ms ${EASE} ${delay}ms`).join(',')},opacity 200ms ease`;
119
+ });
120
+ const itemStyle = (index: number) => {
121
+ if (reducedMotion.current) return `opacity:${wantOpen ? 1 : 0}`;
122
+ const delay = 110 + index * 35;
123
+ return wantOpen
124
+ ? `opacity:1;transform:none;transition:opacity 240ms ease ${delay}ms,transform 420ms cubic-bezier(0.34,1.45,0.5,1) ${delay}ms`
125
+ : 'opacity:0;transform:translateY(6px) scale(0.85);transition:opacity 100ms ease,transform 100ms ease';
126
+ };
127
+ // The touch button's settings glyph hands over to the row.
128
+ const glyphStyle = $derived(
129
+ `opacity:${touch.current && !wantOpen ? 1 : 0};${reducedMotion.current ? '' : `transition:opacity ${wantOpen ? 100 : 100}ms ease ${wantOpen || dismissed ? 0 : 60}ms`}`
130
+ );
131
+
132
+ function scheduleCollapse(delay = touch.current ? 3500 : 150) {
133
+ clearTimeout(idleTimer);
134
+ idleTimer = setTimeout(() => (engaged = false), delay);
135
+ }
136
+
137
+ function engage() {
138
+ engaged = true;
139
+ dismissed = false;
140
+ clearTimeout(idleTimer);
141
+ }
142
+
143
+ function handlePointerMove(event: PointerEvent) {
144
+ if (!dockEl || touch.current || event.pointerType === 'touch') return;
145
+ // Orbiting or box-dragging with the button down should never pop the dock open.
146
+ if (event.buttons) return;
147
+ const host = dockEl.offsetParent;
148
+ if (!host || !(event.target instanceof Node) || !host.contains(event.target)) {
149
+ if (engaged) scheduleCollapse();
150
+ return;
151
+ }
152
+ const r = dockEl.getBoundingClientRect();
153
+ const near =
154
+ event.clientY > r.top - 36 && Math.abs(event.clientX - (r.left + r.width / 2)) < 110;
155
+ if (near) engage();
156
+ else if (engaged) scheduleCollapse();
157
+ }
158
+
159
+ function handleWindowPointerDown(event: PointerEvent) {
160
+ if (!engaged || !dockEl || !(event.target instanceof Node)) return;
161
+ if (!dockEl.contains(event.target)) {
162
+ clearTimeout(idleTimer);
163
+ dismissed = true;
164
+ engaged = false;
165
+ }
166
+ }
167
+
168
+ function openFromCollapsed() {
169
+ if (wantOpen) return;
170
+ engage();
171
+ if (touch.current) scheduleCollapse();
172
+ }
173
+
174
+ // Any use of the open dock on touch pushes the idle collapse back.
175
+ function keepAlive() {
176
+ if (touch.current && engaged) scheduleCollapse();
177
+ }
178
+
179
+ function trackMenu(open: boolean) {
180
+ menusOpen += open ? 1 : -1;
181
+ if (!open && !touch.current) scheduleCollapse(250);
182
+ }
183
+
184
+ // Buttons in the row, in order, so the stagger index stays right when some are hidden.
185
+ const buttons = $derived(
186
+ [
187
+ showDisplay && 'look',
188
+ showSceneManager && 'scene',
189
+ 'measure',
190
+ 'fit',
191
+ showFullscreen && 'full',
192
+ 'more'
193
+ ].filter(Boolean) as string[]
194
+ );
195
+ const idx = (id: string) => buttons.indexOf(id);
196
+
197
+ const BTN =
198
+ 'relative grid size-9.5 shrink-0 place-items-center rounded-lg text-muted-foreground outline-none transition-colors hover:bg-hover hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring aria-pressed:text-foreground data-[state=open]:bg-selected data-[state=open]:text-foreground pointer-coarse:size-11';
199
+ const DOT =
200
+ 'aria-pressed:after:absolute aria-pressed:after:bottom-0.5 aria-pressed:after:size-1 aria-pressed:after:rounded-full aria-pressed:after:bg-primary';
201
+ </script>
202
+
203
+ <!-- eslint-disable shadcn/no-inline-styles -- the pill animates between measured pixel sizes and staggered per-item delays; no class can express those -->
204
+
205
+ <svelte:window onpointermove={handlePointerMove} onpointerdown={handleWindowPointerDown} />
206
+
207
+ <!-- Resting state is a pill; it grows into the row. Only the container is clickable while collapsed. -->
208
+ <div
209
+ bind:this={dockEl}
210
+ role="toolbar"
211
+ aria-label={t.toolsMenu}
212
+ tabindex="-1"
213
+ class="shadow-lg backdrop-blur-md absolute left-1/2 z-50 -translate-x-1/2 overflow-hidden border {hidden
214
+ ? 'pointer-events-none opacity-0'
215
+ : ''} {wantOpen
216
+ ? 'bg-background/70'
217
+ : touch.current
218
+ ? 'cursor-pointer border-border bg-background/90'
219
+ : nightOn
220
+ ? 'border-white/50 bg-white/40 hover:bg-white/65 cursor-pointer'
221
+ : 'cursor-pointer border-border-strong bg-faint-foreground hover:bg-muted-foreground'}"
222
+ style={containerStyle}
223
+ onclick={openFromCollapsed}
224
+ onkeydown={(e) => (e.key === 'Escape' ? (engaged = false) : undefined)}
225
+ onfocusin={() => (focusWithin = true)}
226
+ onfocusout={(e) => {
227
+ if (!dockEl?.contains(e.relatedTarget as Node | null)) focusWithin = false;
228
+ }}
229
+ onpointerdown={keepAlive}
230
+ >
231
+ {#if touch.current}
232
+ <span
233
+ class="inset-0 pointer-events-none absolute grid place-items-center text-foreground"
234
+ style={glyphStyle}
235
+ aria-hidden="true"
236
+ >
237
+ <Settings2 class="size-5" />
238
+ </span>
239
+ {/if}
240
+
241
+ <div
242
+ bind:offsetWidth={rowWidth}
243
+ class="gap-0.5 mx-auto flex w-max items-center"
244
+ inert={!wantOpen}
245
+ >
246
+ {#if showDisplay}
247
+ <div style={itemStyle(idx('look'))}>
248
+ <DropdownMenu.Root onOpenChange={trackMenu}>
249
+ <DropdownMenu.Trigger>
250
+ {#snippet child({ props })}
251
+ <button {...props} class={BTN} title={t.display} aria-label={t.display}>
252
+ <Palette class="size-4.5" />
253
+ </button>
254
+ {/snippet}
255
+ </DropdownMenu.Trigger>
256
+ <DropdownMenu.Content
257
+ side="top"
258
+ align="center"
259
+ sideOffset={12}
260
+ variant="glass"
261
+ class="min-w-52 z-10001 w-auto whitespace-nowrap"
262
+ >
263
+ <DropdownMenu.RadioGroup
264
+ value={renderStyle}
265
+ onValueChange={(look) => onSetLook(look as Look)}
266
+ >
267
+ {#each styleOptions as { look, label } (look)}
268
+ <DropdownMenu.RadioItem value={look} closeOnSelect={false}>
269
+ {label}
270
+ </DropdownMenu.RadioItem>
271
+ {/each}
272
+ </DropdownMenu.RadioGroup>
273
+ <DropdownMenu.Separator />
274
+ <DropdownMenu.CheckboxItem
275
+ switch
276
+ checked={edgesVisible}
277
+ onCheckedChange={onToggleEdges}
278
+ >
279
+ <Spline />
280
+ {t.edges}
281
+ <DropdownMenu.Shortcut class="pointer-coarse:hidden">E</DropdownMenu.Shortcut>
282
+ </DropdownMenu.CheckboxItem>
283
+ </DropdownMenu.Content>
284
+ </DropdownMenu.Root>
285
+ </div>
286
+ {/if}
287
+
288
+ {#if showSceneManager}
289
+ <div style={itemStyle(idx('scene'))}>
290
+ <button
291
+ class="{BTN} {DOT}"
292
+ title="{t.sceneManager} (S)"
293
+ aria-label={t.sceneManager}
294
+ aria-pressed={sceneManagerOpen}
295
+ onclick={onToggleSceneManager}
296
+ >
297
+ <Layers class="size-4.5" />
298
+ </button>
299
+ </div>
300
+ {/if}
301
+
302
+ <div style={itemStyle(idx('measure'))}>
303
+ <button
304
+ class="{BTN} {DOT}"
305
+ title="{t.measure} (M)"
306
+ aria-label={t.measure}
307
+ aria-pressed={measureActive}
308
+ onclick={onToggleMeasure}
309
+ >
310
+ <Ruler class="size-4.5" />
311
+ </button>
312
+ </div>
313
+
314
+ <div style={itemStyle(idx('fit'))}>
315
+ <button class={BTN} title="{t.fitToView} (F)" aria-label="{t.fitToView} (F)" onclick={onFit}>
316
+ <LocateFixed class="size-4.5" />
317
+ </button>
318
+ </div>
319
+
320
+ {#if showFullscreen}
321
+ <div style={itemStyle(idx('full'))}>
322
+ <button
323
+ class={BTN}
324
+ title={isFullscreen ? t.exitFullscreen : t.fullscreen}
325
+ aria-label={isFullscreen ? t.exitFullscreen : t.fullscreen}
326
+ onclick={onToggleFullscreen}
327
+ >
328
+ {#if isFullscreen}<Minimize class="size-4.5" />{:else}<Maximize class="size-4.5" />{/if}
329
+ </button>
330
+ </div>
331
+ {/if}
332
+
333
+ <div style={itemStyle(idx('more'))}>
334
+ <DropdownMenu.Root onOpenChange={trackMenu}>
335
+ <DropdownMenu.Trigger>
336
+ {#snippet child({ props })}
337
+ <button {...props} class={BTN} title={t.toolsMenu} aria-label={t.toolsMenu}>
338
+ <Ellipsis class="size-4.5" />
339
+ </button>
340
+ {/snippet}
341
+ </DropdownMenu.Trigger>
342
+ <DropdownMenu.Content
343
+ side="top"
344
+ align="end"
345
+ sideOffset={12}
346
+ variant="glass"
347
+ class="min-w-60 z-10001 w-auto whitespace-nowrap"
348
+ >
349
+ <DropdownMenu.Label>{t.views}</DropdownMenu.Label>
350
+ <div class="gap-1 px-1 pb-1 grid grid-cols-4">
351
+ {#each viewPresets as { preset, label } (preset)}
352
+ <DropdownMenu.Item
353
+ closeOnSelect={false}
354
+ onSelect={() => onSetView(preset)}
355
+ variant="chip"
356
+ >
357
+ {label}
358
+ </DropdownMenu.Item>
359
+ {/each}
360
+ </div>
361
+
362
+ <DropdownMenu.Separator />
363
+
364
+ <DropdownMenu.CheckboxItem
365
+ switch
366
+ checked={projection === 'orthographic'}
367
+ onCheckedChange={onToggleProjection}
368
+ >
369
+ <Cuboid />
370
+ {t.switchToOrthographic}
371
+ <DropdownMenu.Shortcut class="pointer-coarse:hidden">O</DropdownMenu.Shortcut>
372
+ </DropdownMenu.CheckboxItem>
373
+
374
+ {#if showGrid}
375
+ <DropdownMenu.CheckboxItem switch checked={gridVisible} onCheckedChange={onToggleGrid}>
376
+ <Grid3x3 />
377
+ {t.grid}
378
+ <DropdownMenu.Shortcut class="pointer-coarse:hidden">G</DropdownMenu.Shortcut>
379
+ </DropdownMenu.CheckboxItem>
380
+ {/if}
381
+
382
+ {#if showDisplay}
383
+ <DropdownMenu.CheckboxItem switch checked={nightOn} onCheckedChange={onToggleNight}>
384
+ <Moon />
385
+ {t.nightMode}
386
+ <DropdownMenu.Shortcut class="pointer-coarse:hidden">N</DropdownMenu.Shortcut>
387
+ </DropdownMenu.CheckboxItem>
388
+ {/if}
389
+
390
+ {#if showScreenshot}
391
+ <DropdownMenu.Separator />
392
+ {/if}
393
+
394
+ {#if showScreenshot}
395
+ <DropdownMenu.Item onSelect={onScreenshot}>
396
+ <Camera />
397
+ {t.screenshot}
398
+ </DropdownMenu.Item>
399
+ {/if}
400
+ </DropdownMenu.Content>
401
+ </DropdownMenu.Root>
402
+ </div>
403
+ </div>
404
+ </div>
@@ -41,6 +41,7 @@ export interface ViewerMessages {
41
41
  lookStudio: string;
42
42
  lookShowcase: string;
43
43
  lookNight: string;
44
+ nightMode: string;
44
45
  lookArctic: string;
45
46
  lookXray: string;
46
47
  lookLineart: string;
@@ -132,6 +133,16 @@ export interface ViewerMessages {
132
133
  /** `{max}` is replaced with the field's character limit. */
133
134
  maxLength: string;
134
135
  invalidFormat: string;
136
+ /** Trigger label; `{color}` is the current hex value. */
137
+ colorPick: string;
138
+ colorArea: string;
139
+ /** `{s}` and `{v}` are percentages. */
140
+ colorAreaValue: string;
141
+ colorHue: string;
142
+ colorHex: string;
143
+ colorHexInvalid: string;
144
+ colorPresets: string;
145
+ colorEyedropper: string;
135
146
  invalidPattern: string;
136
147
 
137
148
  // Geometry input (Rhino only)
@@ -270,6 +281,7 @@ const en: ViewerMessages = {
270
281
  lookStudio: 'Studio',
271
282
  lookShowcase: 'Showcase',
272
283
  lookNight: 'Night',
284
+ nightMode: 'Night mode',
273
285
  lookArctic: 'Arctic',
274
286
  lookXray: 'X-Ray',
275
287
  lookLineart: 'Line Art',
@@ -342,6 +354,14 @@ const en: ViewerMessages = {
342
354
  fineDragHint: 'Hold Shift to drag finely',
343
355
  maxLength: 'Maximum {max} characters allowed',
344
356
  invalidFormat: 'Invalid format',
357
+ colorPick: 'Choose color, currently {color}',
358
+ colorArea: 'Saturation and brightness',
359
+ colorAreaValue: 'Saturation {s}%, brightness {v}%',
360
+ colorHue: 'Hue',
361
+ colorHex: 'Hex color',
362
+ colorHexInvalid: 'Enter a hex color such as #2f6fd6',
363
+ colorPresets: 'Presets',
364
+ colorEyedropper: 'Pick a color from the screen',
345
365
  invalidPattern: 'Invalid validation pattern configured',
346
366
 
347
367
  geometryPickAnyOne: 'Pick an object',
@@ -464,6 +484,7 @@ const de: ViewerMessages = {
464
484
  lookStudio: 'Studio',
465
485
  lookShowcase: 'Präsentation',
466
486
  lookNight: 'Nacht',
487
+ nightMode: 'Nachtmodus',
467
488
  lookArctic: 'Arktis',
468
489
  lookXray: 'Röntgen',
469
490
  lookLineart: 'Strichzeichnung',
@@ -537,6 +558,14 @@ const de: ViewerMessages = {
537
558
  fineDragHint: 'Umschalttaste halten für feine Schritte',
538
559
  maxLength: 'Höchstens {max} Zeichen erlaubt',
539
560
  invalidFormat: 'Ungültiges Format',
561
+ colorPick: 'Farbe wählen, aktuell {color}',
562
+ colorArea: 'Sättigung und Helligkeit',
563
+ colorAreaValue: 'Sättigung {s} %, Helligkeit {v} %',
564
+ colorHue: 'Farbton',
565
+ colorHex: 'Hex-Farbe',
566
+ colorHexInvalid: 'Hex-Farbe eingeben, z. B. #2f6fd6',
567
+ colorPresets: 'Vorgaben',
568
+ colorEyedropper: 'Farbe vom Bildschirm aufnehmen',
540
569
  invalidPattern: 'Ungültiges Prüfmuster hinterlegt',
541
570
 
542
571
  geometryPickAnyOne: 'Objekt wählen',
package/src/lib/index.ts CHANGED
@@ -19,6 +19,7 @@ export { default as Viewer } from './components/viewer/Viewer.svelte';
19
19
  export * from './schema/defaults';
20
20
  export * from './schema/dynamic-value-list';
21
21
  export * from './schema/traversal';
22
+ export { visibleInputItems } from './schema/visibility-rules';
22
23
  export {
23
24
  type UIHost,
24
25
  type ElementKind,
package/src/lib/public.ts CHANGED
@@ -80,6 +80,7 @@ export {
80
80
  export * from './schema/defaults';
81
81
  export * from './schema/traversal';
82
82
  export * from './schema/dynamic-value-list';
83
+ export { visibleInputItems } from './schema/visibility-rules';
83
84
 
84
85
  // UI-facing runtime types (not from schema)
85
86
  export type { ActionButton } from './types/actionButton';
@@ -1,10 +1,12 @@
1
1
  import type {
2
+ InputLayoutItem,
2
3
  LayoutItem,
3
4
  OutputLayoutItem,
4
5
  VisibilityRule,
5
6
  VisibilityCondition,
6
7
  GroupVisibilityCondition,
7
- TabConfig
8
+ TabConfig,
9
+ UISchema
8
10
  } from '@selvajs/schemas';
9
11
  import { isFileData } from '../utils/file-download';
10
12
 
@@ -148,6 +150,30 @@ export function isTabVisible(tab: TabConfig, values: Record<string, unknown>): b
148
150
  );
149
151
  }
150
152
 
153
+ /**
154
+ * The input items the form shows for these values: each passes its own condition, inside a
155
+ * shown group and, when tabbed, a shown tab. For a host that repeats the form back, such as a
156
+ * summary before a commit, without re-deriving the renderer's rules.
157
+ */
158
+ export function visibleInputItems(
159
+ schema: UISchema,
160
+ values: Record<string, unknown>
161
+ ): InputLayoutItem[] {
162
+ const layout = schema?.layout;
163
+ const groups =
164
+ layout?.type === 'tabbed'
165
+ ? (layout.tabs ?? []).filter((t) => isTabVisible(t, values)).flatMap((t) => t.groups ?? [])
166
+ : layout?.type === 'flat'
167
+ ? (layout.groups ?? [])
168
+ : [];
169
+ return groups
170
+ .filter((g) => evaluateGroupVisibility(g, values))
171
+ .flatMap((g) => g.items ?? [])
172
+ .filter(
173
+ (i): i is InputLayoutItem => i.type === 'input' && evaluateVisibility(i, values).visible
174
+ );
175
+ }
176
+
151
177
  /**
152
178
  * A linebreak is always "visible" but renders as a divider only, so a group left with
153
179
  * nothing but separators counts as empty.