@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
@@ -15,30 +15,15 @@
15
15
  type Grid,
16
16
  type ThreeViewer
17
17
  } from '@selvajs/visualization/render';
18
- import {
19
- Maximize,
20
- Minimize,
21
- Camera,
22
- Layers,
23
- Settings2,
24
- Box,
25
- Square,
26
- Frame,
27
- Ruler,
28
- Grid3x3,
29
- Palette,
30
- Spline
31
- } from '@lucide/svelte';
32
18
  import { SvelteSet } from 'svelte/reactivity';
33
19
  import type * as THREE from 'three';
34
20
  import type { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
35
21
  import { createSceneOutliner, type SceneOutliner } from '@selvajs/visualization/scene';
36
22
  import { clearGeometryCache } from '@selvajs/visualization/parse';
37
23
  import SceneManager from './SceneManager.svelte';
24
+ import ViewerDock from './ViewerDock.svelte';
38
25
  import MeshMetadataPanel from './MeshMetadataPanel.svelte';
39
26
  import * as Resizable from '../primitives/resizable/index.js';
40
- import * as DropdownMenu from '../primitives/dropdown-menu/index.js';
41
- import { Button } from '../primitives/button/index.js';
42
27
  import type { Locale } from '../../i18n/messages';
43
28
  import { layerLocaleContext } from '../../i18n/localeContext.svelte';
44
29
  import { saveBlob } from '../../utils/host-files';
@@ -52,6 +37,8 @@
52
37
  showDisplayMenu?: boolean;
53
38
  enableMeshClick?: boolean;
54
39
  backgroundColor?: string;
40
+ /** The look to open in. Default `technical`. */
41
+ look?: Look;
55
42
  /** Initial state of the edge overlay and its display-menu checkmark. Default false. */
56
43
  showEdges?: boolean;
57
44
  }
@@ -91,6 +78,7 @@
91
78
  showDisplayMenu: true,
92
79
  enableMeshClick: true,
93
80
  backgroundColor: '#E6E6E6',
81
+ look: 'technical',
94
82
  showEdges: false
95
83
  };
96
84
 
@@ -124,7 +112,8 @@
124
112
  let clearEdges: ThreeViewer['clearEdges'] | null = null;
125
113
  let invalidate: (() => void) | null = null;
126
114
  let captureImage: ThreeViewer['captureImage'] | null = null;
127
- let setLook: ((look: Look) => void) | null = null;
115
+ let setLook: ThreeViewer['setLook'] | null = null;
116
+ let syncEmissiveLights: (() => void) | null = null;
128
117
  let updateGridScale: (() => void) | null = null;
129
118
  let updateShadowBounds: (() => void) | null = null;
130
119
  let fitToView: (() => void) | null = null;
@@ -145,9 +134,16 @@
145
134
  let projection: CameraProjection = $state('perspective');
146
135
  let measureActive = $state(false);
147
136
  let gridVisible = $state(false);
148
- let renderStyle: Look = $state('technical');
137
+ // A saved schema can name a look this build doesn't have, and an unknown look throws on mount.
138
+ const openingLook = untrack(() => {
139
+ const look = viewerConfig.look;
140
+ return look && LOOKS.includes(look) ? look : defaultViewerConfig.look;
141
+ });
142
+ const opensAsLineDrawing = LOOK_PRESETS[openingLook].requiresEdges === true;
143
+ const configuredEdges = untrack(() => viewerConfig.showEdges) ?? defaultViewerConfig.showEdges;
144
+ let renderStyle: Look = $state(openingLook);
149
145
  // Seeded once, then owned by the menu toggle.
150
- let edgesVisible = $state(untrack(() => viewerConfig.showEdges) ?? defaultViewerConfig.showEdges);
146
+ let edgesVisible = $state(opensAsLineDrawing || configuredEdges);
151
147
  let selectedMeshMetadata: Record<string, any> | null = $state(null);
152
148
  let selectedMeshName: string | null = $state(null);
153
149
 
@@ -160,14 +156,19 @@
160
156
  arctic: t.lookArctic,
161
157
  xray: t.lookXray,
162
158
  lineart: t.lookLineart,
163
- wireframe: t.lookWireframe
159
+ wireframe: t.lookWireframe,
160
+ night: t.lookNight
164
161
  });
