@pascal-app/editor 1.0.0-beta.5 → 1.0.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 (297) hide show
  1. package/LICENSE +21 -0
  2. package/bunfig.toml +4 -0
  3. package/package.json +15 -8
  4. package/scripts/generate-print-golden-house.ts +89 -0
  5. package/src/components/editor/bake-exporter.tsx +5 -0
  6. package/src/components/editor/bake-thumbnail.tsx +4 -1
  7. package/src/components/editor/camera-dragging-lifecycle.test.ts +41 -0
  8. package/src/components/editor/camera-dragging-lifecycle.ts +10 -2
  9. package/src/components/editor/capture-camera-rig.tsx +59 -0
  10. package/src/components/editor/custom-camera-controls.tsx +20 -11
  11. package/src/components/editor/editor-layout-mobile.tsx +6 -6
  12. package/src/components/editor/editor-layout-v2.tsx +9 -2
  13. package/src/components/editor/export-manager.tsx +175 -33
  14. package/src/components/editor/first-person/build-collider-world.test.ts +30 -0
  15. package/src/components/editor/first-person-controls.tsx +180 -12
  16. package/src/components/editor/floating-action-menu.tsx +55 -51
  17. package/src/components/editor/floorplan-panel.tsx +99 -15
  18. package/src/components/editor/grid.tsx +12 -7
  19. package/src/components/editor/group-actions.ts +34 -50
  20. package/src/components/editor/group-move-3d.ts +51 -21
  21. package/src/components/editor/group-transform-shared.test.ts +78 -0
  22. package/src/components/editor/group-transform-shared.ts +74 -7
  23. package/src/components/editor/handles/handle-arrow-raycast.test.ts +86 -0
  24. package/src/components/editor/handles/handle-arrow.tsx +109 -48
  25. package/src/components/editor/handles/handle-placement.test.ts +111 -0
  26. package/src/components/editor/handles/handle-placement.ts +69 -0
  27. package/src/components/editor/handles/linear-resize-drag.ts +78 -0
  28. package/src/components/editor/handles/resize-snap.test.ts +46 -0
  29. package/src/components/editor/handles/resize-snap.ts +10 -1
  30. package/src/components/editor/handles/use-handle-drag.ts +23 -3
  31. package/src/components/editor/index.tsx +151 -24
  32. package/src/components/editor/node-action-menu.tsx +14 -1
  33. package/src/components/editor/node-arrow-handles.tsx +67 -119
  34. package/src/components/editor/selection-manager.tsx +153 -42
  35. package/src/components/editor/snapshot-capture-overlay.tsx +298 -39
  36. package/src/components/editor/snapshot-capture.test.ts +422 -0
  37. package/src/components/editor/snapshot-capture.ts +145 -0
  38. package/src/components/editor/thumbnail-generator.tsx +295 -239
  39. package/src/components/editor/use-floorplan-background-placement.ts +53 -5
  40. package/src/components/editor/wall-move-side-handles.tsx +299 -7
  41. package/src/components/editor-2d/floorplan-group-move.tsx +67 -33
  42. package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +3 -1
  43. package/src/components/editor-2d/floorplan-registry-action-menu.tsx +12 -1
  44. package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +129 -38
  45. package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +14 -1
  46. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +16 -0
  47. package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +2 -0
  48. package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +52 -3
  49. package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +196 -1
  50. package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +226 -35
  51. package/src/components/procedural-items/inspector.tsx +223 -0
  52. package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +402 -0
  53. package/src/components/systems/ceiling/ceiling-bracket-batch.ts +371 -0
  54. package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +347 -0
  55. package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +281 -305
  56. package/src/components/systems/ceiling/ceiling-system.tsx +3 -1
  57. package/src/components/systems/roof/roof-edit-system.test.tsx +120 -0
  58. package/src/components/systems/roof/roof-edit-system.tsx +10 -19
  59. package/src/components/systems/selection-affordance-manager.tsx +59 -9
  60. package/src/components/systems/selection-affordance-services.ts +19 -0
  61. package/src/components/systems/stair/stair-edit-system.tsx +5 -2
  62. package/src/components/tools/fence/fence-drafting.test.ts +100 -0
  63. package/src/components/tools/fence/fence-drafting.ts +6 -25
  64. package/src/components/tools/item/draft-creation.test.ts +12 -0
  65. package/src/components/tools/item/draft-creation.ts +10 -0
  66. package/src/components/tools/item/face-host-commit.test.ts +65 -0
  67. package/src/components/tools/item/face-host-commit.ts +56 -0
  68. package/src/components/tools/item/face-host-preview.test.ts +77 -0
  69. package/src/components/tools/item/face-host-preview.ts +82 -0
  70. package/src/components/tools/item/item-surface-placement.test.ts +175 -0
  71. package/src/components/tools/item/item-surface-strategy-resolver.test.ts +210 -0
  72. package/src/components/tools/item/move-tool.tsx +4 -3
  73. package/src/components/tools/item/placement-math.test.ts +11 -1
  74. package/src/components/tools/item/placement-math.ts +0 -20
  75. package/src/components/tools/item/placement-strategies.test.ts +331 -4
  76. package/src/components/tools/item/placement-strategies.ts +304 -171
  77. package/src/components/tools/item/placement-surface.test.ts +61 -0
  78. package/src/components/tools/item/placement-surface.ts +24 -0
  79. package/src/components/tools/item/placement-types.ts +10 -0
  80. package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +257 -0
  81. package/src/components/tools/item/test-face-host.ts +121 -0
  82. package/src/components/tools/item/use-draft-node.test.tsx +336 -0
  83. package/src/components/tools/item/use-draft-node.ts +116 -44
  84. package/src/components/tools/item/use-placement-coordinator.tsx +672 -256
  85. package/src/components/tools/registered-tool-install-lifecycle.test.tsx +256 -0
  86. package/src/components/tools/registry/item-surface-child-frame.test.ts +90 -0
  87. package/src/components/tools/registry/item-surface-move.test.ts +501 -0
  88. package/src/components/tools/registry/item-surface-move.ts +441 -0
  89. package/src/components/tools/registry/item-surface-resolver.test.ts +257 -0
  90. package/src/components/tools/registry/move-registry-node-tool.test.ts +10 -0
  91. package/src/components/tools/registry/move-registry-node-tool.tsx +505 -164
  92. package/src/components/tools/registry-tool-context.tsx +30 -0
  93. package/src/components/tools/select/box-select-tool.tsx +3 -1
  94. package/src/components/tools/select/marquee-footprint.test.ts +53 -0
  95. package/src/components/tools/select/marquee-footprint.ts +29 -0
  96. package/src/components/tools/shared/placement-box.tsx +57 -7
  97. package/src/components/tools/shared/placement-dimension-guides.tsx +121 -0
  98. package/src/components/tools/shared/pointer-support-cap.test.ts +319 -17
  99. package/src/components/tools/shared/pointer-support-cap.ts +56 -17
  100. package/src/components/tools/shared/shelf-stickiness.ts +54 -0
  101. package/src/components/tools/shared/surface-hit.test.ts +56 -0
  102. package/src/components/tools/shared/surface-hit.ts +23 -0
  103. package/src/components/tools/shared/surface-rejection.test.tsx +106 -0
  104. package/src/components/tools/shared/surface-rejection.tsx +29 -0
  105. package/src/components/tools/site/terrain-sculpt-grid.tsx +9 -5
  106. package/src/components/tools/site/terrain-sculpt-tool.tsx +1 -1
  107. package/src/components/tools/stair/stair-tool.tsx +127 -74
  108. package/src/components/tools/tool-manager.tsx +25 -8
  109. package/src/components/tools/wall/wall-drafting.test.ts +320 -2
  110. package/src/components/tools/wall/wall-drafting.ts +23 -114
  111. package/src/components/tools/zone/zone-tool.tsx +13 -4
  112. package/src/components/ui/action-menu/control-modes.tsx +9 -6
  113. package/src/components/ui/action-menu/index.tsx +11 -1
  114. package/src/components/ui/command-palette/editor-commands.tsx +63 -13
  115. package/src/components/ui/command-palette/index.tsx +7 -2
  116. package/src/components/ui/controls/material-paint-panel.tsx +6 -4
  117. package/src/components/ui/controls/material-picker.tsx +4 -4
  118. package/src/components/ui/controls/metric-control.tsx +29 -13
  119. package/src/components/ui/controls/scene-material-list.tsx +6 -5
  120. package/src/components/ui/controls/segmented-control.tsx +8 -1
  121. package/src/components/ui/controls/slider-control.tsx +44 -21
  122. package/src/components/ui/controls/toggle-control.tsx +18 -5
  123. package/src/components/ui/controls/tool-options-panel.tsx +126 -0
  124. package/src/components/ui/floating-level-selector.tsx +103 -10
  125. package/src/components/ui/helpers/contextual-helper-panel.tsx +9 -0
  126. package/src/components/ui/helpers/helper-manager.tsx +47 -11
  127. package/src/components/ui/helpers/item-helper.tsx +1 -1
  128. package/src/components/ui/helpers/registered-tool-helper.tsx +21 -0
  129. package/src/components/ui/panels/homogeneous-selection.test.ts +80 -0
  130. package/src/components/ui/panels/homogeneous-selection.ts +47 -0
  131. package/src/components/ui/panels/mobile-selection-bar.tsx +11 -5
  132. package/src/components/ui/panels/multi-field-value.test.ts +209 -0
  133. package/src/components/ui/panels/multi-field-value.ts +163 -0
  134. package/src/components/ui/panels/multi-height-mode.tsx +185 -0
  135. package/src/components/ui/panels/multi-parametric-inspector.tsx +266 -0
  136. package/src/components/ui/panels/multi-selection-panel.tsx +48 -37
  137. package/src/components/ui/panels/node-display.ts +4 -0
  138. package/src/components/ui/panels/panel-manager.tsx +77 -3
  139. package/src/components/ui/panels/parametric-field-control.tsx +173 -0
  140. package/src/components/ui/panels/parametric-field-utils.ts +18 -0
  141. package/src/components/ui/panels/parametric-inspector.tsx +35 -164
  142. package/src/components/ui/panels/reference-panel.tsx +34 -4
  143. package/src/components/ui/primitives/checkbox.tsx +34 -0
  144. package/src/components/ui/primitives/shortcut-token.tsx +23 -1
  145. package/src/components/ui/scene-loader.tsx +35 -0
  146. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +502 -31
  147. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +20 -23
  148. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.test.ts +167 -0
  149. package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +128 -0
  150. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +8 -1
  151. package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +19 -5
  152. package/src/components/ui/sidebar/panels/site-panel/index.tsx +222 -50
  153. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +2 -7
  154. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +5 -2
  155. package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +28 -11
  156. package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +3 -8
  157. package/src/components/ui/sidebar/panels/site-panel/tree-node.test.ts +8 -0
  158. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +20 -7
  159. package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +224 -0
  160. package/src/components/ui/sidebar/panels/site-panel/zone-membership-checkbox.tsx +22 -0
  161. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +24 -7
  162. package/src/components/ui/sidebar/tab-bar.tsx +2 -0
  163. package/src/components/ui/sidebar/use-plugin-panels.tsx +17 -2
  164. package/src/components/ui/snap-target-badge.tsx +2 -0
  165. package/src/components/viewer/viewer-scene-header.tsx +5 -1
  166. package/src/components/viewer/viewer-stage.tsx +4 -1
  167. package/src/components/viewer/viewer-units-panel.tsx +116 -0
  168. package/src/components/viewer-overlay.tsx +1 -22
  169. package/src/components/viewer-zone-system.tsx +6 -2
  170. package/src/components/walkthrough-hud.tsx +19 -10
  171. package/src/hooks/use-auto-save.test.ts +73 -1
  172. package/src/hooks/use-auto-save.ts +76 -5
  173. package/src/hooks/use-ceiling-events.test.ts +144 -0
  174. package/src/hooks/use-ceiling-events.ts +5 -1
  175. package/src/hooks/use-drag-action.ts +17 -3
  176. package/src/hooks/use-grid-events.test.ts +240 -0
  177. package/src/hooks/use-grid-events.ts +190 -11
  178. package/src/hooks/use-keyboard.test.ts +244 -0
  179. package/src/hooks/use-keyboard.ts +132 -63
  180. package/src/hooks/use-registered-tool-enabled.ts +20 -0
  181. package/src/hooks/use-save-shortcut.ts +33 -0
  182. package/src/index.tsx +87 -4
  183. package/src/lib/active-placement-surface.test.ts +16 -0
  184. package/src/lib/active-placement-surface.ts +14 -0
  185. package/src/lib/ceiling-surface-raycast.test.ts +64 -0
  186. package/src/lib/ceiling-surface-raycast.ts +12 -0
  187. package/src/lib/contextual-help-extension.ts +17 -0
  188. package/src/lib/continuation.test.ts +11 -0
  189. package/src/lib/continuation.ts +8 -1
  190. package/src/lib/direct-manipulation.test.ts +132 -0
  191. package/src/lib/direct-manipulation.ts +38 -1
  192. package/src/lib/editor-api.ts +6 -3
  193. package/src/lib/elevation-guides.test.ts +28 -1
  194. package/src/lib/elevation-guides.ts +22 -7
  195. package/src/lib/export-texture-utils.test.ts +167 -0
  196. package/src/lib/export-texture-utils.ts +121 -0
  197. package/src/lib/export-user-data.test.ts +48 -0
  198. package/src/lib/export-user-data.ts +69 -0
  199. package/src/lib/floating-menu-scale.test.ts +29 -0
  200. package/src/lib/floating-menu-scale.ts +14 -0
  201. package/src/lib/floorplan/apply-alignment.test.ts +47 -2
  202. package/src/lib/floorplan/apply-alignment.ts +10 -1
  203. package/src/lib/floorplan/floorplan-export.test.ts +536 -2
  204. package/src/lib/floorplan/floorplan-export.tsx +120 -19
  205. package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +26 -0
  206. package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +32 -24
  207. package/src/lib/floorplan/floorplan-readonly.ts +5 -0
  208. package/src/lib/floorplan-grid-event-point.test.ts +29 -0
  209. package/src/lib/floorplan-grid-event-point.ts +13 -0
  210. package/src/lib/fresh-planar-placement.ts +13 -5
  211. package/src/lib/glb-export-serialize.test.ts +280 -0
  212. package/src/lib/glb-export.test.ts +1375 -21
  213. package/src/lib/glb-export.ts +781 -106
  214. package/src/lib/grid-event-presentation.ts +16 -0
  215. package/src/lib/history.test.ts +528 -147
  216. package/src/lib/history.ts +81 -7
  217. package/src/lib/host-tree-children.test.ts +27 -0
  218. package/src/lib/host-tree-children.ts +79 -0
  219. package/src/lib/interaction/hot-set.test.ts +10 -1
  220. package/src/lib/interaction/hot-set.ts +3 -1
  221. package/src/lib/interaction/overlay-policy.test.ts +33 -1
  222. package/src/lib/interaction/overlay-policy.ts +16 -0
  223. package/src/lib/interaction/registered-drafting.ts +36 -0
  224. package/src/lib/interaction/scope.ts +31 -0
  225. package/src/lib/level-duplication.test.ts +73 -0
  226. package/src/lib/level-duplication.ts +32 -2
  227. package/src/lib/level-print-export.test.ts +484 -0
  228. package/src/lib/level-print-export.ts +526 -0
  229. package/src/lib/linear-display.test.ts +59 -0
  230. package/src/lib/linear-display.ts +39 -0
  231. package/src/lib/local-guide-image.test.ts +24 -0
  232. package/src/lib/local-guide-image.ts +34 -1
  233. package/src/lib/local-project-presentation-persistence.test.ts +302 -0
  234. package/src/lib/local-project-presentation-persistence.ts +335 -0
  235. package/src/lib/material-paint.ts +2 -0
  236. package/src/lib/model-export.ts +29 -0
  237. package/src/lib/node-action-movement.ts +21 -0
  238. package/src/lib/paint-preview-owner.ts +71 -0
  239. package/src/lib/planar-cursor-placement.test.ts +178 -1
  240. package/src/lib/planar-cursor-placement.ts +61 -5
  241. package/src/lib/plugin-panels.test.ts +92 -1
  242. package/src/lib/plugin-panels.ts +48 -0
  243. package/src/lib/portable-export.test.ts +455 -0
  244. package/src/lib/portable-export.ts +1042 -0
  245. package/src/lib/print-3mf.test.ts +86 -0
  246. package/src/lib/print-3mf.ts +161 -0
  247. package/src/lib/print-content-scope.test.ts +107 -0
  248. package/src/lib/print-content-scope.ts +90 -0
  249. package/src/lib/print-export.test.ts +294 -0
  250. package/src/lib/print-export.ts +735 -0
  251. package/src/lib/print-feature-thickness.test.ts +147 -0
  252. package/src/lib/print-feature-thickness.ts +180 -0
  253. package/src/lib/print-golden-house.test-fixture.ts +352 -0
  254. package/src/lib/print-golden-house.test.ts +249 -0
  255. package/src/lib/print-roof-solids.test.ts +133 -0
  256. package/src/lib/print-roof-solids.ts +630 -0
  257. package/src/lib/print-shell-compiler-baseline.test.ts +419 -0
  258. package/src/lib/print-shell-compiler-baseline.ts +225 -0
  259. package/src/lib/print-shell-compiler-manifold-core.ts +379 -0
  260. package/src/lib/print-shell-compiler-manifold-worker.ts +200 -0
  261. package/src/lib/print-shell-compiler-manifold.worker.ts +20 -0
  262. package/src/lib/print-shell-compiler-mesh-data.ts +32 -0
  263. package/src/lib/print-shell-compiler-protocol.ts +38 -0
  264. package/src/lib/print-shell-compiler.ts +273 -0
  265. package/src/lib/print-wall-solids.test.ts +194 -0
  266. package/src/lib/print-wall-solids.ts +328 -0
  267. package/src/lib/rigid-plan-svg-transform.test.ts +35 -0
  268. package/src/lib/rigid-plan-svg-transform.ts +24 -0
  269. package/src/lib/scene-clipboard.ts +1 -1
  270. package/src/lib/scene.ts +22 -8
  271. package/src/lib/selection-routing.test.ts +105 -1
  272. package/src/lib/selection-routing.ts +19 -0
  273. package/src/lib/send-to-app.test.ts +108 -0
  274. package/src/lib/send-to-app.ts +121 -0
  275. package/src/lib/sfx/movement-tick.ts +37 -0
  276. package/src/lib/sfx-bus.ts +4 -0
  277. package/src/lib/sfx-player.ts +8 -0
  278. package/src/lib/snapping-mode.test.ts +32 -4
  279. package/src/lib/snapping-mode.ts +11 -7
  280. package/src/lib/surface-attachment.ts +77 -0
  281. package/src/lib/units.ts +254 -0
  282. package/src/lib/usdz-export.ts +65 -0
  283. package/src/lib/use-linear-display.ts +8 -32
  284. package/src/lib/walkthrough-pointer-lock.ts +47 -0
  285. package/src/store/live-draft-preview-stores.test.ts +3 -0
  286. package/src/store/terrain-sculpt-mode.test.ts +3 -6
  287. package/src/store/tool-mode.test.ts +87 -0
  288. package/src/store/use-camera-hint-focus.ts +48 -0
  289. package/src/store/use-editor.tsx +299 -188
  290. package/src/store/use-floorplan-draft-preview.ts +9 -0
  291. package/src/store/use-handle-group.ts +4 -0
  292. package/src/store/use-interaction-scope.test.ts +23 -1
  293. package/src/store/use-interaction-scope.ts +63 -2
  294. package/src/store/use-placement-preview.ts +56 -3
  295. package/src/store/use-send-to-app.ts +21 -0
  296. package/src/components/tools/roof/roof-tool.tsx +0 -656
  297. package/src/components/ui/helpers/roof-helper.tsx +0 -14
