@pascal-app/editor 1.0.6 → 1.0.7

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 (317) hide show
  1. package/package.json +18 -15
  2. package/src/components/editor/action-menu-button.tsx +4 -1
  3. package/src/components/editor/alignment-3d-guide-layer.tsx +102 -11
  4. package/src/components/editor/bake-thumbnail.tsx +3 -2
  5. package/src/components/editor/custom-camera-controls.tsx +20 -8
  6. package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
  7. package/src/components/editor/first-person-controls.tsx +72 -61
  8. package/src/components/editor/floating-action-menu.tsx +27 -9
  9. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  10. package/src/components/editor/floorplan-panel.test.ts +220 -0
  11. package/src/components/editor/floorplan-panel.tsx +285 -132
  12. package/src/components/editor/footprint-height-handle.tsx +1 -1
  13. package/src/components/editor/grid.tsx +24 -15
  14. package/src/components/editor/group-actions.ts +19 -1
  15. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  16. package/src/components/editor/group-move-3d.ts +27 -2
  17. package/src/components/editor/group-selection-box-3d.tsx +4 -1
  18. package/src/components/editor/group-transform-shared.ts +20 -0
  19. package/src/components/editor/index.tsx +130 -20
  20. package/src/components/editor/inspector-extension-actions.tsx +36 -0
  21. package/src/components/editor/localized-html.tsx +22 -0
  22. package/src/components/editor/node-action-menu.tsx +46 -12
  23. package/src/components/editor/node-arrow-handles.tsx +1 -2
  24. package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
  25. package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
  26. package/src/components/editor/registry-action-contributions.test.tsx +67 -6
  27. package/src/components/editor/registry-action-contributions.tsx +115 -15
  28. package/src/components/editor/riser-diagram-panel.tsx +59 -9
  29. package/src/components/editor/room-controls.tsx +1 -1
  30. package/src/components/editor/room-handles-2d.tsx +156 -6
  31. package/src/components/editor/room-handles.tsx +241 -51
  32. package/src/components/editor/room-highlight.tsx +2 -0
  33. package/src/components/editor/room-labels.tsx +29 -15
  34. package/src/components/editor/selection-manager.tsx +15 -102
  35. package/src/components/editor/separator-end-grip.tsx +74 -0
  36. package/src/components/editor/site-edge-labels.tsx +1 -1
  37. package/src/components/editor/thumbnail-generator.test.ts +15 -6
  38. package/src/components/editor/thumbnail-generator.tsx +26 -1
  39. package/src/components/editor/use-floorplan-background-placement.ts +55 -52
  40. package/src/components/editor/views/view-bar.tsx +111 -26
  41. package/src/components/editor/wall-measurement-label.tsx +1 -1
  42. package/src/components/editor/wall-move-side-handles.tsx +5 -1
  43. package/src/components/editor/wall-pull-grip.tsx +193 -0
  44. package/src/components/editor/wall-snap-beacon-layer.tsx +81 -1
  45. package/src/components/editor-2d/floorplan-adjacent-walls-layer.test.ts +76 -0
  46. package/src/components/editor-2d/floorplan-adjacent-walls-layer.tsx +186 -0
  47. package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +63 -3
  48. package/src/components/editor-2d/floorplan-group-move.tsx +31 -3
  49. package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +6 -0
  50. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +22 -13
  51. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +11 -3
  52. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +7 -1
  53. package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +96 -4
  54. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +52 -12
  55. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +12 -1
  56. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +197 -37
  57. package/src/components/procedural-items/inspector.tsx +59 -12
  58. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +2 -0
  59. package/src/components/systems/ceiling/ceiling-system.test.ts +141 -0
  60. package/src/components/systems/ceiling/ceiling-system.tsx +53 -18
  61. package/src/components/tools/distribution/distribution-run-tool.tsx +50 -26
  62. package/src/components/tools/item/placement-history.test.tsx +1 -1
  63. package/src/components/tools/item/placement-strategies.test.ts +64 -1
  64. package/src/components/tools/item/placement-strategies.ts +5 -9
  65. package/src/components/tools/item/use-placement-coordinator.tsx +30 -34
  66. package/src/components/tools/registry/item-surface-move.test.ts +11 -0
  67. package/src/components/tools/select/select-candidates.test.ts +45 -0
  68. package/src/components/tools/select/select-candidates.ts +13 -2
  69. package/src/components/tools/shared/cursor-sphere.tsx +10 -3
  70. package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
  71. package/src/components/tools/shared/placement-box.tsx +1 -1
  72. package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
  73. package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
  74. package/src/components/tools/wall/wall-drafting.test.ts +299 -23
  75. package/src/components/tools/wall/wall-drafting.ts +132 -24
  76. package/src/components/tools/wall/wall-pull-snap.test.ts +274 -0
  77. package/src/components/tools/wall/wall-pull-snap.ts +563 -0
  78. package/src/components/tools/wall/wall-snap-geometry.test.ts +113 -1
  79. package/src/components/tools/wall/wall-snap-geometry.ts +137 -0
  80. package/src/components/tools/zone/zone-tool.tsx +3 -3
  81. package/src/components/ui/action-menu/action-button.tsx +2 -0
  82. package/src/components/ui/action-menu/camera-actions.tsx +8 -7
  83. package/src/components/ui/action-menu/control-modes.tsx +9 -5
  84. package/src/components/ui/action-menu/follow-pascal-pill.tsx +3 -1
  85. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +7 -1
  86. package/src/components/ui/action-menu/furnish-tools.tsx +7 -6
  87. package/src/components/ui/action-menu/index.tsx +1 -0
  88. package/src/components/ui/action-menu/measurement-control.tsx +35 -29
  89. package/src/components/ui/action-menu/point-ask-button.tsx +4 -3
  90. package/src/components/ui/action-menu/structure-tools.tsx +25 -23
  91. package/src/components/ui/action-menu/view-toggles.test.ts +18 -0
  92. package/src/components/ui/action-menu/view-toggles.tsx +256 -51
  93. package/src/components/ui/build-panel/build-panel.tsx +163 -25
  94. package/src/components/ui/command-palette/editor-commands.tsx +38 -11
  95. package/src/components/ui/controls/finish-swatch.tsx +45 -19
  96. package/src/components/ui/controls/material-picker.tsx +24 -17
  97. package/src/components/ui/controls/metric-control.test.ts +52 -0
  98. package/src/components/ui/controls/metric-control.tsx +62 -24
  99. package/src/components/ui/controls/segmented-control.tsx +5 -2
  100. package/src/components/ui/controls/slider-control.test.tsx +23 -0
  101. package/src/components/ui/controls/slider-control.tsx +30 -14
  102. package/src/components/ui/floating-level-selector.tsx +100 -48
  103. package/src/components/ui/helpers/building-helper.tsx +5 -3
  104. package/src/components/ui/helpers/contextual-helper-panel.tsx +97 -37
  105. package/src/components/ui/helpers/helper-manager.test.tsx +53 -3
  106. package/src/components/ui/helpers/helper-manager.tsx +82 -57
  107. package/src/components/ui/helpers/item-helper.tsx +6 -4
  108. package/src/components/ui/helpers/registered-tool-helper.tsx +3 -1
  109. package/src/components/ui/level-duplicate-dialog.tsx +32 -23
  110. package/src/components/ui/level-wall-copy-prompt.tsx +124 -0
  111. package/src/components/ui/panels/collections/collections-popover.tsx +11 -11
  112. package/src/components/ui/panels/deck-boards-fields.tsx +136 -0
  113. package/src/components/ui/panels/floor-foundation-panel.tsx +113 -38
  114. package/src/components/ui/panels/floor-opening-panel.tsx +73 -25
  115. package/src/components/ui/panels/inspector-extensions.tsx +66 -0
  116. package/src/components/ui/panels/mobile-panel-sheet.tsx +3 -1
  117. package/src/components/ui/panels/mobile-selection-bar.tsx +8 -6
  118. package/src/components/ui/panels/multi-field-value.test.ts +27 -0
  119. package/src/components/ui/panels/multi-field-value.ts +20 -1
  120. package/src/components/ui/panels/multi-height-mode.tsx +8 -7
  121. package/src/components/ui/panels/multi-parametric-inspector.tsx +122 -34
  122. package/src/components/ui/panels/multi-selection-panel.tsx +75 -35
  123. package/src/components/ui/panels/node-display.ts +35 -24
  124. package/src/components/ui/panels/panel-manager.tsx +5 -3
  125. package/src/components/ui/panels/panel-wrapper.tsx +22 -109
  126. package/src/components/ui/panels/parametric-field-control.tsx +13 -10
  127. package/src/components/ui/panels/parametric-inspector.tsx +18 -12
  128. package/src/components/ui/panels/reference-panel.tsx +51 -35
  129. package/src/components/ui/panels/room-construction-rows.tsx +464 -65
  130. package/src/components/ui/panels/room-name-options.test.ts +36 -3
  131. package/src/components/ui/panels/room-name-options.ts +110 -11
  132. package/src/components/ui/panels/room-openings-field.tsx +21 -12
  133. package/src/components/ui/panels/room-panel.tsx +206 -99
  134. package/src/components/ui/panels/shape-choice.tsx +7 -5
  135. package/src/components/ui/panels/wall-region-list.tsx +7 -7
  136. package/src/components/ui/primitives/shortcut-token.tsx +10 -6
  137. package/src/components/ui/sidebar/icon-rail.tsx +11 -9
  138. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +10 -10
  139. package/src/components/ui/sidebar/panels/items-panel/index.tsx +10 -9
  140. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +3 -0
  141. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +102 -32
  142. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +4 -3
  143. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -2
  144. package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +4 -2
  145. package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +7 -5
  146. package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +4 -2
  147. package/src/components/ui/sidebar/panels/site-panel/default-names.ts +75 -0
  148. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +4 -2
  149. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +4 -2
  150. package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +4 -2
  151. package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +4 -2
  152. package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +4 -2
  153. package/src/components/ui/sidebar/panels/site-panel/index.tsx +117 -64
  154. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +36 -3
  155. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +4 -2
  156. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +7 -4
  157. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +8 -2
  158. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +8 -5
  159. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +4 -2
  160. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +8 -3
  161. package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +4 -2
  162. package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +4 -2
  163. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +8 -7
  164. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +7 -6
  165. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +16 -1
  166. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +15 -8
  167. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +4 -2
  168. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +4 -2
  169. package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +3 -1
  170. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +18 -8
  171. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +25 -14
  172. package/src/components/ui/sidebar/tab-bar.tsx +4 -1
  173. package/src/components/ui/sidebar/use-plugin-panels.tsx +19 -10
  174. package/src/components/ui/snap-target-badge.tsx +7 -6
  175. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +721 -0
  176. package/src/components/ui/wall-type/wall-type-menu.tsx +167 -0
  177. package/src/components/viewer/floorplan-compass-button.tsx +12 -3
  178. package/src/components/viewer/floorplan-preview.tsx +27 -19
  179. package/src/components/viewer/render-settings-menu.tsx +294 -0
  180. package/src/components/viewer/sky-control.tsx +710 -0
  181. package/src/components/viewer/use-viewer-ui-sounds.test.ts +15 -0
  182. package/src/components/viewer/use-viewer-ui-sounds.ts +46 -0
  183. package/src/components/viewer/viewer-controls-bar.tsx +278 -260
  184. package/src/components/viewer/viewer-hover-label.tsx +12 -4
  185. package/src/components/viewer/viewer-rooms.tsx +61 -4
  186. package/src/components/viewer/viewer-scene-header.tsx +28 -17
  187. package/src/components/viewer/viewer-stage-switcher.tsx +4 -2
  188. package/src/components/viewer-overlay.tsx +8 -0
  189. package/src/components/walkthrough-hud.tsx +75 -2
  190. package/src/hooks/use-keyboard.test.ts +85 -4
  191. package/src/hooks/use-keyboard.ts +65 -7
  192. package/src/hooks/use-selected-room.ts +12 -1
  193. package/src/i18n/floor-footprint-name.ts +30 -0
  194. package/src/i18n/level-name.ts +39 -0
  195. package/src/i18n/locales/en/messages.po +15117 -0
  196. package/src/i18n/locales/en/messages.ts +1 -0
  197. package/src/i18n/locales/en-XA/messages.ts +1 -0
  198. package/src/i18n/locales/es/messages.po +37501 -0
  199. package/src/i18n/locales/es/messages.ts +1 -0
  200. package/src/i18n/locales/fr/messages.po +37501 -0
  201. package/src/i18n/locales/fr/messages.ts +1 -0
  202. package/src/i18n/locales/ja/messages.po +37501 -0
  203. package/src/i18n/locales/ja/messages.ts +1 -0
  204. package/src/i18n/locales/zh-CN/messages.po +37501 -0
  205. package/src/i18n/locales/zh-CN/messages.ts +1 -0
  206. package/src/i18n/locales.test.ts +63 -0
  207. package/src/i18n/locales.ts +32 -0
  208. package/src/i18n/message-slot.tsx +25 -0
  209. package/src/i18n/node-messages.ts +169 -0
  210. package/src/i18n/pseudo-locale.ts +17 -0
  211. package/src/i18n/published-entry.test.ts +17 -0
  212. package/src/i18n/scene-theme-name.ts +16 -0
  213. package/src/index.tsx +156 -4
  214. package/src/lib/add-storey.test.ts +3 -3
  215. package/src/lib/contextual-help.test.ts +20 -1
  216. package/src/lib/contextual-help.ts +95 -19
  217. package/src/lib/continuation.ts +38 -11
  218. package/src/lib/floor-footprints.test.ts +2 -1
  219. package/src/lib/floor-foundation-support.test.ts +7 -3
  220. package/src/lib/floorplan/dimension-drive.test.ts +36 -2
  221. package/src/lib/floorplan/dimension-drive.ts +68 -3
  222. package/src/lib/floorplan/floorplan-export.test.ts +16 -5
  223. package/src/lib/floorplan/floorplan-export.tsx +13 -16
  224. package/src/lib/floorplan/floorplan-extension.test.ts +11 -12
  225. package/src/lib/floorplan/floorplan-extension.ts +37 -18
  226. package/src/lib/floorplan/floorplan-mode.test.ts +13 -6
  227. package/src/lib/floorplan/floorplan-mode.ts +14 -12
  228. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +1 -1
  229. package/src/lib/floorplan/floorplan-readonly.ts +3 -3
  230. package/src/lib/floorplan/schedules.ts +10 -6
  231. package/src/lib/floorplan/site-plan/site-annotations.test.ts +46 -1
  232. package/src/lib/floorplan/site-plan/site-annotations.ts +3 -0
  233. package/src/lib/floorplan/site-plan/site-parts.ts +6 -3
  234. package/src/lib/geometry-script/author.test.ts +114 -0
  235. package/src/lib/geometry-script/author.ts +55 -23
  236. package/src/lib/geometry-script/client.ts +3 -0
  237. package/src/lib/geometry-script/finish-scripts.ts +7 -7
  238. package/src/lib/glb-export.test.ts +327 -0
  239. package/src/lib/glb-export.ts +70 -2
  240. package/src/lib/grid-frame.test.ts +56 -3
  241. package/src/lib/grid-frame.ts +28 -1
  242. package/src/lib/history.test.ts +2 -1
  243. package/src/lib/hud-title.ts +48 -24
  244. package/src/lib/ifc-export.test.ts +41 -1
  245. package/src/lib/inspector-card-mode.test.ts +3 -31
  246. package/src/lib/inspector-card-mode.ts +5 -10
  247. package/src/lib/inspector-expanded.test.ts +13 -0
  248. package/src/lib/inspector-expanded.ts +13 -1
  249. package/src/lib/keyboard-pan.test.ts +7 -0
  250. package/src/lib/keyboard-pan.ts +9 -2
  251. package/src/lib/layer-thickness.test.ts +22 -0
  252. package/src/lib/layer-thickness.ts +79 -0
  253. package/src/lib/lot/drop-in.test.ts +59 -1
  254. package/src/lib/lot/drop-in.ts +42 -50
  255. package/src/lib/material-swatch-style.ts +24 -0
  256. package/src/lib/measurement-parser.ts +7 -3
  257. package/src/lib/measurements.test.ts +17 -0
  258. package/src/lib/measurements.ts +20 -5
  259. package/src/lib/mezzanine-selection.test.ts +3 -1
  260. package/src/lib/orbit-near-plane.test.ts +12 -0
  261. package/src/lib/orbit-near-plane.ts +13 -0
  262. package/src/lib/paint-regions.test.ts +8 -5
  263. package/src/lib/paint-scope.test.ts +53 -0
  264. package/src/lib/paint-scope.ts +18 -4
  265. package/src/lib/panel-rows.ts +17 -8
  266. package/src/lib/reference-scale.ts +83 -0
  267. package/src/lib/room-built-on.test.ts +26 -12
  268. package/src/lib/room-built-on.ts +20 -2
  269. package/src/lib/room-construction.test.ts +28 -1
  270. package/src/lib/room-construction.ts +14 -0
  271. package/src/lib/room-controls-phase6.test.ts +140 -0
  272. package/src/lib/room-handle-drag.ts +557 -42
  273. package/src/lib/room-name-catalog.test.ts +8 -0
  274. package/src/lib/room-name-catalog.ts +8 -1
  275. package/src/lib/room-name-messages.test.ts +132 -0
  276. package/src/lib/room-name-messages.ts +147 -0
  277. package/src/lib/room-name-storage.test.ts +334 -0
  278. package/src/lib/room-push-dimensions.test.ts +37 -0
  279. package/src/lib/room-push-dimensions.ts +226 -2
  280. package/src/lib/room-selection.test.ts +23 -6
  281. package/src/lib/scene-clipboard.test.ts +70 -49
  282. package/src/lib/scene-clipboard.ts +9 -4
  283. package/src/lib/selection-cycle.ts +63 -0
  284. package/src/lib/separator-edit.test.ts +431 -0
  285. package/src/lib/separator-edit.ts +179 -0
  286. package/src/lib/separator-lines.ts +38 -0
  287. package/src/lib/sfx-bus.ts +27 -1
  288. package/src/lib/sfx-player.ts +16 -0
  289. package/src/lib/tool-cursor-icon.ts +5 -1
  290. package/src/lib/tool-shortcuts.ts +130 -0
  291. package/src/lib/viewer-selection.test.ts +28 -24
  292. package/src/lib/viewer-selection.ts +20 -5
  293. package/src/lib/wall-draw-variant.test.ts +7 -6
  294. package/src/lib/wall-draw-variant.ts +107 -6
  295. package/src/lib/wall-nudge.test.ts +44 -0
  296. package/src/lib/wall-nudge.ts +131 -0
  297. package/src/lib/wall-parallel-snap.test.ts +83 -0
  298. package/src/lib/wall-parallel-snap.ts +176 -0
  299. package/src/lib/wall-placement-pointer.test.ts +23 -0
  300. package/src/lib/wall-placement-pointer.ts +18 -0
  301. package/src/lib/wall-pull.test.ts +245 -0
  302. package/src/lib/wall-pull.ts +209 -0
  303. package/src/lib/wall-push-line-up.test.ts +132 -0
  304. package/src/lib/wall-region-list.ts +14 -5
  305. package/src/lib/wall-run-snap.test.ts +188 -0
  306. package/src/lib/wall-run-snap.ts +83 -0
  307. package/src/lib/wall-type.test.ts +125 -0
  308. package/src/lib/wall-type.ts +119 -0
  309. package/src/lib/window-interaction.test.ts +42 -0
  310. package/src/lib/window-interaction.ts +47 -5
  311. package/src/lib/world-grid-snap.test.ts +139 -2
  312. package/src/lib/world-grid-snap.ts +102 -105
  313. package/src/store/use-alignment-guides.ts +27 -1
  314. package/src/store/use-editor.tsx +48 -0
  315. package/src/store/use-floorplan-annotation-visibility.test.ts +64 -0
  316. package/src/store/use-floorplan-annotation-visibility.ts +47 -22
  317. package/src/store/use-wall-snap-indicator.ts +15 -2
