@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.
Files changed (95) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/dist/assets/{Arrow.dom-BjDQoB2M.js → Arrow.dom-BGPQieQQ.js} +1 -1
  3. package/dist/assets/ifc-lite_bg-DyIN_nBM.wasm +0 -0
  4. package/dist/assets/{index-YBtrHPu3.js → index-dgdgiQ9p.js} +40212 -30008
  5. package/dist/assets/index-yTqs8kgX.css +1 -0
  6. package/dist/assets/{native-bridge-CULtTDX3.js → native-bridge-DD0SNyQ5.js} +1 -1
  7. package/dist/assets/{wasm-bridge-CjL-lSak.js → wasm-bridge-D54YMO7X.js} +1 -1
  8. package/dist/index.html +2 -2
  9. package/package.json +18 -15
  10. package/src/components/viewer/BCFPanel.tsx +7 -789
  11. package/src/components/viewer/Drawing2DCanvas.tsx +1048 -0
  12. package/src/components/viewer/DrawingSettingsPanel.tsx +3 -3
  13. package/src/components/viewer/HierarchyPanel.tsx +110 -842
  14. package/src/components/viewer/IDSExportDialog.tsx +281 -0
  15. package/src/components/viewer/IDSPanel.tsx +126 -17
  16. package/src/components/viewer/KeyboardShortcutsDialog.tsx +9 -0
  17. package/src/components/viewer/LensPanel.tsx +603 -0
  18. package/src/components/viewer/MainToolbar.tsx +188 -21
  19. package/src/components/viewer/PropertiesPanel.tsx +171 -663
  20. package/src/components/viewer/PropertyEditor.tsx +866 -77
  21. package/src/components/viewer/Section2DPanel.tsx +76 -2648
  22. package/src/components/viewer/ToolOverlays.tsx +3 -1097
  23. package/src/components/viewer/ViewerLayout.tsx +132 -45
  24. package/src/components/viewer/Viewport.tsx +237 -1659
  25. package/src/components/viewer/ViewportContainer.tsx +11 -3
  26. package/src/components/viewer/bcf/BCFCreateTopicForm.tsx +134 -0
  27. package/src/components/viewer/bcf/BCFTopicDetail.tsx +388 -0
  28. package/src/components/viewer/bcf/BCFTopicList.tsx +239 -0
  29. package/src/components/viewer/bcf/bcfHelpers.tsx +109 -0
  30. package/src/components/viewer/hierarchy/HierarchyNode.tsx +328 -0
  31. package/src/components/viewer/hierarchy/treeDataBuilder.ts +464 -0
  32. package/src/components/viewer/hierarchy/types.ts +54 -0
  33. package/src/components/viewer/hierarchy/useHierarchyTree.ts +280 -0
  34. package/src/components/viewer/lists/ListBuilder.tsx +486 -0
  35. package/src/components/viewer/lists/ListPanel.tsx +540 -0
  36. package/src/components/viewer/lists/ListResultsTable.tsx +193 -0
  37. package/src/components/viewer/properties/ClassificationCard.tsx +70 -0
  38. package/src/components/viewer/properties/CoordinateDisplay.tsx +49 -0
  39. package/src/components/viewer/properties/DocumentCard.tsx +89 -0
  40. package/src/components/viewer/properties/MaterialCard.tsx +201 -0
  41. package/src/components/viewer/properties/ModelMetadataPanel.tsx +335 -0
  42. package/src/components/viewer/properties/PropertySetCard.tsx +132 -0
  43. package/src/components/viewer/properties/QuantitySetCard.tsx +79 -0
  44. package/src/components/viewer/properties/RelationshipsCard.tsx +100 -0
  45. package/src/components/viewer/properties/encodingUtils.ts +29 -0
  46. package/src/components/viewer/tools/MeasurePanel.tsx +218 -0
  47. package/src/components/viewer/tools/MeasurementVisuals.tsx +644 -0
  48. package/src/components/viewer/tools/SectionPanel.tsx +183 -0
  49. package/src/components/viewer/tools/SectionVisualization.tsx +78 -0
  50. package/src/components/viewer/tools/formatDistance.ts +18 -0
  51. package/src/components/viewer/tools/sectionConstants.ts +14 -0
  52. package/src/components/viewer/useAnimationLoop.ts +166 -0
  53. package/src/components/viewer/useGeometryStreaming.ts +398 -0
  54. package/src/components/viewer/useKeyboardControls.ts +221 -0
  55. package/src/components/viewer/useMouseControls.ts +1009 -0
  56. package/src/components/viewer/useRenderUpdates.ts +165 -0
  57. package/src/components/viewer/useTouchControls.ts +245 -0
  58. package/src/hooks/ids/idsColorSystem.ts +125 -0
  59. package/src/hooks/ids/idsDataAccessor.ts +237 -0
  60. package/src/hooks/ids/idsExportService.ts +444 -0
  61. package/src/hooks/useBCF.ts +7 -0
  62. package/src/hooks/useDrawingExport.ts +627 -0
  63. package/src/hooks/useDrawingGeneration.ts +627 -0
  64. package/src/hooks/useFloorplanView.ts +108 -0
  65. package/src/hooks/useIDS.ts +270 -463
  66. package/src/hooks/useIfc.ts +26 -1628
  67. package/src/hooks/useIfcFederation.ts +803 -0
  68. package/src/hooks/useIfcLoader.ts +508 -0
  69. package/src/hooks/useIfcServer.ts +465 -0
  70. package/src/hooks/useKeyboardShortcuts.ts +1 -1
  71. package/src/hooks/useLens.ts +129 -0
  72. package/src/hooks/useMeasure2D.ts +365 -0
  73. package/src/hooks/useViewControls.ts +218 -0
  74. package/src/lib/ifc4-pset-definitions.test.ts +161 -0
  75. package/src/lib/ifc4-pset-definitions.ts +621 -0
  76. package/src/lib/ifc4-qto-definitions.ts +315 -0
  77. package/src/lib/lens/adapter.ts +138 -0
  78. package/src/lib/lens/index.ts +5 -0
  79. package/src/lib/lists/adapter.ts +69 -0
  80. package/src/lib/lists/index.ts +28 -0
  81. package/src/lib/lists/persistence.ts +64 -0
  82. package/src/services/fs-cache.ts +1 -1
  83. package/src/services/tauri-modules.d.ts +25 -0
  84. package/src/store/index.ts +38 -2
  85. package/src/store/slices/cameraSlice.ts +14 -1
  86. package/src/store/slices/dataSlice.ts +14 -1
  87. package/src/store/slices/lensSlice.ts +184 -0
  88. package/src/store/slices/listSlice.ts +74 -0
  89. package/src/store/slices/pinboardSlice.ts +114 -0
  90. package/src/store/types.ts +5 -0
  91. package/src/utils/ifcConfig.ts +16 -3
  92. package/src/utils/serverDataModel.ts +64 -101
  93. package/src/vite-env.d.ts +3 -0
  94. package/dist/assets/ifc-lite_bg-C6kblxf9.wasm +0 -0
  95. package/dist/assets/index-v3mcCUPN.css +0 -1
