@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,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
- <Tooltip>
539
- <TooltipTrigger asChild>
540
- <Button variant="ghost" size="sm" className="h-8 w-8 p-0" onClick={exportReportJSON}>
541
- <FileJson className="h-4 w-4 text-blue-500" />
542
- </Button>
543
- </TooltipTrigger>
544
- <TooltipContent>Export JSON Report</TooltipContent>
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();