@@ -0,0 +1,167 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type Assembly,
5
+ getWallAssemblyPreset,
6
+ usePascalI18n,
7
+ type WallTypeDefinition,
8
+ } from '@pascal-app/core'
9
+ import { Copy, Pencil, Plus, Trash2 } from 'lucide-react'
10
+ import { useState } from 'react'
11
+ import { cn } from '../../../lib/utils'
12
+ import {
13
+ deleteProjectWallType,
14
+ newWallTypeBlockedReason,
15
+ useProjectWallTypes,
16
+ type WallTypePlacement,
17
+ wallTypePlacementOf,
18
+ } from '../../../lib/wall-type'
19
+ import {
20
+ DropdownMenu,
21
+ DropdownMenuContent,
22
+ DropdownMenuItem,
23
+ DropdownMenuLabel,
24
+ DropdownMenuSeparator,
25
+ DropdownMenuTrigger,
26
+ } from '../primitives/dropdown-menu'
27
+ import { type WallTypeEditorRequest, WallTypeEditorDialog } from './wall-type-editor-dialog'
28
+
29
+ /**
30
+ * A wall's project wall types, beside its Assembly preset: make a new type, edit the project
31
+ * type the wall is built with, duplicate its stack (a preset's, a project type's or its own
32
+ * custom layers) into a new type, or delete the project type. A type saved here is applied to
33
+ * the wall (`onApply`) in the same undo step. "New" and "Duplicate" are off, and say why, once
34
+ * the project keeps as many types as it can (`newWallTypeBlockedReason`).
35
+ */
36
+ export function WallTypeMenu({
37
+ assembly,
38
+ className,
39
+ onApply,
40
+ placement,
41
+ }: {
42
+ /** The wall's stack; absent for a plain single-layer wall. */
43
+ assembly: Assembly | undefined
44
+ className?: string
45
+ onApply: (type: WallTypeDefinition) => void
46
+ /** The kind of wall a new type starts as when the wall has no stack to read it from. */
47
+ placement: WallTypePlacement
48
+ }) {
49
+ const i18n = usePascalI18n()
50
+ const projectTypes = useProjectWallTypes()
51
+ const [editing, setEditing] = useState<WallTypeEditorRequest | null>(null)
52
+ const presetId = assembly?.presetId
53
+ const project = presetId ? projectTypes.find((type) => type.id === presetId) : undefined
54
+ const preset = presetId ? getWallAssemblyPreset(presetId) : undefined
55
+ const label =
56
+ project?.label ??
57
+ preset?.label ??
58
+ (assembly
59
+ ? i18n._({ id: 'editor.wallTypeMenu.custom', message: 'Custom' })
60
+ : i18n._({ id: 'editor.buildPanel.wallTypes.plainWall', message: 'Plain wall' }))
61
+ const blocked = newWallTypeBlockedReason(projectTypes)
62
+ const startPlacement = assembly ? wallTypePlacementOf(assembly) : placement
63
+
64
+ return (
65
+ <>
66
+ <DropdownMenu>
67
+ <DropdownMenuTrigger asChild>
68
+ <button
69
+ aria-label={i18n._({
70
+ id: 'editor.wallTypeMenu.wallTypesOf',
71
+ message: 'Wall types: new, edit or duplicate {label}',
72
+ values: { label },
73
+ })}
74
+ className={cn(
75
+ 'flex size-7 shrink-0 items-center justify-center rounded border border-border bg-background text-muted-foreground transition-colors hover:bg-accent hover:text-foreground data-[state=open]:bg-accent',
76
+ className,
77
+ )}
78
+ title={i18n._({
79
+ id: 'editor.buildPanel.wallTypeControls.editOrDuplicateThisWallType',
80
+ message: 'Edit or duplicate this wall type',
81
+ })}
82
+ type="button"
83
+ >
84
+ <Pencil className="size-3.5" />
85
+ </button>
86
+ </DropdownMenuTrigger>
87
+ <DropdownMenuContent align="end" className="w-56">
88
+ <DropdownMenuLabel className="truncate text-muted-foreground text-xs">
89
+ {label}
90
+ </DropdownMenuLabel>
91
+ <DropdownMenuItem
92
+ disabled={!project}
93
+ onSelect={() =>
94
+ project &&
95
+ setEditing({
96
+ mode: 'edit',
97
+ placement: wallTypePlacementOf(project.assembly),
98
+ id: project.id,
99
+ label: project.label,
100
+ assembly: project.assembly,
101
+ })
102
+ }
103
+ >
104
+ <Pencil />
105
+ {i18n._({
106
+ id: 'editor.buildPanel.wallTypeControls.editLayers',
107
+ message: 'Edit layers…',
108
+ })}
109
+ </DropdownMenuItem>
110
+ <DropdownMenuItem
111
+ disabled={!assembly || !!blocked}
112
+ onSelect={() =>
113
+ assembly &&
114
+ setEditing({ mode: 'duplicate', placement: startPlacement, label, assembly })
115
+ }
116
+ >
117
+ <Copy />
118
+ {i18n._({
119
+ id: 'editor.buildPanel.wallTypeControls.duplicateAsANewType',
120
+ message: 'Duplicate as a new type…',
121
+ })}
122
+ </DropdownMenuItem>
123
+ {project ? (
124
+ <DropdownMenuItem
125
+ onSelect={() => deleteProjectWallType(project.id)}
126
+ variant="destructive"
127
+ >
128
+ <Trash2 />
129
+ {i18n._({
130
+ id: 'editor.buildPanel.wallTypeControls.deleteFromThisProject',
131
+ message: 'Delete from this project',
132
+ })}
133
+ </DropdownMenuItem>
134
+ ) : null}
135
+ <DropdownMenuSeparator />
136
+ <DropdownMenuItem
137
+ disabled={!!blocked}
138
+ onSelect={() => setEditing({ mode: 'new', placement: startPlacement })}
139
+ >
140
+ <Plus />
141
+ {i18n._({
142
+ id: 'editor.buildPanel.wallTypeControls.newWallType',
143
+ message: 'New wall type…',
144
+ })}
145
+ </DropdownMenuItem>
146
+ {blocked ? (
147
+ <p className="px-2 pt-1 pb-1.5 text-[11px] text-muted-foreground leading-snug">
148
+ {blocked}
149
+ </p>
150
+ ) : preset ? (
151
+ <p className="px-2 pt-1 pb-1.5 text-[11px] text-muted-foreground leading-snug">
152
+ {i18n._({
153
+ id: 'editor.buildPanel.wallTypeControls.presetsStayAsCitedDuplicateOne',
154
+ message: 'Presets stay as cited. Duplicate one to change its layers.',
155
+ })}
156
+ </p>
157
+ ) : null}
158
+ </DropdownMenuContent>
159
+ </DropdownMenu>
160
+ <WallTypeEditorDialog
161
+ onClose={() => setEditing(null)}
162
+ onSaved={onApply}
163
+ request={editing}
164
+ />
165
+ </>
166
+ )
167
+ }
@@ -1,8 +1,10 @@
1
1
  'use client'
