@pascal-app/editor 0.4.0

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 (165) hide show
  1. package/package.json +62 -0
  2. package/src/components/editor/custom-camera-controls.tsx +387 -0
  3. package/src/components/editor/editor-layout-v2.tsx +220 -0
  4. package/src/components/editor/export-manager.tsx +78 -0
  5. package/src/components/editor/first-person-controls.tsx +249 -0
  6. package/src/components/editor/floating-action-menu.tsx +231 -0
  7. package/src/components/editor/floorplan-panel.tsx +9609 -0
  8. package/src/components/editor/grid.tsx +161 -0
  9. package/src/components/editor/index.tsx +928 -0
  10. package/src/components/editor/node-action-menu.tsx +66 -0
  11. package/src/components/editor/preset-thumbnail-generator.tsx +125 -0
  12. package/src/components/editor/selection-manager.tsx +897 -0
  13. package/src/components/editor/site-edge-labels.tsx +90 -0
  14. package/src/components/editor/thumbnail-generator.tsx +166 -0
  15. package/src/components/editor/wall-measurement-label.tsx +258 -0
  16. package/src/components/feedback-dialog.tsx +265 -0
  17. package/src/components/pascal-radio.tsx +280 -0
  18. package/src/components/preview-button.tsx +16 -0
  19. package/src/components/systems/ceiling/ceiling-system.tsx +77 -0
  20. package/src/components/systems/roof/roof-edit-system.tsx +69 -0
  21. package/src/components/systems/stair/stair-edit-system.tsx +69 -0
  22. package/src/components/systems/zone/zone-label-editor-system.tsx +320 -0
  23. package/src/components/systems/zone/zone-system.tsx +87 -0
  24. package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +42 -0
  25. package/src/components/tools/ceiling/ceiling-hole-editor.tsx +47 -0
  26. package/src/components/tools/ceiling/ceiling-tool.tsx +465 -0
  27. package/src/components/tools/door/door-math.ts +110 -0
  28. package/src/components/tools/door/door-tool.tsx +293 -0
  29. package/src/components/tools/door/move-door-tool.tsx +373 -0
  30. package/src/components/tools/item/item-tool.tsx +26 -0
  31. package/src/components/tools/item/move-tool.tsx +90 -0
  32. package/src/components/tools/item/placement-math.ts +85 -0
  33. package/src/components/tools/item/placement-strategies.ts +556 -0
  34. package/src/components/tools/item/placement-types.ts +117 -0
  35. package/src/components/tools/item/use-draft-node.ts +227 -0
  36. package/src/components/tools/item/use-placement-coordinator.tsx +877 -0
  37. package/src/components/tools/roof/move-roof-tool.tsx +288 -0
  38. package/src/components/tools/roof/roof-tool.tsx +318 -0
  39. package/src/components/tools/select/box-select-tool.tsx +626 -0
  40. package/src/components/tools/shared/cursor-sphere.tsx +119 -0
  41. package/src/components/tools/shared/polygon-editor.tsx +361 -0
  42. package/src/components/tools/site/site-boundary-editor.tsx +42 -0
  43. package/src/components/tools/slab/slab-boundary-editor.tsx +42 -0
  44. package/src/components/tools/slab/slab-hole-editor.tsx +47 -0
  45. package/src/components/tools/slab/slab-tool.tsx +322 -0
  46. package/src/components/tools/stair/stair-defaults.ts +7 -0
  47. package/src/components/tools/stair/stair-tool.tsx +194 -0
  48. package/src/components/tools/tool-manager.tsx +120 -0
  49. package/src/components/tools/wall/wall-drafting.ts +140 -0
  50. package/src/components/tools/wall/wall-tool.tsx +210 -0
  51. package/src/components/tools/window/move-window-tool.tsx +410 -0
  52. package/src/components/tools/window/window-math.ts +117 -0
  53. package/src/components/tools/window/window-tool.tsx +303 -0
  54. package/src/components/tools/zone/zone-boundary-editor.tsx +39 -0
  55. package/src/components/tools/zone/zone-tool.tsx +364 -0
  56. package/src/components/ui/action-menu/action-button.tsx +59 -0
  57. package/src/components/ui/action-menu/camera-actions.tsx +74 -0
  58. package/src/components/ui/action-menu/control-modes.tsx +240 -0
  59. package/src/components/ui/action-menu/furnish-tools.tsx +102 -0
  60. package/src/components/ui/action-menu/index.tsx +152 -0
  61. package/src/components/ui/action-menu/structure-tools.tsx +100 -0
  62. package/src/components/ui/action-menu/view-toggles.tsx +397 -0
  63. package/src/components/ui/command-palette/editor-commands.tsx +396 -0
  64. package/src/components/ui/command-palette/index.tsx +730 -0
  65. package/src/components/ui/controls/action-button.tsx +33 -0
  66. package/src/components/ui/controls/material-picker.tsx +194 -0
  67. package/src/components/ui/controls/metric-control.tsx +262 -0
  68. package/src/components/ui/controls/panel-section.tsx +65 -0
  69. package/src/components/ui/controls/segmented-control.tsx +45 -0
  70. package/src/components/ui/controls/slider-control.tsx +245 -0
  71. package/src/components/ui/controls/toggle-control.tsx +38 -0
  72. package/src/components/ui/floating-level-selector.tsx +355 -0
  73. package/src/components/ui/helpers/ceiling-helper.tsx +20 -0
  74. package/src/components/ui/helpers/helper-manager.tsx +33 -0
  75. package/src/components/ui/helpers/item-helper.tsx +40 -0
  76. package/src/components/ui/helpers/roof-helper.tsx +16 -0
  77. package/src/components/ui/helpers/slab-helper.tsx +20 -0
  78. package/src/components/ui/helpers/wall-helper.tsx +20 -0
  79. package/src/components/ui/item-catalog/catalog-items.tsx +1580 -0
  80. package/src/components/ui/item-catalog/item-catalog.tsx +219 -0
  81. package/src/components/ui/panels/ceiling-panel.tsx +230 -0
  82. package/src/components/ui/panels/collections/collections-popover.tsx +356 -0
  83. package/src/components/ui/panels/door-panel.tsx +600 -0
  84. package/src/components/ui/panels/item-panel.tsx +306 -0
  85. package/src/components/ui/panels/panel-manager.tsx +59 -0
  86. package/src/components/ui/panels/panel-wrapper.tsx +80 -0
  87. package/src/components/ui/panels/presets/presets-popover.tsx +511 -0
  88. package/src/components/ui/panels/reference-panel.tsx +177 -0
  89. package/src/components/ui/panels/roof-panel.tsx +262 -0
  90. package/src/components/ui/panels/roof-segment-panel.tsx +326 -0
  91. package/src/components/ui/panels/slab-panel.tsx +228 -0
  92. package/src/components/ui/panels/stair-panel.tsx +304 -0
  93. package/src/components/ui/panels/stair-segment-panel.tsx +339 -0
  94. package/src/components/ui/panels/wall-panel.tsx +123 -0
  95. package/src/components/ui/panels/window-panel.tsx +441 -0
  96. package/src/components/ui/primitives/button.tsx +69 -0
  97. package/src/components/ui/primitives/card.tsx +75 -0
  98. package/src/components/ui/primitives/color-dot.tsx +61 -0
  99. package/src/components/ui/primitives/context-menu.tsx +227 -0
  100. package/src/components/ui/primitives/dialog.tsx +129 -0
  101. package/src/components/ui/primitives/dropdown-menu.tsx +228 -0
  102. package/src/components/ui/primitives/error-boundary.tsx +52 -0
  103. package/src/components/ui/primitives/input.tsx +21 -0
  104. package/src/components/ui/primitives/number-input.tsx +187 -0
  105. package/src/components/ui/primitives/opacity-control.tsx +79 -0
  106. package/src/components/ui/primitives/popover.tsx +42 -0
  107. package/src/components/ui/primitives/separator.tsx +28 -0
  108. package/src/components/ui/primitives/sheet.tsx +130 -0
  109. package/src/components/ui/primitives/shortcut-token.tsx +64 -0
  110. package/src/components/ui/primitives/sidebar.tsx +855 -0
  111. package/src/components/ui/primitives/skeleton.tsx +13 -0
  112. package/src/components/ui/primitives/slider.tsx +58 -0
  113. package/src/components/ui/primitives/switch.tsx +29 -0
  114. package/src/components/ui/primitives/tooltip.tsx +57 -0
  115. package/src/components/ui/scene-loader.tsx +40 -0
  116. package/src/components/ui/sidebar/app-sidebar.tsx +103 -0
  117. package/src/components/ui/sidebar/icon-rail.tsx +147 -0
  118. package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +100 -0
  119. package/src/components/ui/sidebar/panels/settings-panel/index.tsx +438 -0
  120. package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +188 -0
  121. package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +80 -0
  122. package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +126 -0
  123. package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +64 -0
  124. package/src/components/ui/sidebar/panels/site-panel/index.tsx +1543 -0
  125. package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +98 -0
  126. package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +117 -0
  127. package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +65 -0
  128. package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +214 -0
  129. package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +96 -0
  130. package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +216 -0
  131. package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +115 -0
  132. package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +342 -0
  133. package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +271 -0
  134. package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +106 -0
  135. package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +64 -0
  136. package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +87 -0
  137. package/src/components/ui/sidebar/panels/zone-panel/index.tsx +167 -0
  138. package/src/components/ui/sidebar/tab-bar.tsx +39 -0
  139. package/src/components/ui/slider-demo.tsx +36 -0
  140. package/src/components/ui/slider.tsx +81 -0
  141. package/src/components/ui/viewer-toolbar.tsx +342 -0
  142. package/src/components/viewer-overlay.tsx +499 -0
  143. package/src/components/viewer-zone-system.tsx +48 -0
  144. package/src/contexts/presets-context.tsx +121 -0
  145. package/src/hooks/use-auto-save.ts +194 -0
  146. package/src/hooks/use-contextual-tools.ts +52 -0
  147. package/src/hooks/use-grid-events.ts +106 -0
  148. package/src/hooks/use-keyboard.ts +214 -0
  149. package/src/hooks/use-mobile.ts +19 -0
  150. package/src/hooks/use-reduced-motion.ts +20 -0
  151. package/src/index.tsx +33 -0
  152. package/src/lib/constants.ts +3 -0
  153. package/src/lib/level-selection.ts +31 -0
  154. package/src/lib/scene.ts +394 -0
  155. package/src/lib/sfx/index.ts +2 -0
  156. package/src/lib/sfx-bus.ts +49 -0
  157. package/src/lib/sfx-player.ts +60 -0
  158. package/src/lib/utils.ts +43 -0
  159. package/src/store/use-audio.tsx +45 -0
  160. package/src/store/use-command-registry.ts +36 -0
  161. package/src/store/use-editor.tsx +522 -0
  162. package/src/store/use-palette-view-registry.ts +45 -0
  163. package/src/store/use-upload.ts +90 -0
  164. package/src/three-types.ts +3 -0
  165. package/tsconfig.json +9 -0
