@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.
- package/package.json +62 -0
- package/src/components/editor/custom-camera-controls.tsx +387 -0
- package/src/components/editor/editor-layout-v2.tsx +220 -0
- package/src/components/editor/export-manager.tsx +78 -0
- package/src/components/editor/first-person-controls.tsx +249 -0
- package/src/components/editor/floating-action-menu.tsx +231 -0
- package/src/components/editor/floorplan-panel.tsx +9609 -0
- package/src/components/editor/grid.tsx +161 -0
- package/src/components/editor/index.tsx +928 -0
- package/src/components/editor/node-action-menu.tsx +66 -0
- package/src/components/editor/preset-thumbnail-generator.tsx +125 -0
- package/src/components/editor/selection-manager.tsx +897 -0
- package/src/components/editor/site-edge-labels.tsx +90 -0
- package/src/components/editor/thumbnail-generator.tsx +166 -0
- package/src/components/editor/wall-measurement-label.tsx +258 -0
- package/src/components/feedback-dialog.tsx +265 -0
- package/src/components/pascal-radio.tsx +280 -0
- package/src/components/preview-button.tsx +16 -0
- package/src/components/systems/ceiling/ceiling-system.tsx +77 -0
- package/src/components/systems/roof/roof-edit-system.tsx +69 -0
- package/src/components/systems/stair/stair-edit-system.tsx +69 -0
- package/src/components/systems/zone/zone-label-editor-system.tsx +320 -0
- package/src/components/systems/zone/zone-system.tsx +87 -0
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +42 -0
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +47 -0
- package/src/components/tools/ceiling/ceiling-tool.tsx +465 -0
- package/src/components/tools/door/door-math.ts +110 -0
- package/src/components/tools/door/door-tool.tsx +293 -0
- package/src/components/tools/door/move-door-tool.tsx +373 -0
- package/src/components/tools/item/item-tool.tsx +26 -0
- package/src/components/tools/item/move-tool.tsx +90 -0
- package/src/components/tools/item/placement-math.ts +85 -0
- package/src/components/tools/item/placement-strategies.ts +556 -0
- package/src/components/tools/item/placement-types.ts +117 -0
- package/src/components/tools/item/use-draft-node.ts +227 -0
- package/src/components/tools/item/use-placement-coordinator.tsx +877 -0
- package/src/components/tools/roof/move-roof-tool.tsx +288 -0
- package/src/components/tools/roof/roof-tool.tsx +318 -0
- package/src/components/tools/select/box-select-tool.tsx +626 -0
- package/src/components/tools/shared/cursor-sphere.tsx +119 -0
- package/src/components/tools/shared/polygon-editor.tsx +361 -0
- package/src/components/tools/site/site-boundary-editor.tsx +42 -0
- package/src/components/tools/slab/slab-boundary-editor.tsx +42 -0
- package/src/components/tools/slab/slab-hole-editor.tsx +47 -0
- package/src/components/tools/slab/slab-tool.tsx +322 -0
- package/src/components/tools/stair/stair-defaults.ts +7 -0
- package/src/components/tools/stair/stair-tool.tsx +194 -0
- package/src/components/tools/tool-manager.tsx +120 -0
- package/src/components/tools/wall/wall-drafting.ts +140 -0
- package/src/components/tools/wall/wall-tool.tsx +210 -0
- package/src/components/tools/window/move-window-tool.tsx +410 -0
- package/src/components/tools/window/window-math.ts +117 -0
- package/src/components/tools/window/window-tool.tsx +303 -0
- package/src/components/tools/zone/zone-boundary-editor.tsx +39 -0
- package/src/components/tools/zone/zone-tool.tsx +364 -0
- package/src/components/ui/action-menu/action-button.tsx +59 -0
- package/src/components/ui/action-menu/camera-actions.tsx +74 -0
- package/src/components/ui/action-menu/control-modes.tsx +240 -0
- package/src/components/ui/action-menu/furnish-tools.tsx +102 -0
- package/src/components/ui/action-menu/index.tsx +152 -0
- package/src/components/ui/action-menu/structure-tools.tsx +100 -0
- package/src/components/ui/action-menu/view-toggles.tsx +397 -0
- package/src/components/ui/command-palette/editor-commands.tsx +396 -0
- package/src/components/ui/command-palette/index.tsx +730 -0
- package/src/components/ui/controls/action-button.tsx +33 -0
- package/src/components/ui/controls/material-picker.tsx +194 -0
- package/src/components/ui/controls/metric-control.tsx +262 -0
- package/src/components/ui/controls/panel-section.tsx +65 -0
- package/src/components/ui/controls/segmented-control.tsx +45 -0
- package/src/components/ui/controls/slider-control.tsx +245 -0
- package/src/components/ui/controls/toggle-control.tsx +38 -0
- package/src/components/ui/floating-level-selector.tsx +355 -0
- package/src/components/ui/helpers/ceiling-helper.tsx +20 -0
- package/src/components/ui/helpers/helper-manager.tsx +33 -0
- package/src/components/ui/helpers/item-helper.tsx +40 -0
- package/src/components/ui/helpers/roof-helper.tsx +16 -0
- package/src/components/ui/helpers/slab-helper.tsx +20 -0
- package/src/components/ui/helpers/wall-helper.tsx +20 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1580 -0
- package/src/components/ui/item-catalog/item-catalog.tsx +219 -0
- package/src/components/ui/panels/ceiling-panel.tsx +230 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +356 -0
- package/src/components/ui/panels/door-panel.tsx +600 -0
- package/src/components/ui/panels/item-panel.tsx +306 -0
- package/src/components/ui/panels/panel-manager.tsx +59 -0
- package/src/components/ui/panels/panel-wrapper.tsx +80 -0
- package/src/components/ui/panels/presets/presets-popover.tsx +511 -0
- package/src/components/ui/panels/reference-panel.tsx +177 -0
- package/src/components/ui/panels/roof-panel.tsx +262 -0
- package/src/components/ui/panels/roof-segment-panel.tsx +326 -0
- package/src/components/ui/panels/slab-panel.tsx +228 -0
- package/src/components/ui/panels/stair-panel.tsx +304 -0
- package/src/components/ui/panels/stair-segment-panel.tsx +339 -0
- package/src/components/ui/panels/wall-panel.tsx +123 -0
- package/src/components/ui/panels/window-panel.tsx +441 -0
- package/src/components/ui/primitives/button.tsx +69 -0
- package/src/components/ui/primitives/card.tsx +75 -0
- package/src/components/ui/primitives/color-dot.tsx +61 -0
- package/src/components/ui/primitives/context-menu.tsx +227 -0
- package/src/components/ui/primitives/dialog.tsx +129 -0
- package/src/components/ui/primitives/dropdown-menu.tsx +228 -0
- package/src/components/ui/primitives/error-boundary.tsx +52 -0
- package/src/components/ui/primitives/input.tsx +21 -0
- package/src/components/ui/primitives/number-input.tsx +187 -0
- package/src/components/ui/primitives/opacity-control.tsx +79 -0
- package/src/components/ui/primitives/popover.tsx +42 -0
- package/src/components/ui/primitives/separator.tsx +28 -0
- package/src/components/ui/primitives/sheet.tsx +130 -0
- package/src/components/ui/primitives/shortcut-token.tsx +64 -0
- package/src/components/ui/primitives/sidebar.tsx +855 -0
- package/src/components/ui/primitives/skeleton.tsx +13 -0
- package/src/components/ui/primitives/slider.tsx +58 -0
- package/src/components/ui/primitives/switch.tsx +29 -0
- package/src/components/ui/primitives/tooltip.tsx +57 -0
- package/src/components/ui/scene-loader.tsx +40 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +103 -0
- package/src/components/ui/sidebar/icon-rail.tsx +147 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +100 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +438 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +188 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +80 -0
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +126 -0
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +64 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +1543 -0
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +98 -0
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +117 -0
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +65 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +214 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +96 -0
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +216 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +115 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +342 -0
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +271 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +106 -0
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +64 -0
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +87 -0
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +167 -0
- package/src/components/ui/sidebar/tab-bar.tsx +39 -0
- package/src/components/ui/slider-demo.tsx +36 -0
- package/src/components/ui/slider.tsx +81 -0
- package/src/components/ui/viewer-toolbar.tsx +342 -0
- package/src/components/viewer-overlay.tsx +499 -0
- package/src/components/viewer-zone-system.tsx +48 -0
- package/src/contexts/presets-context.tsx +121 -0
- package/src/hooks/use-auto-save.ts +194 -0
- package/src/hooks/use-contextual-tools.ts +52 -0
- package/src/hooks/use-grid-events.ts +106 -0
- package/src/hooks/use-keyboard.ts +214 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/use-reduced-motion.ts +20 -0
- package/src/index.tsx +33 -0
- package/src/lib/constants.ts +3 -0
- package/src/lib/level-selection.ts +31 -0
- package/src/lib/scene.ts +394 -0
- package/src/lib/sfx/index.ts +2 -0
- package/src/lib/sfx-bus.ts +49 -0
- package/src/lib/sfx-player.ts +60 -0
- package/src/lib/utils.ts +43 -0
- package/src/store/use-audio.tsx +45 -0
- package/src/store/use-command-registry.ts +36 -0
- package/src/store/use-editor.tsx +522 -0
- package/src/store/use-palette-view-registry.ts +45 -0
- package/src/store/use-upload.ts +90 -0
- package/src/three-types.ts +3 -0
- 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
|
+
}
|