@@ -0,0 +1,266 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type AnyNode,
5
+ type AnyNodeId,
6
+ nodeRegistry,
7
+ type ParamField,
8
+ type ParametricDescriptor,
9
+ useScene,
10
+ } from '@pascal-app/core'
11
+ import { useViewer } from '@pascal-app/viewer'
12
+ import { useCallback, useMemo } from 'react'
13
+ import { useShallow } from 'zustand/react/shallow'
14
+ import { selectionMatchesSessionGroup } from '../../../lib/session-groups'
15
+ import useSessionGroups from '../../../store/use-session-groups'
16
+ import { PanelSection } from '../controls/panel-section'
17
+ import { SliderControl } from '../controls/slider-control'
18
+ import { resolveUniqueSelectionIds } from './homogeneous-selection'
19
+ import {
20
+ commitMultiNodeFields,
21
+ fieldVisibleForAll,
22
+ firstNumericFieldValue,
23
+ firstVec3FieldValue,
24
+ previewMultiNodeFields,
25
+ reduceFieldValue,
26
+ } from './multi-field-value'
27
+ import { MultiHeightModeField } from './multi-height-mode'
28
+ import { MultiSelectionActions } from './multi-selection-panel'
29
+ import { getTypeDisplay } from './node-display'
30
+ import { ParametricFieldControl } from './parametric-field-control'
31
+ import { PanelWrapper } from './panel-wrapper'
32
+ import { formatSelectionBreakdown } from './selection-breakdown'
33
+
34
+ export function MultiParametricInspector({ footer }: { footer?: React.ReactNode }) {
35
+ const selectedIds = useViewer((s) => s.selection.selectedIds)
36
+ const setSelection = useViewer((s) => s.setSelection)
37
+ const nodeIds = useScene(
38
+ useShallow((s) => resolveUniqueSelectionIds(selectedIds, s.nodes)),
39
+ )
40
+ const nodeType = useScene((s) => {
41
+ const first = nodeIds[0] ? s.nodes[nodeIds[0]] : undefined
42
+ return first?.type ?? null
43
+ })
44
+ const breakdown = useScene((s) =>
45
+ formatSelectionBreakdown(nodeIds.map((id) => s.nodes[id]?.type)),
46
+ )
47
+ const sessionGroups = useSessionGroups((s) => s.groups)
48
+ const matchedGroup = useMemo(
49
+ () => selectionMatchesSessionGroup(sessionGroups, selectedIds),
50
+ [sessionGroups, selectedIds],
51
+ )
52
+
53
+ const def = nodeType ? nodeRegistry.get(nodeType) : undefined
54
+ const parametrics = def?.parametrics as ParametricDescriptor<AnyNode> | undefined
55
+
56
+ const handleClose = useCallback(() => {
57
+ setSelection({ selectedIds: [] })
58
+ }, [setSelection])
59
+
60
+ if (nodeIds.length < 2 || !nodeType || !parametrics) return null
61
+
62
+ const display = getTypeDisplay(nodeType)
63
+ const title = matchedGroup ? `${matchedGroup.label} · ${breakdown}` : breakdown || display.label
64
+
65
+ return (
66
+ <PanelWrapper footer={footer} icon={display.icon} onClose={handleClose} title={title} width={320}>
67
+ {matchedGroup && (
68
+ <div className="border-border/50 border-b px-3 py-2 text-muted-foreground text-xs">
69
+ {matchedGroup.label} (session only). Plain click reselects all members. Not saved with the
70
+ project.
71
+ </div>
72
+ )}
73
+ {parametrics.groups.map((group, gi) => (
74
+ <MultiGroupFields
75
+ defaultExpanded={group.defaultExpanded}
76
+ fields={group.fields as ParamField<AnyNode>[]}
77
+ key={`${nodeType}-${group.label}-${gi}`}
78
+ nodeIds={nodeIds}
79
+ nodeType={nodeType}
80
+ parametrics={parametrics}
81
+ title={group.label}
82
+ />
83
+ ))}
84
+ <div className="border-border/50 border-t p-3">
85
+ <MultiSelectionActions />
86
+ </div>
87
+ </PanelWrapper>
88
+ )
89
+ }
90
+
91
+ function MultiGroupFields({
92
+ defaultExpanded,
93
+ title,
94
+ fields,
95
+ nodeIds,
96
+ nodeType,
97
+ parametrics,
98
+ }: {
99
+ defaultExpanded?: boolean
100
+ title: string
101
+ fields: ParamField<AnyNode>[]
102
+ nodeIds: AnyNodeId[]
103
+ nodeType: AnyNode['type']
104
+ parametrics: ParametricDescriptor<AnyNode>
105
+ }) {
106
+ const genericFields = fields.filter((field) => field.kind !== 'custom')
107
+ const anyVisible = useScene((s) =>
108
+ genericFields.some((field) =>
109
+ fieldVisibleForAll(nodeIds, (field as { visibleIf?: (n: AnyNode) => boolean }).visibleIf, s.nodes),
110
+ ),
111
+ )
112
+ if (genericFields.length === 0 || !anyVisible) return null
113
+ return (
114
+ <PanelSection defaultExpanded={defaultExpanded} title={title}>
115
+ {genericFields.map((field, fi) => {
116
+ if (
117
+ String(field.key) === 'height' &&
118
+ (nodeType === 'wall' || nodeType === 'ceiling') &&
119
+ field.kind === 'number'
120
+ ) {
121
+ return (
122
+ <MultiHeightModeField
123
+ key={`field-${fi}-height-mode`}
124
+ max={field.max}
125
+ min={field.min}
126
+ nodeIds={nodeIds}
127
+ nodeType={nodeType}
128
+ parametrics={parametrics}
129
+ step={field.step}
130
+ />
131
+ )
132
+ }
133
+ return (
134
+ <MultiFieldRenderer
135
+ field={field}
136
+ key={`field-${fi}-${String(field.key)}`}
137
+ nodeIds={nodeIds}
138
+ parametrics={parametrics}
139
+ />
140
+ )
141
+ })}
142
+ </PanelSection>
143
+ )
144
+ }
145
+
146
+ function MultiFieldRenderer({
147
+ field,
148
+ nodeIds,
149
+ parametrics,
150
+ }: {
151
+ field: ParamField<AnyNode>
152
+ nodeIds: AnyNodeId[]
153
+ parametrics: ParametricDescriptor<AnyNode>
154
+ }) {
155
+ const key = String(field.key)
156
+ const visible = useScene((s) =>
157
+ fieldVisibleForAll(nodeIds, (field as { visibleIf?: (n: AnyNode) => boolean }).visibleIf, s.nodes),
158
+ )
159
+ const reduced = useScene(useShallow((s) => reduceFieldValue(nodeIds, key, s.nodes)))
160
+ const numericOrigin = useScene((s) => firstNumericFieldValue(nodeIds, key, s.nodes))
161
+ const vecOrigin = useScene(useShallow((s) => firstVec3FieldValue(nodeIds, key, s.nodes)))
162
+
163
+ const preview = useCallback(
164
+ (patch: Partial<AnyNode>) => {
165
+ previewMultiNodeFields(nodeIds.map((id) => [id, patch] as const))
166
+ },
167
+ [nodeIds],
168
+ )
169
+ const commit = useCallback(
170
+ (patch: Partial<AnyNode>) => {
171
+ commitMultiNodeFields(nodeIds, () => patch, parametrics)
172
+ },
173
+ [nodeIds, parametrics],
174
+ )
175
+
176
+ if (!visible) return null
177
+
178
+ if (field.kind === 'vec3') {
179
+ return (
180
+ <MultiVec3Field
181
+ fieldKey={key}
182
+ mixed={reduced.kind === 'mixed'}
183
+ nodeIds={nodeIds}
184
+ origin={vecOrigin}
185
+ parametrics={parametrics}
186
+ value={reduced.kind === 'same' && Array.isArray(reduced.value) ? (reduced.value as [number, number, number]) : vecOrigin}
187
+ />
188
+ )
189
+ }
190
+
191
+ const value =
192
+ reduced.kind === 'same' ? reduced.value : field.kind === 'number' ? numericOrigin : undefined
193
+
194
+ return (
195
+ <ParametricFieldControl
196
+ field={field}
197
+ mixed={reduced.kind === 'mixed'}
198
+ onChange={field.kind === 'number' ? preview : commit}
199
+ onCommit={field.kind === 'number' ? commit : undefined}
200
+ value={value}
201
+ />
202
+ )
203
+ }
204
+
205
+ function MultiVec3Field({
206
+ fieldKey,
207
+ mixed,
208
+ nodeIds,
209
+ origin,
210
+ parametrics,
211
+ value,
212
+ }: {
213
+ fieldKey: string
214
+ mixed: boolean
215
+ nodeIds: AnyNodeId[]
216
+ origin: [number, number, number]
217
+ parametrics: ParametricDescriptor<AnyNode>
218
+ value: [number, number, number]
219
+ }) {
220
+ const axes: Array<{ label: string; index: 0 | 1 | 2 }> = [
221
+ { label: 'X', index: 0 },
222
+ { label: 'Y', index: 1 },
223
+ { label: 'Z', index: 2 },
224
+ ]
225
+ return (
226
+ <>
227
+ {axes.map(({ label, index }) => {
228
+ const axisValue = value[index] ?? origin[index] ?? 0
229
+ const patchAxis = (next: number): Array<readonly [AnyNodeId, Partial<AnyNode>]> => {
230
+ const nodes = useScene.getState().nodes
231
+ return nodeIds.flatMap((id) => {
232
+ const node = nodes[id]
233
+ if (!node) return []
234
+ const current = (node as Record<string, unknown>)[fieldKey]
235
+ const nextVec = (
236
+ Array.isArray(current) && current.length >= 3 ? [...current] : [...origin]
237
+ ) as [number, number, number]
238
+ nextVec[index] = next
239
+ return [[id, { [fieldKey]: nextVec } as Partial<AnyNode>] as const]
240
+ })
241
+ }
242
+ return (
243
+ <SliderControl
244
+ key={`${fieldKey}-${label}`}
245
+ label={label}
246
+ mixed={mixed}
247
+ onChange={(next) => previewMultiNodeFields(patchAxis(next))}
248
+ onCommit={(next) => {
249
+ const entries = patchAxis(next)
250
+ commitMultiNodeFields(
251
+ nodeIds,
252
+ (node) => entries.find(([id]) => id === node.id)?.[1] ?? {},
253
+ parametrics,
254
+ )
255
+ }}
256
+ precision={2}
257
+ restoreOnCommit={false}
258
+ step={0.05}
259
+ unit="m"
260
+ value={axisValue}
261
+ />
262
+ )
263
+ })}
264
+ </>
265
+ )
266
+ }
@@ -18,6 +18,53 @@ import { ActionButton, ActionGroup } from '../controls/action-button'
18
18
  import { PanelWrapper } from './panel-wrapper'
