@pascal-app/editor 1.0.7 → 1.1.1

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 (336) hide show
  1. package/package.json +9 -15
  2. package/src/components/editor/action-menu-button.tsx +1 -1
  3. package/src/components/editor/alignment-3d-guide-layer.tsx +1 -1
  4. package/src/components/editor/custom-camera-controls.tsx +101 -45
  5. package/src/components/editor/duplicate-node.ts +2 -1
  6. package/src/components/editor/editor-layout-v2.tsx +1 -1
  7. package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
  8. package/src/components/editor/first-person-controls.tsx +20 -1983
  9. package/src/components/editor/fit-scene-framing.test.ts +318 -0
  10. package/src/components/editor/fit-scene-framing.ts +142 -0
  11. package/src/components/editor/floating-action-menu.tsx +5 -3
  12. package/src/components/editor/floating-building-action-menu.tsx +1 -1
  13. package/src/components/editor/floor-openings-overlay.tsx +1 -1
  14. package/src/components/editor/floor-region-controls.tsx +8 -4
  15. package/src/components/editor/floorplan-navigation-presentation.test.ts +0 -40
  16. package/src/components/editor/floorplan-navigation-presentation.ts +0 -66
  17. package/src/components/editor/floorplan-panel.tsx +199 -30
  18. package/src/components/editor/floorplan-wall-typing-keys.test.tsx +152 -0
  19. package/src/components/editor/floorplan-wall-typing-keys.ts +79 -0
  20. package/src/components/editor/follow-pascal.test.tsx +1 -2
  21. package/src/components/editor/follow-pascal.tsx +1 -1
  22. package/src/components/editor/footprint-height-handle.tsx +2 -2
  23. package/src/components/editor/group-actions.ts +1 -2
  24. package/src/components/editor/group-floating-action-menu.tsx +1 -1
  25. package/src/components/editor/group-move-3d.ts +1 -2
  26. package/src/components/editor/group-rotate-handle.tsx +1 -2
  27. package/src/components/editor/group-transform-shared.ts +3 -1
  28. package/src/components/editor/handles/use-handle-drag.ts +1 -2
  29. package/src/components/editor/index.tsx +43 -13
  30. package/src/components/editor/inspector-extension-actions.tsx +1 -1
  31. package/src/components/editor/measurement-pill.tsx +1 -1
  32. package/src/components/editor/node-arrow-handles.tsx +2 -2
  33. package/src/components/editor/open-wall-ends-3d-layer.tsx +2 -3
  34. package/src/components/editor/open-wall-ends-hint.tsx +1 -1
  35. package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
  36. package/src/components/editor/quick-measurement-card.tsx +1 -1
  37. package/src/components/editor/room-controls.tsx +10 -6
  38. package/src/components/editor/room-handles-2d.tsx +3 -3
  39. package/src/components/editor/room-handles.tsx +4 -5
  40. package/src/components/editor/room-highlight.tsx +6 -105
  41. package/src/components/editor/room-labels.test.ts +8 -822
  42. package/src/components/editor/room-labels.tsx +41 -1670
  43. package/src/components/editor/room-webgpu-materials.test.ts +1 -1
  44. package/src/components/editor/selection-manager.tsx +5 -3
  45. package/src/components/editor/site-edge-labels.tsx +7 -3
  46. package/src/components/editor/snapshot-capture-overlay.tsx +2 -5
  47. package/src/components/editor/use-floorplan-background-placement.ts +2 -1
  48. package/src/components/editor/views/plugins-view.tsx +1 -1
  49. package/src/components/editor/views/view-bar.tsx +9 -9
  50. package/src/components/editor/wall-measurement-label.tsx +31 -15
  51. package/src/components/editor/wall-move-side-handles.tsx +11 -7
  52. package/src/components/editor/wall-pull-grip.tsx +2 -2
  53. package/src/components/editor/wall-region-handles.tsx +2 -2
  54. package/src/components/editor/wall-snap-beacon-layer.tsx +53 -32
  55. package/src/components/editor-2d/floorplan-group-move.tsx +3 -4
  56. package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +13 -13
  57. package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +2 -3
  58. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +2 -1
  59. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +23 -6
  60. package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +0 -37
  61. package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +0 -30
  62. package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +13 -3
  63. package/src/components/editor-2d/renderers/floorplan-draft-measurement.tsx +7 -1
  64. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +14 -3
  65. package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.test.tsx +35 -0
  66. package/src/components/editor-2d/renderers/floorplan-placement-typing-hud.tsx +65 -0
  67. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +30 -10
  68. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +39 -35
  69. package/src/components/systems/roof/roof-edit-system.test.tsx +15 -1
  70. package/src/components/systems/roof/roof-edit-system.tsx +33 -21
  71. package/src/components/tools/distribution/distribution-run-tool.test.tsx +371 -3
  72. package/src/components/tools/distribution/distribution-run-tool.tsx +151 -33
  73. package/src/components/tools/item/move-tool.tsx +1 -6
  74. package/src/components/tools/item/placement-history.test.tsx +1 -1
  75. package/src/components/tools/item/use-placement-coordinator.tsx +3 -4
  76. package/src/components/tools/paint-region/wall-paint-region-tool.tsx +7 -3
  77. package/src/components/tools/registry/move-registry-node-tool.test.ts +42 -1
  78. package/src/components/tools/registry/move-registry-node-tool.tsx +39 -16
  79. package/src/components/tools/shared/cursor-sphere.tsx +1 -1
  80. package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
  81. package/src/components/tools/shared/placement-box.tsx +2 -6
  82. package/src/components/tools/shared/placement-coordinate-input.tsx +94 -0
  83. package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
  84. package/src/components/tools/shared/segment-angle.ts +0 -4
  85. package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
  86. package/src/components/tools/tool-manager.tsx +1 -24
  87. package/src/components/tools/wall/wall-typed-entry.test.ts +99 -0
  88. package/src/components/tools/wall/wall-typed-entry.ts +70 -0
  89. package/src/components/ui/action-menu/camera-actions.tsx +2 -2
  90. package/src/components/ui/action-menu/control-modes.tsx +1 -1
  91. package/src/components/ui/action-menu/follow-pascal-pill.tsx +1 -1
  92. package/src/components/ui/action-menu/index.tsx +3 -3
  93. package/src/components/ui/action-menu/measurement-control.tsx +3 -3
  94. package/src/components/ui/action-menu/placement.ts +0 -18
  95. package/src/components/ui/action-menu/point-ask-button.tsx +1 -1
  96. package/src/components/ui/action-menu/view-toggles.tsx +5 -5
  97. package/src/components/ui/controls/finish-swatch.tsx +1 -1
  98. package/src/components/ui/controls/material-paint-panel.tsx +2 -2
  99. package/src/components/ui/controls/scene-material-list.tsx +2 -2
  100. package/src/components/ui/controls/select-control.tsx +1 -1
  101. package/src/components/ui/controls/slider-control.tsx +18 -6
  102. package/src/components/ui/controls/terrain-sculpt-panel.tsx +1 -1
  103. package/src/components/ui/controls/tool-options-panel.tsx +74 -44
  104. package/src/components/ui/floating-level-selector.tsx +3 -3
  105. package/src/components/ui/helpers/contextual-helper-panel.tsx +1 -1
  106. package/src/components/ui/helpers/helper-manager.test.tsx +54 -1
  107. package/src/components/ui/helpers/helper-manager.tsx +55 -1
  108. package/src/components/ui/level-duplicate-dialog.tsx +1 -1
  109. package/src/components/ui/panels/collections/collections-popover.tsx +2 -2
  110. package/src/components/ui/panels/floor-foundation-panel.tsx +3 -3
  111. package/src/components/ui/panels/floor-opening-panel.tsx +5 -5
  112. package/src/components/ui/panels/mobile-selection-bar.tsx +28 -24
  113. package/src/components/ui/panels/multi-height-mode.tsx +3 -3
  114. package/src/components/ui/panels/multi-selection-panel.tsx +1 -1
  115. package/src/components/ui/panels/node-display.ts +1 -1
  116. package/src/components/ui/panels/panel-manager.tsx +5 -3
  117. package/src/components/ui/panels/panel-wrapper.tsx +42 -19
  118. package/src/components/ui/panels/parametric-inspector.tsx +1 -1
  119. package/src/components/ui/panels/room-construction-rows.tsx +7 -7
  120. package/src/components/ui/panels/room-name-options.ts +1 -5
  121. package/src/components/ui/panels/room-openings-field.tsx +1 -1
  122. package/src/components/ui/panels/room-panel.tsx +7 -9
  123. package/src/components/ui/primitives/color-dot.tsx +2 -2
  124. package/src/components/ui/primitives/opacity-control.tsx +2 -2
  125. package/src/components/ui/primitives/sidebar.tsx +3 -3
  126. package/src/components/ui/right-stack.test.ts +13 -6
  127. package/src/components/ui/right-stack.tsx +22 -11
  128. package/src/components/ui/sidebar/icon-rail.tsx +1 -1
  129. package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
  130. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
  131. package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +1 -1
  132. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +2 -2
  133. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +1 -1
  134. package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +2 -6
  135. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
  136. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +1 -1
  137. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +1 -1
  138. package/src/components/ui/sidebar/panels/site-panel/index.tsx +3 -10
  139. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +1 -1
  140. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
  141. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +46 -7
  142. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +1 -1
  143. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +1 -1
  144. package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +14 -5
  145. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -2
  146. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +1 -1
  147. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +3 -3
  148. package/src/components/ui/sidebar/tab-bar.tsx +1 -1
  149. package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +2 -2
  150. package/src/components/ui/wall-type/wall-type-menu.tsx +1 -1
  151. package/src/components/viewer-overlay.tsx +1 -2
  152. package/src/hooks/use-auto-frame.ts +1 -2
  153. package/src/hooks/use-keyboard.ts +1 -2
  154. package/src/hooks/use-selected-room.ts +5 -7
  155. package/src/i18n/floor-footprint-name.ts +1 -1
  156. package/src/i18n/locales/en/messages.po +1036 -306
  157. package/src/i18n/locales/en/messages.ts +1 -1
  158. package/src/i18n/locales/en-XA/messages.ts +1 -1
  159. package/src/i18n/locales/es/messages.po +2267 -439
  160. package/src/i18n/locales/fr/messages.po +2267 -439
  161. package/src/i18n/locales/ja/messages.po +2267 -439
  162. package/src/i18n/locales/zh-CN/messages.po +2267 -439
  163. package/src/i18n/locales.test.ts +1 -1
  164. package/src/index.tsx +183 -178
  165. package/src/lib/below-grade.test.tsx +4 -4
  166. package/src/lib/camera-facing.test.ts +1 -1
  167. package/src/lib/camera-facing.ts +2 -3
  168. package/src/lib/contextual-help.ts +6 -0
  169. package/src/lib/editor-level-display.ts +1 -1
  170. package/src/lib/elevation-guides.ts +21 -6
  171. package/src/lib/elevator-support.ts +1 -72
  172. package/src/lib/floor-footprints.test.ts +3 -3
  173. package/src/lib/floor-footprints.ts +1 -9
  174. package/src/lib/floor-foundation-support.test.ts +8 -3
  175. package/src/lib/floor-opening-draft.ts +2 -2
  176. package/src/lib/floor-region-session.ts +1 -1
  177. package/src/lib/floorplan/annotation-visibility.test.ts +1 -1
  178. package/src/lib/floorplan/annotation-visibility.ts +1 -1
  179. package/src/lib/floorplan/floorplan-export.test.ts +1 -1
  180. package/src/lib/floorplan/floorplan-export.tsx +8 -11
  181. package/src/lib/floorplan/floorplan-extension.ts +4 -157
  182. package/src/lib/floorplan/floorplan-mode.ts +2 -1
  183. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +1 -1
  184. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +8 -6
  185. package/src/lib/floorplan/floorplan-plugin-install.test.tsx +3 -4
  186. package/src/lib/floorplan/geometry.ts +2 -112
  187. package/src/lib/floorplan/index.ts +0 -9
  188. package/src/lib/floorplan/items.ts +2 -1
  189. package/src/lib/floorplan/open-wall-ends.ts +2 -1
  190. package/src/lib/floorplan/schedules.ts +6 -3
  191. package/src/lib/floorplan/sheet-export.ts +7 -3
  192. package/src/lib/floorplan/site-plan/site-parts.ts +7 -1
  193. package/src/lib/floorplan/site-plan/site-plan-layer.tsx +1 -1
  194. package/src/lib/follow-pascal-presentation.ts +1 -1
  195. package/src/lib/gesture-lifecycle.ts +3 -1
  196. package/src/lib/glb-export.ts +1 -1
  197. package/src/lib/interaction/scope.ts +1 -0
  198. package/src/lib/layer-thickness.ts +1 -1
  199. package/src/lib/level-selection.test.ts +18 -2
  200. package/src/lib/level-selection.ts +31 -8
  201. package/src/lib/linear-display.ts +1 -1
  202. package/src/lib/live-gesture-round-trip.test.ts +1 -1
  203. package/src/lib/material-paint.ts +1 -1
  204. package/src/lib/measurement-label.test.ts +1 -1
  205. package/src/lib/measurements.test.ts +7 -5
  206. package/src/lib/mezzanine-selection.test.ts +1 -1
  207. package/src/lib/node-action-movement.test.tsx +115 -0
  208. package/src/lib/node-action-movement.ts +2 -0
  209. package/src/lib/paint-region-mode.ts +1 -1
  210. package/src/lib/placement-metadata.ts +4 -0
  211. package/src/lib/point-ask/describe-target.ts +6 -8
  212. package/src/lib/point-ask/region.ts +1 -1
  213. package/src/lib/point-ask/session.ts +1 -2
  214. package/src/lib/print-shell-compiler.ts +12 -1
  215. package/src/lib/print-wall-solids.test.ts +11 -0
  216. package/src/lib/print-wall-solids.ts +9 -0
  217. package/src/lib/roof-duplication.test.ts +100 -0
  218. package/src/lib/roof-duplication.ts +30 -5
  219. package/src/lib/room-built-on.test.ts +2 -5
  220. package/src/lib/room-built-on.ts +1 -15
  221. package/src/lib/room-construction-commands.ts +3 -2
  222. package/src/lib/room-controls-phase6.test.ts +1 -1
  223. package/src/lib/room-handle-drag.ts +8 -37
  224. package/src/lib/room-name-catalog.test.ts +2 -1
  225. package/src/lib/room-name-messages.test.ts +2 -1
  226. package/src/lib/room-name-messages.ts +1 -118
  227. package/src/lib/room-name-storage.test.ts +2 -1
  228. package/src/lib/room-push-dimensions.test.ts +1 -1
  229. package/src/lib/room-selection-commands.ts +2 -2
  230. package/src/lib/room-selection.test.ts +6 -6
  231. package/src/lib/room-transform-session.ts +1 -2
  232. package/src/lib/room-zone-routing.ts +2 -2
  233. package/src/lib/scene-clipboard.ts +2 -1
  234. package/src/lib/selection-routing.test.ts +0 -28
  235. package/src/lib/selection-routing.ts +1 -40
  236. package/src/lib/sfx-bus.ts +8 -32
  237. package/src/lib/sfx-player.test.ts +72 -1
  238. package/src/lib/sfx-player.ts +49 -337
  239. package/src/lib/snapping-mode.test.ts +23 -1
  240. package/src/lib/viewer-selection.test.ts +38 -10
  241. package/src/lib/wall-pull.test.ts +1 -1
  242. package/src/lib/wall-pull.ts +1 -27
  243. package/src/lib/wall-region-list.ts +3 -1
  244. package/src/lib/window-interaction.test.ts +4 -2
  245. package/src/store/use-editor.tsx +22 -26
  246. package/src/store/use-floorplan-annotation-visibility.ts +5 -5
  247. package/src/store/use-placement-typing.test.ts +165 -0
  248. package/src/store/use-placement-typing.ts +186 -0
  249. package/src/store/use-wall-region-handles-selection.ts +1 -2
  250. package/src/components/editor/__bench__/room-label-placement.bench.ts +0 -108
  251. package/src/components/editor/first-person/build-collider-world.test.ts +0 -596
  252. package/src/components/editor/first-person/build-collider-world.ts +0 -549
  253. package/src/components/editor/first-person/ground-holes.test.ts +0 -128
  254. package/src/components/editor/first-person/ground-holes.ts +0 -130
  255. package/src/components/editor/first-person/mechanism-targets.test.ts +0 -162
  256. package/src/components/editor/first-person/mechanism-targets.ts +0 -88
  257. package/src/components/editor/first-person/terrain-collider.test.ts +0 -132
  258. package/src/components/editor/first-person/terrain-collider.ts +0 -212
  259. package/src/components/editor/floorplan-camera-sync.test.ts +0 -249
  260. package/src/components/editor/floorplan-camera-sync.ts +0 -213
  261. package/src/components/editor/localized-html.tsx +0 -22
  262. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +0 -236
  263. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +0 -708
  264. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +0 -385
  265. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +0 -670
  266. package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +0 -89
  267. package/src/components/editor-2d/renderers/floorplan-label-angle.ts +0 -106
  268. package/src/components/editor-2d/svg-paths.ts +0 -124
  269. package/src/components/tools/elevator/elevator-defaults.ts +0 -8
  270. package/src/components/tools/elevator/elevator-tool.tsx +0 -372
  271. package/src/components/tools/elevator/move-elevator-tool.tsx +0 -262
  272. package/src/components/ui/action-menu/action-button.tsx +0 -89
  273. package/src/components/ui/action-menu/follow-pascal-toggle.tsx +0 -42
  274. package/src/components/ui/primitives/button.tsx +0 -69
  275. package/src/components/ui/primitives/dropdown-menu.tsx +0 -232
  276. package/src/components/ui/primitives/popover.tsx +0 -42
  277. package/src/components/ui/primitives/switch.tsx +0 -29
  278. package/src/components/ui/primitives/tooltip.tsx +0 -57
  279. package/src/components/ui/scene-loader.tsx +0 -73
  280. package/src/components/viewer/floorplan-compass-button.tsx +0 -67
  281. package/src/components/viewer/floorplan-preview-geometry.test.ts +0 -107
  282. package/src/components/viewer/floorplan-preview-geometry.ts +0 -281
  283. package/src/components/viewer/floorplan-preview-navigation.test.ts +0 -43
  284. package/src/components/viewer/floorplan-preview-navigation.ts +0 -62
  285. package/src/components/viewer/floorplan-preview-visibility.test.ts +0 -35
  286. package/src/components/viewer/floorplan-preview-visibility.ts +0 -19
  287. package/src/components/viewer/floorplan-preview.tsx +0 -1065
  288. package/src/components/viewer/render-settings-menu.tsx +0 -294
  289. package/src/components/viewer/sky-control.tsx +0 -710
  290. package/src/components/viewer/use-viewer-camera-navigation-sync.ts +0 -140
  291. package/src/components/viewer/use-viewer-ui-sounds.test.ts +0 -15
  292. package/src/components/viewer/use-viewer-ui-sounds.ts +0 -46
  293. package/src/components/viewer/viewer-controls-bar.tsx +0 -496
  294. package/src/components/viewer/viewer-hover-label.tsx +0 -42
  295. package/src/components/viewer/viewer-rooms.tsx +0 -248
  296. package/src/components/viewer/viewer-scene-header.tsx +0 -286
  297. package/src/components/viewer/viewer-selection-manager.tsx +0 -124
  298. package/src/components/viewer/viewer-stage-modes.test.ts +0 -38
  299. package/src/components/viewer/viewer-stage-modes.ts +0 -49
  300. package/src/components/viewer/viewer-stage-switcher.tsx +0 -97
  301. package/src/components/viewer/viewer-stage.test.tsx +0 -47
  302. package/src/components/viewer/viewer-stage.tsx +0 -221
  303. package/src/components/walkthrough-hud.tsx +0 -193
  304. package/src/hooks/use-mobile.ts +0 -19
  305. package/src/i18n/level-name.ts +0 -39
  306. package/src/i18n/scene-theme-name.ts +0 -16
  307. package/src/lib/below-grade.ts +0 -133
  308. package/src/lib/camera-pose.test.ts +0 -237
  309. package/src/lib/camera-pose.ts +0 -172
  310. package/src/lib/door-interaction.ts +0 -102
  311. package/src/lib/floorplan/floorplan-extension.test.ts +0 -42
  312. package/src/lib/floorplan/floorplan-readonly.test.ts +0 -47
  313. package/src/lib/floorplan/floorplan-readonly.ts +0 -95
  314. package/src/lib/floorplan/marks.test.ts +0 -213
  315. package/src/lib/floorplan/marks.ts +0 -238
  316. package/src/lib/measurement-label.ts +0 -68
  317. package/src/lib/measurements.ts +0 -225
  318. package/src/lib/node-display-label.ts +0 -37
  319. package/src/lib/room-assembly-overlay.test.ts +0 -275
  320. package/src/lib/room-assembly-overlay.ts +0 -265
  321. package/src/lib/room-selection.ts +0 -417
  322. package/src/lib/scene-bounds.test.ts +0 -183
  323. package/src/lib/scene-bounds.ts +0 -169
  324. package/src/lib/typing-target.ts +0 -9
  325. package/src/lib/viewer-selection.ts +0 -595
  326. package/src/lib/window-interaction.ts +0 -142
  327. package/src/lib/zone-content.ts +0 -133
  328. package/src/store/camera-pose-store.test.ts +0 -29
  329. package/src/store/camera-pose-store.ts +0 -27
  330. package/src/store/navigation-sync-pose-store.test.ts +0 -34
  331. package/src/store/navigation-sync-pose-store.ts +0 -27
  332. package/src/store/use-audio.test.ts +0 -146
  333. package/src/store/use-audio.tsx +0 -169
  334. package/src/store/use-first-person-hud.ts +0 -40
  335. package/src/store/use-follow-pascal.test.ts +0 -62
  336. package/src/store/use-follow-pascal.ts +0 -53
