@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 '../primitives/dropdown-menu/index.js';
20
+ import type { ViewerMessages } from '../../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>
@@ -0,0 +1,42 @@
1
+ import type { CameraProjection, Look, ViewPreset } from '@selvajs/visualization/render';
2
+ import type { ViewerMessages } from '../../i18n/messages';
3
+ interface Props {
4
+ t: ViewerMessages;
5
+ /** Fades the dock out while a host drawer covers the viewer. */
6
+ hidden?: boolean;
7
+ showDisplay: boolean;
8
+ showGrid: boolean;
9
+ showSceneManager: boolean;
10
+ showScreenshot: boolean;
11
+ showFullscreen: boolean;
12
+ styleOptions: {
13
+ look: Look;
14
+ label: string;
15
+ }[];
16
+ viewPresets: {
17
+ preset: ViewPreset;
18
+ label: string;
19
+ }[];
20
+ renderStyle: Look;
21
+ nightOn: boolean;
22
+ edgesVisible: boolean;
23
+ measureActive: boolean;
24
+ gridVisible: boolean;
25
+ sceneManagerOpen: boolean;
26
+ projection: CameraProjection;
27
+ isFullscreen: boolean;
28
+ onSetLook: (look: Look) => void;
29
+ onToggleNight: () => void;
30
+ onToggleEdges: () => void;
31
+ onToggleMeasure: () => void;
32
+ onToggleGrid: () => void;
33
+ onToggleSceneManager: () => void;
34
+ onToggleProjection: () => void;
35
+ onSetView: (preset: ViewPreset) => void;
36
+ onFit: () => void;
37
+ onScreenshot: () => void;
38
+ onToggleFullscreen: () => void;
39
+ }
40
+ declare const ViewerDock: import("svelte").Component<Props, {}, "">;
41
+ type ViewerDock = ReturnType<typeof ViewerDock>;
42
+ export default ViewerDock;
@@ -24,6 +24,7 @@ export interface ViewerMessages {
24
24
  lookStudio: string;
25
25
  lookShowcase: string;
26
26
  lookNight: string;
27
+ nightMode: string;
27
28
  lookArctic: string;
28
29
  lookXray: string;
29
30
  lookLineart: string;
@@ -103,6 +104,16 @@ export interface ViewerMessages {
103
104
  /** `{max}` is replaced with the field's character limit. */
104
105
  maxLength: string;
105
106
  invalidFormat: string;
107
+ /** Trigger label; `{color}` is the current hex value. */
108
+ colorPick: string;
109
+ colorArea: string;
110
+ /** `{s}` and `{v}` are percentages. */
111
+ colorAreaValue: string;
112
+ colorHue: string;
113
+ colorHex: string;
114
+ colorHexInvalid: string;
115
+ colorPresets: string;
116
+ colorEyedropper: string;
106
117
  invalidPattern: string;
107
118
  /** The pick button when the input takes several kinds of object. */
108
119
  geometryPickAnyOne: string;
@@ -33,6 +33,7 @@ const en = {
33
33
  lookStudio: 'Studio',
34
34
  lookShowcase: 'Showcase',
35
35
  lookNight: 'Night',
36
+ nightMode: 'Night mode',
36
37
  lookArctic: 'Arctic',
37
38
  lookXray: 'X-Ray',
38
39
  lookLineart: 'Line Art',
@@ -99,6 +100,14 @@ const en = {
99
100
  fineDragHint: 'Hold Shift to drag finely',
100
101
  maxLength: 'Maximum {max} characters allowed',
101
102
  invalidFormat: 'Invalid format',
103
+ colorPick: 'Choose color, currently {color}',
104
+ colorArea: 'Saturation and brightness',
105
+ colorAreaValue: 'Saturation {s}%, brightness {v}%',
106
+ colorHue: 'Hue',
107
+ colorHex: 'Hex color',
108
+ colorHexInvalid: 'Enter a hex color such as #2f6fd6',
109
+ colorPresets: 'Presets',
110
+ colorEyedropper: 'Pick a color from the screen',
102
111
  invalidPattern: 'Invalid validation pattern configured',
103
112
  geometryPickAnyOne: 'Pick an object',
104
113
  geometryPickAnyOther: 'Pick objects',
@@ -208,6 +217,7 @@ const de = {
208
217
  lookStudio: 'Studio',
209
218
  lookShowcase: 'Präsentation',
210
219
  lookNight: 'Nacht',
220
+ nightMode: 'Nachtmodus',
211
221
  lookArctic: 'Arktis',
212
222
  lookXray: 'Röntgen',
213
223
  lookLineart: 'Strichzeichnung',
@@ -274,6 +284,14 @@ const de = {
274
284
  fineDragHint: 'Umschalttaste halten für feine Schritte',
275
285
  maxLength: 'Höchstens {max} Zeichen erlaubt',
276
286
  invalidFormat: 'Ungültiges Format',
287
+ colorPick: 'Farbe wählen, aktuell {color}',
288
+ colorArea: 'Sättigung und Helligkeit',
289
+ colorAreaValue: 'Sättigung {s} %, Helligkeit {v} %',
290
+ colorHue: 'Farbton',
291
+ colorHex: 'Hex-Farbe',
292
+ colorHexInvalid: 'Hex-Farbe eingeben, z. B. #2f6fd6',
293
+ colorPresets: 'Vorgaben',
294
+ colorEyedropper: 'Farbe vom Bildschirm aufnehmen',
277
295
  invalidPattern: 'Ungültiges Prüfmuster hinterlegt',
278
296
  geometryPickAnyOne: 'Objekt wählen',
279
297
  geometryPickAnyOther: 'Objekte wählen',
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ 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 { visibleInputItems } from './schema/visibility-rules';
12
13
  export { type UIHost, type ElementKind, HOST_LABELS, isGeometryParamType, layoutItemKind, hostsFor, isSupportedOn, valuelessInputIds, unsupportedItems } from './schema/host-support';
13
14
  export * from './compute/solving.svelte';
14
15
  export { useSolveSession } from './compute/useSolveSession.svelte';
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 { visibleInputItems } from './schema/visibility-rules';
16
17
  export { HOST_LABELS, isGeometryParamType, layoutItemKind, hostsFor, isSupportedOn, valuelessInputIds, unsupportedItems } from './schema/host-support';
17
18
  export * from './compute/solving.svelte';
18
19
  // Solve Session seam. Re-exported so transports outside this package — e.g.
package/dist/public.d.ts CHANGED
@@ -14,6 +14,7 @@ export { writeExternalValue, readExternalValue, clearExternalValue, getExternalI
14
14
  export * from './schema/defaults';
15
15
  export * from './schema/traversal';
16
16
  export * from './schema/dynamic-value-list';
17
+ export { visibleInputItems } from './schema/visibility-rules';
17
18
  export type { ActionButton } from './types/actionButton';
18
19
  export type { SolveFn, SolveResult, SolveDiagnostic } from '@selvajs/solve/shared';
19
20
  export { DEFAULT_PRESET_LABELS, type PresetLabels } from './types/presetLabels';
package/dist/public.js CHANGED
@@ -28,4 +28,5 @@ export { writeExternalValue, readExternalValue, clearExternalValue, getExternalI
28
28
  export * from './schema/defaults';
29
29
  export * from './schema/traversal';
30
30
  export * from './schema/dynamic-value-list';
31
+ export { visibleInputItems } from './schema/visibility-rules';
31
32
  export { DEFAULT_PRESET_LABELS } from './types/presetLabels';
@@ -1,4 +1,4 @@
1
- import type { LayoutItem, VisibilityRule, VisibilityCondition, GroupVisibilityCondition, TabConfig } from '@selvajs/schemas';
1
+ import type { InputLayoutItem, LayoutItem, VisibilityRule, VisibilityCondition, GroupVisibilityCondition, TabConfig, UISchema } from '@selvajs/schemas';
2
2
  export interface VisibilityResult {
3
3
  visible: boolean;
4
4
  disabled: boolean;
@@ -20,6 +20,12 @@ export declare function evaluateGroupVisibility(group: {
20
20
  items?: LayoutItem[];
21
21
  }, values: Record<string, unknown>): boolean;
22
22
  export declare function isTabVisible(tab: TabConfig, values: Record<string, unknown>): boolean;
23
+ /**
24
+ * The input items the form shows for these values: each passes its own condition, inside a
25
+ * shown group and, when tabbed, a shown tab. For a host that repeats the form back, such as a
26
+ * summary before a commit, without re-deriving the renderer's rules.
27
+ */
28
+ export declare function visibleInputItems(schema: UISchema, values: Record<string, unknown>): InputLayoutItem[];
23
29
  /**
24
30
  * A linebreak is always "visible" but renders as a divider only, so a group left with
25
31
  * nothing but separators counts as empty.
@@ -106,6 +106,23 @@ export function evaluateGroupVisibility(group, values) {
106
106
  export function isTabVisible(tab, values) {
107
107
  return (tab.visible !== false && tab.groups.some((group) => evaluateGroupVisibility(group, values)));
108
108
  }
109
+ /**
110
+ * The input items the form shows for these values: each passes its own condition, inside a
111
+ * shown group and, when tabbed, a shown tab. For a host that repeats the form back, such as a
112
+ * summary before a commit, without re-deriving the renderer's rules.
113
+ */
114
+ export function visibleInputItems(schema, values) {
115
+ const layout = schema?.layout;
116
+ const groups = layout?.type === 'tabbed'
117
+ ? (layout.tabs ?? []).filter((t) => isTabVisible(t, values)).flatMap((t) => t.groups ?? [])
118
+ : layout?.type === 'flat'
119
+ ? (layout.groups ?? [])
120
+ : [];
121
+ return groups
122
+ .filter((g) => evaluateGroupVisibility(g, values))
123
+ .flatMap((g) => g.items ?? [])
124
+ .filter((i) => i.type === 'input' && evaluateVisibility(i, values).visible);
125
+ }
109
126
  /**
110
127
  * A linebreak is always "visible" but renders as a divider only, so a group left with
111
128
  * nothing but separators counts as empty.