@selvajs/ui 6.6.0-beta.14 → 6.6.0-beta.16

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 (205) hide show
  1. package/README.md +0 -3
  2. package/dist/components/compute/AppLayout.svelte +10 -5
  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 +3 -2
  26. package/dist/components/primitives/CalculateButton.svelte +2 -2
  27. package/dist/components/primitives/Callout.svelte +7 -5
  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 +8 -2
  79. package/dist/components/primitives/toggle/toggle.svelte.d.ts +10 -4
  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 +90 -164
  97. package/dist/components/viewer/Viewer.svelte.d.ts +3 -1
  98. package/dist/components/viewer/ViewerDock.svelte +404 -0
  99. package/dist/components/viewer/ViewerDock.svelte.d.ts +42 -0
  100. package/dist/i18n/messages.d.ts +12 -0
  101. package/dist/i18n/messages.js +20 -0
  102. package/dist/styles/base.css +81 -80
  103. package/dist/utils/color-space.d.ts +21 -0
  104. package/dist/utils/color-space.js +90 -0
  105. package/package.json +12 -13
  106. package/src/lib/components/compute/AppLayout.svelte +10 -5
  107. package/src/lib/components/compute/CollapsedPanelStrip.svelte +1 -1
  108. package/src/lib/components/compute/ParameterPresetManager.svelte +1 -1
  109. package/src/lib/components/compute/SolveMessages.svelte +2 -2
  110. package/src/lib/components/layout/FooterItemRenderer.svelte +1 -1
  111. package/src/lib/components/layout/PageFooter.svelte +2 -2
  112. package/src/lib/components/layout/PageHeader.svelte +1 -1
  113. package/src/lib/components/layout/SideNav.svelte +3 -3
  114. package/src/lib/components/layout/SubNav.svelte +2 -2
  115. package/src/lib/components/preview/ChartOutput.svelte +3 -3
  116. package/src/lib/components/preview/Group.svelte +7 -7
  117. package/src/lib/components/preview/ImageOutput.svelte +6 -6
  118. package/src/lib/components/preview/InputControl.svelte +1 -0
  119. package/src/lib/components/preview/OutputDisplay.svelte +8 -6
  120. package/src/lib/components/preview/TabBar.svelte +6 -4
  121. package/src/lib/components/preview/TabContent.svelte +1 -1
  122. package/src/lib/components/preview/TabLayout.svelte +3 -4
  123. package/src/lib/components/preview/inputs/ChecklistInput.svelte +3 -3
  124. package/src/lib/components/preview/inputs/ColorInput.svelte +356 -14
  125. package/src/lib/components/preview/inputs/FileInput.svelte +3 -3
  126. package/src/lib/components/preview/inputs/SegmentedInput.svelte +2 -1
  127. package/src/lib/components/preview/inputs/TileInput.svelte +3 -2
  128. package/src/lib/components/primitives/CalculateButton.svelte +2 -2
  129. package/src/lib/components/primitives/Callout.svelte +7 -5
  130. package/src/lib/components/primitives/DataTable.svelte +1 -1
  131. package/src/lib/components/primitives/ImageUploadField.svelte +1 -1
  132. package/src/lib/components/primitives/alert-dialog/alert-dialog-content.svelte +1 -1
  133. package/src/lib/components/primitives/badge/badge.svelte +2 -2
  134. package/src/lib/components/primitives/button/button.svelte +6 -7
  135. package/src/lib/components/primitives/card/card.svelte +1 -1
  136. package/src/lib/components/primitives/checkbox/checkbox.svelte +1 -1
  137. package/src/lib/components/primitives/command/command-input.svelte +1 -1
  138. package/src/lib/components/primitives/command/command-item.svelte +1 -1
  139. package/src/lib/components/primitives/command/command-link-item.svelte +1 -1
  140. package/src/lib/components/primitives/command/command.svelte +1 -1
  141. package/src/lib/components/primitives/context-menu/context-menu-checkbox-item.svelte +1 -1
  142. package/src/lib/components/primitives/context-menu/context-menu-content.svelte +1 -1
  143. package/src/lib/components/primitives/context-menu/context-menu-item.svelte +1 -1
  144. package/src/lib/components/primitives/context-menu/context-menu-radio-item.svelte +1 -1
  145. package/src/lib/components/primitives/context-menu/context-menu-sub-content.svelte +1 -1
  146. package/src/lib/components/primitives/context-menu/context-menu-sub-trigger.svelte +1 -1
  147. package/src/lib/components/primitives/dialog/dialog-content.svelte +2 -2
  148. package/src/lib/components/primitives/drawer/Drawer.svelte +1 -1
  149. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-checkbox-item.svelte +31 -11
  150. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-content.svelte +5 -1
  151. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-item.svelte +4 -2
  152. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-radio-item.svelte +1 -1
  153. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-shortcut.svelte +1 -1
  154. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-sub-content.svelte +1 -1
  155. package/src/lib/components/primitives/dropdown-menu/dropdown-menu-sub-trigger.svelte +1 -1
  156. package/src/lib/components/primitives/index.ts +1 -1
  157. package/src/lib/components/primitives/input/input.svelte +2 -2
  158. package/src/lib/components/primitives/input-group/input-group.svelte +1 -1
  159. package/src/lib/components/primitives/popover/popover-content.svelte +1 -1
  160. package/src/lib/components/primitives/resizable/resizable-handle.svelte +2 -2
  161. package/src/lib/components/primitives/search/search.svelte +1 -1
  162. package/src/lib/components/primitives/select/select-content.svelte +1 -1
  163. package/src/lib/components/primitives/select/select-item.svelte +1 -1
  164. package/src/lib/components/primitives/select/select-trigger.svelte +1 -1
  165. package/src/lib/components/primitives/slider/slider.svelte +1 -1
  166. package/src/lib/components/primitives/switch/switch.svelte +1 -1
  167. package/src/lib/components/primitives/tabs/tabs-list.svelte +1 -1
  168. package/src/lib/components/primitives/tabs/tabs-trigger.svelte +1 -1
  169. package/src/lib/components/primitives/textarea/textarea.svelte +1 -1
  170. package/src/lib/components/primitives/toggle/toggle.svelte +8 -2
  171. package/src/lib/components/primitives/toggle-group/toggle-group-item.svelte +1 -1
  172. package/src/lib/components/primitives/toggle-group/toggle-group.svelte +1 -1
  173. package/src/lib/components/primitives/tooltip/index.ts +19 -0
  174. package/src/lib/components/primitives/tooltip/tooltip-content.svelte +34 -0
  175. package/src/lib/components/primitives/tooltip/tooltip-portal.svelte +7 -0
  176. package/src/lib/components/primitives/tooltip/tooltip-provider.svelte +7 -0
  177. package/src/lib/components/primitives/tooltip/tooltip-trigger.svelte +7 -0
  178. package/src/lib/components/primitives/tooltip/tooltip.svelte +7 -0
  179. package/src/lib/components/viewer/MeshMetadataPanel.svelte +1 -1
  180. package/src/lib/components/viewer/SceneManager.svelte +19 -19
  181. package/src/lib/components/viewer/Viewer.svelte +90 -164
  182. package/src/lib/components/viewer/ViewerDock.svelte +404 -0
  183. package/src/lib/i18n/messages.ts +32 -0
  184. package/src/lib/styles/base.css +81 -80
  185. package/src/lib/utils/color-space.ts +107 -0
  186. package/dist/components/primitives/theme-switcher/ThemeSwitcher.svelte +0 -39
  187. package/dist/components/primitives/theme-switcher/ThemeSwitcher.svelte.d.ts +0 -3
  188. package/dist/components/primitives/theme-switcher/index.d.ts +0 -1
  189. package/dist/components/primitives/theme-switcher/index.js +0 -1
  190. package/dist/styles/themes/cyberpunk.css +0 -98
  191. package/dist/styles/themes/neutral.css +0 -72
  192. package/dist/styles/themes/ocean.css +0 -75
  193. package/dist/styles/themes/selva.css +0 -105
  194. package/dist/theme/themeStore.svelte.d.ts +0 -12
  195. package/dist/theme/themeStore.svelte.js +0 -40
  196. package/dist/theme/themes.d.ts +0 -5
  197. package/dist/theme/themes.js +0 -13
  198. package/src/lib/components/primitives/theme-switcher/ThemeSwitcher.svelte +0 -39
  199. package/src/lib/components/primitives/theme-switcher/index.ts +0 -1
  200. package/src/lib/styles/themes/cyberpunk.css +0 -98
  201. package/src/lib/styles/themes/neutral.css +0 -72
  202. package/src/lib/styles/themes/ocean.css +0 -75
  203. package/src/lib/styles/themes/selva.css +0 -105
  204. package/src/lib/theme/themeStore.svelte.ts +0 -50
  205. 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;
