@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,281 @@
|
|
|
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
|
+
* IDS BCF Export Dialog
|
|
7
|
+
*
|
|
8
|
+
* Provides a configuration dialog for exporting IDS validation results to BCF.
|
|
9
|
+
* Options include:
|
|
10
|
+
* - Topic grouping strategy (per-entity, per-specification, per-requirement)
|
|
11
|
+
* - Include passing entities
|
|
12
|
+
* - Include per-entity camera positions (from entity bounds)
|
|
13
|
+
* - Capture per-entity snapshots (batch render)
|
|
14
|
+
* - Load into BCF panel after export
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useState, useCallback } from 'react';
|
|
18
|
+
import {
|
|
19
|
+
FileBox,
|
|
20
|
+
Loader2,
|
|
21
|
+
Camera,
|
|
22
|
+
Focus,
|
|
23
|
+
Upload,
|
|
24
|
+
} from 'lucide-react';
|
|
25
|
+
import { Button } from '@/components/ui/button';
|
|
26
|
+
import { Label } from '@/components/ui/label';
|
|
27
|
+
import { Switch } from '@/components/ui/switch';
|
|
28
|
+
import { Progress } from '@/components/ui/progress';
|
|
29
|
+
import {
|
|
30
|
+
Select,
|
|
31
|
+
SelectContent,
|
|
32
|
+
SelectItem,
|
|
33
|
+
SelectTrigger,
|
|
34
|
+
SelectValue,
|
|
35
|
+
} from '@/components/ui/select';
|
|
36
|
+
import {
|
|
37
|
+
Dialog,
|
|
38
|
+
DialogContent,
|
|
39
|
+
DialogDescription,
|
|
40
|
+
DialogFooter,
|
|
41
|
+
DialogHeader,
|
|
42
|
+
DialogTitle,
|
|
43
|
+
DialogTrigger,
|
|
44
|
+
} from '@/components/ui/dialog';
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// Types
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
export type TopicGrouping = 'per-entity' | 'per-specification' | 'per-requirement';
|
|
51
|
+
|
|
52
|
+
export interface IDSBCFExportSettings {
|
|
53
|
+
topicGrouping: TopicGrouping;
|
|
54
|
+
includePassingEntities: boolean;
|
|
55
|
+
includeCamera: boolean;
|
|
56
|
+
includeSnapshots: boolean;
|
|
57
|
+
loadIntoBcfPanel: boolean;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface IDSExportProgress {
|
|
61
|
+
phase: 'building' | 'snapshots' | 'writing' | 'done';
|
|
62
|
+
current: number;
|
|
63
|
+
total: number;
|
|
64
|
+
message: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
interface IDSExportDialogProps {
|
|
68
|
+
/** Trigger element (e.g., a button) — only used for uncontrolled mode */
|
|
69
|
+
trigger?: React.ReactNode;
|
|
70
|
+
/** Whether a report is available */
|
|
71
|
+
hasReport: boolean;
|
|
72
|
+
/** Total failing entity count for display */
|
|
73
|
+
failedCount: number;
|
|
74
|
+
/** Called when export is confirmed */
|
|
75
|
+
onExport: (settings: IDSBCFExportSettings) => Promise<void>;
|
|
76
|
+
/** Export progress (controlled externally) */
|
|
77
|
+
progress: IDSExportProgress | null;
|
|
78
|
+
/** Controlled open state (if provided, dialog is controlled externally) */
|
|
79
|
+
open?: boolean;
|
|
80
|
+
/** Controlled open state callback */
|
|
81
|
+
onOpenChange?: (open: boolean) => void;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// Component
|
|
86
|
+
// ============================================================================
|
|
87
|
+
|
|
88
|
+
export function IDSExportDialog({
|
|
89
|
+
trigger,
|
|
90
|
+
hasReport,
|
|
91
|
+
failedCount,
|
|
92
|
+
onExport,
|
|
93
|
+
progress,
|
|
94
|
+
open: controlledOpen,
|
|
95
|
+
onOpenChange: controlledOnOpenChange,
|
|
96
|
+
}: IDSExportDialogProps) {
|
|
97
|
+
const [internalOpen, setInternalOpen] = useState(false);
|
|
98
|
+
const open = controlledOpen ?? internalOpen;
|
|
99
|
+
const setOpen = controlledOnOpenChange ?? setInternalOpen;
|
|
100
|
+
const [settings, setSettings] = useState<IDSBCFExportSettings>({
|
|
101
|
+
topicGrouping: 'per-entity',
|
|
102
|
+
includePassingEntities: false,
|
|
103
|
+
includeCamera: true,
|
|
104
|
+
includeSnapshots: false,
|
|
105
|
+
loadIntoBcfPanel: false,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const isExporting = progress !== null && progress.phase !== 'done';
|
|
109
|
+
|
|
110
|
+
const handleExport = useCallback(async () => {
|
|
111
|
+
await onExport(settings);
|
|
112
|
+
// Don't close — let the progress indicator finish, then user closes
|
|
113
|
+
}, [onExport, settings]);
|
|
114
|
+
|
|
115
|
+
const handleOpenChange = useCallback((value: boolean) => {
|
|
116
|
+
// Don't allow closing during export
|
|
117
|
+
if (isExporting) return;
|
|
118
|
+
setOpen(value);
|
|
119
|
+
}, [isExporting, setOpen]);
|
|
120
|
+
|
|
121
|
+
const progressPercent = progress && progress.total > 0
|
|
122
|
+
? Math.round((progress.current / progress.total) * 100)
|
|
123
|
+
: 0;
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<Dialog open={open} onOpenChange={handleOpenChange}>
|
|
127
|
+
{trigger && (
|
|
128
|
+
<DialogTrigger asChild>
|
|
129
|
+
{trigger}
|
|
130
|
+
</DialogTrigger>
|
|
131
|
+
)}
|
|
132
|
+
<DialogContent className="sm:max-w-md">
|
|
133
|
+
<DialogHeader>
|
|
134
|
+
<DialogTitle className="flex items-center gap-2">
|
|
135
|
+
<FileBox className="h-5 w-5 text-green-500" />
|
|
136
|
+
Export IDS Report as BCF
|
|
137
|
+
</DialogTitle>
|
|
138
|
+
<DialogDescription>
|
|
139
|
+
Create BCF topics from IDS validation failures.
|
|
140
|
+
{failedCount > 0 && ` ${failedCount} failing entities found.`}
|
|
141
|
+
</DialogDescription>
|
|
142
|
+
</DialogHeader>
|
|
143
|
+
|
|
144
|
+
<div className="grid gap-4 py-4">
|
|
145
|
+
{/* Topic Grouping */}
|
|
146
|
+
<div className="grid gap-2">
|
|
147
|
+
<Label htmlFor="grouping">Topic Grouping</Label>
|
|
148
|
+
<Select
|
|
149
|
+
value={settings.topicGrouping}
|
|
150
|
+
onValueChange={(v) => setSettings(s => ({
|
|
151
|
+
...s,
|
|
152
|
+
topicGrouping: v as TopicGrouping,
|
|
153
|
+
// Reset includePassingEntities when switching away from per-entity (only valid in per-entity mode)
|
|
154
|
+
...(v !== 'per-entity' && { includePassingEntities: false }),
|
|
155
|
+
}))}
|
|
156
|
+
disabled={isExporting}
|
|
157
|
+
>
|
|
158
|
+
<SelectTrigger id="grouping">
|
|
159
|
+
<SelectValue />
|
|
160
|
+
</SelectTrigger>
|
|
161
|
+
<SelectContent>
|
|
162
|
+
<SelectItem value="per-entity">Per Entity (recommended)</SelectItem>
|
|
163
|
+
<SelectItem value="per-specification">Per Specification</SelectItem>
|
|
164
|
+
<SelectItem value="per-requirement">Per Requirement</SelectItem>
|
|
165
|
+
</SelectContent>
|
|
166
|
+
</Select>
|
|
167
|
+
<p className="text-xs text-muted-foreground">
|
|
168
|
+
{settings.topicGrouping === 'per-entity' && 'One topic per failing entity. Failed requirements listed as comments.'}
|
|
169
|
+
{settings.topicGrouping === 'per-specification' && 'One topic per failing specification. Entities listed as comments.'}
|
|
170
|
+
{settings.topicGrouping === 'per-requirement' && 'One topic per failed requirement per entity (most granular).'}
|
|
171
|
+
</p>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{/* Include Passing */}
|
|
175
|
+
<div className="flex items-center justify-between">
|
|
176
|
+
<div className="space-y-0.5">
|
|
177
|
+
<Label htmlFor="include-passing">Include Passing Entities</Label>
|
|
178
|
+
<p className="text-xs text-muted-foreground">Add topics for entities that passed validation</p>
|
|
179
|
+
</div>
|
|
180
|
+
<Switch
|
|
181
|
+
id="include-passing"
|
|
182
|
+
checked={settings.includePassingEntities}
|
|
183
|
+
onCheckedChange={(v) => setSettings(s => ({ ...s, includePassingEntities: v }))}
|
|
184
|
+
disabled={isExporting || settings.topicGrouping !== 'per-entity'}
|
|
185
|
+
/>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
{/* Include Camera */}
|
|
189
|
+
<div className="flex items-center justify-between">
|
|
190
|
+
<div className="space-y-0.5">
|
|
191
|
+
<Label htmlFor="include-camera" className="flex items-center gap-1.5">
|
|
192
|
+
<Focus className="h-3.5 w-3.5" />
|
|
193
|
+
Per-Entity Camera
|
|
194
|
+
</Label>
|
|
195
|
+
<p className="text-xs text-muted-foreground">Compute camera framing each entity from its bounding box</p>
|
|
196
|
+
</div>
|
|
197
|
+
<Switch
|
|
198
|
+
id="include-camera"
|
|
199
|
+
checked={settings.includeCamera}
|
|
200
|
+
onCheckedChange={(v) => setSettings(s => ({ ...s, includeCamera: v }))}
|
|
201
|
+
disabled={isExporting}
|
|
202
|
+
/>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
{/* Include Snapshots */}
|
|
206
|
+
<div className="flex items-center justify-between">
|
|
207
|
+
<div className="space-y-0.5">
|
|
208
|
+
<Label htmlFor="include-snapshots" className="flex items-center gap-1.5">
|
|
209
|
+
<Camera className="h-3.5 w-3.5" />
|
|
210
|
+
Capture Snapshots
|
|
211
|
+
</Label>
|
|
212
|
+
<p className="text-xs text-muted-foreground">
|
|
213
|
+
Render a screenshot for each entity (slow for large reports)
|
|
214
|
+
</p>
|
|
215
|
+
</div>
|
|
216
|
+
<Switch
|
|
217
|
+
id="include-snapshots"
|
|
218
|
+
checked={settings.includeSnapshots}
|
|
219
|
+
onCheckedChange={(v) => setSettings(s => ({ ...s, includeSnapshots: v }))}
|
|
220
|
+
disabled={isExporting}
|
|
221
|
+
/>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
{/* Load into BCF Panel */}
|
|
225
|
+
<div className="flex items-center justify-between">
|
|
226
|
+
<div className="space-y-0.5">
|
|
227
|
+
<Label htmlFor="load-panel" className="flex items-center gap-1.5">
|
|
228
|
+
<Upload className="h-3.5 w-3.5" />
|
|
229
|
+
Load into BCF Panel
|
|
230
|
+
</Label>
|
|
231
|
+
<p className="text-xs text-muted-foreground">Open the BCF panel with exported topics after export</p>
|
|
232
|
+
</div>
|
|
233
|
+
<Switch
|
|
234
|
+
id="load-panel"
|
|
235
|
+
checked={settings.loadIntoBcfPanel}
|
|
236
|
+
onCheckedChange={(v) => setSettings(s => ({ ...s, loadIntoBcfPanel: v }))}
|
|
237
|
+
disabled={isExporting}
|
|
238
|
+
/>
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
{/* Progress */}
|
|
242
|
+
{progress && (
|
|
243
|
+
<div className="space-y-2 pt-2 border-t">
|
|
244
|
+
<div className="flex items-center justify-between text-sm">
|
|
245
|
+
<span className="text-muted-foreground">{progress.message}</span>
|
|
246
|
+
<span className="font-mono text-xs">{progress.current}/{progress.total}</span>
|
|
247
|
+
</div>
|
|
248
|
+
<Progress value={progressPercent} className="h-2" />
|
|
249
|
+
</div>
|
|
250
|
+
)}
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
<DialogFooter>
|
|
254
|
+
<Button
|
|
255
|
+
variant="outline"
|
|
256
|
+
onClick={() => setOpen(false)}
|
|
257
|
+
disabled={isExporting}
|
|
258
|
+
>
|
|
259
|
+
{progress?.phase === 'done' ? 'Close' : 'Cancel'}
|
|
260
|
+
</Button>
|
|
261
|
+
<Button
|
|
262
|
+
onClick={handleExport}
|
|
263
|
+
disabled={isExporting || !hasReport}
|
|
264
|
+
>
|
|
265
|
+
{isExporting ? (
|
|
266
|
+
<>
|
|
267
|
+
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
268
|
+
Exporting...
|
|
269
|
+
</>
|
|
270
|
+
) : (
|
|
271
|
+
<>
|
|
272
|
+
<FileBox className="h-4 w-4 mr-2" />
|
|
273
|
+
Export BCF
|
|
274
|
+
</>
|
|
275
|
+
)}
|
|
276
|
+
</Button>
|
|
277
|
+
</DialogFooter>
|
|
278
|
+
</DialogContent>
|
|
279
|
+
</Dialog>
|
|
280
|
+
);
|
|
281
|
+
}
|
|
@@ -36,6 +36,8 @@ import {
|
|
|
36
36
|
Trash2,
|
|
37
37
|
FileJson,
|
|
38
38
|
FileCode,
|
|
39
|
+
FileBox,
|
|
40
|
+
Download,
|
|
39
41
|
} from 'lucide-react';
|
|
40
42
|
import { Button } from '@/components/ui/button';
|
|
41
43
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
@@ -55,6 +57,13 @@ import {
|
|
|
55
57
|
CollapsibleTrigger,
|
|
56
58
|
} from '@/components/ui/collapsible';
|
|
57
59
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
60
|
+
import {
|
|
61
|
+
DropdownMenu,
|
|
62
|
+
DropdownMenuContent,
|
|
63
|
+
DropdownMenuItem,
|
|
64
|
+
DropdownMenuSeparator,
|
|
65
|
+
DropdownMenuTrigger,
|
|
66
|
+
} from '@/components/ui/dropdown-menu';
|
|
58
67
|
import { useIDS } from '@/hooks/useIDS';
|
|
59
68
|
import type {
|
|
60
69
|
IDSSpecificationResult,
|
|
@@ -62,6 +71,8 @@ import type {
|
|
|
62
71
|
IDSRequirementResult,
|
|
63
72
|
} from '@ifc-lite/ids';
|
|
64
73
|
import { cn } from '@/lib/utils';
|
|
74
|
+
import { IDSExportDialog } from './IDSExportDialog';
|
|
75
|
+
import type { IDSBCFExportSettings, IDSExportProgress } from './IDSExportDialog';
|
|
65
76
|
|
|
66
77
|
// ============================================================================
|
|
67
78
|
// Types
|
|
@@ -323,6 +334,112 @@ function RequirementResultRow({ result }: RequirementResultRowProps) {
|
|
|
323
334
|
);
|
|
324
335
|
}
|
|
325
336
|
|
|
337
|
+
// ============================================================================
|
|
338
|
+
// Report Export Split Button
|
|
339
|
+
// ============================================================================
|
|
340
|
+
|
|
341
|
+
type ExportFormat = 'html' | 'json' | 'bcf';
|
|
342
|
+
|
|
343
|
+
const FORMAT_LABELS: Record<ExportFormat, string> = {
|
|
344
|
+
html: 'HTML',
|
|
345
|
+
json: 'JSON',
|
|
346
|
+
bcf: 'BCF',
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
interface ReportExportButtonProps {
|
|
350
|
+
onExportJSON: () => void;
|
|
351
|
+
onExportHTML: () => void;
|
|
352
|
+
onExportBCF: (settings: IDSBCFExportSettings) => Promise<void>;
|
|
353
|
+
bcfExportProgress: IDSExportProgress | null;
|
|
354
|
+
report: ReturnType<typeof useIDS>['report'];
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function ReportExportButton({
|
|
358
|
+
onExportJSON,
|
|
359
|
+
onExportHTML,
|
|
360
|
+
onExportBCF,
|
|
361
|
+
bcfExportProgress,
|
|
362
|
+
report,
|
|
363
|
+
}: ReportExportButtonProps) {
|
|
364
|
+
const [lastFormat, setLastFormat] = useState<ExportFormat>('html');
|
|
365
|
+
const [bcfDialogOpen, setBcfDialogOpen] = useState(false);
|
|
366
|
+
|
|
367
|
+
const handleDirectExport = useCallback(() => {
|
|
368
|
+
if (lastFormat === 'html') onExportHTML();
|
|
369
|
+
else if (lastFormat === 'json') onExportJSON();
|
|
370
|
+
else setBcfDialogOpen(true);
|
|
371
|
+
}, [lastFormat, onExportHTML, onExportJSON]);
|
|
372
|
+
|
|
373
|
+
const handleSelectFormat = useCallback((format: ExportFormat) => {
|
|
374
|
+
setLastFormat(format);
|
|
375
|
+
if (format === 'html') onExportHTML();
|
|
376
|
+
else if (format === 'json') onExportJSON();
|
|
377
|
+
else setBcfDialogOpen(true);
|
|
378
|
+
}, [onExportHTML, onExportJSON]);
|
|
379
|
+
|
|
380
|
+
const label = FORMAT_LABELS[lastFormat];
|
|
381
|
+
|
|
382
|
+
return (
|
|
383
|
+
<>
|
|
384
|
+
<Tooltip>
|
|
385
|
+
<TooltipTrigger asChild>
|
|
386
|
+
<div className="flex items-center">
|
|
387
|
+
<Button
|
|
388
|
+
variant="outline"
|
|
389
|
+
size="sm"
|
|
390
|
+
className="h-8 px-2 rounded-r-none border-r-0 gap-1.5"
|
|
391
|
+
onClick={handleDirectExport}
|
|
392
|
+
>
|
|
393
|
+
<Download className="h-3.5 w-3.5" />
|
|
394
|
+
<span className="text-xs">{label}</span>
|
|
395
|
+
</Button>
|
|
396
|
+
<DropdownMenu>
|
|
397
|
+
<DropdownMenuTrigger asChild>
|
|
398
|
+
<Button
|
|
399
|
+
variant="outline"
|
|
400
|
+
size="sm"
|
|
401
|
+
className="h-8 w-6 p-0 rounded-l-none"
|
|
402
|
+
>
|
|
403
|
+
<ChevronDown className="h-3 w-3" />
|
|
404
|
+
</Button>
|
|
405
|
+
</DropdownMenuTrigger>
|
|
406
|
+
<DropdownMenuContent align="end" className="w-44">
|
|
407
|
+
<DropdownMenuItem onClick={() => handleSelectFormat('html')}>
|
|
408
|
+
<FileCode className="h-4 w-4 text-orange-500 mr-2" />
|
|
409
|
+
HTML Report
|
|
410
|
+
{lastFormat === 'html' && <span className="ml-auto text-xs text-muted-foreground">default</span>}
|
|
411
|
+
</DropdownMenuItem>
|
|
412
|
+
<DropdownMenuItem onClick={() => handleSelectFormat('json')}>
|
|
413
|
+
<FileJson className="h-4 w-4 text-blue-500 mr-2" />
|
|
414
|
+
JSON Report
|
|
415
|
+
{lastFormat === 'json' && <span className="ml-auto text-xs text-muted-foreground">default</span>}
|
|
416
|
+
</DropdownMenuItem>
|
|
417
|
+
<DropdownMenuSeparator />
|
|
418
|
+
<DropdownMenuItem onClick={() => handleSelectFormat('bcf')}>
|
|
419
|
+
<FileBox className="h-4 w-4 text-green-500 mr-2" />
|
|
420
|
+
BCF Report...
|
|
421
|
+
{lastFormat === 'bcf' && <span className="ml-auto text-xs text-muted-foreground">default</span>}
|
|
422
|
+
</DropdownMenuItem>
|
|
423
|
+
</DropdownMenuContent>
|
|
424
|
+
</DropdownMenu>
|
|
425
|
+
</div>
|
|
426
|
+
</TooltipTrigger>
|
|
427
|
+
<TooltipContent>Export Report ({label})</TooltipContent>
|
|
428
|
+
</Tooltip>
|
|
429
|
+
|
|
430
|
+
{/* BCF Export Dialog (controlled open) */}
|
|
431
|
+
<IDSExportDialog
|
|
432
|
+
hasReport={!!report}
|
|
433
|
+
failedCount={report?.specificationResults.reduce((sum, s) => sum + s.failedCount, 0) ?? 0}
|
|
434
|
+
onExport={onExportBCF}
|
|
435
|
+
progress={bcfExportProgress}
|
|
436
|
+
open={bcfDialogOpen}
|
|
437
|
+
onOpenChange={setBcfDialogOpen}
|
|
438
|
+
/>
|
|
439
|
+
</>
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
326
443
|
// ============================================================================
|
|
327
444
|
// Main Panel Component
|
|
328
445
|
// ============================================================================
|
|
@@ -354,6 +471,8 @@ export function IDSPanel({ onClose }: IDSPanelProps) {
|
|
|
354
471
|
clearIsolation,
|
|
355
472
|
exportReportJSON,
|
|
356
473
|
exportReportHTML,
|
|
474
|
+
exportReportBCF,
|
|
475
|
+
bcfExportProgress,
|
|
357
476
|
} = useIDS();
|
|
358
477
|
|
|
359
478
|
// Handle file selection
|
|
@@ -535,23 +654,13 @@ export function IDSPanel({ onClose }: IDSPanelProps) {
|
|
|
535
654
|
|
|
536
655
|
<Separator orientation="vertical" className="h-4 mx-1" />
|
|
537
656
|
|
|
538
|
-
<
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
</Tooltip>
|
|
546
|
-
|
|
547
|
-
<Tooltip>
|
|
548
|
-
<TooltipTrigger asChild>
|
|
549
|
-
<Button variant="ghost" size="sm" className="h-8 w-8 p-0" onClick={exportReportHTML}>
|
|
550
|
-
<FileCode className="h-4 w-4 text-orange-500" />
|
|
551
|
-
</Button>
|
|
552
|
-
</TooltipTrigger>
|
|
553
|
-
<TooltipContent>Export HTML Report</TooltipContent>
|
|
554
|
-
</Tooltip>
|
|
657
|
+
<ReportExportButton
|
|
658
|
+
onExportJSON={exportReportJSON}
|
|
659
|
+
onExportHTML={exportReportHTML}
|
|
660
|
+
onExportBCF={exportReportBCF}
|
|
661
|
+
bcfExportProgress={bcfExportProgress}
|
|
662
|
+
report={report}
|
|
663
|
+
/>
|
|
555
664
|
</div>
|
|
556
665
|
|
|
557
666
|
{/* Specifications List */}
|
|
@@ -297,6 +297,15 @@ export function useKeyboardShortcutsDialog() {
|
|
|
297
297
|
// Listen for '?' key to toggle
|
|
298
298
|
useEffect(() => {
|
|
299
299
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
300
|
+
// Ignore if typing in an input or textarea
|
|
301
|
+
const target = e.target as HTMLElement;
|
|
302
|
+
if (
|
|
303
|
+
target.tagName === 'INPUT' ||
|
|
304
|
+
target.tagName === 'TEXTAREA' ||
|
|
305
|
+
target.isContentEditable
|
|
306
|
+
) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
300
309
|
if (e.key === '?' || (e.key === '/' && e.shiftKey)) {
|
|
301
310
|
e.preventDefault();
|
|
302
311
|
toggle();
|