@@ -0,0 +1,356 @@
1
+ 'use client'
2
+
3
+ import type { AnyNodeId, Collection, CollectionId } from '@pascal-app/core'
4
+ import { useScene } from '@pascal-app/core'
5
+ import {
6
+ Check,
7
+ ChevronDown,
8
+ ChevronRight,
9
+ Layers,
10
+ MoreHorizontal,
11
+ Pencil,
12
+ Plus,
13
+ Trash2,
14
+ X,
15
+ } from 'lucide-react'
16
+ import { useState } from 'react'
17
+ import { ColorDot } from '../../../../components/ui/primitives/color-dot'
18
+ import {
19
+ DropdownMenu,
20
+ DropdownMenuContent,
21
+ DropdownMenuItem,
22
+ DropdownMenuTrigger,
23
+ } from '../../../../components/ui/primitives/dropdown-menu'
24
+ import {
25
+ Popover,
26
+ PopoverContent,
27
+ PopoverTrigger,
28
+ } from '../../../../components/ui/primitives/popover'
29
+ import { cn } from '../../../../lib/utils'
30
+
31
+ interface CollectionsPopoverProps {
32
+ nodeId: AnyNodeId
33
+ collectionIds?: CollectionId[]
34
+ children: React.ReactNode
35
+ }
36
+
37
+ export function CollectionsPopover({ nodeId, collectionIds, children }: CollectionsPopoverProps) {
38
+ const collections = useScene((s) => s.collections)
39
+ const nodes = useScene((s) => s.nodes)
40
+ const createCollection = useScene((s) => s.createCollection)
41
+ const deleteCollection = useScene((s) => s.deleteCollection)
42
+ const updateCollection = useScene((s) => s.updateCollection)
43
+ const addToCollection = useScene((s) => s.addToCollection)
44
+ const removeFromCollection = useScene((s) => s.removeFromCollection)
45
+
46
+ const [open, setOpen] = useState(false)
47
+ const [showCreateInput, setShowCreateInput] = useState(false)
48
+ const [createName, setCreateName] = useState('')
49
+
50
+ const [renamingId, setRenamingId] = useState<CollectionId | null>(null)
51
+ const [renameValue, setRenameValue] = useState('')
52
+ const [renameColor, setRenameColor] = useState('')
53
+
54
+ const [deletingId, setDeletingId] = useState<CollectionId | null>(null)
55
+ const [expandedIds, setExpandedIds] = useState<Set<CollectionId>>(new Set())
56
+
57
+ const memberIds = collectionIds ?? []
58
+ const allCollections = Object.values(collections)
59
+
60
+ const handleCreate = () => {
61
+ if (!createName.trim()) return
62
+ createCollection(createName.trim(), [nodeId])
63
+ setCreateName('')
64
+ setShowCreateInput(false)
65
+ }
66
+
67
+ const handleRenameConfirm = (id: CollectionId) => {
68
+ if (!renameValue.trim()) return
69
+ updateCollection(id, { name: renameValue.trim(), color: renameColor || undefined })
70
+ setRenamingId(null)
71
+ }
72
+
73
+ const toggleMembership = (collectionId: CollectionId) => {
74
+ if (memberIds.includes(collectionId)) {
75
+ removeFromCollection(collectionId, nodeId)
76
+ } else {
77
+ addToCollection(collectionId, nodeId)
78
+ }
79
+ }
80
+
81
+ const toggleExpand = (collectionId: CollectionId) => {
82
+ setExpandedIds((prev) => {
83
+ const next = new Set(prev)
84
+ if (next.has(collectionId)) next.delete(collectionId)
85
+ else next.add(collectionId)
86
+ return next
87
+ })
88
+ }
89
+
90
+ return (
91
+ <Popover onOpenChange={setOpen} open={open}>
92
+ <PopoverTrigger asChild>{children}</PopoverTrigger>
93
+ <PopoverContent
94
+ align="start"
95
+ className="w-72 overflow-hidden rounded-xl border-border/50 bg-sidebar/95 p-0 shadow-2xl backdrop-blur-xl"
96
+ side="left"
97
+ sideOffset={8}
98
+ >
99
+ {/* Header */}
100
+ <div className="flex items-center justify-between border-border/50 border-b px-3 py-2.5">
101
+ <div className="flex items-center gap-1.5">
102
+ <Layers className="h-3.5 w-3.5 text-muted-foreground" />
103
+ <span className="font-semibold text-foreground text-xs tracking-tight">
104
+ Collections
105
+ </span>
106
+ </div>
107
+ <button
108
+ className="flex items-center gap-1 rounded-md px-2 py-1 font-medium text-[11px] text-muted-foreground transition-colors hover:bg-white/10 hover:text-foreground"
109
+ onClick={() => {
110
+ setShowCreateInput((v) => !v)
111
+ setCreateName('')
112
+ }}
113
+ type="button"
114
+ >
115
+ <Plus className="h-3 w-3" />
116
+ New
117
+ </button>
118
+ </div>
119
+
120
+ {/* Create input */}
121
+ {showCreateInput && (
122
+ <div className="flex items-center gap-1.5 border-border/50 border-b bg-white/5 px-3 py-2">
123
+ <input
124
+ autoFocus
125
+ className="min-w-0 flex-1 rounded-md border border-border/50 bg-background/50 px-2 py-1 text-foreground text-xs outline-none placeholder:text-muted-foreground/60 focus:border-ring focus:ring-1 focus:ring-ring/30"
126
+ onChange={(e) => setCreateName(e.target.value)}
127
+ onKeyDown={(e) => {
128
+ if (e.key === 'Enter') handleCreate()
129
+ if (e.key === 'Escape') {
130
+ setShowCreateInput(false)
131
+ setCreateName('')
132
+ }
133
+ }}
134
+ placeholder="Collection name…"
135
+ value={createName}
136
+ />
137
+ <button
138
+ className="flex h-6 w-6 items-center justify-center rounded-md bg-primary/20 text-primary transition-colors hover:bg-primary/30 disabled:opacity-40"
139
+ disabled={!createName.trim()}
140
+ onClick={handleCreate}
141
+ type="button"
142
+ >
143
+ <Check className="h-3.5 w-3.5" />
144
+ </button>
145
+ <button
146
+ className="flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-white/10"
147
+ onClick={() => {
148
+ setShowCreateInput(false)
149
+ setCreateName('')
150
+ }}
151
+ type="button"
152
+ >
153
+ <X className="h-3.5 w-3.5" />
154
+ </button>
155
+ </div>
156
+ )}
157
+
158
+ {/* Collections list */}
159
+ <div className="no-scrollbar max-h-72 overflow-y-auto">
160
+ {allCollections.length === 0 ? (
161
+ <div className="flex flex-col items-center justify-center gap-2 px-4 py-8 text-center">
162
+ <Layers className="h-6 w-6 text-muted-foreground/40" />
163
+ <p className="text-muted-foreground text-xs">
164
+ No collections yet. Create one to group items together.
165
+ </p>
166
+ </div>
167
+ ) : (
168
+ <ul className="divide-y divide-border/30">
169
+ {allCollections.map((collection) => {
170
+ const isIn = memberIds.includes(collection.id)
171
+ const isExpanded = expandedIds.has(collection.id)
172
+ const isRenaming = renamingId === collection.id
173
+ const isDeleting = deletingId === collection.id
174
+
175
+ if (isDeleting) {
176
+ return (
177
+ <li
178
+ className="flex items-center justify-between gap-2 bg-red-500/10 px-3 py-2.5"
179
+ key={collection.id}
180
+ >
181
+ <span className="truncate text-foreground/80 text-xs">
182
+ Delete "{collection.name}"?
183
+ </span>
184
+ <div className="flex shrink-0 items-center gap-1">
185
+ <button
186
+ className="rounded-md bg-red-500/20 px-2 py-0.5 font-medium text-[11px] text-red-400 transition-colors hover:bg-red-500/30"
187
+ onClick={() => {
188
+ deleteCollection(collection.id)
189
+ setDeletingId(null)
190
+ }}
191
+ type="button"
192
+ >
193
+ Delete
194
+ </button>
195
+ <button
196
+ className="rounded-md px-2 py-0.5 font-medium text-[11px] text-muted-foreground transition-colors hover:bg-white/10"
197
+ onClick={() => setDeletingId(null)}
198
+ type="button"
199
+ >
200
+ Cancel
201
+ </button>
202
+ </div>
203
+ </li>
204
+ )
205
+ }
206
+
207
+ if (isRenaming) {
208
+ return (
209
+ <li className="flex items-center gap-1.5 px-3 py-2" key={collection.id}>
210
+ <ColorDot color={renameColor || '#6366f1'} onChange={setRenameColor} />
211
+ <input
212
+ autoFocus
213
+ className="min-w-0 flex-1 rounded-md border border-border/50 bg-background/50 px-2 py-1 text-foreground text-xs outline-none focus:border-ring focus:ring-1 focus:ring-ring/30"
214
+ onChange={(e) => setRenameValue(e.target.value)}
215
+ onKeyDown={(e) => {
216
+ if (e.key === 'Enter') handleRenameConfirm(collection.id)
217
+ if (e.key === 'Escape') setRenamingId(null)
218
+ }}
219
+ value={renameValue}
220
+ />
221
+ <button
222
+ className="flex h-6 w-6 items-center justify-center rounded-md bg-primary/20 text-primary transition-colors hover:bg-primary/30"
223
+ onClick={() => handleRenameConfirm(collection.id)}
224
+ type="button"
225
+ >
226
+ <Check className="h-3.5 w-3.5" />
227
+ </button>
228
+ <button
229
+ className="flex h-6 w-6 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-white/10"
230
+ onClick={() => setRenamingId(null)}
231
+ type="button"
232
+ >
233
+ <X className="h-3.5 w-3.5" />
234
+ </button>
235
+ </li>
236
+ )
237
+ }
238
+
239
+ return (
240
+ <li key={collection.id}>
241
+ <div className="group flex items-center gap-2 px-3 py-2 transition-colors hover:bg-white/5">
242
+ {/* Color dot — click to pick color */}
243
+ <ColorDot
244
+ color={collection.color ?? '#6366f1'}
245
+ onChange={(c) => updateCollection(collection.id, { color: c })}
246
+ />
247
+
248
+ {/* Name + count — clicking toggles membership */}
249
+ <button
250
+ className="flex min-w-0 flex-1 items-center gap-1.5 text-left"
251
+ onClick={() => toggleMembership(collection.id)}
252
+ type="button"
253
+ >
254
+ <span
255
+ className={cn(
256
+ 'truncate font-medium text-xs',
257
+ isIn ? 'text-foreground' : 'text-muted-foreground',
258
+ )}
259
+ >
260
+ {collection.name}
261
+ </span>
262
+ <span className="shrink-0 text-[10px] text-muted-foreground/60">
263
+ {collection.nodeIds.length}
264
+ </span>
265
+ </button>
266
+
267
+ {/* Membership check */}
268
+ <div
269
+ className={cn(
270
+ 'pointer-events-none flex h-4 w-4 shrink-0 items-center justify-center rounded border transition-colors',
271
+ isIn ? 'border-primary bg-primary/20 text-primary' : 'border-border/50',
272
+ )}
273
+ >
274
+ {isIn && <Check className="h-2.5 w-2.5" />}
275
+ </div>
276
+
277
+ {/* Expand toggle (only if has members) */}
278
+ {collection.nodeIds.length > 0 && (
279
+ <button
280
+ className="flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground transition-colors hover:text-foreground"
281
+ onClick={() => toggleExpand(collection.id)}
282
+ type="button"
283
+ >
284
+ {isExpanded ? (
285
+ <ChevronDown className="h-3 w-3" />
286
+ ) : (
287
+ <ChevronRight className="h-3 w-3" />
288
+ )}
289
+ </button>
290
+ )}
291
+
292
+ {/* More dropdown */}
293
+ <DropdownMenu>
294
+ <DropdownMenuTrigger asChild>
295
+ <button
296
+ className="flex h-5 w-5 shrink-0 items-center justify-center rounded text-muted-foreground opacity-0 transition-colors hover:bg-white/10 hover:text-foreground group-hover:opacity-100"
297
+ type="button"
298
+ >
299
+ <MoreHorizontal className="h-3.5 w-3.5" />
300
+ </button>
301
+ </DropdownMenuTrigger>
302
+ <DropdownMenuContent align="start" className="min-w-40" side="left">
303
+ <DropdownMenuItem
304
+ onClick={() => {
305
+ setRenamingId(collection.id)
306
+ setRenameValue(collection.name)
307
+ setRenameColor(collection.color ?? '')
308
+ }}
309
+ >
310
+ <Pencil className="h-3.5 w-3.5" />
311
+ Rename
312
+ </DropdownMenuItem>
313
+ <DropdownMenuItem
314
+ onClick={() => setDeletingId(collection.id)}
315
+ variant="destructive"
316
+ >
317
+ <Trash2 className="h-3.5 w-3.5" />
318
+ Delete
319
+ </DropdownMenuItem>
320
+ </DropdownMenuContent>
321
+ </DropdownMenu>
322
+ </div>
323
+
324
+ {/* Expanded member list */}
325
+ {isExpanded && (
326
+ <ul className="flex flex-col gap-0.5 pr-3 pb-1 pl-6">
327
+ {collection.nodeIds.map((nid) => {
328
+ const n = nodes[nid]
329
+ return (
330
+ <li className="flex items-center gap-1.5 py-0.5" key={nid}>
331
+ <span className="h-1 w-1 shrink-0 rounded-full bg-muted-foreground/40" />
332
+ <span
333
+ className={cn(
334
+ 'truncate text-[11px]',
335
+ nid === nodeId
336
+ ? 'font-medium text-foreground'
337
+ : 'text-muted-foreground',
338
+ )}
339
+ >
340
+ {n?.name ?? nid}
341
+ </span>
342
+ </li>
343
+ )
344
+ })}
345
+ </ul>
346
+ )}
347
+ </li>
348
+ )
349
+ })}
350
+ </ul>
351
+ )}
352
+ </div>
353
+ </PopoverContent>
354
+ </Popover>
355
+ )
356
+ }