@@ -0,0 +1,540 @@
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
+ * ListPanel - Main container for the Lists feature
7
+ *
8
+ * Shows either:
9
+ * - List builder (when creating/editing a list)
10
+ * - List results table (when a list has been executed)
11
+ * - List library (saved lists + presets)
12
+ */
13
+
14
+ import React, { useCallback, useState, useMemo } from 'react';
15
+ import {
16
+ X,
17
+ Plus,
18
+ Play,
19
+ FileSpreadsheet,
20
+ Trash2,
21
+ Download,
22
+ Upload,
23
+ Loader2,
24
+ Table2,
25
+ Pencil,
26
+ Copy,
27
+ Settings2,
28
+ } from 'lucide-react';
29
+ import { Button } from '@/components/ui/button';
30
+ import { ScrollArea } from '@/components/ui/scroll-area';
31
+ import { Separator } from '@/components/ui/separator';
32
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
33
+ import { useViewerStore } from '@/store';
34
+ import { useIfc } from '@/hooks/useIfc';
35
+ import {
36
+ executeList,
37
+ listResultToCSV,
38
+ LIST_PRESETS,
39
+ importListDefinition,
40
+ exportListDefinition,
41
+ createListDataProvider,
42
+ } from '@/lib/lists';
43
+ import type { ListDefinition, ListResult, ListDataProvider } from '@/lib/lists';
44
+ import { ListBuilder } from './ListBuilder';
45
+ import { ListResultsTable } from './ListResultsTable';
46
+
47
+ interface ListPanelProps {
48
+ onClose?: () => void;
49
+ }
50
+
51
+ type PanelView = 'library' | 'builder' | 'results';
52
+
53
+ export function ListPanel({ onClose }: ListPanelProps) {
54
+ const { ifcDataStore, models } = useIfc();
55
+ const [view, setView] = useState<PanelView>('library');
56
+ const [editingList, setEditingList] = useState<ListDefinition | null>(null);
57
+
58
+ const listDefinitions = useViewerStore((s) => s.listDefinitions);
59
+ const activeListId = useViewerStore((s) => s.activeListId);
60
+ const listResult = useViewerStore((s) => s.listResult);
61
+ const listExecuting = useViewerStore((s) => s.listExecuting);
62
+ const addListDefinition = useViewerStore((s) => s.addListDefinition);
63
+ const updateListDefinition = useViewerStore((s) => s.updateListDefinition);
64
+ const deleteListDefinition = useViewerStore((s) => s.deleteListDefinition);
65
+ const setActiveListId = useViewerStore((s) => s.setActiveListId);
66
+ const setListResult = useViewerStore((s) => s.setListResult);
67
+ const setListExecuting = useViewerStore((s) => s.setListExecuting);
68
+
69
+ const importInputRef = React.useRef<HTMLInputElement>(null);
70
+
71
+ // Collect all available data providers for multi-model support
72
+ const allProviders = useMemo(() => {
73
+ const providers: ListDataProvider[] = [];
74
+ if (models.size > 0) {
75
+ for (const [, model] of models) {
76
+ providers.push(createListDataProvider(model.ifcDataStore));
77
+ }
78
+ } else if (ifcDataStore) {
79
+ providers.push(createListDataProvider(ifcDataStore));
80
+ }
81
+ return providers;
82
+ }, [models, ifcDataStore]);
83
+
84
+ const hasData = allProviders.length > 0;
85
+
86
+ // Build a stable map of modelId → provider index for execution
87
+ const modelProviderPairs = useMemo(() => {
88
+ const pairs: Array<{ modelId: string; provider: ListDataProvider }> = [];
89
+ if (models.size > 0) {
90
+ let i = 0;
91
+ for (const [modelId] of models) {
92
+ pairs.push({ modelId, provider: allProviders[i++] });
93
+ }
94
+ } else if (allProviders.length > 0) {
95
+ pairs.push({ modelId: 'default', provider: allProviders[0] });
96
+ }
97
+ return pairs;
98
+ }, [models, allProviders]);
99
+
100
+ const handleExecuteList = useCallback((definition: ListDefinition) => {
101
+ if (!hasData) return;
102
+
103
+ setListExecuting(true);
104
+ setActiveListId(definition.id);
105
+ setEditingList(definition);
106
+
107
+ // Use requestAnimationFrame to avoid blocking UI during execution
108
+ requestAnimationFrame(() => {
109
+ try {
110
+ const resultParts: ListResult[] = [];
111
+ for (const { modelId, provider } of modelProviderPairs) {
112
+ resultParts.push(executeList(definition, provider, modelId));
113
+ }
114
+
115
+ const allRows = resultParts.flatMap(r => r.rows);
116
+ const totalTime = resultParts.reduce((sum, r) => sum + r.executionTime, 0);
117
+
118
+ setListResult({
119
+ columns: definition.columns,
120
+ rows: allRows,
121
+ totalCount: allRows.length,
122
+ executionTime: totalTime,
123
+ });
124
+ setView('results');
125
+ } catch (err) {
126
+ console.error('[Lists] Execution failed:', err);
127
+ } finally {
128
+ setListExecuting(false);
129
+ }
130
+ });
131
+ }, [hasData, modelProviderPairs, setActiveListId, setListResult, setListExecuting]);
132
+
133
+ const handleCreateNew = useCallback(() => {
134
+ setEditingList(null);
135
+ setView('builder');
136
+ }, []);
137
+
138
+ const handleEdit = useCallback((definition: ListDefinition) => {
139
+ setEditingList(definition);
140
+ setView('builder');
141
+ }, []);
142
+
143
+ const handleDuplicate = useCallback((definition: ListDefinition) => {
144
+ const clone: ListDefinition = {
145
+ ...definition,
146
+ id: crypto.randomUUID(),
147
+ name: `${definition.name} (Copy)`,
148
+ createdAt: Date.now(),
149
+ updatedAt: Date.now(),
150
+ };
151
+ addListDefinition(clone);
152
+ }, [addListDefinition]);
153
+
154
+ const handleSaveList = useCallback((definition: ListDefinition) => {
155
+ // Check if updating existing or adding new
156
+ const exists = listDefinitions.some(d => d.id === definition.id);
157
+ if (exists) {
158
+ updateListDefinition(definition.id, definition);
159
+ } else {
160
+ addListDefinition(definition);
161
+ }
162
+ setView('library');
163
+ }, [listDefinitions, addListDefinition, updateListDefinition]);
164
+
165
+ const handleDelete = useCallback((id: string) => {
166
+ deleteListDefinition(id);
167
+ }, [deleteListDefinition]);
168
+
169
+ const handleEditFromResults = useCallback(() => {
170
+ if (editingList) {
171
+ setView('builder');
172
+ }
173
+ }, [editingList]);
174
+
175
+ const handleExportCSV = useCallback(() => {
176
+ if (!listResult) return;
177
+ const csv = listResultToCSV(listResult);
178
+ const blob = new Blob([csv], { type: 'text/csv' });
179
+ const url = URL.createObjectURL(blob);
180
+ const a = document.createElement('a');
181
+ a.href = url;
182
+ a.download = 'list-export.csv';
183
+ a.click();
184
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
185
+ }, [listResult]);
186
+
187
+ const handleImport = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
188
+ const file = e.target.files?.[0];
189
+ if (!file) return;
190
+ try {
191
+ const definition = await importListDefinition(file);
192
+ addListDefinition(definition);
193
+ } catch (err) {
194
+ console.error('[Lists] Import failed:', err);
195
+ }
196
+ e.target.value = '';
197
+ }, [addListDefinition]);
198
+
199
+ const handleExportDefinition = useCallback((definition: ListDefinition) => {
200
+ exportListDefinition(definition);
201
+ }, []);
202
+
203
+ return (
204
+ <div className="flex flex-col h-full">
205
+ {/* Header */}
206
+ <div className="flex items-center justify-between px-3 py-2 border-b">
207
+ <div className="flex items-center gap-2">
208
+ <Table2 className="h-4 w-4" />
209
+ <span className="font-medium text-sm">
210
+ {view === 'library' && 'Lists'}
211
+ {view === 'builder' && (editingList ? 'Edit List' : 'New List')}
212
+ {view === 'results' && 'Results'}
213
+ </span>
214
+ {view === 'results' && listResult && (
215
+ <span className="text-xs text-muted-foreground">
216
+ ({listResult.totalCount} rows, {listResult.executionTime.toFixed(0)}ms)
217
+ </span>
218
+ )}
219
+ </div>
220
+ <div className="flex items-center gap-1">
221
+ {view === 'results' && (
222
+ <>
223
+ <Tooltip>
224
+ <TooltipTrigger asChild>
225
+ <Button variant="ghost" size="icon-sm" onClick={handleEditFromResults}>
226
+ <Settings2 className="h-3.5 w-3.5" />
227
+ </Button>
228
+ </TooltipTrigger>
229
+ <TooltipContent>Edit Configuration</TooltipContent>
230
+ </Tooltip>
231
+ <Tooltip>
232
+ <TooltipTrigger asChild>
233
+ <Button variant="ghost" size="icon-sm" onClick={handleExportCSV}>
234
+ <Download className="h-3.5 w-3.5" />
235
+ </Button>
236
+ </TooltipTrigger>
237
+ <TooltipContent>Export CSV</TooltipContent>
238
+ </Tooltip>
239
+ <Tooltip>
240
+ <TooltipTrigger asChild>
241
+ <Button variant="ghost" size="icon-sm" onClick={() => setView('library')}>
242
+ <Table2 className="h-3.5 w-3.5" />
243
+ </Button>
244
+ </TooltipTrigger>
245
+ <TooltipContent>Back to Lists</TooltipContent>
246
+ </Tooltip>
247
+ </>
248
+ )}
249
+ {view === 'builder' && (
250
+ <Button variant="ghost" size="sm" onClick={() => setView('library')} className="text-xs h-7">
251
+ Cancel
252
+ </Button>
253
+ )}
254
+ {onClose && (
255
+ <Button variant="ghost" size="icon-sm" onClick={onClose}>
256
+ <X className="h-3.5 w-3.5" />
257
+ </Button>
258
+ )}
259
+ </div>
260
+ </div>
261
+
262
+ {/* Content */}
263
+ {view === 'library' && (
264
+ <ListLibrary
265
+ definitions={listDefinitions}
266
+ activeListId={activeListId}
267
+ executing={listExecuting}
268
+ hasData={hasData}
269
+ onExecute={handleExecuteList}
270
+ onCreateNew={handleCreateNew}
271
+ onEdit={handleEdit}
272
+ onDuplicate={handleDuplicate}
273
+ onDelete={handleDelete}
274
+ onExport={handleExportDefinition}
275
+ onImport={() => importInputRef.current?.click()}
276
+ />
277
+ )}
278
+
279
+ {view === 'builder' && hasData && (
280
+ <ListBuilder
281
+ providers={allProviders}
282
+ initial={editingList}
283
+ onSave={handleSaveList}
284
+ onCancel={() => setView('library')}
285
+ onExecute={handleExecuteList}
286
+ />
287
+ )}
288
+
289
+ {view === 'results' && listResult && (
290
+ <ListResultsTable result={listResult} />
291
+ )}
292
+
293
+ {/* Hidden import input */}
294
+ <input
295
+ ref={importInputRef}
296
+ type="file"
297
+ accept=".json"
298
+ onChange={handleImport}
299
+ className="hidden"
300
+ />
301
+ </div>
302
+ );
303
+ }
304
+
305
+ // ============================================================================
306
+ // List Library Sub-Component
307
+ // ============================================================================
308
+
309
+ interface ListLibraryProps {
310
+ definitions: ListDefinition[];
311
+ activeListId: string | null;
312
+ executing: boolean;
313
+ hasData: boolean;
314
+ onExecute: (def: ListDefinition) => void;
315
+ onCreateNew: () => void;
316
+ onEdit: (def: ListDefinition) => void;
317
+ onDuplicate: (def: ListDefinition) => void;
318
+ onDelete: (id: string) => void;
319
+ onExport: (def: ListDefinition) => void;
320
+ onImport: () => void;
321
+ }
322
+
323
+ function ListLibrary({
324
+ definitions,
325
+ activeListId,
326
+ executing,
327
+ hasData,
328
+ onExecute,
329
+ onCreateNew,
330
+ onEdit,
331
+ onDuplicate,
332
+ onDelete,
333
+ onExport,
334
+ onImport,
335
+ }: ListLibraryProps) {
336
+ return (
337
+ <div className="flex-1 flex flex-col min-h-0">
338
+ {/* Actions */}
339
+ <div className="flex items-center gap-1 px-3 py-2 border-b">
340
+ <Button
341
+ variant="outline"
342
+ size="sm"
343
+ onClick={onCreateNew}
344
+ disabled={!hasData}
345
+ className="text-xs h-7"
346
+ >
347
+ <Plus className="h-3 w-3 mr-1" />
348
+ New List
349
+ </Button>
350
+ <Button variant="ghost" size="sm" onClick={onImport} className="text-xs h-7">
351
+ <Upload className="h-3 w-3 mr-1" />
352
+ Import
353
+ </Button>
354
+ </div>
355
+
356
+ <ScrollArea className="flex-1">
357
+ {/* User's saved lists */}
358
+ {definitions.length > 0 && (
359
+ <div className="px-3 py-2">
360
+ <span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
361
+ Saved Lists
362
+ </span>
363
+ <div className="mt-1 space-y-1">
364
+ {definitions.map(def => (
365
+ <ListItem
366
+ key={def.id}
367
+ definition={def}
368
+ isActive={activeListId === def.id}
369
+ executing={executing && activeListId === def.id}
370
+ hasData={hasData}
371
+ onExecute={onExecute}
372
+ onEdit={onEdit}
373
+ onDuplicate={onDuplicate}
374
+ onDelete={onDelete}
375
+ onExport={onExport}
376
+ />
377
+ ))}
378
+ </div>
379
+ </div>
380
+ )}
381
+
382
+ {definitions.length > 0 && <Separator className="my-1" />}
383
+
384
+ {/* Presets */}
385
+ <div className="px-3 py-2">
386
+ <span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">
387
+ Templates
388
+ </span>
389
+ <div className="mt-1 space-y-1">
390
+ {LIST_PRESETS.map(preset => (
391
+ <ListItem
392
+ key={preset.id}
393
+ definition={preset}
394
+ isActive={activeListId === preset.id}
395
+ executing={executing && activeListId === preset.id}
396
+ hasData={hasData}
397
+ onExecute={onExecute}
398
+ onDuplicate={onDuplicate}
399
+ isPreset
400
+ />
401
+ ))}
402
+ </div>
403
+ </div>
404
+ </ScrollArea>
405
+ </div>
406
+ );
407
+ }
408
+
409
+ // ============================================================================
410
+ // List Item
411
+ // ============================================================================
412
+
413
+ interface ListItemProps {
414
+ definition: ListDefinition;
415
+ isActive: boolean;
416
+ executing: boolean;
417
+ hasData: boolean;
418
+ onExecute: (def: ListDefinition) => void;
419
+ onEdit?: (def: ListDefinition) => void;
420
+ onDuplicate?: (def: ListDefinition) => void;
421
+ onDelete?: (id: string) => void;
422
+ onExport?: (def: ListDefinition) => void;
423
+ isPreset?: boolean;
424
+ }
425
+
426
+ function ListItem({ definition, isActive, executing, hasData, onExecute, onEdit, onDuplicate, onDelete, onExport, isPreset }: ListItemProps) {
427
+ return (
428
+ <div
429
+ className={`group flex items-center gap-2 px-2 py-1.5 rounded-md text-sm cursor-pointer hover:bg-muted/50 ${
430
+ isActive ? 'bg-muted' : ''
431
+ }`}
432
+ onClick={() => hasData && onExecute(definition)}
433
+ >
434
+ <FileSpreadsheet className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
435
+ <div className="flex-1 min-w-0">
436
+ <div className="truncate text-xs font-medium">{definition.name}</div>
437
+ {definition.description && (
438
+ <div className="truncate text-xs text-muted-foreground">{definition.description}</div>
439
+ )}
440
+ </div>
441
+ <div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
442
+ {executing ? (
443
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
444
+ ) : (
445
+ <>
446
+ <Tooltip>
447
+ <TooltipTrigger asChild>
448
+ <Button
449
+ variant="ghost"
450
+ size="icon-sm"
451
+ className="h-6 w-6"
452
+ onClick={(e) => {
453
+ e.stopPropagation();
454
+ if (hasData) onExecute(definition);
455
+ }}
456
+ disabled={!hasData}
457
+ >
458
+ <Play className="h-3 w-3" />
459
+ </Button>
460
+ </TooltipTrigger>
461
+ <TooltipContent>Run</TooltipContent>
462
+ </Tooltip>
463
+ {!isPreset && onEdit && (
464
+ <Tooltip>
465
+ <TooltipTrigger asChild>
466
+ <Button
467
+ variant="ghost"
468
+ size="icon-sm"
469
+ className="h-6 w-6"
470
+ onClick={(e) => {
471
+ e.stopPropagation();
472
+ onEdit(definition);
473
+ }}
474
+ >
475
+ <Pencil className="h-3 w-3" />
476
+ </Button>
477
+ </TooltipTrigger>
478
+ <TooltipContent>Edit</TooltipContent>
479
+ </Tooltip>
480
+ )}
481
+ {onDuplicate && (
482
+ <Tooltip>
483
+ <TooltipTrigger asChild>
484
+ <Button
485
+ variant="ghost"
486
+ size="icon-sm"
487
+ className="h-6 w-6"
488
+ onClick={(e) => {
489
+ e.stopPropagation();
490
+ onDuplicate(definition);
491
+ }}
492
+ >
493
+ <Copy className="h-3 w-3" />
494
+ </Button>
495
+ </TooltipTrigger>
496
+ <TooltipContent>{isPreset ? 'Use as Template' : 'Duplicate'}</TooltipContent>
497
+ </Tooltip>
498
+ )}
499
+ {!isPreset && onExport && (
500
+ <Tooltip>
501
+ <TooltipTrigger asChild>
502
+ <Button
503
+ variant="ghost"
504
+ size="icon-sm"
505
+ className="h-6 w-6"
506
+ onClick={(e) => {
507
+ e.stopPropagation();
508
+ onExport(definition);
509
+ }}
510
+ >
511
+ <Download className="h-3 w-3" />
512
+ </Button>
513
+ </TooltipTrigger>
514
+ <TooltipContent>Export</TooltipContent>
515
+ </Tooltip>
516
+ )}
517
+ {!isPreset && onDelete && (
518
+ <Tooltip>
519
+ <TooltipTrigger asChild>
520
+ <Button
521
+ variant="ghost"
522
+ size="icon-sm"
523
+ className="h-6 w-6 hover:text-destructive"
524
+ onClick={(e) => {
525
+ e.stopPropagation();
526
+ onDelete(definition.id);
527
+ }}
528
+ >
529
+ <Trash2 className="h-3 w-3" />
530
+ </Button>
531
+ </TooltipTrigger>
532
+ <TooltipContent>Delete</TooltipContent>
533
+ </Tooltip>
534
+ )}
535
+ </>
536
+ )}
537
+ </div>
538
+ </div>
539
+ );
540
+ }