19
19
  import { formatSelectionBreakdown } from './selection-breakdown'
20
20
 
21
+ export function MultiSelectionActions() {
22
+ const selectedIds = useViewer((s) => s.selection.selectedIds)
23
+ const sessionGroups = useSessionGroups((s) => s.groups)
24
+ const sceneNodes = useScene((s) => s.nodes)
25
+ const liveIds = useMemo(() => new Set(Object.keys(sceneNodes)), [sceneNodes])
26
+ const showGroup = useMemo(
27
+ () => canCreateSessionGroup(sessionGroups, selectedIds, liveIds),
28
+ [sessionGroups, selectedIds, liveIds],
29
+ )
30
+ const showUngroup = useMemo(
31
+ () => selectionIntersectsSessionGroup(sessionGroups, selectedIds, liveIds),
32
+ [sessionGroups, selectedIds, liveIds],
33
+ )
34
+
35
+ return (
36
+ <ActionGroup>
37
+ {showGroup && (
38
+ <ActionButton
39
+ icon={<Group className="h-4 w-4" />}
40
+ label="Group"
41
+ onClick={() => groupCurrentSelection()}
42
+ title="Group (Ctrl/Cmd+G)"
43
+ />
44
+ )}
45
+ {showUngroup && (
46
+ <ActionButton
47
+ icon={<Ungroup className="h-4 w-4" />}
48
+ label="Ungroup"
49
+ onClick={() => ungroupCurrentSelection()}
50
+ title="Ungroup (Ctrl/Cmd+Shift+G)"
51
+ />
52
+ )}
53
+ <ActionButton
54
+ icon={<Copy className="h-4 w-4" />}
55
+ label="Duplicate"
56
+ onClick={() => duplicateSelectionAndPickUp()}
57
+ />
58
+ <ActionButton
59
+ className="border-red-500/40 text-red-200 hover:bg-red-500/15"
60
+ icon={<Trash2 className="h-4 w-4 text-red-400" />}
61
+ label="Delete"
62
+ onClick={() => deleteSelection()}
63
+ />
64
+ </ActionGroup>
65
+ )
66
+ }
67
+
21
68
  /**
22
69
  * Docked multi-selection panel. Includes Group / Ungroup for session selection sets.
23
70
  */