@@ -23,6 +23,8 @@ export interface ViewerMessages {
23
23
  lookTechnical: string;
24
24
  lookStudio: string;
25
25
  lookShowcase: string;
26
+ lookNight: string;
27
+ nightMode: string;
26
28
  lookArctic: string;
27
29
  lookXray: string;
28
30
  lookLineart: string;
@@ -102,6 +104,16 @@ export interface ViewerMessages {
102
104
  /** `{max}` is replaced with the field's character limit. */
103
105
  maxLength: string;
104
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;
105
117
  invalidPattern: string;
106
118
  /** The pick button when the input takes several kinds of object. */
107
119
  geometryPickAnyOne: string;
@@ -32,6 +32,8 @@ const en = {
32
32
  lookTechnical: 'Technical',
33
33
  lookStudio: 'Studio',
34
34
  lookShowcase: 'Showcase',
35
+ lookNight: 'Night',
36
+ nightMode: 'Night mode',
35
37
  lookArctic: 'Arctic',
36
38
  lookXray: 'X-Ray',
37
39
  lookLineart: 'Line Art',
@@ -98,6 +100,14 @@ const en = {
98
100
  fineDragHint: 'Hold Shift to drag finely',
99
101
  maxLength: 'Maximum {max} characters allowed',
100
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',
101
111
  invalidPattern: 'Invalid validation pattern configured',
102
112
  geometryPickAnyOne: 'Pick an object',
103
113
  geometryPickAnyOther: 'Pick objects',
@@ -206,6 +216,8 @@ const de = {
206
216
  lookTechnical: 'Technisch',
207
217
  lookStudio: 'Studio',
208
218
  lookShowcase: 'Präsentation',
219
+ lookNight: 'Nacht',
220
+ nightMode: 'Nachtmodus',
209
221
  lookArctic: 'Arktis',
210
222
  lookXray: 'Röntgen',
211
223
  lookLineart: 'Strichzeichnung',
@@ -272,6 +284,14 @@ const de = {
272
284
  fineDragHint: 'Umschalttaste halten für feine Schritte',
273
285
  maxLength: 'Höchstens {max} Zeichen erlaubt',
274
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',
275
295
  invalidPattern: 'Ungültiges Prüfmuster hinterlegt',
276
296
  geometryPickAnyOne: 'Objekt wählen',
277
297
  geometryPickAnyOther: 'Objekte wählen',