2
2
 
3
+ import { usePascalI18n } from '@pascal-app/core'
3
4
  import type React from 'react'
4
5
  import { cn } from '../../lib/utils'
5
6
  import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/primitives/tooltip'
7
+ import { SKY_COMPASS_BOTTOM, useSkyPanel } from './sky-control'
6
8
 
7
9
  export type FloorplanCompassButtonProps = {
8
10
  northRotationDeg: number
@@ -18,16 +20,23 @@ export function FloorplanCompassButton({
18
20
  needleRef,
19
21
  style,
20
22
  }: FloorplanCompassButtonProps) {
23
+ const i18n = usePascalI18n()
24
+ // Beside the sky chip (not when moved to the plan's right corner), centre on the docks' row.
25
+ const besideSky = useSkyPanel((s) => s.hosts > 0) && style?.right === undefined
26
+ const label = i18n._({
27
+ id: 'viewer.floorplan.alignNorth',
28
+ message: 'Align view to north',
29
+ })
21
30
  return (
22
31
  <Tooltip>
23
32
  <TooltipTrigger asChild>
24
33
  <button
25
- aria-label="Align view to north"
34
+ aria-label={label}
26
35
  className={cn(
27
36
  'group pointer-events-auto absolute bottom-3 z-30 flex h-8 w-8 items-center justify-center rounded-full border border-black/10 bg-white/85 shadow-sm backdrop-blur-md transition hover:bg-white hover:shadow-md dark:border-white/10 dark:bg-neutral-900/85 dark:hover:bg-neutral-900',
28
37
  !style && 'left-3',
29
38
  )}
30
- style={style}
39
+ style={besideSky ? { bottom: SKY_COMPASS_BOTTOM, ...style } : style}
31
40
  onClick={(event) => {
32
41
  event.preventDefault()
33
42
  event.stopPropagation()
@@ -52,7 +61,7 @@ export function FloorplanCompassButton({
52
61
  </span>
53
62
  </button>
54
63
  </TooltipTrigger>
55
- <TooltipContent side="right">Align view to north</TooltipContent>
64
+ <TooltipContent side="right">{label}</TooltipContent>
56
65
  </Tooltip>
57
66
  )
58
67
  }
@@ -6,9 +6,10 @@ import {
6
6
  type FloorplanGeometry,
7
7
  type FloorplanPalette,
8
8
  type GeometryContext,
9
- getLevelDisplayName,
10
9
  isNodeKindEnabled,
11
10
  nodeRegistry,
11
+ useLocale,
12
+ usePascalI18n,
12
13
  useScene,
13
14
  } from '@pascal-app/core'
14
15
  import { AnyNode as AnyNodeSchema } from '@pascal-app/core/schema'
@@ -27,6 +28,7 @@ import {
27
28
  useState,
28
29
  } from 'react'
29
30
  import { createPortal } from 'react-dom'
31
+ import { localizedLevelName } from '../../i18n/level-name'
30
32
  import {
31
33
  FLOORPLAN_VIEW_ROTATION_DEG,
32
34
  floorplanLocalToWorldPoint,
@@ -143,13 +145,6 @@ function boundsToViewBox(bounds: FloorplanBounds): FloorplanViewBox {
143
145
  }
144
146
  }
145
147
 
146
- function levelLabel(level: AnyNode): string {
147
- return getLevelDisplayName({
148
- name: (level as { name?: string }).name,
149
- level: (level as { level?: number }).level ?? 0,
150
- })
151
- }
152
-
153
148
  function collectLevelTree(root: AnyNode, nodes: Record<string, AnyNode>): AnyNode[] {
154
149
  const result: AnyNode[] = [root]
155
150
  const seen = new Set<string>()
@@ -309,6 +304,8 @@ export function FloorplanPreview({
309
304
  showLevelSelector = true,
310
305
  synchronizeNavigation = false,
311
306
  }: FloorplanPreviewProps) {
307
+ const i18n = usePascalI18n()
308
+ const locale = useLocale()
312
309
  const storeNodes = useScene((state) => (scene ? EMPTY_PREVIEW_NODES : state.nodes))
313
310
  const storeInstalledPlugins = useScene((state) =>
314
311
  scene ? EMPTY_INSTALLED_PLUGINS : state.installedPlugins,
@@ -869,7 +866,10 @@ export function FloorplanPreview({
869
866
  className={className}
870
867
  style={{ display: 'grid', placeItems: 'center', background: '#f8fafc', color: '#64748b' }}
871
868
  >
872
- No floor plans are available for this scene.
869
+ {i18n._({
870
+ id: 'viewer.floorplan.empty',
871
+ message: 'No floor plans are available for this scene.',
872
+ })}
873
873
  </div>
874
874
  )
875
875
  }
@@ -882,7 +882,15 @@ export function FloorplanPreview({
882
882
  {synchronizeNavigation ? <FloorplanCameraSyncMount /> : null}
883
883
  <div
884
884
  aria-keyshortcuts="ArrowUp ArrowDown ArrowLeft ArrowRight + - 0 F"
885
- aria-label={`${activeLevel ? levelLabel(activeLevel) : 'Floor plan'} 2D view`}
885
+ aria-label={i18n._({
886
+ id: 'viewer.floorplan.viewLabel',
887
+ message: '{floor} 2D view',
888
+ values: {
889
+ floor: activeLevel
890
+ ? localizedLevelName(activeLevel, i18n, locale)
891
+ : i18n._({ id: 'viewer.floorplan.floorPlan', message: 'Floor plan' }),
892
+ },
893
+ })}
886
894
  onKeyDown={onKeyDown}
887
895
  role="application"
888
896
  ref={interactionRef}
@@ -969,9 +977,9 @@ export function FloorplanPreview({
969
977
  fontWeight: 600,
970
978
  }}
971
979
  >
972
- Floor
980
+ {i18n._({ id: 'viewer.floorplan.floor', message: 'Floor' })}
973
981
  <select
974
- aria-label="Floor"
982
+ aria-label={i18n._({ id: 'viewer.floorplan.floor', message: 'Floor' })}
975
983
  onChange={(event) => chooseLevel(event.target.value)}
976
984
  style={{
977
985
  border: '1px solid rgba(148,163,184,.55)',
@@ -985,7 +993,7 @@ export function FloorplanPreview({
985
993
  >
986
994
  {levels.map((level) => (
987
995
  <option key={level.id} value={level.id}>
988
- {levelLabel(level)}
996
+ {localizedLevelName(level, i18n, locale)}
989
997
  </option>
990
998
  ))}
991
999
  </select>
@@ -1013,28 +1021,28 @@ export function FloorplanPreview({
1013
1021
  }}
1014
1022
  >
1015
1023
  <button
1016
- aria-label="Zoom out"
1024
+ aria-label={i18n._({ id: 'viewer.floorplan.zoomOut', message: 'Zoom out' })}
1017
1025
  onClick={() => zoom(1.2)}
1018
1026
  style={controlStyle}
1019
- title="Zoom out"
1027
+ title={i18n._({ id: 'viewer.floorplan.zoomOut', message: 'Zoom out' })}
1020
1028
  type="button"
1021
1029
  >
1022
1030
  <Minus size={16} />
1023
1031
  </button>
1024
1032
  <button
1025
- aria-label="Fit floor plan"
1033
+ aria-label={i18n._({ id: 'viewer.floorplan.fit', message: 'Fit floor plan' })}
1026
1034
  onClick={() => updateLocalViewBox(fittedViewBox)}
1027
1035
  style={controlStyle}
1028
- title="Fit floor plan"
1036
+ title={i18n._({ id: 'viewer.floorplan.fit', message: 'Fit floor plan' })}
1029
1037
  type="button"
1030
1038
  >
1031
1039
  <Maximize2 size={15} />
1032
1040
  </button>
1033
1041
  <button
1034
- aria-label="Zoom in"
1042
+ aria-label={i18n._({ id: 'viewer.floorplan.zoomIn', message: 'Zoom in' })}
1035
1043
  onClick={() => zoom(0.8)}
1036
1044
  style={controlStyle}
1037
- title="Zoom in"
1045
+ title={i18n._({ id: 'viewer.floorplan.zoomIn', message: 'Zoom in' })}
1038
1046
  type="button"
1039
1047
  >
1040
1048
  <Plus size={16} />
@@ -0,0 +1,294 @@
1
+ 'use client'
2
+
3
+ import { type PascalI18n, usePascalI18n, useScene } from '@pascal-app/core'
4
+ import {
5
+ type RenderQuality,
6
+ requestDeviceLocation,
7
+ resolveLocation,
8
+ type TimeOfDay,
9
+ useViewer,
10
+ type Weather,
11
+ } from '@pascal-app/viewer'
12
+ import {
13
+ ArrowDownLeft,
14
+ Check,
15
+ Cloud,
16
+ CloudSun,
17
+ Gauge,
18
+ LocateFixed,
19
+ Moon,
20
+ PenLine,
21
+ Sun,
22
+ Sunrise,
23
+ Sunset,
24
+ } from 'lucide-react'
25
+ import { useState } from 'react'
26
+ import {
27
+ DropdownMenuItem,
28
+ DropdownMenuSub,
29
+ DropdownMenuSubContent,
30
+ DropdownMenuSubTrigger,
31
+ } from '../ui/primitives/dropdown-menu'
32
+ import { openSkyPanel, useSkyPanel, weatherIcon } from './sky-control'
33
+
34
+ /**
35
+ * Render settings shared by every display dropdown:
36
+ * - `QualityMenu` (both appearances): Low / Medium / High.
37
+ * - `DaylightMenu` (colored): time-of-day and weather presets, plus "Use my location" when the
38
+ * site has no geocoded point — or, while the sky chip is in the viewport, a row opening it.
39
+ * - `EdgesToggleItem` (monochrome): ink edges on or off.
40
+ */
41
+
42
+ function timeOptions(i18n: PascalI18n) {
43
+ return [
44
+ {
45
+ id: 'golden',
46
+ icon: Sunrise,
47
+ name: i18n._({ id: 'viewer.daylight.time.golden', message: 'Golden hour' }),
48
+ },
49
+ {
50
+ id: 'midday',
51
+ icon: Sun,
52
+ name: i18n._({ id: 'viewer.daylight.time.midday', message: 'Midday' }),
53
+ },
54
+ {
55
+ id: 'blue',
56
+ icon: Sunset,
57
+ name: i18n._({ id: 'viewer.daylight.time.blue', message: 'Blue hour' }),
58
+ },
59
+ {
60
+ id: 'night',
61
+ icon: Moon,
62
+ name: i18n._({ id: 'viewer.daylight.time.night', message: 'Night' }),
63
+ },
64
+ ] as const satisfies readonly { id: TimeOfDay; icon: typeof Sun; name: string }[]
65
+ }
66
+
67
+ function weatherOptions(i18n: PascalI18n) {
68
+ return [
69
+ { id: 'clear', name: i18n._({ id: 'viewer.daylight.weather.clear', message: 'Clear' }) },
70
+ { id: 'fair', name: i18n._({ id: 'viewer.daylight.weather.fair', message: 'Fair' }) },
71
+ { id: 'cloudy', name: i18n._({ id: 'viewer.daylight.weather.cloudy', message: 'Cloudy' }) },
72
+ {
73
+ id: 'overcast',
74
+ name: i18n._({ id: 'viewer.daylight.weather.overcast', message: 'Overcast' }),
75
+ },
76
+ ] as const satisfies readonly { id: Weather; name: string }[]
77
+ }
78
+
79
+ function qualityOptions(i18n: PascalI18n) {
80
+ return [
81
+ {
82
+ id: 'low',
83
+ name: i18n._({ id: 'viewer.daylight.quality.low', message: 'Low' }),
84
+ detail: i18n._({
85
+ id: 'viewer.daylight.quality.lowDetail',
86
+ message: 'Fastest — no ambient occlusion or bloom',
87
+ }),
88
+ },
89
+ {
90
+ id: 'medium',
91
+ name: i18n._({ id: 'viewer.daylight.quality.medium', message: 'Medium' }),
92
+ detail: i18n._({
93
+ id: 'viewer.daylight.quality.mediumDetail',
94
+ message: 'Half-resolution ambient occlusion',
95
+ }),
96
+ },
97
+ {
98
+ id: 'high',
99
+ name: i18n._({ id: 'viewer.daylight.quality.high', message: 'High' }),
100
+ detail: i18n._({
101
+ id: 'viewer.daylight.quality.highDetail',
102
+ message: 'Lighting-aware ambient occlusion and bloom',
103
+ }),
104
+ },
105
+ ] as const satisfies readonly { id: RenderQuality; name: string; detail: string }[]
106
+ }
107
+
108
+ export function QualityMenu({
109
+ subContentClassName = 'dark min-w-56',
110
+ }: {
111
+ subContentClassName?: string
112
+ }) {
113
+ const i18n = usePascalI18n()
114
+ const quality = useViewer((s) => s.renderQuality)
115
+ const choice = useViewer((s) => s.renderQualityChoice)
116
+ const qualities = qualityOptions(i18n)
117
+ const active = qualities.find((o) => o.id === quality) ?? qualities[2]
118
+ const autoName = i18n._({ id: 'viewer.daylight.quality.auto', message: 'Auto' })
119
+ return (
120
+ <DropdownMenuSub>
121
+ <DropdownMenuSubTrigger>
122
+ <Gauge className="h-4 w-4" />
123
+ <span>{i18n._({ id: 'viewer.daylight.quality.title', message: 'Quality' })}</span>
124
+ <span className="ml-auto text-muted-foreground text-xs">
125
+ {choice === 'auto' ? `${autoName} (${active.name})` : active.name}
126
+ </span>
127
+ </DropdownMenuSubTrigger>
128
+ <DropdownMenuSubContent className={subContentClassName}>
129
+ <DropdownMenuItem onSelect={() => useViewer.getState().setRenderQuality('auto')}>
130
+ <div className="flex flex-col">
131
+ <span className="text-foreground">{autoName}</span>
132
+ <span className="text-muted-foreground text-xs">
133
+ {i18n._({
134
+ id: 'viewer.daylight.quality.autoDetail',
135
+ message: 'Picked for this device: {tier}',
136
+ values: { tier: active.name },
137
+ })}
138
+ </span>
139
+ </div>
140
+ {choice === 'auto' ? <Check className="ml-auto h-4 w-4 text-foreground" /> : null}
141
+ </DropdownMenuItem>
142
+ {qualities.map((option) => (
143
+ <DropdownMenuItem
144
+ key={option.id}
145
+ onSelect={() => useViewer.getState().setRenderQuality(option.id)}
146
+ >
147
+ <div className="flex flex-col">
148
+ <span className="text-foreground">{option.name}</span>
149
+ <span className="text-muted-foreground text-xs">{option.detail}</span>
150
+ </div>
151
+ {choice === option.id ? <Check className="ml-auto h-4 w-4 text-foreground" /> : null}
152
+ </DropdownMenuItem>
153
+ ))}
154
+ </DropdownMenuSubContent>
155
+ </DropdownMenuSub>
156
+ )
157
+ }
158
+
159
+ export function DaylightMenu({
160
+ subContentClassName = 'dark min-w-48',
161
+ }: {
162
+ subContentClassName?: string
163
+ }) {
164
+ const i18n = usePascalI18n()
165
+ const timeOfDay = useViewer((s) => s.timeOfDay)
166
+ const weather = useViewer((s) => s.weather)
167
+ const deviceLocation = useViewer((s) => s.deviceLocation)
168
+ const siteHasLocation = useScene((state) => {
169
+ for (const id of state.rootNodeIds) {
170
+ const node = state.nodes[id] as
171
+ | { type?: string; parcel?: { originLngLat?: unknown }; dossier?: { point?: unknown } }
172
+ | undefined
173
+ if (node?.type === 'site') return Boolean(node.parcel?.originLngLat || node.dossier?.point)
174
+ }
175
+ return false
176
+ })
177
+ const [locating, setLocating] = useState(false)
178
+ // The sky chip beside the compass carries time and weather wherever it is mounted.
179
+ const skyChip = useSkyPanel((s) => s.hosts > 0)
180
+ const times = timeOptions(i18n)
181
+ const weathers = weatherOptions(i18n)
182
+ const activeTime = times.find((o) => o.id === timeOfDay) ?? times[1]
183
+ const activeWeather = weathers.find((o) => o.id === weather) ?? weathers[1]
184
+ const source = siteHasLocation ? 'site' : resolveLocation(null, deviceLocation).source
185
+
186
+ if (skyChip) {
187
+ const WeatherIcon = weatherIcon(weather)
188
+ return (
189
+ <DropdownMenuItem onSelect={openSkyPanel}>
190
+ <WeatherIcon className="h-4 w-4" />
191
+ <span>{i18n._({ id: 'viewer.daylight.sky.menuItem', message: 'Time & weather' })}</span>
192
+ <span className="ml-auto flex items-center gap-1 text-violet-600 text-xs dark:text-violet-300">
193
+ {i18n._({ id: 'viewer.daylight.sky.inViewport', message: 'In viewport' })}
194
+ <ArrowDownLeft className="h-3.5 w-3.5" />
195
+ </span>
196
+ </DropdownMenuItem>
197
+ )
198
+ }
199
+
200
+ return (
201
+ <>
202
+ <DropdownMenuSub>
203
+ <DropdownMenuSubTrigger>
204
+ <activeTime.icon className="h-4 w-4" />
205
+ <span>{i18n._({ id: 'viewer.daylight.time.title', message: 'Time of day' })}</span>
206
+ <span className="ml-auto text-muted-foreground text-xs">{activeTime.name}</span>
207
+ </DropdownMenuSubTrigger>
208
+ <DropdownMenuSubContent className={subContentClassName}>
209
+ {times.map((option) => (
210
+ <DropdownMenuItem
211
+ key={option.id}
212
+ onSelect={() => useViewer.getState().setTimeOfDay(option.id)}
213
+ >
214
+ <option.icon className="h-4 w-4" />
215
+ <span>{option.name}</span>
216
+ {timeOfDay === option.id ? <Check className="ml-auto h-4 w-4" /> : null}
217
+ </DropdownMenuItem>
218
+ ))}
219
+ {source !== 'site' ? (
220
+ <DropdownMenuItem
221
+ disabled={locating}
222
+ onSelect={(event) => {
223
+ event.preventDefault()
224
+ setLocating(true)
225
+ requestDeviceLocation()
226
+ .then((location) => useViewer.getState().setDeviceLocation(location))
227
+ .catch(() => {})
228
+ .finally(() => setLocating(false))
229
+ }}
230
+ >
231
+ <LocateFixed className="h-4 w-4" />
232
+ <div className="flex flex-col">
233
+ <span>
234
+ {i18n._({ id: 'viewer.daylight.location.use', message: 'Use my location' })}
235
+ </span>
236
+ <span className="text-muted-foreground text-xs">
237
+ {source === 'device'
238
+ ? i18n._({
239
+ id: 'viewer.daylight.location.device',
240
+ message: 'Sun follows your location',
241
+ })
242
+ : i18n._({
243
+ id: 'viewer.daylight.location.approximate',
244
+ message: 'Sun position is approximate',
245
+ })}
246
+ </span>
247
+ </div>
248
+ </DropdownMenuItem>
249
+ ) : null}
250
+ </DropdownMenuSubContent>
251
+ </DropdownMenuSub>
252
+ <DropdownMenuSub>
253
+ <DropdownMenuSubTrigger>
254
+ {weather === 'clear' ? <CloudSun className="h-4 w-4" /> : <Cloud className="h-4 w-4" />}
255
+ <span>{i18n._({ id: 'viewer.daylight.weather.title', message: 'Weather' })}</span>
256
+ <span className="ml-auto text-muted-foreground text-xs">{activeWeather.name}</span>
257
+ </DropdownMenuSubTrigger>
258
+ <DropdownMenuSubContent className={subContentClassName}>
259
+ {weathers.map((option) => (
260
+ <DropdownMenuItem
261
+ key={option.id}
262
+ onSelect={() => useViewer.getState().setWeather(option.id)}
263
+ >
264
+ <span>{option.name}</span>
265
+ {weather === option.id ? <Check className="ml-auto h-4 w-4" /> : null}
266
+ </DropdownMenuItem>
267
+ ))}
268
+ </DropdownMenuSubContent>
269
+ </DropdownMenuSub>
270
+ </>
271
+ )
272
+ }
273
+
274
+ /** Ink edges on (soft) or off. The menu stays open on toggle. */
275
+ export function EdgesToggleItem() {
276
+ const i18n = usePascalI18n()
277
+ const on = useViewer((s) => s.edges !== 'off')
278
+ return (
279
+ <DropdownMenuItem
280
+ onSelect={(event) => {
281
+ event.preventDefault()
282
+ useViewer.getState().setEdges(on ? 'off' : 'soft')
283
+ }}
284
+ >
285
+ <PenLine className="h-4 w-4" />
286
+ <span>{i18n._({ id: 'viewer.display.edges.title', message: 'Edges' })}</span>
287
+ <span className="ml-auto text-muted-foreground text-xs">
288
+ {on
289
+ ? i18n._({ id: 'viewer.common.on', message: 'On' })
290
+ : i18n._({ id: 'viewer.common.off', message: 'Off' })}
291
+ </span>
292
+ </DropdownMenuItem>
293
+ )
294
+ }