@@ -34,14 +81,6 @@ export function MultiSelectionPanel({ footer }: { footer?: React.ReactNode }) {
34
81
  () => selectionMatchesSessionGroup(sessionGroups, selectedIds, liveIds),
35
82
  [sessionGroups, selectedIds, liveIds],
36
83
  )
37
- const showGroup = useMemo(
38
- () => canCreateSessionGroup(sessionGroups, selectedIds, liveIds),
39
- [sessionGroups, selectedIds, liveIds],
40
- )
41
- const showUngroup = useMemo(
42
- () => selectionIntersectsSessionGroup(sessionGroups, selectedIds, liveIds),
43
- [sessionGroups, selectedIds, liveIds],
44
- )
45
84
 
46
85
  return (
47
86
  <PanelWrapper
@@ -63,35 +102,7 @@ export function MultiSelectionPanel({ footer }: { footer?: React.ReactNode }) {
63
102
  </div>
64
103
  )}
65
104
  <div className="border-border/50 border-t p-3">
66
- <ActionGroup>
67
- {showGroup && (
68
- <ActionButton
69
- icon={<Group className="h-4 w-4" />}
70
- label="Group"
71
- onClick={() => groupCurrentSelection()}
72
- title="Group (Ctrl/Cmd+G)"
73
- />
74
- )}
75
- {showUngroup && (
76
- <ActionButton
77
- icon={<Ungroup className="h-4 w-4" />}
78
- label="Ungroup"
79
- onClick={() => ungroupCurrentSelection()}
80
- title="Ungroup (Ctrl/Cmd+Shift+G)"
81
- />
82
- )}
83
- <ActionButton
84
- icon={<Copy className="h-4 w-4" />}
85
- label="Duplicate"
86
- onClick={() => duplicateSelectionAndPickUp()}
87
- />
88
- <ActionButton
89
- className="border-red-500/40 text-red-200 hover:bg-red-500/15"
90
- icon={<Trash2 className="h-4 w-4" />}
91
- label="Delete"
92
- onClick={() => deleteSelection()}
93
- />
94
- </ActionGroup>
105
+ <MultiSelectionActions />
95
106
  </div>