@@ -1,49 +0,0 @@
1
- export const VIEWER_STAGE_MODES = ['3d', '2d', 'split'] as const
2
-
3
- export type ViewerStageMode = (typeof VIEWER_STAGE_MODES)[number]
4
-
5
- const THREE_D_MODES = ['3d'] as const
6
- const TWO_D_MODES = ['2d'] as const
7
- const SPLIT_MODES = ['split'] as const
8
- const THREE_D_TWO_D_MODES = ['3d', '2d'] as const
9
- const THREE_D_SPLIT_MODES = ['3d', 'split'] as const
10
- const TWO_D_SPLIT_MODES = ['2d', 'split'] as const
11
-
12
- export function normalizeViewerStageModes(
13
- modes: readonly ViewerStageMode[] | undefined,
14
- ): readonly ViewerStageMode[] {
15
- if (!modes) return VIEWER_STAGE_MODES
16
-
17
- const has3D = modes.includes('3d')
18
- const has2D = modes.includes('2d')
19
- const hasSplit = modes.includes('split')
20
-
21
- if (has3D && has2D && hasSplit) return VIEWER_STAGE_MODES
22
- if (has3D && has2D) return THREE_D_TWO_D_MODES
23
- if (has3D && hasSplit) return THREE_D_SPLIT_MODES
24
- if (has2D && hasSplit) return TWO_D_SPLIT_MODES
25
- if (has2D) return TWO_D_MODES
26
- if (hasSplit) return SPLIT_MODES
27
- return THREE_D_MODES
28
- }
29
-
30
- export function resolveViewerStageMode(
31
- mode: ViewerStageMode | undefined,
32
- modes: readonly ViewerStageMode[],
33
- ): ViewerStageMode {
34
- return mode && modes.includes(mode) ? mode : (modes[0] ?? '3d')
35
- }
36
-
37
- export function resolveMobileViewerStageMode(
38
- mode: ViewerStageMode,
39
- modes: readonly ViewerStageMode[],
40
- ): ViewerStageMode {
41
- if (mode !== 'split') return mode
42
- if (modes.includes('2d')) return '2d'
43
- if (modes.includes('3d')) return '3d'
44
- return 'split'
45
- }
46
-
47
- export function viewerStageIncludes3D(modes: readonly ViewerStageMode[]) {
48
- return modes.includes('3d') || modes.includes('split')
49
- }
@@ -1,97 +0,0 @@
1
- 'use client'
2
-
3
- import { usePascalI18n } from '@pascal-app/core'
4
- import { Box, Columns2, Map as MapIcon } from 'lucide-react'
5
- import type { ReactNode } from 'react'
6
- import { cn } from '../../lib/utils'
7
- import { normalizeViewerStageModes, type ViewerStageMode } from './viewer-stage-modes'
8
-
9
- export type { ViewerStageMode } from './viewer-stage-modes'
10
-
11
- export type ViewerStageSwitcherProps = {
12
- className?: string
13
- hideSplitOnMobile?: boolean
14
- mode: ViewerStageMode
15
- modes?: readonly ViewerStageMode[]
16
- onChange: (mode: ViewerStageMode) => void
17
- }
18
-
19
- export function ViewerStageSwitcher({
20
- className,
21
- hideSplitOnMobile = true,
22
- mode,
23
- modes,
24
- onChange,
25
- }: ViewerStageSwitcherProps) {
26
- const i18n = usePascalI18n()
27
- const enabledModes = normalizeViewerStageModes(modes)
28
-
29
- return (
30
- <div
31
- aria-label={i18n._({ id: 'viewer.stage.layout', message: 'Viewer layout' })}
32
- className={cn(
33
- 'dark absolute top-4 left-1/2 z-30 flex -translate-x-1/2 items-center gap-1 rounded-full border border-white/10 bg-neutral-950/82 p-1 text-white shadow-elevation-4 backdrop-blur-xl',
34
- className,
35
- )}
36
- role="group"
37
- >
38
- {enabledModes.includes('3d') ? (
39
- <StageButton
40
- active={mode === '3d'}
41
- icon={<Box />}
42
- label="3D"
43
- onClick={() => onChange('3d')}
44
- />
45
- ) : null}
46
- {enabledModes.includes('2d') ? (
47
- <StageButton
48
- active={mode === '2d'}
49
- icon={<MapIcon />}
50
- label="2D"
51
- onClick={() => onChange('2d')}
52
- />
53
- ) : null}
54
- {enabledModes.includes('split') ? (
55
- <StageButton
56
- active={mode === 'split'}
57
- className={hideSplitOnMobile && enabledModes.length > 1 ? 'hidden md:flex' : undefined}
58
- icon={<Columns2 />}
59
- label={i18n._({ id: 'viewer.stage.split', message: 'Split' })}
60
- onClick={() => onChange('split')}
61
- />
62
- ) : null}
63
- </div>
64
- )
65
- }
66
-
67
- function StageButton({
68
- active,
69
- className,
70
- icon,
71
- label,
72
- onClick,
73
- }: {
74
- active: boolean
75
- className?: string
76
- icon: ReactNode
77
- label: string
78
- onClick: () => void
79
- }) {
80
- return (
81
- <button
82
- aria-pressed={active}
83
- className={cn(
84
- 'flex h-8 items-center gap-1.5 rounded-full px-3 font-medium text-xs',
85
- active
86
- ? 'bg-white text-neutral-950 shadow-sm'
87
- : 'text-neutral-300 transition-colors hover:bg-white/10 hover:text-white',
88
- className,
89
- )}
90
- onClick={onClick}
91
- type="button"
92
- >
93
- <span className="[&>svg]:size-3.5">{icon}</span>
94
- {label}
95
- </button>
96
- )
97
- }
@@ -1,47 +0,0 @@
1
- import { describe, expect, test } from 'bun:test'
2
- import { LevelNode } from '@pascal-app/core/schema'
3
- import { renderToStaticMarkup } from 'react-dom/server'
4
- import type { FloorplanPreviewScene } from './floorplan-preview'
5
- import { ViewerStage } from './viewer-stage'
6
-
7
- const level = LevelNode.parse({ id: 'level_ground', type: 'level' })
8
- const scene: FloorplanPreviewScene = { nodes: { [level.id]: level } }
9
-
10
- describe('ViewerStage', () => {
11
- test('owns synchronized 2D and 3D composition by default', () => {
12
- const markup = renderToStaticMarkup(
13
- <ViewerStage mode="split" scene={scene} showLevelSelector={false}>
14
- <div data-test-viewer-content="" />
15
- </ViewerStage>,
16
- )
17
-
18
- expect(markup).toContain('data-pascal-viewer-stage="split"')
19
- expect(markup).toContain('data-pascal-navigation-sync="on"')
20
- expect(markup).toContain('data-pascal-viewer-3d="true"')
21
- expect(markup).toContain('data-floorplan-preview=""')
22
- expect(markup).toContain('viewBox="0 0 48 48"')
23
- })
24
-
25
- test('keeps a 2D-only embed free of a 3D canvas mount', () => {
26
- const markup = renderToStaticMarkup(
27
- <ViewerStage mode="2d" modes={['2d']} scene={scene} showLevelSelector={false} />,
28
- )
29
-
30
- expect(markup).toContain('data-pascal-viewer-stage="2d"')
31
- expect(markup).not.toContain('data-pascal-viewer-3d')
32
- expect(markup).toContain('data-floorplan-preview=""')
33
- })
34
-
35
- test('supports an explicit navigation synchronization opt-out', () => {
36
- const markup = renderToStaticMarkup(
37
- <ViewerStage
38
- mode="split"
39
- scene={scene}
40
- showLevelSelector={false}
41
- synchronizeNavigation={false}
42
- />,
43
- )
44
-
45
- expect(markup).toContain('data-pascal-navigation-sync="off"')
46
- })
47
- })
@@ -1,221 +0,0 @@
1
- 'use client'
2
-
3
- import { type AnyNode, type AnyNodeId, useScene } from '@pascal-app/core'
4
- import { markPerfAction, useViewer } from '@pascal-app/viewer'
5
- import type { ReactNode } from 'react'
6
- import { useCallback, useEffect, useMemo, useState } from 'react'
7
- import { useShallow } from 'zustand/react/shallow'
8
- import { cn } from '../../lib/utils'
9
- import { FloorplanPreview, type FloorplanPreviewScene } from './floorplan-preview'
10
- import {
11
- normalizeViewerStageModes,
12
- resolveMobileViewerStageMode,
13
- resolveViewerStageMode,
14
- type ViewerStageMode,
15
- viewerStageIncludes3D,
16
- } from './viewer-stage-modes'
17
- import { ViewerStageSwitcher } from './viewer-stage-switcher'
18
-
19
- export type ViewerStageProps = {
20
- children?: ReactNode
21
- className?: string
22
- collapseSplitOnMobile?: boolean
23
- compassHost?: Element | null
24
- defaultMode?: ViewerStageMode
25
- floorplanClassName?: string
26
- levelId?: string | null
27
- mode?: ViewerStageMode
28
- modes?: readonly ViewerStageMode[]
29
- onLevelChange?: (levelId: string) => void
30
- onModeChange?: (mode: ViewerStageMode) => void
31
- scene?: FloorplanPreviewScene | null
32
- showCompass?: boolean
33
- showLevelSelector?: boolean
34
- showSwitcher?: boolean
35
- switcherClassName?: string
36
- synchronizeNavigation?: boolean
37
- threeDClassName?: string
38
- }
39
-
40
- const EMPTY_LEVEL_IDS: string[] = []
41
-
42
- function levelNodeIds(nodes: Record<string, AnyNode>) {
43
- return Object.values(nodes)
44
- .filter((node) => node.type === 'level')
45
- .sort(
46
- (left, right) =>
47
- ((left as { level?: number }).level ?? 0) - ((right as { level?: number }).level ?? 0),
48
- )
49
- .map((node) => node.id)
50
- }
51
-
52
- function selectViewerLevel(nodes: Record<string, AnyNode>, levelId: string) {
53
- const level = nodes[levelId as AnyNodeId]
54
- if (level?.type !== 'level') return
55
- const building = level.parentId ? nodes[level.parentId as AnyNodeId] : null
56
- const viewer = useViewer.getState()
57
- viewer.setSelection({
58
- buildingId: building?.type === 'building' ? building.id : null,
59
- levelId: level.id,
60
- selectedIds: [],
61
- zoneId: null,
62
- })
63
- }
64
-
65
- export function ViewerStage({
66
- children,
67
- className,
68
- collapseSplitOnMobile = true,
69
- compassHost,
70
- defaultMode,
71
- floorplanClassName,
72
- levelId,
73
- mode: controlledMode,
74
- modes,
75
- onLevelChange,
76
- onModeChange,
77
- scene,
78
- showCompass = true,
79
- showLevelSelector = true,
80
- showSwitcher = true,
81
- switcherClassName,
82
- synchronizeNavigation = true,
83
- threeDClassName,
84
- }: ViewerStageProps) {
85
- const enabledModes = normalizeViewerStageModes(modes)
86
- const [internalMode, setInternalMode] = useState(() =>
87
- resolveViewerStageMode(defaultMode, enabledModes),
88
- )
89
- const activeMode = resolveViewerStageMode(controlledMode ?? internalMode, enabledModes)
90
- const storeLevelIds = useScene(
91
- useShallow((state) => (scene ? EMPTY_LEVEL_IDS : levelNodeIds(state.nodes))),
92
- )
93
- const externalLevelIds = useMemo(() => (scene ? levelNodeIds(scene.nodes) : null), [scene])
94
- const levelIds = externalLevelIds ?? storeLevelIds
95
- const selectedLevelId = useViewer((state) => state.selection.levelId)
96
- const [internalLevelId, setInternalLevelId] = useState<string | null>(null)
97
- const [internalCompassHost, setInternalCompassHost] = useState<HTMLDivElement | null>(null)
98
- const resolvedCompassHost = compassHost ?? internalCompassHost
99
- const floorplanEnabled = enabledModes.includes('2d') || enabledModes.includes('split')
100
- const threeDEnabled = viewerStageIncludes3D(enabledModes)
101
- const mountFloorplan = floorplanEnabled || showCompass
102
-
103
- const requestMode = useCallback(
104
- (nextMode: ViewerStageMode) => {
105
- if (!enabledModes.includes(nextMode)) return
106
- if (controlledMode === undefined) setInternalMode(nextMode)
107
- onModeChange?.(nextMode)
108
- },
109
- [controlledMode, enabledModes, onModeChange],
110
- )
111
-
112
- useEffect(() => {
113
- if (controlledMode !== undefined) {
114
- if (controlledMode !== activeMode) onModeChange?.(activeMode)
115
- return
116
- }
117
- if (internalMode !== activeMode) setInternalMode(activeMode)
118
- }, [activeMode, controlledMode, internalMode, onModeChange])
119
-
120
- useEffect(() => {
121
- if (!collapseSplitOnMobile) return
122
- const mediaQuery = window.matchMedia('(max-width: 767px)')
123
- const resolveMode = () => {
124
- if (!mediaQuery.matches) return
125
- const nextMode = resolveMobileViewerStageMode(activeMode, enabledModes)
126
- if (nextMode !== activeMode) requestMode(nextMode)
127
- }
128
- resolveMode()
129
- mediaQuery.addEventListener('change', resolveMode)
130
- return () => mediaQuery.removeEventListener('change', resolveMode)
131
- }, [activeMode, collapseSplitOnMobile, enabledModes, requestMode])
132
-
133
- const chooseLevel = useCallback(
134
- (nextLevelId: string, notify = true) => {
135
- setInternalLevelId(nextLevelId)
136
- if (notify && nextLevelId !== useViewer.getState().selection.levelId) {
137
- markPerfAction('level-switch', nextLevelId)
138
- }
139
- if (nextLevelId !== useViewer.getState().selection.levelId)
140
- selectViewerLevel(scene?.nodes ?? useScene.getState().nodes, nextLevelId)
141
- if (notify) onLevelChange?.(nextLevelId)
142
- },
143
- [onLevelChange, scene],
144
- )
145
-
146
- useEffect(() => {
147
- if (activeMode === '3d' || levelIds.length === 0) return
148
- const nextLevelId =
149
- (levelId && levelIds.includes(levelId) ? levelId : null) ??
150
- (selectedLevelId && levelIds.includes(selectedLevelId) ? selectedLevelId : null) ??
151
- (internalLevelId && levelIds.includes(internalLevelId) ? internalLevelId : null) ??
152
- levelIds[0] ??
153
- null
154
- if (nextLevelId) chooseLevel(nextLevelId, false)
155
- }, [activeMode, chooseLevel, internalLevelId, levelId, levelIds, selectedLevelId])
156
-
157
- return (
158
- <div
159
- className={cn('relative h-full w-full overflow-hidden bg-neutral-100', className)}
160
- data-pascal-navigation-sync={synchronizeNavigation ? 'on' : 'off'}
161
- data-pascal-viewer-stage={activeMode}
162
- >
163
- {showCompass && compassHost === undefined ? (
164
- <div className="pointer-events-none absolute inset-0 z-30" ref={setInternalCompassHost} />
165
- ) : null}
166
-
167
- {showSwitcher && enabledModes.length > 1 ? (
168
- <ViewerStageSwitcher
169
- className={switcherClassName}
170
- hideSplitOnMobile={collapseSplitOnMobile}
171
- mode={activeMode}
172
- modes={enabledModes}
173
- onChange={requestMode}
174
- />
175
- ) : null}
176
-
177
- <div
178
- className={
179
- activeMode === 'split'
180
- ? 'absolute inset-0 grid grid-rows-2 md:grid-cols-2 md:grid-rows-1'
181
- : 'absolute inset-0'
182
- }
183
- >
184
- {threeDEnabled ? (
185
- <div
186
- className={cn(
187
- activeMode === '2d'
188
- ? 'pointer-events-none invisible absolute inset-0 h-full w-full'
189
- : 'relative h-full min-h-0 w-full min-w-0',
190
- threeDClassName,
191
- )}
192
- data-pascal-viewer-3d
193
- >
194
- {children}
195
- </div>
196
- ) : null}
197
-
198
- {mountFloorplan ? (
199
- <FloorplanPreview
200
- className={cn(
201
- activeMode === '3d'
202
- ? 'hidden'
203
- : activeMode === 'split'
204
- ? 'min-h-0 min-w-0 border-border border-t md:border-t-0 md:border-l'
205
- : 'h-full w-full',
206
- floorplanClassName,
207
- )}
208
- compassHost={resolvedCompassHost}
209
- levelId={levelId ?? internalLevelId}
210
- navigationVisible={activeMode !== '3d'}
211
- onLevelChange={chooseLevel}
212
- scene={scene}
213
- showCompass={showCompass}
214
- showLevelSelector={showLevelSelector}
215
- synchronizeNavigation={synchronizeNavigation}
216
- />
217
- ) : null}
218
- </div>
219
- </div>
220
- )
221
- }
@@ -1,193 +0,0 @@
1
- 'use client'
2
-
3
- import { usePascalI18n } from '@pascal-app/core'
4
- import { type ReactNode, useMemo } from 'react'
5
- import { displayRoomName } from '../lib/room-name-messages'
6
- import { cn } from '../lib/utils'
7
- import type { WalkthroughInteract } from '../store/use-first-person-hud'
8
-
9
- export type { WalkthroughInteract } from '../store/use-first-person-hud'
10
-
11
- /** A control shown in the HUD's key strip: its keys (as printed on the keycaps) and what they do. */
12
- export type WalkthroughHint = { keys: readonly string[]; action: string }
13
-
14
- export type WalkthroughHudProps = {
15
- floorLabel?: string | null
16
- zoneLabel?: string | null
17
- interact?: WalkthroughInteract
18
- /** Movement controls listed before the cursor and exit pills. */
19
- hints?: readonly WalkthroughHint[]
20
- /** Pointer lock temporarily released (OS screenshot) — the pill flips to
21
- * "Click to resume" and lets clicks fall through to the canvas. */
22
- suspended?: boolean
23
- onExit?: () => void
24
- children?: ReactNode
25
- }
26
-
27
- /** The centered walkthrough pointer: a dot that grows into a green ring over
28
- * an interactable (door / window / elevator). Also mounted by the snapshot
29
- * capture overlay so walk / drone framing keeps the same E-to-open pointer. */
30
- export function WalkthroughCrosshair({ interact }: { interact: WalkthroughInteract }) {
31
- return (
32
- <div className="pointer-events-none absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
33
- <div
34
- className={cn(
35
- 'rounded-full transition-all duration-150',
36
- interact
37
- ? 'h-4 w-4 border-2 border-emerald-400 bg-emerald-400/10'
38
- : 'h-1.5 w-1.5 bg-white/80 shadow-[0_0_2px_rgba(0,0,0,0.6)]',
39
- )}
40
- />
41
- </div>
42
- )
43
- }
44
-
45
- export function WalkthroughHud({
46
- floorLabel,
47
- zoneLabel,
48
- interact = null,
49
- hints,
50
- suspended = false,
51
- onExit,
52
- children,
53
- }: WalkthroughHudProps) {
54
- const kbdClass = 'rounded border border-border/60 bg-white/10 px-1.5 py-0.5 font-mono text-[10px]'
55
- const pillClass =
56
- 'flex items-center gap-1.5 rounded-full border border-border/40 bg-background/70 px-3 py-1 text-muted-foreground text-xs backdrop-blur-xl'
57
- const exitContent = (
58
- <>
59
- <kbd className={kbdClass}>Esc</kbd>
60
- to exit
61
- </>
62
- )
63
-
64
- return (
65
- <div className="dark pointer-events-none absolute inset-0 z-30 text-foreground">
66
- <div className="absolute top-6 left-1/2 flex -translate-x-1/2 flex-col items-center gap-1.5">
67
- {floorLabel && (
68
- <div className="font-medium text-[10px] text-muted-foreground uppercase tracking-wider">
69
- {floorLabel}
70
- </div>
71
- )}
72
- {zoneLabel && (
73
- <div className="corner-smooth rounded-full border border-border/40 bg-background/80 px-3 py-1 font-medium text-sm shadow-elevation-3 backdrop-blur-xl">
74
- <WalkthroughZoneName name={zoneLabel} />
75
- </div>
76
- )}
77
- {children}
78
- </div>
79
-
80
- <WalkthroughCrosshair interact={interact} />
81
-
82
- <div className="absolute bottom-6 left-1/2 flex -translate-x-1/2 items-center gap-2">
83
- {suspended ? (
84
- <div className={pillClass}>
85
- <span className="font-medium text-foreground">Click</span>
86
- <span>or</span>
87
- <kbd className={kbdClass}>P</kbd>
88
- <span>to resume</span>
89
- <span className="text-muted-foreground/60">·</span>
90
- {exitContent}
91
- </div>
92
- ) : (
93
- <>
94
- {hints && hints.length > 0 ? (
95
- <div className={pillClass}>
96
- {hints.map(({ keys, action }, index) => (
97
- <span className="flex items-center gap-1.5" key={action}>
98
- {index > 0 && <span className="text-muted-foreground/60">·</span>}
99
- {keys.map((key, keyIndex) => (
100
- <span className="flex items-center gap-1" key={key}>
101
- {keyIndex > 0 && <span className="text-muted-foreground/60">/</span>}
102
- <kbd className={kbdClass}>{key}</kbd>
103
- </span>
104
- ))}
105
- {action}
106
- </span>
107
- ))}
108
- </div>
109
- ) : null}
110
- <div className={pillClass}>
111
- <kbd className={kbdClass}>P</kbd>
112
- free cursor
113
- </div>
114
- {onExit ? (
115
- <button
116
- className={cn(pillClass, 'pointer-events-auto')}
117
- onClick={onExit}
118
- type="button"
119
- >
120
- {exitContent}
121
- </button>
122
- ) : (
123
- <div className={pillClass}>{exitContent}</div>
124
- )}
125
- </>
126
- )}
127
- </div>
128
-
129
- {interact && (
130
- <div className="absolute top-1/2 left-1/2 mt-7 -translate-x-1/2 whitespace-nowrap">
131
- <div className="corner-smooth flex items-center gap-1.5 rounded-full border border-border/40 bg-background/80 px-3 py-1 text-xs shadow-elevation-3 backdrop-blur-xl">
132
- <kbd className="rounded border border-border/60 bg-white/10 px-1.5 py-0.5 font-mono text-[10px]">
133
- E
134
- </kbd>
135
- <span className="text-muted-foreground">or click to</span>
136
- <span className="font-medium">
137
- {interact.verb} {interact.label}
138
- </span>
139
- </div>
140
- </div>
141
- )}
142
- </div>
143
- )
144
- }
145
-
146
- /** The drone's key strip: the movement controls of the viewer's `DroneController`. */
147
- function useDroneHints(): readonly WalkthroughHint[] {
148
- const i18n = usePascalI18n()
149
- return useMemo(
150
- () => [
151
- {
152
- keys: ['WASD'],
153
- action: i18n._({ id: 'editor.walkthroughHud.drone.move', message: 'move' }),
154
- },
155
- {
156
- keys: ['Space', 'E'],
157
- action: i18n._({ id: 'editor.walkthroughHud.drone.up', message: 'up' }),
158
- },
159
- {
160
- keys: ['Q'],
161
- action: i18n._({ id: 'editor.walkthroughHud.drone.down', message: 'down' }),
162
- },
163
- {
164
- keys: ['Shift'],
165
- action: i18n._({ id: 'editor.walkthroughHud.drone.boost', message: 'boost' }),
166
- },
167
- {
168
- keys: ['Alt'],
169
- action: i18n._({ id: 'editor.walkthroughHud.drone.slow', message: 'slow' }),
170
- },
171
- ],
172
- [i18n],
173
- )
174
- }
175
-
176
- /** The walkthrough HUD for the drone: the plain pointer dot and the flight keys, no room or door prompts. */
177
- export function DroneHud({
178
- suspended,
179
- onExit,
180
- children,
181
- }: Pick<WalkthroughHudProps, 'suspended' | 'onExit' | 'children'>) {
182
- return (
183
- <WalkthroughHud hints={useDroneHints()} onExit={onExit} suspended={suspended}>
184
- {children}
185
- </WalkthroughHud>
186
- )
187
- }
188
-
189
- /** The room the walker is in, in the UI language (the scene keeps its stored name). */
190
- function WalkthroughZoneName({ name }: { name: string }) {
191
- const i18n = usePascalI18n()
192
- return displayRoomName(name, i18n)
193
- }
@@ -1,19 +0,0 @@
1
- import * as React from 'react'
2
-
3
- const MOBILE_BREAKPOINT = 768
4
-
5
- const subscribe = (callback: () => void): (() => void) => {
6
- const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
7
- mql.addEventListener('change', callback)
8
- return () => mql.removeEventListener('change', callback)
9
- }
10
-
11
- const getClientSnapshot = (): boolean => window.innerWidth < MOBILE_BREAKPOINT
12
-
13
- // Server can't know the viewport — assume desktop. React's useSyncExternalStore
14
- // reconciles the SSR / client snapshots without a hydration mismatch warning.
15
- const getServerSnapshot = (): boolean => false
16
-
17
- export function useIsMobile(): boolean {
18
- return React.useSyncExternalStore(subscribe, getClientSnapshot, getServerSnapshot)
19
- }
@@ -1,39 +0,0 @@
1
- import { getDefaultLevelName, levelDisplayNumber, type PascalI18n } from '@pascal-app/core'
2
-
3
- type DisplayLevel = {
4
- level?: number
5
- name?: string
6
- }
7
-
8
- /** The level's own name, or undefined while it carries a generated default. */
9
- export function authoredLevelName(level: DisplayLevel): string | undefined {
10
- const levelNumber = level.level ?? 0
11
- const name = level.name?.trim()
12
- return name && name !== `Level ${levelNumber}` && name !== getDefaultLevelName(levelNumber)
13
- ? name
14
- : undefined
15
- }
16
-
17
- export function localizedLevelName(level: DisplayLevel, i18n: PascalI18n, locale: string): string {
18
- const levelNumber = level.level ?? 0
19
- const authoredName = authoredLevelName(level)
20
- if (authoredName) return authoredName
21
- if (levelNumber === 0) {
22
- return i18n._({ id: 'core.level.groundFloor', message: 'Ground floor' })
23
- }
24
- if (levelNumber === -1) {
25
- return i18n._({ id: 'core.level.basement', message: 'Basement' })
26
- }
27
- if (levelNumber < -1) {
28
- return i18n._({
29
- id: 'core.level.basementNumber',
30
- message: 'Basement {number}',
31
- values: { number: -levelNumber },
32
- })
33
- }
34
- return i18n._({
35
- id: 'core.level.floorNumber',
36
- message: 'Floor {number}',
37
- values: { number: levelDisplayNumber(levelNumber, locale) },
38
- })
39
- }
@@ -1,16 +0,0 @@
1
- import type { PascalI18n } from '@pascal-app/core'
2
-
3
- /** Display name of a built-in scene theme; unknown (plugin) themes keep their own name. */
4
- export function sceneThemeName(id: string, fallback: string, i18n: PascalI18n): string {
5
- if (id === 'studio') return i18n._({ id: 'viewer.theme.studio', message: 'Studio' })
6
- if (id === 'paper') return i18n._({ id: 'viewer.theme.paper', message: 'Paper' })
7
- if (id === 'sunset') return i18n._({ id: 'viewer.theme.sunset', message: 'Sunset' })
8
- if (id === 'overcast') return i18n._({ id: 'viewer.theme.overcast', message: 'Overcast' })
9
- if (id === 'blueprint') return i18n._({ id: 'viewer.theme.blueprint', message: 'Blueprint' })
10
- if (id === 'mediterranean')
11
- return i18n._({ id: 'viewer.theme.mediterranean', message: 'Mediterranean' })
12
- if (id === 'twilight') return i18n._({ id: 'viewer.theme.twilight', message: 'Twilight' })
13
- if (id === 'night') return i18n._({ id: 'viewer.theme.night', message: 'Night' })
14
- if (id === 'verdant') return i18n._({ id: 'viewer.theme.verdant', message: 'Verdant' })
15
- return fallback
16
- }