162
+ // Night has its own toggle, so it stays out of the radio list.
165
163
  const styleOptions = $derived<{ look: Look; label: string }[]>(
166
- LOOKS.map((look) => ({
164
+ LOOKS.filter((look) => look !== 'night').map((look) => ({
167
165
  look,
168
166
  label: lookLabels[look] ?? look.charAt(0).toUpperCase() + look.slice(1)
169
167
  }))
170
168
  );
169
+ const nightOn = $derived(renderStyle === 'night');
170
+ // What the night toggle returns to.
171
+ let dayLook: Look = $state(openingLook === 'night' ? defaultViewerConfig.look : openingLook);
171
172
 
172
173
  const VIEW_PRESETS: { preset: ViewPreset; label: () => string }[] = [
173
174
  { preset: 'top', label: () => t.viewTop },
@@ -234,6 +235,7 @@
234
235
  invalidate = init.invalidate;
235
236
  captureImage = init.captureImage;
236
237
  setLook = init.setLook;
238
+ syncEmissiveLights = init.syncEmissiveLights;
237
239
  updateGridScale = init.updateGridScale;
238
240
  updateShadowBounds = init.updateShadowBounds;
239
241
  fitToView = init.fitToView;
@@ -279,12 +281,13 @@
279
281
 
280
282
  // Edges the lineart look switched on, so leaving it can put the user's own choice back rather
281
283
  // than stranding them with overlays they never asked for.
282
- let edgesBeforeLineDrawing: boolean | null = null;
284
+ let edgesBeforeLineDrawing: boolean | null = opensAsLineDrawing ? configuredEdges : null;
283
285
 
284
286
  function setRenderStyle(look: Look) {
285
287
  if (!setLook || look === renderStyle) return;
286
288
  const wasLineDrawing = LOOK_PRESETS[renderStyle].requiresEdges === true;
287
289
  renderStyle = look;
290
+ if (look !== 'night') dayLook = look;
288
291
  setLook(look);
289
292
 
290
293
  // A look never drives the overlay itself; honouring `requiresEdges` is the host's call. Without
@@ -350,6 +353,10 @@
350
353
  else clearEdges?.(scene);
351
354
  }
352
355
 
356
+ function toggleNight() {
357
+ setRenderStyle(nightOn ? dayLook : 'night');
358
+ }
359
+
353
360
  function toggleEdges() {
354
361
  edgesVisible = !edgesVisible;
355
362
  // Toggling by hand inside lineart overrides what that look forced, so the pre-look value is
@@ -366,7 +373,8 @@
366
373
  untrack(() => {
367
374
  // The new meshes carry the materials the parser built, so a look that overrides them
368
375
  // (arctic, x-ray) has to be re-applied or the solve silently reverts to shaded.
369
- setLook?.(renderStyle);
376
+ // Emissive lights wait for `applyTo` below, or a hidden lamp would count as lit.
377
+ setLook?.(renderStyle, { deferEmissiveLights: true });
370
378
  // updateScene discarded the previous solve's overlays along with its content.
371
379
  if (edgesVisible) applyEdges?.(scene!, edgeOverrides());
372
380
  // Rescale the grid so cells and fade match the new content's extent.
@@ -377,6 +385,7 @@
377
385
  // The rebuild un-hid everything; the outliner keys hidden state on Grasshopper
378
386
  // identity, not on the instances just discarded, so it can re-hide it.
379
387
  outliner?.applyTo();
388
+ syncEmissiveLights?.();
380
389
  sceneVersion++;
381
390
  // Repaint now rather than on the render loop's safety interval.
382
391
  invalidate?.();
@@ -433,6 +442,27 @@
433
442
  isFullscreen = !isFullscreen;
434
443
  }
435
444
 
445
+ // Single letters; F (fit) is handled by the canvas itself. Held back while typing, e.g. in the
446
+ // scene manager's search box.
447
+ const SHORTCUTS: Record<string, () => void> = {
448
+ o: toggleProjection,
449
+ m: toggleMeasure,
450
+ g: toggleGrid,
451
+ n: toggleNight,
452
+ e: toggleEdges,
453
+ s: () => (sceneManagerOpen = !sceneManagerOpen)
454
+ };
455
+
456
+ function handleShortcut(event: KeyboardEvent) {
457
+ if (event.ctrlKey || event.metaKey || event.altKey || event.repeat) return;
458
+ const target = event.target as HTMLElement;
459
+ if (target.closest('input, textarea, select, [contenteditable]')) return;
460
+ const action = SHORTCUTS[event.key.toLowerCase()];
461
+ if (!action) return;
462
+ event.preventDefault();
463
+ action();
464
+ }
465
+
436
466
  function hasUsefulMetadata(metadata: Record<string, any>): boolean {
437
467
  if (!metadata) return false;
438
468
 
@@ -463,10 +493,13 @@
463
493
  }
464
494
  </script>
465
495
 
496
+ <!-- Key events bubble up from the focused canvas or any control inside; the div itself is not interactive. -->
497
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
466
498
  <div
467
499
  class="min-h-64 sm:min-h-96 lg:min-h-125 relative h-full flex-1 border {isFullscreen
468
500
  ? 'fullscreen-viewer'
469
501
  : 'overflow-hidden rounded-lg'}"
502
+ onkeydown={handleShortcut}
470
503
  >
471
504
  <Resizable.PaneGroup direction="horizontal" class="h-full w-full">
472
505
  <!-- `defaultSize` must sum to 100 across the live panes. Panes register a frame before the
@@ -480,7 +513,7 @@
480
513
  the DOM, so this is the only observable proof a solve's geometry decoded and
481
514
  rendered; e2e asserts on it. -->
482
515
  <canvas
483
- class="viewer-canvas block h-full w-full select-none"
516
+ class="viewer-canvas block h-full w-full outline-none select-none"
484
517
  data-testid="viewer-canvas"
485
518
  data-mesh-count={meshes.length}
486
519
  bind:this={canvas}
@@ -496,10 +529,10 @@
496
529
  solve hides the model the user is adjusting. -->
497
530
  {#if isSolving}
498
531
  <div
499
- class="inset-x-0 top-0 h-0.5 animate-in fade-in pointer-events-none absolute z-20 overflow-hidden bg-primary/20 duration-200"
532
+ class="inset-x-0 top-0 animate-in fade-in pointer-events-none absolute z-20 h-px overflow-hidden duration-300"
500
533
  aria-hidden="true"
501
534
  >
502
- <div class="solve-bar h-full w-1/3 bg-primary"></div>
535
+ <div class="solve-bar h-full w-1/3 bg-faint-foreground"></div>
503
536
  </div>
504
537
  {/if}
505
538
  {#if isSlow}
@@ -533,143 +566,36 @@
533
566
  {/if}
534
567
 
535
568
  {#if config.showToolsMenu}
536
- <div
537
- class="left-4 bottom-4 gap-2 *:shadow-lg absolute z-50 flex items-center *:rounded-md {hideButton
538
- ? 'pointer-events-none opacity-0'
539
- : ''}"
540
- >
541
- <DropdownMenu.Root>
542
- <DropdownMenu.Trigger>
543
- {#snippet child({ props })}
544
- <Button
545
- {...props}
546
- variant="overlay"
547
- size="icon-lg"
548
- title={t.toolsMenu}
549
- aria-label={t.toolsMenu}
550
- >
551
- <Settings2 />
552
- </Button>
553
- {/snippet}
554
- </DropdownMenu.Trigger>
555
- <!-- Above the fullscreen viewer, which sits at z-index 10000. -->
556
- <DropdownMenu.Content sideOffset={6} class="min-w-44 z-10001 w-auto">
557
- <DropdownMenu.Item onSelect={toggleProjection}>
558
- {#if projection === 'perspective'}
559
- <Square />
560
- {t.switchToOrthographic}
561
- {:else}
562
- <Box />
563
- {t.switchToPerspective}
564
- {/if}
565
- </DropdownMenu.Item>
566
- <DropdownMenu.Item onSelect={() => fitToView?.()}>
567
- <Frame />
568
- {t.fitToView}
569
- </DropdownMenu.Item>
570
-
571
- <DropdownMenu.Sub>
572
- <DropdownMenu.SubTrigger>
573
- <Box />
574
- {t.views}
575
- </DropdownMenu.SubTrigger>
576
- <DropdownMenu.SubContent class="z-10001">
577
- {#each VIEW_PRESETS as { preset, label } (preset)}
578
- <DropdownMenu.Item onSelect={() => setView(preset)}
579
- >{label()}</DropdownMenu.Item
580
- >
581
- {/each}
582
- </DropdownMenu.SubContent>
583
- </DropdownMenu.Sub>
584
-
585
- {#if config.showDisplayMenu}
586
- <DropdownMenu.Sub>
587
- <DropdownMenu.SubTrigger>
588
- <Palette />
589
- {t.display}
590
- </DropdownMenu.SubTrigger>
591
- <DropdownMenu.SubContent class="min-w-40 z-10001">
592
- <DropdownMenu.RadioGroup
593
- value={renderStyle}
594
- onValueChange={(look) => setRenderStyle(look as typeof renderStyle)}
595
- >
596
- {#each styleOptions as { look, label } (look)}
597
- <DropdownMenu.RadioItem value={look} closeOnSelect={false}>
598
- {label}
599
- </DropdownMenu.RadioItem>
600
- {/each}
601
- </DropdownMenu.RadioGroup>
602
- <DropdownMenu.Separator />
603
- <DropdownMenu.CheckboxItem
604
- checked={edgesVisible}
605
- onCheckedChange={toggleEdges}
606
- >
607
- <Spline />
608
- {t.edges}
609
- </DropdownMenu.CheckboxItem>
610
- </DropdownMenu.SubContent>
611
- </DropdownMenu.Sub>
612
- {/if}
613
-
614
- <DropdownMenu.CheckboxItem checked={measureActive} onCheckedChange={toggleMeasure}>
615
- <Ruler />
616
- {t.measure}
617
- </DropdownMenu.CheckboxItem>
618
-
619
- {#if config.showGridToggle}
620
- <DropdownMenu.CheckboxItem checked={gridVisible} onCheckedChange={toggleGrid}>
621
- <Grid3x3 />
622
- {t.grid}
623
- </DropdownMenu.CheckboxItem>
624
- {/if}
625
-
626
- {#if config.showSceneManager || config.showScreenshotButton || config.showFullscreenButton}
627
- <DropdownMenu.Separator />
628
- {/if}
629
-
630
- {#if config.showSceneManager}
631
- <DropdownMenu.CheckboxItem
632
- checked={sceneManagerOpen}
633
- onCheckedChange={() => (sceneManagerOpen = !sceneManagerOpen)}
634
- >
635
- <Layers />
636
- {t.sceneManager}
637
- </DropdownMenu.CheckboxItem>
638
- {/if}
639
-
640
- {#if config.showScreenshotButton}
641
- <DropdownMenu.Item onSelect={downloadScreenshot}>
642
- <Camera />
643
- {t.screenshot}
644
- </DropdownMenu.Item>
645
- {/if}
646
-
647
- {#if config.showFullscreenButton}
648
- <DropdownMenu.Item onSelect={toggleFullscreen}>
649
- {#if isFullscreen}
650
- <Minimize />
651
- {t.exitFullscreen}
652
- {:else}
653
- <Maximize />
654
- {t.fullscreen}
655
- {/if}
656
- </DropdownMenu.Item>
657
- {/if}
658
- </DropdownMenu.Content>
659
- </DropdownMenu.Root>
660
- <!-- Touch has no F key, and fit-to-view is the way back from a framed part. -->
661
- <div class="hidden pointer-coarse:block">
662
- <Button
663
- variant="overlay"
664
- size="icon-lg"
665
- title={t.fitToView}
666
- aria-label={t.fitToView}
667
- onclick={() => fitToView?.()}
668
- >
669
- <Frame />
670
- </Button>
671
- </div>
672
- </div>
569
+ <ViewerDock
570
+ {t}
571
+ hidden={hideButton}
572
+ showDisplay={config.showDisplayMenu}
573
+ showGrid={config.showGridToggle}
574
+ showSceneManager={config.showSceneManager}
575
+ showScreenshot={config.showScreenshotButton}
576
+ showFullscreen={config.showFullscreenButton}
577
+ {styleOptions}
578
+ viewPresets={VIEW_PRESETS.map(({ preset, label }) => ({ preset, label: label() }))}
579
+ {renderStyle}
580
+ {nightOn}
581
+ {edgesVisible}
582
+ {measureActive}
583
+ {gridVisible}
584
+ {sceneManagerOpen}
585
+ {projection}
586
+ {isFullscreen}
587
+ onSetLook={setRenderStyle}
588
+ onToggleNight={toggleNight}
589
+ onToggleEdges={toggleEdges}
590
+ onToggleMeasure={toggleMeasure}
591
+ onToggleGrid={toggleGrid}
592
+ onToggleSceneManager={() => (sceneManagerOpen = !sceneManagerOpen)}
593
+ onToggleProjection={toggleProjection}
594
+ onSetView={setView}
595
+ onFit={() => fitToView?.()}
596
+ onScreenshot={downloadScreenshot}
597
+ onToggleFullscreen={toggleFullscreen}
598
+ />
673
599
  {/if}
674
600
  </div>
675
601
  </Resizable.Pane>
@@ -680,7 +606,7 @@
680
606
  <SceneManager
681
607
  {outliner}
682
608
  {sceneVersion}
683
- onVisibilityChange={() => invalidate?.()}
609
+ onVisibilityChange={() => syncEmissiveLights?.()}
684
610
  onHover={(entry) => setHover?.(entry?.object ?? null, entry?.memberIndex ?? null)}
685
611
  />
686
612
  </Resizable.Pane>
@@ -1,4 +1,4 @@
1
- import { type ThreeViewer } from '@selvajs/visualization/render';
1
+ import { type Look, type ThreeViewer } from '@selvajs/visualization/render';
2
2
  import type { Locale } from '../../i18n/messages';
3
3
  export interface ViewerConfig {
4
4
  showScreenshotButton?: boolean;
@@ -9,6 +9,8 @@ export interface ViewerConfig {
9
9
  showDisplayMenu?: boolean;
10
10
  enableMeshClick?: boolean;
11
11
  backgroundColor?: string;
12
+ /** The look to open in. Default `technical`. */
13
+ look?: Look;
12
14
  /** Initial state of the edge overlay and its display-menu checkmark. Default false. */
13
15
  showEdges?: boolean;
14
16
  }