96
107
  </PanelWrapper>
97
108
  )
@@ -23,6 +23,10 @@ const TYPE_DEFAULTS: Record<string, NodeDisplay> = {
23
23
  guide: { icon: '/icons/floorplan.webp', label: 'Guide image' },
24
24
  }
25
25
 
26
+ export function getTypeDisplay(type: string): NodeDisplay {
27
+ return TYPE_DEFAULTS[type] ?? { icon: '/icons/select.webp', label: type }
28
+ }
29
+
26
30
  export function getNodeDisplay(node: AnyNode | null | undefined): NodeDisplay {
27
31
  if (!node) return { icon: '/icons/select.webp', label: 'Selection' }
28
32
  const fallback = TYPE_DEFAULTS[node.type] ?? { icon: '/icons/select.webp', label: node.type }
@@ -24,15 +24,20 @@ import {
24
24
  import { useViewer } from '@pascal-app/viewer'
25
25
  import { useCallback, useEffect, useState } from 'react'
26
26
  import { useIsMobile } from '../../../hooks/use-mobile'
27
+ import { shouldShowEditingControls } from '../../../lib/interaction/overlay-policy'
27
28
  import { sfxEmitter } from '../../../lib/sfx-bus'
28
29
  import useEditor from '../../../store/use-editor'
30
+ import { deleteSelection, duplicateSelectionAndPickUp, startGroupPickUp } from '../../editor/group-actions'
31
+ import { resolveHomogeneousSelection } from './homogeneous-selection'
29
32
  import { MobilePanelSheet } from './mobile-panel-sheet'
30
33
  import { MobileSelectionBar } from './mobile-selection-bar'
34
+ import { MultiParametricInspector } from './multi-parametric-inspector'
31
35
  import { MultiSelectionPanel } from './multi-selection-panel'
32
- import { getNodeDisplay } from './node-display'
36
+ import { getNodeDisplay, getTypeDisplay } from './node-display'
33
37
  import { resetDesktopInspectorCollapsed } from './panel-wrapper'
34
38
  import { ParametricInspector } from './parametric-inspector'
35
39
  import { ReferencePanel } from './reference-panel'
40
+ import { formatSelectionBreakdown } from './selection-breakdown'
36
41
 
37
42
  type MovableNode =
38
43
  | ItemNode
@@ -169,6 +174,46 @@ function MobilePanelLayer({
169
174
  )
170
175
  }
171
176
 
177
+ function MobileMultiPanelLayer({
178
+ breakdown,
179
+ panel,
180
+ type,
181
+ }: {
182
+ breakdown: string
183
+ panel: React.ReactNode
184
+ type: string | null
185
+ }) {
186
+ const [isSheetOpen, setIsSheetOpen] = useState(false)
187
+ const display = type ? getTypeDisplay(type) : { icon: '/icons/select.webp', label: 'Selection' }
188
+ const title = breakdown || display.label
189
+
190
+ useEffect(() => {
191
+ setIsSheetOpen(false)
192
+ }, [breakdown])
193
+
194
+ return (
195
+ <>
196
+ <MobileSelectionBar
197
+ icon={display.icon}
198
+ label={title}
199
+ node={null}
200
+ onDelete={() => deleteSelection()}
201
+ onDuplicate={() => duplicateSelectionAndPickUp()}
202
+ onEdit={() => setIsSheetOpen((v) => !v)}
203
+ onMove={() => startGroupPickUp()}
204
+ />
205
+ <MobilePanelSheet
206
+ icon={display.icon}
207
+ onClose={() => setIsSheetOpen(false)}
208
+ open={isSheetOpen}
209
+ title={title}
210
+ >
211
+ {panel}
212
+ </MobilePanelSheet>
213
+ </>
214
+ )
215
+ }
216
+
172
217
  export function PanelManager({
173
218
  inspectorFooter,
174
219
  multiSelectionFooter,
@@ -181,6 +226,7 @@ export function PanelManager({
181
226
  const selectedZoneId = useViewer((s) => s.selection.zoneId)
182
227
  const setSelection = useViewer((s) => s.setSelection)
183
228
  const selectedReferenceId = useEditor((s) => s.selectedReferenceId)
229
+ const readOnly = useScene((s) => s.readOnly)
184
230
  // Only subscribe to the *type* of the single-selected node — string primitive
185
231
  // so we don't re-render on unrelated scene mutations.
186
232
  const selectedNodeType = useScene((s) => {
@@ -193,6 +239,14 @@ export function PanelManager({
193
239
  const id = selectedIds[0]
194
240
  return id ? (s.nodes[id as AnyNodeId] ?? null) : null
195
241
  })
242
+ const homogeneousType = useScene((s) =>
243
+ selectedIds.length > 1 ? resolveHomogeneousSelection(selectedIds, s.nodes) : null,
244
+ )
245
+ const multiBreakdown = useScene((s) =>
246
+ selectedIds.length > 1
247
+ ? formatSelectionBreakdown(selectedIds.map((id) => s.nodes[id as AnyNodeId]?.type))
248
+ : '',
249
+ )
196
250
 
197
251
  // Node and reference selection are mutually exclusive: selecting a guide
198
252
  // clears the node selection (handleGuideSelect), but node selection never
@@ -215,10 +269,27 @@ export function PanelManager({
215
269
  }
216
270
  }, [hasAnySelection])
217
271
 
272
+ if (!shouldShowEditingControls(readOnly)) return null
273
+
218
274
  if (isMobile) {
219
275
  if (selectedReferenceId) {
220
276
  return <MobilePanelLayer isReference={true} node={null} panel={<ReferencePanel />} />
221
277
  }
278
+ if (selectedIds.length > 1) {
279
+ return (
280
+ <MobileMultiPanelLayer
281
+ breakdown={multiBreakdown}
282
+ panel={
283
+ homogeneousType ? (
284
+ <MultiParametricInspector footer={multiSelectionFooter} />
285
+ ) : (
286
+ <MultiSelectionPanel footer={multiSelectionFooter} />
287
+ )
288
+ }
289
+ type={homogeneousType}
290
+ />
291
+ )
292
+ }
222
293
  return (
223
294
  <MobilePanelLayer
224
295
  isReference={false}
@@ -244,9 +315,12 @@ export function PanelManager({
244
315
  )
245
316
  }
246
317
 
247
- // Multi-selection: compact docked panel (desktop only — the mobile branch
248
- // above keeps today's behavior and renders nothing for multi-selections).
318
+ // Multi-selection: parametric inspector when every resolved id shares a type,
319
+ // otherwise the actions-only panel. Mobile uses the same panels in a sheet.
249
320
  if (selectedIds.length > 1) {
321
+ if (homogeneousType) {
322
+ return <MultiParametricInspector footer={multiSelectionFooter} />
323
+ }
250
324
  return <MultiSelectionPanel footer={multiSelectionFooter} />
251
325
  }
252
326
 
@@ -0,0 +1,173 @@
1
+ 'use client'
2
+
3
+ import type { AnyNode, ParamField } from '@pascal-app/core'
4
+ import { SegmentedControl } from '../controls/segmented-control'
5
+ import { SliderControl } from '../controls/slider-control'
6
+ import { ToggleControl } from '../controls/toggle-control'
7
+ import { precisionForStep, prettifyEnumValue, prettifyKey } from './parametric-field-utils'
8
+
9
+ interface ParametricFieldControlProps {
10
+ field: ParamField<AnyNode>
11
+ value: unknown
12
+ mixed?: boolean
13
+ onChange: (patch: Partial<AnyNode>) => void
14
+ onCommit?: (patch: Partial<AnyNode>) => void
15
+ }
16
+
17
+ export function ParametricFieldControl({
18
+ field,
19
+ value,
20
+ mixed = false,
21
+ onChange,
22
+ onCommit,
23
+ }: ParametricFieldControlProps) {
24
+ const key = String(field.key)
25
+
26
+ switch (field.kind) {
27
+ case 'number': {
28
+ const num = typeof value === 'number' ? value : 0
29
+ const step = field.step ?? 0.01
30
+ const precision = precisionForStep(step)
31
+ return (
32
+ <SliderControl
33
+ label={prettifyKey(key)}
34
+ max={field.max}
35
+ min={field.min}
36
+ mixed={mixed}
37
+ onChange={(next) => onChange({ [key]: next } as Partial<AnyNode>)}
38
+ onCommit={onCommit ? (next) => onCommit({ [key]: next } as Partial<AnyNode>) : undefined}
39
+ precision={precision}
40
+ restoreOnCommit={!onCommit}
41
+ step={step}
42
+ unit={field.unit ?? ''}
43
+ value={num}
44
+ />
45
+ )
46
+ }
47
+
48
+ case 'boolean': {
49
+ const checked = !mixed && value === true
50
+ return (
51
+ <ToggleControl
52
+ checked={checked}
53
+ label={prettifyKey(key)}
54
+ mixed={mixed}
55
+ onChange={(next) => {
56
+ const patch = { [key]: next } as Partial<AnyNode>
57
+ onChange(patch)
58
+ onCommit?.(patch)
59
+ }}
60
+ />
61
+ )
62
+ }
63
+
64
+ case 'enum': {
65
+ const str = typeof value === 'string' ? value : (field.options[0] ?? '')
66
+ const apply = (next: string) => {
67
+ const patch = { [key]: next } as Partial<AnyNode>
68
+ onChange(patch)
69
+ onCommit?.(patch)
70
+ }
71
+ if (field.display === 'segmented') {
72
+ return (
73
+ <SegmentedControl
74
+ mixed={mixed}
75
+ onChange={apply}
76
+ options={field.options.map((opt) => ({ label: prettifyEnumValue(opt), value: opt }))}
77
+ value={str}
78
+ />
79
+ )
80
+ }
81
+ return (
82
+ <div className="flex items-center justify-between px-3 py-2">
83
+ <span className="text-foreground/80 text-xs">{prettifyKey(key)}</span>
84
+ <select
85
+ className="rounded-md border border-border/50 bg-[#2C2C2E] px-2 py-1 text-foreground text-xs focus:outline-none focus:ring-1 focus:ring-foreground/30"
86
+ onChange={(e) => apply(e.target.value)}
87
+ value={mixed ? '' : str}
88
+ >
89
+ {mixed && (
90
+ <option disabled value="">
91
+ Mixed
92
+ </option>
93
+ )}
94
+ {field.options.map((opt) => (
95
+ <option key={opt} value={opt}>
96
+ {prettifyEnumValue(opt)}
97
+ </option>
98
+ ))}
99
+ </select>
100
+ </div>
101
+ )
102
+ }
103
+
104
+ case 'color': {
105
+ const str = typeof value === 'string' ? value : '#888888'
106
+ const apply = (next: string) => {
107
+ const patch = { [key]: next } as Partial<AnyNode>
108
+ onChange(patch)
109
+ onCommit?.(patch)
110
+ }
111
+ return (
112
+ <div className="flex items-center justify-between px-3 py-2">
113
+ <span className="text-foreground/80 text-xs">{prettifyKey(key)}</span>
114
+ <div className="flex items-center gap-2">
115
+ <input
116
+ className="h-6 w-8 cursor-pointer rounded border border-border/50 bg-transparent"
117
+ onChange={(e) => apply(e.target.value)}
118
+ type="color"
119
+ value={str}
120
+ />
121
+ <input
122
+ className="w-20 rounded-md border border-border/50 bg-[#2C2C2E] px-2 py-1 text-foreground text-xs focus:outline-none focus:ring-1 focus:ring-foreground/30"
123
+ onChange={(e) => apply(e.target.value)}
124
+ type="text"
125
+ value={mixed ? 'Mixed' : str}
126
+ />
127
+ </div>
128
+ </div>
129
+ )
130
+ }
131
+
132
+ case 'vec3': {
133
+ const v =
134
+ Array.isArray(value) && value.length >= 3
135
+ ? (value as [number, number, number])
136
+ : ([0, 0, 0] as [number, number, number])
137
+ const axes: Array<{ label: string; index: 0 | 1 | 2 }> = [
138
+ { label: 'X', index: 0 },
139
+ { label: 'Y', index: 1 },
140
+ { label: 'Z', index: 2 },
141
+ ]
142
+ return (
143
+ <>
144
+ {axes.map(({ label, index }) => {
145
+ const axisValue = v[index] ?? 0
146
+ const apply = (next: number): Partial<AnyNode> => {
147
+ const updated = [...v] as [number, number, number]
148
+ updated[index] = next
149
+ return { [key]: updated } as Partial<AnyNode>
150
+ }
151
+ return (
152
+ <SliderControl
153
+ key={`${key}-${label}`}
154
+ label={label}
155
+ mixed={mixed}
156
+ onChange={(next) => onChange(apply(next))}
157
+ onCommit={onCommit ? (next) => onCommit(apply(next)) : undefined}
158
+ precision={2}
159
+ restoreOnCommit={!onCommit}
160
+ step={0.05}
161
+ unit="m"
162
+ value={axisValue}
163
+ />
164
+ )
165
+ })}
166
+ </>
167
+ )
168
+ }
169
+
170
+ default:
171
+ return null
172
+ }
173
+ }