@ifc-lite/viewer 1.6.0 → 1.7.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/CHANGELOG.md +78 -0
- package/dist/assets/{Arrow.dom-BjDQoB2M.js → Arrow.dom-BGPQieQQ.js} +1 -1
- package/dist/assets/ifc-lite_bg-DyIN_nBM.wasm +0 -0
- package/dist/assets/{index-YBtrHPu3.js → index-dgdgiQ9p.js} +40212 -30008
- package/dist/assets/index-yTqs8kgX.css +1 -0
- package/dist/assets/{native-bridge-CULtTDX3.js → native-bridge-DD0SNyQ5.js} +1 -1
- package/dist/assets/{wasm-bridge-CjL-lSak.js → wasm-bridge-D54YMO7X.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +18 -15
- package/src/components/viewer/BCFPanel.tsx +7 -789
- package/src/components/viewer/Drawing2DCanvas.tsx +1048 -0
- package/src/components/viewer/DrawingSettingsPanel.tsx +3 -3
- package/src/components/viewer/HierarchyPanel.tsx +110 -842
- package/src/components/viewer/IDSExportDialog.tsx +281 -0
- package/src/components/viewer/IDSPanel.tsx +126 -17
- package/src/components/viewer/KeyboardShortcutsDialog.tsx +9 -0
- package/src/components/viewer/LensPanel.tsx +603 -0
- package/src/components/viewer/MainToolbar.tsx +188 -21
- package/src/components/viewer/PropertiesPanel.tsx +171 -663
- package/src/components/viewer/PropertyEditor.tsx +866 -77
- package/src/components/viewer/Section2DPanel.tsx +76 -2648
- package/src/components/viewer/ToolOverlays.tsx +3 -1097
- package/src/components/viewer/ViewerLayout.tsx +132 -45
- package/src/components/viewer/Viewport.tsx +237 -1659
- package/src/components/viewer/ViewportContainer.tsx +11 -3
- package/src/components/viewer/bcf/BCFCreateTopicForm.tsx +134 -0
- package/src/components/viewer/bcf/BCFTopicDetail.tsx +388 -0
- package/src/components/viewer/bcf/BCFTopicList.tsx +239 -0
- package/src/components/viewer/bcf/bcfHelpers.tsx +109 -0
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +328 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +464 -0
- package/src/components/viewer/hierarchy/types.ts +54 -0
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +280 -0
- package/src/components/viewer/lists/ListBuilder.tsx +486 -0
- package/src/components/viewer/lists/ListPanel.tsx +540 -0
- package/src/components/viewer/lists/ListResultsTable.tsx +193 -0
- package/src/components/viewer/properties/ClassificationCard.tsx +70 -0
- package/src/components/viewer/properties/CoordinateDisplay.tsx +49 -0
- package/src/components/viewer/properties/DocumentCard.tsx +89 -0
- package/src/components/viewer/properties/MaterialCard.tsx +201 -0
- package/src/components/viewer/properties/ModelMetadataPanel.tsx +335 -0
- package/src/components/viewer/properties/PropertySetCard.tsx +132 -0
- package/src/components/viewer/properties/QuantitySetCard.tsx +79 -0
- package/src/components/viewer/properties/RelationshipsCard.tsx +100 -0
- package/src/components/viewer/properties/encodingUtils.ts +29 -0
- package/src/components/viewer/tools/MeasurePanel.tsx +218 -0
- package/src/components/viewer/tools/MeasurementVisuals.tsx +644 -0
- package/src/components/viewer/tools/SectionPanel.tsx +183 -0
- package/src/components/viewer/tools/SectionVisualization.tsx +78 -0
- package/src/components/viewer/tools/formatDistance.ts +18 -0
- package/src/components/viewer/tools/sectionConstants.ts +14 -0
- package/src/components/viewer/useAnimationLoop.ts +166 -0
- package/src/components/viewer/useGeometryStreaming.ts +398 -0
- package/src/components/viewer/useKeyboardControls.ts +221 -0
- package/src/components/viewer/useMouseControls.ts +1009 -0
- package/src/components/viewer/useRenderUpdates.ts +165 -0
- package/src/components/viewer/useTouchControls.ts +245 -0
- package/src/hooks/ids/idsColorSystem.ts +125 -0
- package/src/hooks/ids/idsDataAccessor.ts +237 -0
- package/src/hooks/ids/idsExportService.ts +444 -0
- package/src/hooks/useBCF.ts +7 -0
- package/src/hooks/useDrawingExport.ts +627 -0
- package/src/hooks/useDrawingGeneration.ts +627 -0
- package/src/hooks/useFloorplanView.ts +108 -0
- package/src/hooks/useIDS.ts +270 -463
- package/src/hooks/useIfc.ts +26 -1628
- package/src/hooks/useIfcFederation.ts +803 -0
- package/src/hooks/useIfcLoader.ts +508 -0
- package/src/hooks/useIfcServer.ts +465 -0
- package/src/hooks/useKeyboardShortcuts.ts +1 -1
- package/src/hooks/useLens.ts +129 -0
- package/src/hooks/useMeasure2D.ts +365 -0
- package/src/hooks/useViewControls.ts +218 -0
- package/src/lib/ifc4-pset-definitions.test.ts +161 -0
- package/src/lib/ifc4-pset-definitions.ts +621 -0
- package/src/lib/ifc4-qto-definitions.ts +315 -0
- package/src/lib/lens/adapter.ts +138 -0
- package/src/lib/lens/index.ts +5 -0
- package/src/lib/lists/adapter.ts +69 -0
- package/src/lib/lists/index.ts +28 -0
- package/src/lib/lists/persistence.ts +64 -0
- package/src/services/fs-cache.ts +1 -1
- package/src/services/tauri-modules.d.ts +25 -0
- package/src/store/index.ts +38 -2
- package/src/store/slices/cameraSlice.ts +14 -1
- package/src/store/slices/dataSlice.ts +14 -1
- package/src/store/slices/lensSlice.ts +184 -0
- package/src/store/slices/listSlice.ts +74 -0
- package/src/store/slices/pinboardSlice.ts +114 -0
- package/src/store/types.ts +5 -0
- package/src/utils/ifcConfig.ts +16 -3
- package/src/utils/serverDataModel.ts +64 -101
- package/src/vite-env.d.ts +3 -0
- package/dist/assets/ifc-lite_bg-C6kblxf9.wasm +0 -0
- package/dist/assets/index-v3mcCUPN.css +0 -1
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* ListBuilder - Configure a list by selecting entity types, columns, and conditions
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
10
|
+
import {
|
|
11
|
+
Play,
|
|
12
|
+
Plus,
|
|
13
|
+
Trash2,
|
|
14
|
+
ChevronDown,
|
|
15
|
+
ChevronRight,
|
|
16
|
+
ChevronUp,
|
|
17
|
+
Save,
|
|
18
|
+
} from 'lucide-react';
|
|
19
|
+
import { Button } from '@/components/ui/button';
|
|
20
|
+
import { Input } from '@/components/ui/input';
|
|
21
|
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
22
|
+
import { Separator } from '@/components/ui/separator';
|
|
23
|
+
import { Badge } from '@/components/ui/badge';
|
|
24
|
+
import { IfcTypeEnum } from '@ifc-lite/data';
|
|
25
|
+
import type {
|
|
26
|
+
ListDataProvider,
|
|
27
|
+
ListDefinition,
|
|
28
|
+
ColumnDefinition,
|
|
29
|
+
DiscoveredColumns,
|
|
30
|
+
PropertyCondition,
|
|
31
|
+
} from '@ifc-lite/lists';
|
|
32
|
+
import { discoverColumns } from '@ifc-lite/lists';
|
|
33
|
+
|
|
34
|
+
// Building element types available for selection
|
|
35
|
+
const SELECTABLE_TYPES: { type: IfcTypeEnum; label: string }[] = [
|
|
36
|
+
{ type: IfcTypeEnum.IfcWall, label: 'Walls' },
|
|
37
|
+
{ type: IfcTypeEnum.IfcWallStandardCase, label: 'Walls (Standard)' },
|
|
38
|
+
{ type: IfcTypeEnum.IfcDoor, label: 'Doors' },
|
|
39
|
+
{ type: IfcTypeEnum.IfcWindow, label: 'Windows' },
|
|
40
|
+
{ type: IfcTypeEnum.IfcSlab, label: 'Slabs' },
|
|
41
|
+
{ type: IfcTypeEnum.IfcColumn, label: 'Columns' },
|
|
42
|
+
{ type: IfcTypeEnum.IfcBeam, label: 'Beams' },
|
|
43
|
+
{ type: IfcTypeEnum.IfcStair, label: 'Stairs' },
|
|
44
|
+
{ type: IfcTypeEnum.IfcRamp, label: 'Ramps' },
|
|
45
|
+
{ type: IfcTypeEnum.IfcRoof, label: 'Roofs' },
|
|
46
|
+
{ type: IfcTypeEnum.IfcCovering, label: 'Coverings' },
|
|
47
|
+
{ type: IfcTypeEnum.IfcCurtainWall, label: 'Curtain Walls' },
|
|
48
|
+
{ type: IfcTypeEnum.IfcRailing, label: 'Railings' },
|
|
49
|
+
{ type: IfcTypeEnum.IfcSpace, label: 'Spaces' },
|
|
50
|
+
{ type: IfcTypeEnum.IfcBuildingStorey, label: 'Storeys' },
|
|
51
|
+
{ type: IfcTypeEnum.IfcDistributionElement, label: 'MEP Distribution' },
|
|
52
|
+
{ type: IfcTypeEnum.IfcFlowTerminal, label: 'MEP Terminals' },
|
|
53
|
+
{ type: IfcTypeEnum.IfcFlowSegment, label: 'MEP Segments' },
|
|
54
|
+
{ type: IfcTypeEnum.IfcFlowFitting, label: 'MEP Fittings' },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
interface ListBuilderProps {
|
|
58
|
+
providers: ListDataProvider[];
|
|
59
|
+
initial: ListDefinition | null;
|
|
60
|
+
onSave: (definition: ListDefinition) => void;
|
|
61
|
+
onCancel: () => void;
|
|
62
|
+
onExecute: (definition: ListDefinition) => void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function ListBuilder({ providers, initial, onSave, onCancel, onExecute }: ListBuilderProps) {
|
|
66
|
+
const [name, setName] = useState(initial?.name ?? '');
|
|
67
|
+
const [description, setDescription] = useState(initial?.description ?? '');
|
|
68
|
+
const [selectedTypes, setSelectedTypes] = useState<Set<IfcTypeEnum>>(
|
|
69
|
+
new Set(initial?.entityTypes ?? [])
|
|
70
|
+
);
|
|
71
|
+
const [columns, setColumns] = useState<ColumnDefinition[]>(initial?.columns ?? []);
|
|
72
|
+
const [conditions, setConditions] = useState<PropertyCondition[]>(initial?.conditions ?? []);
|
|
73
|
+
const [columnsExpanded, setColumnsExpanded] = useState(true);
|
|
74
|
+
|
|
75
|
+
// Count entities per type across all providers
|
|
76
|
+
const typeCounts = useMemo(() => {
|
|
77
|
+
const counts = new Map<IfcTypeEnum, number>();
|
|
78
|
+
for (const { type } of SELECTABLE_TYPES) {
|
|
79
|
+
let total = 0;
|
|
80
|
+
for (const p of providers) {
|
|
81
|
+
total += p.getEntitiesByType(type).length;
|
|
82
|
+
}
|
|
83
|
+
if (total > 0) counts.set(type, total);
|
|
84
|
+
}
|
|
85
|
+
return counts;
|
|
86
|
+
}, [providers]);
|
|
87
|
+
|
|
88
|
+
// Discover available columns whenever selected types change (across all providers)
|
|
89
|
+
const discovered = useMemo<DiscoveredColumns | null>(() => {
|
|
90
|
+
if (selectedTypes.size === 0) return null;
|
|
91
|
+
return discoverColumns(providers, Array.from(selectedTypes));
|
|
92
|
+
}, [providers, selectedTypes]);
|
|
93
|
+
|
|
94
|
+
const toggleType = useCallback((type: IfcTypeEnum) => {
|
|
95
|
+
setSelectedTypes(prev => {
|
|
96
|
+
const next = new Set(prev);
|
|
97
|
+
if (next.has(type)) {
|
|
98
|
+
next.delete(type);
|
|
99
|
+
} else {
|
|
100
|
+
next.add(type);
|
|
101
|
+
}
|
|
102
|
+
return next;
|
|
103
|
+
});
|
|
104
|
+
}, []);
|
|
105
|
+
|
|
106
|
+
const addColumn = useCallback((col: ColumnDefinition) => {
|
|
107
|
+
setColumns(prev => {
|
|
108
|
+
if (prev.some(c => c.id === col.id)) return prev;
|
|
109
|
+
return [...prev, col];
|
|
110
|
+
});
|
|
111
|
+
}, []);
|
|
112
|
+
|
|
113
|
+
const removeColumn = useCallback((id: string) => {
|
|
114
|
+
setColumns(prev => prev.filter(c => c.id !== id));
|
|
115
|
+
}, []);
|
|
116
|
+
|
|
117
|
+
const moveColumn = useCallback((idx: number, direction: -1 | 1) => {
|
|
118
|
+
setColumns(prev => {
|
|
119
|
+
const target = idx + direction;
|
|
120
|
+
if (target < 0 || target >= prev.length) return prev;
|
|
121
|
+
const next = [...prev];
|
|
122
|
+
const tmp = next[idx];
|
|
123
|
+
next[idx] = next[target];
|
|
124
|
+
next[target] = tmp;
|
|
125
|
+
return next;
|
|
126
|
+
});
|
|
127
|
+
}, []);
|
|
128
|
+
|
|
129
|
+
const buildDefinition = useCallback((): ListDefinition => {
|
|
130
|
+
return {
|
|
131
|
+
id: initial?.id ?? crypto.randomUUID(),
|
|
132
|
+
name: name || 'Untitled List',
|
|
133
|
+
description: description || undefined,
|
|
134
|
+
createdAt: initial?.createdAt ?? Date.now(),
|
|
135
|
+
updatedAt: Date.now(),
|
|
136
|
+
entityTypes: Array.from(selectedTypes),
|
|
137
|
+
conditions,
|
|
138
|
+
columns,
|
|
139
|
+
};
|
|
140
|
+
}, [initial, name, description, selectedTypes, conditions, columns]);
|
|
141
|
+
|
|
142
|
+
const handleSave = useCallback(() => {
|
|
143
|
+
onSave(buildDefinition());
|
|
144
|
+
}, [buildDefinition, onSave]);
|
|
145
|
+
|
|
146
|
+
const handleRun = useCallback(() => {
|
|
147
|
+
const def = buildDefinition();
|
|
148
|
+
onExecute(def);
|
|
149
|
+
}, [buildDefinition, onExecute]);
|
|
150
|
+
|
|
151
|
+
const selectedColumnIds = useMemo(() => new Set(columns.map(c => c.id)), [columns]);
|
|
152
|
+
|
|
153
|
+
const totalSelectedEntities = useMemo(() => {
|
|
154
|
+
let count = 0;
|
|
155
|
+
for (const type of selectedTypes) {
|
|
156
|
+
count += typeCounts.get(type) ?? 0;
|
|
157
|
+
}
|
|
158
|
+
return count;
|
|
159
|
+
}, [selectedTypes, typeCounts]);
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<div className="flex-1 flex flex-col min-h-0">
|
|
163
|
+
<ScrollArea className="flex-1">
|
|
164
|
+
<div className="p-3 space-y-4">
|
|
165
|
+
{/* Name & Description */}
|
|
166
|
+
<div className="space-y-2">
|
|
167
|
+
<Input
|
|
168
|
+
placeholder="List name..."
|
|
169
|
+
value={name}
|
|
170
|
+
onChange={e => setName(e.target.value)}
|
|
171
|
+
className="h-8 text-sm"
|
|
172
|
+
/>
|
|
173
|
+
<Input
|
|
174
|
+
placeholder="Description (optional)"
|
|
175
|
+
value={description}
|
|
176
|
+
onChange={e => setDescription(e.target.value)}
|
|
177
|
+
className="h-8 text-sm"
|
|
178
|
+
/>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<Separator />
|
|
182
|
+
|
|
183
|
+
{/* Entity Type Selection */}
|
|
184
|
+
<div>
|
|
185
|
+
<div className="flex items-center justify-between mb-2">
|
|
186
|
+
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
|
|
187
|
+
Entity Types
|
|
188
|
+
</span>
|
|
189
|
+
{selectedTypes.size > 0 && (
|
|
190
|
+
<Badge variant="secondary" className="text-xs h-5">
|
|
191
|
+
{totalSelectedEntities} entities
|
|
192
|
+
</Badge>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
<div className="flex flex-wrap gap-1">
|
|
196
|
+
{SELECTABLE_TYPES.map(({ type, label }) => {
|
|
197
|
+
const count = typeCounts.get(type);
|
|
198
|
+
if (!count) return null; // Don't show types not in model
|
|
199
|
+
const selected = selectedTypes.has(type);
|
|
200
|
+
return (
|
|
201
|
+
<button
|
|
202
|
+
key={type}
|
|
203
|
+
onClick={() => toggleType(type)}
|
|
204
|
+
className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs border transition-colors ${
|
|
205
|
+
selected
|
|
206
|
+
? 'bg-primary text-primary-foreground border-primary'
|
|
207
|
+
: 'bg-background border-border hover:bg-muted'
|
|
208
|
+
}`}
|
|
209
|
+
>
|
|
210
|
+
{label}
|
|
211
|
+
<span className={selected ? 'opacity-75' : 'text-muted-foreground'}>
|
|
212
|
+
{count}
|
|
213
|
+
</span>
|
|
214
|
+
</button>
|
|
215
|
+
);
|
|
216
|
+
})}
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
{selectedTypes.size > 0 && discovered && (
|
|
221
|
+
<>
|
|
222
|
+
<Separator />
|
|
223
|
+
|
|
224
|
+
{/* Column Selection */}
|
|
225
|
+
<div>
|
|
226
|
+
<button
|
|
227
|
+
className="flex items-center gap-1 text-xs font-medium text-muted-foreground uppercase tracking-wider w-full"
|
|
228
|
+
onClick={() => setColumnsExpanded(!columnsExpanded)}
|
|
229
|
+
>
|
|
230
|
+
{columnsExpanded ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
|
|
231
|
+
Columns ({columns.length} selected)
|
|
232
|
+
</button>
|
|
233
|
+
|
|
234
|
+
{columnsExpanded && (
|
|
235
|
+
<div className="mt-2 space-y-2">
|
|
236
|
+
{/* Selected columns (reorderable) */}
|
|
237
|
+
{columns.length > 0 && (
|
|
238
|
+
<div className="space-y-0.5 mb-2">
|
|
239
|
+
{columns.map((col, idx) => (
|
|
240
|
+
<div
|
|
241
|
+
key={col.id}
|
|
242
|
+
className="flex items-center gap-1 px-2 py-1 rounded bg-muted/50 text-xs"
|
|
243
|
+
>
|
|
244
|
+
<span className="text-muted-foreground w-4 text-right">{idx + 1}</span>
|
|
245
|
+
<span className="flex-1 truncate">
|
|
246
|
+
{col.label ?? col.propertyName}
|
|
247
|
+
{col.psetName && (
|
|
248
|
+
<span className="text-muted-foreground ml-1">({col.psetName})</span>
|
|
249
|
+
)}
|
|
250
|
+
</span>
|
|
251
|
+
<button
|
|
252
|
+
onClick={() => moveColumn(idx, -1)}
|
|
253
|
+
disabled={idx === 0}
|
|
254
|
+
className={`${idx === 0 ? 'text-muted-foreground/30' : 'text-muted-foreground hover:text-foreground'}`}
|
|
255
|
+
>
|
|
256
|
+
<ChevronUp className="h-3 w-3" />
|
|
257
|
+
</button>
|
|
258
|
+
<button
|
|
259
|
+
onClick={() => moveColumn(idx, 1)}
|
|
260
|
+
disabled={idx === columns.length - 1}
|
|
261
|
+
className={`${idx === columns.length - 1 ? 'text-muted-foreground/30' : 'text-muted-foreground hover:text-foreground'}`}
|
|
262
|
+
>
|
|
263
|
+
<ChevronDown className="h-3 w-3" />
|
|
264
|
+
</button>
|
|
265
|
+
<button
|
|
266
|
+
onClick={() => removeColumn(col.id)}
|
|
267
|
+
className="text-muted-foreground hover:text-destructive"
|
|
268
|
+
>
|
|
269
|
+
<Trash2 className="h-3 w-3" />
|
|
270
|
+
</button>
|
|
271
|
+
</div>
|
|
272
|
+
))}
|
|
273
|
+
</div>
|
|
274
|
+
)}
|
|
275
|
+
|
|
276
|
+
{/* Available columns */}
|
|
277
|
+
<ColumnPicker
|
|
278
|
+
discovered={discovered}
|
|
279
|
+
selectedIds={selectedColumnIds}
|
|
280
|
+
onAdd={addColumn}
|
|
281
|
+
/>
|
|
282
|
+
</div>
|
|
283
|
+
)}
|
|
284
|
+
</div>
|
|
285
|
+
</>
|
|
286
|
+
)}
|
|
287
|
+
</div>
|
|
288
|
+
</ScrollArea>
|
|
289
|
+
|
|
290
|
+
{/* Bottom Actions */}
|
|
291
|
+
<div className="flex items-center gap-2 px-3 py-2 border-t">
|
|
292
|
+
<Button
|
|
293
|
+
variant="default"
|
|
294
|
+
size="sm"
|
|
295
|
+
onClick={handleRun}
|
|
296
|
+
disabled={selectedTypes.size === 0 || columns.length === 0}
|
|
297
|
+
className="text-xs h-7"
|
|
298
|
+
>
|
|
299
|
+
<Play className="h-3 w-3 mr-1" />
|
|
300
|
+
Run
|
|
301
|
+
</Button>
|
|
302
|
+
<Button
|
|
303
|
+
variant="outline"
|
|
304
|
+
size="sm"
|
|
305
|
+
onClick={handleSave}
|
|
306
|
+
disabled={selectedTypes.size === 0 || columns.length === 0}
|
|
307
|
+
className="text-xs h-7"
|
|
308
|
+
>
|
|
309
|
+
<Save className="h-3 w-3 mr-1" />
|
|
310
|
+
Save
|
|
311
|
+
</Button>
|
|
312
|
+
<div className="flex-1" />
|
|
313
|
+
<Button variant="ghost" size="sm" onClick={onCancel} className="text-xs h-7">
|
|
314
|
+
Cancel
|
|
315
|
+
</Button>
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// ============================================================================
|
|
322
|
+
// Column Picker
|
|
323
|
+
// ============================================================================
|
|
324
|
+
|
|
325
|
+
interface ColumnPickerProps {
|
|
326
|
+
discovered: DiscoveredColumns;
|
|
327
|
+
selectedIds: Set<string>;
|
|
328
|
+
onAdd: (col: ColumnDefinition) => void;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function ColumnPicker({ discovered, selectedIds, onAdd }: ColumnPickerProps) {
|
|
332
|
+
const [expandedSections, setExpandedSections] = useState<Set<string>>(new Set(['attributes']));
|
|
333
|
+
|
|
334
|
+
const toggleSection = (section: string) => {
|
|
335
|
+
setExpandedSections(prev => {
|
|
336
|
+
const next = new Set(prev);
|
|
337
|
+
if (next.has(section)) next.delete(section);
|
|
338
|
+
else next.add(section);
|
|
339
|
+
return next;
|
|
340
|
+
});
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
return (
|
|
344
|
+
<div className="space-y-1 text-xs">
|
|
345
|
+
{/* Attributes */}
|
|
346
|
+
<CollapsibleSection
|
|
347
|
+
title="Attributes"
|
|
348
|
+
expanded={expandedSections.has('attributes')}
|
|
349
|
+
onToggle={() => toggleSection('attributes')}
|
|
350
|
+
>
|
|
351
|
+
{discovered.attributes.map(attr => {
|
|
352
|
+
const id = `attr-${attr.toLowerCase()}`;
|
|
353
|
+
const isSelected = selectedIds.has(id);
|
|
354
|
+
return (
|
|
355
|
+
<PickerItem
|
|
356
|
+
key={id}
|
|
357
|
+
label={attr}
|
|
358
|
+
selected={isSelected}
|
|
359
|
+
onClick={() => {
|
|
360
|
+
if (!isSelected) {
|
|
361
|
+
onAdd({ id, source: 'attribute', propertyName: attr });
|
|
362
|
+
}
|
|
363
|
+
}}
|
|
364
|
+
/>
|
|
365
|
+
);
|
|
366
|
+
})}
|
|
367
|
+
</CollapsibleSection>
|
|
368
|
+
|
|
369
|
+
{/* Property Sets */}
|
|
370
|
+
{Array.from(discovered.properties.entries())
|
|
371
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
372
|
+
.map(([psetName, propNames]) => (
|
|
373
|
+
<CollapsibleSection
|
|
374
|
+
key={`pset-${psetName}`}
|
|
375
|
+
title={psetName}
|
|
376
|
+
badge="P"
|
|
377
|
+
expanded={expandedSections.has(`pset-${psetName}`)}
|
|
378
|
+
onToggle={() => toggleSection(`pset-${psetName}`)}
|
|
379
|
+
>
|
|
380
|
+
{propNames.map(propName => {
|
|
381
|
+
const id = `prop-${psetName}-${propName}`.toLowerCase().replace(/\s+/g, '-');
|
|
382
|
+
const isSelected = selectedIds.has(id);
|
|
383
|
+
return (
|
|
384
|
+
<PickerItem
|
|
385
|
+
key={id}
|
|
386
|
+
label={propName}
|
|
387
|
+
selected={isSelected}
|
|
388
|
+
onClick={() => {
|
|
389
|
+
if (!isSelected) {
|
|
390
|
+
onAdd({ id, source: 'property', psetName, propertyName: propName, label: propName });
|
|
391
|
+
}
|
|
392
|
+
}}
|
|
393
|
+
/>
|
|
394
|
+
);
|
|
395
|
+
})}
|
|
396
|
+
</CollapsibleSection>
|
|
397
|
+
))}
|
|
398
|
+
|
|
399
|
+
{/* Quantity Sets */}
|
|
400
|
+
{Array.from(discovered.quantities.entries())
|
|
401
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
402
|
+
.map(([qsetName, quantNames]) => (
|
|
403
|
+
<CollapsibleSection
|
|
404
|
+
key={`qset-${qsetName}`}
|
|
405
|
+
title={qsetName}
|
|
406
|
+
badge="Q"
|
|
407
|
+
expanded={expandedSections.has(`qset-${qsetName}`)}
|
|
408
|
+
onToggle={() => toggleSection(`qset-${qsetName}`)}
|
|
409
|
+
>
|
|
410
|
+
{quantNames.map(quantName => {
|
|
411
|
+
const id = `quant-${qsetName}-${quantName}`.toLowerCase().replace(/\s+/g, '-');
|
|
412
|
+
const isSelected = selectedIds.has(id);
|
|
413
|
+
return (
|
|
414
|
+
<PickerItem
|
|
415
|
+
key={id}
|
|
416
|
+
label={quantName}
|
|
417
|
+
selected={isSelected}
|
|
418
|
+
onClick={() => {
|
|
419
|
+
if (!isSelected) {
|
|
420
|
+
onAdd({ id, source: 'quantity', psetName: qsetName, propertyName: quantName, label: quantName });
|
|
421
|
+
}
|
|
422
|
+
}}
|
|
423
|
+
/>
|
|
424
|
+
);
|
|
425
|
+
})}
|
|
426
|
+
</CollapsibleSection>
|
|
427
|
+
))}
|
|
428
|
+
</div>
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function CollapsibleSection({
|
|
433
|
+
title,
|
|
434
|
+
badge,
|
|
435
|
+
expanded,
|
|
436
|
+
onToggle,
|
|
437
|
+
children,
|
|
438
|
+
}: {
|
|
439
|
+
title: string;
|
|
440
|
+
badge?: string;
|
|
441
|
+
expanded: boolean;
|
|
442
|
+
onToggle: () => void;
|
|
443
|
+
children: React.ReactNode;
|
|
444
|
+
}) {
|
|
445
|
+
return (
|
|
446
|
+
<div>
|
|
447
|
+
<button
|
|
448
|
+
className="flex items-center gap-1 w-full px-1 py-0.5 rounded hover:bg-muted/50 text-xs"
|
|
449
|
+
onClick={onToggle}
|
|
450
|
+
>
|
|
451
|
+
{expanded ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
|
|
452
|
+
<span className="font-medium truncate">{title}</span>
|
|
453
|
+
{badge && (
|
|
454
|
+
<span className="ml-auto text-[10px] bg-muted px-1 rounded">{badge}</span>
|
|
455
|
+
)}
|
|
456
|
+
</button>
|
|
457
|
+
{expanded && <div className="pl-4 space-y-0">{children}</div>}
|
|
458
|
+
</div>
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function PickerItem({
|
|
463
|
+
label,
|
|
464
|
+
selected,
|
|
465
|
+
onClick,
|
|
466
|
+
}: {
|
|
467
|
+
label: string;
|
|
468
|
+
selected: boolean;
|
|
469
|
+
onClick: () => void;
|
|
470
|
+
}) {
|
|
471
|
+
return (
|
|
472
|
+
<button
|
|
473
|
+
className={`flex items-center gap-1 w-full px-1 py-0.5 rounded text-xs ${
|
|
474
|
+
selected
|
|
475
|
+
? 'text-muted-foreground cursor-default'
|
|
476
|
+
: 'hover:bg-muted/50 cursor-pointer'
|
|
477
|
+
}`}
|
|
478
|
+
onClick={onClick}
|
|
479
|
+
disabled={selected}
|
|
480
|
+
>
|
|
481
|
+
<Plus className={`h-2.5 w-2.5 ${selected ? 'invisible' : ''}`} />
|
|
482
|
+
<span className="truncate">{label}</span>
|
|
483
|
+
{selected && <span className="ml-auto text-[10px] text-muted-foreground">added</span>}
|
|
484
|
+
</button>
|
|
485
|
+
);
|
|
486
|
+
}
|