@ifc-lite/viewer 1.1.7 → 1.5.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 (164) hide show
  1. package/LICENSE +373 -0
  2. package/dist/apple-touch-icon.png +0 -0
  3. package/dist/assets/Arrow.dom-B0e15b_b.js +20 -0
  4. package/dist/assets/arrow2-bb-jcVEo.js +2 -0
  5. package/dist/assets/arrow2_bg-4Y7xYo54.wasm +0 -0
  6. package/dist/assets/arrow2_bg-BlXl-cSQ.js +1 -0
  7. package/dist/assets/arrow2_bg-BoXCojjR.wasm +0 -0
  8. package/dist/assets/desktop-cache-oPzaWXYE.js +1 -0
  9. package/dist/assets/event-DIOks52T.js +1 -0
  10. package/dist/assets/ifc-cache-BAN4vcd4.js +1 -0
  11. package/dist/assets/ifc-lite_bg-C6kblxf9.wasm +0 -0
  12. package/dist/assets/index-Dgd6vzw_.js +65252 -0
  13. package/dist/assets/index-v3mcCUPN.css +1 -0
  14. package/dist/assets/native-bridge-Ci7NLjlZ.js +111 -0
  15. package/dist/assets/wasm-bridge-Dc82YpdZ.js +1 -0
  16. package/dist/favicon-16x16-cropped.png +0 -0
  17. package/dist/favicon-16x16.png +0 -0
  18. package/dist/favicon-192x192-cropped.png +0 -0
  19. package/dist/favicon-192x192.png +0 -0
  20. package/dist/favicon-32x32-cropped.png +0 -0
  21. package/dist/favicon-32x32.png +0 -0
  22. package/dist/favicon-48x48-cropped.png +0 -0
  23. package/dist/favicon-48x48.png +0 -0
  24. package/dist/favicon-512x512-cropped.png +0 -0
  25. package/dist/favicon-512x512.png +0 -0
  26. package/dist/favicon-64x64-cropped.png +0 -0
  27. package/dist/favicon-64x64.png +0 -0
  28. package/dist/favicon-96x96-cropped.png +0 -0
  29. package/dist/favicon-96x96.png +0 -0
  30. package/dist/favicon-square-512.png +0 -0
  31. package/dist/favicon.ico +0 -0
  32. package/dist/favicon.png +0 -0
  33. package/dist/favicon.svg +3 -0
  34. package/dist/index.html +44 -0
  35. package/dist/logo.png +0 -0
  36. package/dist/manifest.json +48 -0
  37. package/index.html +33 -2
  38. package/package.json +34 -17
  39. package/public/apple-touch-icon.png +0 -0
  40. package/public/favicon-16x16-cropped.png +0 -0
  41. package/public/favicon-16x16.png +0 -0
  42. package/public/favicon-192x192-cropped.png +0 -0
  43. package/public/favicon-192x192.png +0 -0
  44. package/public/favicon-32x32-cropped.png +0 -0
  45. package/public/favicon-32x32.png +0 -0
  46. package/public/favicon-48x48-cropped.png +0 -0
  47. package/public/favicon-48x48.png +0 -0
  48. package/public/favicon-512x512-cropped.png +0 -0
  49. package/public/favicon-512x512.png +0 -0
  50. package/public/favicon-64x64-cropped.png +0 -0
  51. package/public/favicon-64x64.png +0 -0
  52. package/public/favicon-96x96-cropped.png +0 -0
  53. package/public/favicon-96x96.png +0 -0
  54. package/public/favicon-square-512.png +0 -0
  55. package/public/favicon.ico +0 -0
  56. package/public/favicon.png +0 -0
  57. package/public/favicon.svg +3 -0
  58. package/public/logo.png +0 -0
  59. package/public/manifest.json +48 -0
  60. package/src/App.tsx +2 -0
  61. package/src/components/ui/alert.tsx +62 -0
  62. package/src/components/ui/badge.tsx +39 -0
  63. package/src/components/ui/dialog.tsx +120 -0
  64. package/src/components/ui/label.tsx +27 -0
  65. package/src/components/ui/select.tsx +151 -0
  66. package/src/components/ui/switch.tsx +30 -0
  67. package/src/components/ui/table.tsx +120 -0
  68. package/src/components/ui/tabs.tsx +1 -1
  69. package/src/components/viewer/BCFPanel.tsx +1164 -0
  70. package/src/components/viewer/BulkPropertyEditor.tsx +875 -0
  71. package/src/components/viewer/DataConnector.tsx +840 -0
  72. package/src/components/viewer/DrawingSettingsPanel.tsx +536 -0
  73. package/src/components/viewer/EntityContextMenu.tsx +45 -17
  74. package/src/components/viewer/ExportChangesButton.tsx +195 -0
  75. package/src/components/viewer/ExportDialog.tsx +402 -0
  76. package/src/components/viewer/HierarchyPanel.tsx +1132 -218
  77. package/src/components/viewer/IDSPanel.tsx +661 -0
  78. package/src/components/viewer/KeyboardShortcutsDialog.tsx +245 -39
  79. package/src/components/viewer/MainToolbar.tsx +418 -94
  80. package/src/components/viewer/PropertiesPanel.tsx +1355 -91
  81. package/src/components/viewer/PropertyEditor.tsx +611 -0
  82. package/src/components/viewer/Section2DPanel.tsx +3313 -0
  83. package/src/components/viewer/SheetSetupPanel.tsx +502 -0
  84. package/src/components/viewer/StatusBar.tsx +27 -16
  85. package/src/components/viewer/TitleBlockEditor.tsx +437 -0
  86. package/src/components/viewer/ToolOverlays.tsx +935 -127
  87. package/src/components/viewer/ViewerLayout.tsx +40 -11
  88. package/src/components/viewer/Viewport.tsx +1276 -336
  89. package/src/components/viewer/ViewportContainer.tsx +554 -18
  90. package/src/components/viewer/ViewportOverlays.tsx +24 -7
  91. package/src/hooks/useBCF.ts +504 -0
  92. package/src/hooks/useIDS.ts +1065 -0
  93. package/src/hooks/useIfc.ts +1534 -205
  94. package/src/hooks/useIfcCache.ts +279 -0
  95. package/src/hooks/useKeyboardShortcuts.ts +50 -8
  96. package/src/hooks/useModelSelection.ts +61 -0
  97. package/src/hooks/useViewerSelectors.ts +218 -0
  98. package/src/hooks/useWebGPU.ts +80 -0
  99. package/src/index.css +265 -27
  100. package/src/lib/platform.ts +23 -0
  101. package/src/services/cacheService.ts +142 -0
  102. package/src/services/desktop-cache.ts +143 -0
  103. package/src/services/fs-cache.ts +212 -0
  104. package/src/services/ifc-cache.ts +14 -6
  105. package/src/store/constants.ts +85 -0
  106. package/src/store/index.ts +214 -0
  107. package/src/store/slices/bcfSlice.ts +372 -0
  108. package/src/store/slices/cameraSlice.ts +63 -0
  109. package/src/store/slices/dataSlice.test.ts +226 -0
  110. package/src/store/slices/dataSlice.ts +112 -0
  111. package/src/store/slices/drawing2DSlice.ts +340 -0
  112. package/src/store/slices/hoverSlice.ts +40 -0
  113. package/src/store/slices/idsSlice.ts +310 -0
  114. package/src/store/slices/loadingSlice.ts +33 -0
  115. package/src/store/slices/measurementSlice.test.ts +217 -0
  116. package/src/store/slices/measurementSlice.ts +293 -0
  117. package/src/store/slices/modelSlice.test.ts +271 -0
  118. package/src/store/slices/modelSlice.ts +211 -0
  119. package/src/store/slices/mutationSlice.ts +502 -0
  120. package/src/store/slices/sectionSlice.test.ts +125 -0
  121. package/src/store/slices/sectionSlice.ts +58 -0
  122. package/src/store/slices/selectionSlice.test.ts +286 -0
  123. package/src/store/slices/selectionSlice.ts +263 -0
  124. package/src/store/slices/sheetSlice.ts +565 -0
  125. package/src/store/slices/uiSlice.ts +58 -0
  126. package/src/store/slices/visibilitySlice.test.ts +304 -0
  127. package/src/store/slices/visibilitySlice.ts +277 -0
  128. package/src/store/types.test.ts +135 -0
  129. package/src/store/types.ts +248 -0
  130. package/src/store.ts +40 -515
  131. package/src/utils/ifcConfig.ts +82 -0
  132. package/src/utils/localParsingUtils.ts +287 -0
  133. package/src/utils/serverDataModel.ts +783 -0
  134. package/src/utils/spatialHierarchy.ts +283 -0
  135. package/src/utils/viewportUtils.ts +334 -0
  136. package/src/vite-env.d.ts +23 -0
  137. package/src/webgpu-types.d.ts +128 -0
  138. package/src-tauri/Cargo.toml +29 -0
  139. package/src-tauri/build.rs +7 -0
  140. package/src-tauri/capabilities/default.json +18 -0
  141. package/src-tauri/icons/128x128.png +0 -0
  142. package/src-tauri/icons/128x128@2x.png +0 -0
  143. package/src-tauri/icons/32x32.png +0 -0
  144. package/src-tauri/icons/Square107x107Logo.png +0 -0
  145. package/src-tauri/icons/Square142x142Logo.png +0 -0
  146. package/src-tauri/icons/Square150x150Logo.png +0 -0
  147. package/src-tauri/icons/Square284x284Logo.png +0 -0
  148. package/src-tauri/icons/Square30x30Logo.png +0 -0
  149. package/src-tauri/icons/Square310x310Logo.png +0 -0
  150. package/src-tauri/icons/Square44x44Logo.png +0 -0
  151. package/src-tauri/icons/Square71x71Logo.png +0 -0
  152. package/src-tauri/icons/Square89x89Logo.png +0 -0
  153. package/src-tauri/icons/StoreLogo.png +0 -0
  154. package/src-tauri/icons/icon.icns +0 -0
  155. package/src-tauri/icons/icon.ico +0 -0
  156. package/src-tauri/icons/icon.png +0 -0
  157. package/src-tauri/src/lib.rs +21 -0
  158. package/src-tauri/src/main.rs +10 -0
  159. package/src-tauri/tauri.conf.json +39 -0
  160. package/vite.config.ts +174 -26
  161. package/public/ifc-lite_bg.wasm +0 -0
  162. package/public/web-ifc.wasm +0 -0
  163. package/src/components/Viewport.tsx +0 -723
  164. package/src/components/viewer/BoxSelectionOverlay.tsx +0 -53
@@ -0,0 +1,661 @@
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
+ * IDSPanel - IDS (Information Delivery Specification) validation panel
7
+ *
8
+ * Provides:
9
+ * - Load IDS files
10
+ * - Run validation against loaded models
11
+ * - View validation results with pass/fail status
12
+ * - Filter by specification, status
13
+ * - Click to select entities in 3D view
14
+ * - Isolate failed/passed entities
15
+ * - Multi-language support (EN/DE/FR)
16
+ */
17
+
18
+ import React, { useCallback, useState, useMemo, useRef } from 'react';
19
+ import {
20
+ X,
21
+ Upload,
22
+ Play,
23
+ CheckCircle,
24
+ XCircle,
25
+ AlertCircle,
26
+ ChevronDown,
27
+ ChevronRight,
28
+ Filter,
29
+ Focus,
30
+ EyeOff,
31
+ Eye,
32
+ FileText,
33
+ Loader2,
34
+ Building2,
35
+ RefreshCw,
36
+ Trash2,
37
+ FileJson,
38
+ FileCode,
39
+ } from 'lucide-react';
40
+ import { Button } from '@/components/ui/button';
41
+ import { ScrollArea } from '@/components/ui/scroll-area';
42
+ import { Separator } from '@/components/ui/separator';
43
+ import { Badge } from '@/components/ui/badge';
44
+ import { Progress } from '@/components/ui/progress';
45
+ import {
46
+ Select,
47
+ SelectContent,
48
+ SelectItem,
49
+ SelectTrigger,
50
+ SelectValue,
51
+ } from '@/components/ui/select';
52
+ import {
53
+ Collapsible,
54
+ CollapsibleContent,
55
+ CollapsibleTrigger,
56
+ } from '@/components/ui/collapsible';
57
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
58
+ import { useIDS } from '@/hooks/useIDS';
59
+ import type {
60
+ IDSSpecificationResult,
61
+ IDSEntityResult,
62
+ IDSRequirementResult,
63
+ } from '@ifc-lite/ids';
64
+ import { cn } from '@/lib/utils';
65
+
66
+ // ============================================================================
67
+ // Types
68
+ // ============================================================================
69
+
70
+ interface IDSPanelProps {
71
+ onClose?: () => void;
72
+ }
73
+
74
+ // ============================================================================
75
+ // Helper Components
76
+ // ============================================================================
77
+
78
+ function StatusIcon({ status, showLabel = false }: { status: 'pass' | 'fail' | 'not_applicable'; showLabel?: boolean }) {
79
+ const labels = {
80
+ pass: 'Passed',
81
+ fail: 'Failed',
82
+ not_applicable: 'Not Applicable',
83
+ };
84
+
85
+ const icons = {
86
+ pass: <CheckCircle className="h-4 w-4 text-green-500" aria-hidden="true" />,
87
+ fail: <XCircle className="h-4 w-4 text-red-500" aria-hidden="true" />,
88
+ not_applicable: <AlertCircle className="h-4 w-4 text-yellow-500" aria-hidden="true" />,
89
+ };
90
+
91
+ return (
92
+ <span className="inline-flex items-center gap-1" role="status" aria-label={labels[status]}>
93
+ {icons[status]}
94
+ {showLabel && <span className="sr-only">{labels[status]}</span>}
95
+ </span>
96
+ );
97
+ }
98
+
99
+ function StatusBadge({ status }: { status: 'pass' | 'fail' | 'not_applicable' }) {
100
+ const variant = status === 'pass' ? 'default' : status === 'fail' ? 'destructive' : 'secondary';
101
+ const label = status === 'pass' ? 'PASS' : status === 'fail' ? 'FAIL' : 'N/A';
102
+
103
+ return (
104
+ <Badge variant={variant} className="text-xs">
105
+ {label}
106
+ </Badge>
107
+ );
108
+ }
109
+
110
+ function PassRateBar({ passRate }: { passRate: number }) {
111
+ const color = passRate >= 80 ? 'bg-green-500' : passRate >= 50 ? 'bg-yellow-500' : 'bg-red-500';
112
+
113
+ return (
114
+ <div className="flex items-center gap-2">
115
+ <div className="flex-1 h-2 bg-muted rounded-full overflow-hidden">
116
+ <div
117
+ className={cn('h-full rounded-full transition-all', color)}
118
+ style={{ width: `${passRate}%` }}
119
+ />
120
+ </div>
121
+ <span className="text-xs text-muted-foreground w-10 text-right">{passRate}%</span>
122
+ </div>
123
+ );
124
+ }
125
+
126
+ // ============================================================================
127
+ // Specification Card Component
128
+ // ============================================================================
129
+
130
+ interface SpecificationCardProps {
131
+ result: IDSSpecificationResult;
132
+ isActive: boolean;
133
+ onSelect: () => void;
134
+ onEntityClick: (modelId: string, expressId: number) => void;
135
+ filterMode: 'all' | 'failed' | 'passed';
136
+ }
137
+
138
+ function SpecificationCard({
139
+ result,
140
+ isActive,
141
+ onSelect,
142
+ onEntityClick,
143
+ filterMode,
144
+ }: SpecificationCardProps) {
145
+ const [isExpanded, setIsExpanded] = useState(false);
146
+
147
+ // Filter entity results based on mode
148
+ const filteredEntities = useMemo(() => {
149
+ if (filterMode === 'all') return result.entityResults;
150
+ return result.entityResults.filter((e) =>
151
+ filterMode === 'failed' ? !e.passed : e.passed
152
+ );
153
+ }, [result.entityResults, filterMode]);
154
+
155
+ return (
156
+ <Collapsible open={isExpanded} onOpenChange={setIsExpanded}>
157
+ <div
158
+ className={cn(
159
+ 'rounded-lg border transition-colors',
160
+ isActive ? 'border-primary bg-primary/5' : 'border-border hover:border-primary/50'
161
+ )}
162
+ >
163
+ {/* Specification Header */}
164
+ <CollapsibleTrigger asChild>
165
+ <button
166
+ className="w-full p-3 text-left"
167
+ onClick={onSelect}
168
+ >
169
+ <div className="flex items-start gap-2">
170
+ {isExpanded ? (
171
+ <ChevronDown className="h-4 w-4 mt-0.5 shrink-0" />
172
+ ) : (
173
+ <ChevronRight className="h-4 w-4 mt-0.5 shrink-0" />
174
+ )}
175
+ <div className="flex-1 min-w-0">
176
+ <div className="flex items-center gap-2 mb-1">
177
+ <StatusIcon status={result.status} />
178
+ <span className="font-medium text-sm truncate">
179
+ {result.specification.name}
180
+ </span>
181
+ </div>
182
+ {result.specification.description && (
183
+ <p className="text-xs text-muted-foreground line-clamp-2 mb-2">
184
+ {result.specification.description}
185
+ </p>
186
+ )}
187
+ <div className="flex items-center gap-4 text-xs text-muted-foreground">
188
+ <span className="flex items-center gap-1">
189
+ <Building2 className="h-3 w-3" />
190
+ {result.applicableCount} entities
191
+ </span>
192
+ <span className="text-green-600">{result.passedCount} passed</span>
193
+ <span className="text-red-600">{result.failedCount} failed</span>
194
+ </div>
195
+ <div className="mt-2">
196
+ <PassRateBar passRate={result.passRate} />
197
+ </div>
198
+ </div>
199
+ </div>
200
+ </button>
201
+ </CollapsibleTrigger>
202
+
203
+ {/* Entity Results */}
204
+ <CollapsibleContent>
205
+ <Separator />
206
+ <div className="max-h-64 overflow-auto">
207
+ {filteredEntities.length === 0 ? (
208
+ <div className="p-3 text-sm text-muted-foreground text-center">
209
+ No {filterMode === 'failed' ? 'failed' : filterMode === 'passed' ? 'passed' : ''} entities
210
+ </div>
211
+ ) : (
212
+ <div className="divide-y">
213
+ {filteredEntities.slice(0, 100).map((entity) => (
214
+ <EntityResultRow
215
+ key={`${entity.modelId}:${entity.expressId}`}
216
+ entity={entity}
217
+ onClick={() => onEntityClick(entity.modelId, entity.expressId)}
218
+ />
219
+ ))}
220
+ {filteredEntities.length > 100 && (
221
+ <div className="p-2 text-xs text-muted-foreground text-center">
222
+ Showing 100 of {filteredEntities.length} entities
223
+ </div>
224
+ )}
225
+ </div>
226
+ )}
227
+ </div>
228
+ </CollapsibleContent>
229
+ </div>
230
+ </Collapsible>
231
+ );
232
+ }
233
+
234
+ // ============================================================================
235
+ // Entity Result Row Component
236
+ // ============================================================================
237
+
238
+ interface EntityResultRowProps {
239
+ entity: IDSEntityResult;
240
+ onClick: () => void;
241
+ }
242
+
243
+ function EntityResultRow({ entity, onClick }: EntityResultRowProps) {
244
+ const [showDetails, setShowDetails] = useState(false);
245
+
246
+ const handleKeyDown = (e: React.KeyboardEvent) => {
247
+ if (e.key === 'Enter' || e.key === ' ') {
248
+ e.preventDefault();
249
+ onClick();
250
+ } else if (e.key === 'ArrowRight') {
251
+ e.preventDefault();
252
+ setShowDetails(true);
253
+ } else if (e.key === 'ArrowLeft') {
254
+ e.preventDefault();
255
+ setShowDetails(false);
256
+ }
257
+ };
258
+
259
+ return (
260
+ <div className="hover:bg-muted/50 focus-within:bg-muted/50 focus-within:ring-2 focus-within:ring-primary focus-within:ring-inset rounded-md">
261
+ <button
262
+ className="w-full p-2 text-left flex items-center gap-2 focus:outline-none"
263
+ onClick={onClick}
264
+ onKeyDown={handleKeyDown}
265
+ tabIndex={0}
266
+ aria-expanded={showDetails}
267
+ aria-label={`${entity.entityName || '#' + entity.expressId} - ${entity.entityType} - ${entity.passed ? 'Passed' : 'Failed'}`}
268
+ >
269
+ <StatusIcon status={entity.passed ? 'pass' : 'fail'} />
270
+ <div className="flex-1 min-w-0">
271
+ <div className="text-sm truncate">
272
+ {entity.entityName || `#${entity.expressId}`}
273
+ </div>
274
+ <div className="text-xs text-muted-foreground truncate">
275
+ {entity.entityType}
276
+ {entity.globalId && ` · ${entity.globalId}`}
277
+ </div>
278
+ </div>
279
+ {/* Chevron - shrink-0 keeps it visible */}
280
+ <span
281
+ role="button"
282
+ tabIndex={-1}
283
+ className="shrink-0 p-1 rounded hover:bg-accent"
284
+ onClick={(e) => {
285
+ e.stopPropagation();
286
+ setShowDetails(!showDetails);
287
+ }}
288
+ aria-label={showDetails ? 'Hide details' : 'Show details'}
289
+ >
290
+ {showDetails ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
291
+ </span>
292
+ </button>
293
+ {showDetails && (
294
+ <div className="pl-8 pr-2 pb-2 space-y-1">
295
+ {entity.requirementResults.map((req, idx) => (
296
+ <RequirementResultRow key={req.requirement.id || idx} result={req} />
297
+ ))}
298
+ </div>
299
+ )}
300
+ </div>
301
+ );
302
+ }
303
+
304
+ // ============================================================================
305
+ // Requirement Result Row Component
306
+ // ============================================================================
307
+
308
+ interface RequirementResultRowProps {
309
+ result: IDSRequirementResult;
310
+ }
311
+
312
+ function RequirementResultRow({ result }: RequirementResultRowProps) {
313
+ return (
314
+ <div className="text-xs flex items-start gap-2 py-1">
315
+ <StatusIcon status={result.status} />
316
+ <div className="flex-1 min-w-0">
317
+ <div className="text-muted-foreground">{result.checkedDescription}</div>
318
+ {result.failureReason && (
319
+ <div className="text-red-600 mt-0.5">{result.failureReason}</div>
320
+ )}
321
+ </div>
322
+ </div>
323
+ );
324
+ }
325
+
326
+ // ============================================================================
327
+ // Main Panel Component
328
+ // ============================================================================
329
+
330
+ export function IDSPanel({ onClose }: IDSPanelProps) {
331
+ const fileInputRef = useRef<HTMLInputElement>(null);
332
+
333
+ const {
334
+ // State
335
+ document,
336
+ report,
337
+ loading,
338
+ progress,
339
+ error,
340
+ activeSpecificationId,
341
+ filterMode,
342
+
343
+ // Actions
344
+ loadIDSFile,
345
+ clearIDS,
346
+ runValidation,
347
+ clearValidation,
348
+ setActiveSpecification,
349
+ selectEntity,
350
+ setFilterMode,
351
+ applyColors,
352
+ isolateFailed,
353
+ isolatePassed,
354
+ clearIsolation,
355
+ exportReportJSON,
356
+ exportReportHTML,
357
+ } = useIDS();
358
+
359
+ // Handle file selection
360
+ const handleFileSelect = useCallback(async (e: React.ChangeEvent<HTMLInputElement>) => {
361
+ const file = e.target.files?.[0];
362
+ if (file) {
363
+ await loadIDSFile(file);
364
+ }
365
+ // Reset input for re-selection of same file
366
+ e.target.value = '';
367
+ }, [loadIDSFile]);
368
+
369
+ // Handle entity click
370
+ const handleEntityClick = useCallback((modelId: string, expressId: number) => {
371
+ selectEntity(modelId, expressId);
372
+ }, [selectEntity]);
373
+
374
+ // Render validation progress
375
+ const renderProgress = () => {
376
+ if (!progress) return null;
377
+
378
+ return (
379
+ <div className="p-3 border-b">
380
+ <div className="flex items-center gap-2 mb-2">
381
+ <Loader2 className="h-4 w-4 animate-spin" />
382
+ <span className="text-sm">
383
+ {progress.phase === 'filtering' && 'Finding applicable entities...'}
384
+ {progress.phase === 'validating' && `Validating... (${progress.entitiesProcessed}/${progress.totalEntities})`}
385
+ {progress.phase === 'complete' && 'Complete'}
386
+ </span>
387
+ </div>
388
+ <Progress value={progress.percentage} className="h-2" />
389
+ </div>
390
+ );
391
+ };
392
+
393
+ // Render empty state
394
+ const renderEmptyState = () => {
395
+ if (document) return null;
396
+
397
+ return (
398
+ <div className="flex flex-col items-center justify-center h-full p-6 text-center">
399
+ <FileText className="h-12 w-12 text-muted-foreground mb-4" />
400
+ <h3 className="font-medium text-sm mb-2">No IDS Loaded</h3>
401
+ <p className="text-xs text-muted-foreground mb-4">
402
+ Load an IDS (Information Delivery Specification) file to validate your model
403
+ </p>
404
+ <input
405
+ ref={fileInputRef}
406
+ type="file"
407
+ accept=".ids,.xml"
408
+ className="hidden"
409
+ onChange={handleFileSelect}
410
+ />
411
+ <Button onClick={() => fileInputRef.current?.click()}>
412
+ <Upload className="h-4 w-4 mr-2" />
413
+ Load IDS File
414
+ </Button>
415
+ </div>
416
+ );
417
+ };
418
+
419
+ // Render document loaded but no validation
420
+ const renderDocumentLoaded = () => {
421
+ if (!document || report) return null;
422
+
423
+ return (
424
+ <div className="p-4">
425
+ <div className="rounded-lg border p-4 mb-4">
426
+ <h3 className="font-medium text-sm mb-1">{document.info.title}</h3>
427
+ {document.info.description && (
428
+ <p className="text-xs text-muted-foreground mb-2">{document.info.description}</p>
429
+ )}
430
+ <div className="flex items-center gap-4 text-xs text-muted-foreground">
431
+ <span>{document.specifications.length} specifications</span>
432
+ {document.info.version && <span>v{document.info.version}</span>}
433
+ </div>
434
+ </div>
435
+
436
+ <Button className="w-full" onClick={runValidation} disabled={loading}>
437
+ {loading ? (
438
+ <Loader2 className="h-4 w-4 mr-2 animate-spin" />
439
+ ) : (
440
+ <Play className="h-4 w-4 mr-2" />
441
+ )}
442
+ Run Validation
443
+ </Button>
444
+ </div>
445
+ );
446
+ };
447
+
448
+ // Render validation results
449
+ const renderResults = () => {
450
+ if (!report) return null;
451
+
452
+ return (
453
+ <>
454
+ {/* Summary Header */}
455
+ <div className="p-3 border-b bg-muted/30">
456
+ <div className="flex items-center gap-2 mb-2">
457
+ <StatusIcon status={report.summary.failedSpecifications > 0 ? 'fail' : 'pass'} />
458
+ <span className="font-medium text-sm">
459
+ {report.summary.passedSpecifications}/{report.summary.totalSpecifications} Specifications Passed
460
+ </span>
461
+ </div>
462
+ <div className="grid grid-cols-3 gap-2 text-xs text-center">
463
+ <div className="bg-background rounded p-2">
464
+ <div className="font-medium">{report.summary.totalEntitiesChecked}</div>
465
+ <div className="text-muted-foreground">Checked</div>
466
+ </div>
467
+ <div className="bg-background rounded p-2">
468
+ <div className="font-medium text-green-600">{report.summary.totalEntitiesPassed}</div>
469
+ <div className="text-muted-foreground">Passed</div>
470
+ </div>
471
+ <div className="bg-background rounded p-2">
472
+ <div className="font-medium text-red-600">{report.summary.totalEntitiesFailed}</div>
473
+ <div className="text-muted-foreground">Failed</div>
474
+ </div>
475
+ </div>
476
+ <div className="mt-2">
477
+ <PassRateBar passRate={report.summary.overallPassRate} />
478
+ </div>
479
+ <p className="text-xs text-muted-foreground mt-2 text-center">
480
+ 💡 Click any entity to select and zoom to it in the 3D view
481
+ </p>
482
+ </div>
483
+
484
+ {/* Filter & Actions Bar */}
485
+ <div className="p-2 border-b flex items-center gap-1 flex-wrap">
486
+ <Select value={filterMode} onValueChange={(v) => setFilterMode(v as 'all' | 'failed' | 'passed')}>
487
+ <SelectTrigger className="h-8 w-24">
488
+ <Filter className="h-3 w-3 mr-1" />
489
+ <SelectValue />
490
+ </SelectTrigger>
491
+ <SelectContent>
492
+ <SelectItem value="all">All</SelectItem>
493
+ <SelectItem value="failed">Failed</SelectItem>
494
+ <SelectItem value="passed">Passed</SelectItem>
495
+ </SelectContent>
496
+ </Select>
497
+
498
+ <div className="flex-1 min-w-2" />
499
+
500
+ <Tooltip>
501
+ <TooltipTrigger asChild>
502
+ <Button variant="ghost" size="sm" className="h-8 w-8 p-0" onClick={isolateFailed}>
503
+ <EyeOff className="h-4 w-4" />
504
+ </Button>
505
+ </TooltipTrigger>
506
+ <TooltipContent>Isolate Failed</TooltipContent>
507
+ </Tooltip>
508
+
509
+ <Tooltip>
510
+ <TooltipTrigger asChild>
511
+ <Button variant="ghost" size="sm" className="h-8 w-8 p-0" onClick={isolatePassed}>
512
+ <Eye className="h-4 w-4" />
513
+ </Button>
514
+ </TooltipTrigger>
515
+ <TooltipContent>Isolate Passed</TooltipContent>
516
+ </Tooltip>
517
+
518
+ <Tooltip>
519
+ <TooltipTrigger asChild>
520
+ <Button variant="ghost" size="sm" className="h-8 w-8 p-0" onClick={clearIsolation}>
521
+ <Focus className="h-4 w-4" />
522
+ </Button>
523
+ </TooltipTrigger>
524
+ <TooltipContent>Clear Isolation</TooltipContent>
525
+ </Tooltip>
526
+
527
+ <Tooltip>
528
+ <TooltipTrigger asChild>
529
+ <Button variant="ghost" size="sm" className="h-8 w-8 p-0" onClick={applyColors}>
530
+ <RefreshCw className="h-4 w-4" />
531
+ </Button>
532
+ </TooltipTrigger>
533
+ <TooltipContent>Reapply Colors</TooltipContent>
534
+ </Tooltip>
535
+
536
+ <Separator orientation="vertical" className="h-4 mx-1" />
537
+
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>
555
+ </div>
556
+
557
+ {/* Specifications List */}
558
+ <ScrollArea className="flex-1">
559
+ <div className="p-2 space-y-2">
560
+ {report.specificationResults.map((specResult) => (
561
+ <SpecificationCard
562
+ key={specResult.specification.id}
563
+ result={specResult}
564
+ isActive={activeSpecificationId === specResult.specification.id}
565
+ onSelect={() => setActiveSpecification(specResult.specification.id)}
566
+ onEntityClick={handleEntityClick}
567
+ filterMode={filterMode}
568
+ />
569
+ ))}
570
+ </div>
571
+ </ScrollArea>
572
+ </>
573
+ );
574
+ };
575
+
576
+ return (
577
+ <div className="h-full flex flex-col bg-background">
578
+ {/* Header */}
579
+ <div className="flex items-center justify-between p-3 border-b">
580
+ <div className="flex items-center gap-2">
581
+ <FileText className="h-4 w-4" />
582
+ <span className="font-medium text-sm">IDS Validation</span>
583
+ </div>
584
+ <div className="flex items-center gap-1">
585
+ {/* Load New IDS */}
586
+ {document && (
587
+ <>
588
+ <input
589
+ ref={fileInputRef}
590
+ type="file"
591
+ accept=".ids,.xml"
592
+ className="hidden"
593
+ onChange={handleFileSelect}
594
+ />
595
+ <Tooltip>
596
+ <TooltipTrigger asChild>
597
+ <Button
598
+ variant="ghost"
599
+ size="sm"
600
+ className="h-7 w-7 p-0"
601
+ onClick={() => fileInputRef.current?.click()}
602
+ >
603
+ <Upload className="h-3 w-3" />
604
+ </Button>
605
+ </TooltipTrigger>
606
+ <TooltipContent>Load New IDS</TooltipContent>
607
+ </Tooltip>
608
+ </>
609
+ )}
610
+
611
+ {/* Clear */}
612
+ {document && (
613
+ <Tooltip>
614
+ <TooltipTrigger asChild>
615
+ <Button
616
+ variant="ghost"
617
+ size="sm"
618
+ className="h-7 w-7 p-0"
619
+ onClick={() => {
620
+ clearIDS();
621
+ clearValidation();
622
+ }}
623
+ >
624
+ <Trash2 className="h-3 w-3" />
625
+ </Button>
626
+ </TooltipTrigger>
627
+ <TooltipContent>Clear IDS</TooltipContent>
628
+ </Tooltip>
629
+ )}
630
+
631
+ {/* Close */}
632
+ {onClose && (
633
+ <Button variant="ghost" size="sm" className="h-7 w-7 p-0" onClick={onClose}>
634
+ <X className="h-4 w-4" />
635
+ </Button>
636
+ )}
637
+ </div>
638
+ </div>
639
+
640
+ {/* Error Display */}
641
+ {error && (
642
+ <div className="p-3 bg-red-50 dark:bg-red-900/20 border-b border-red-200 dark:border-red-800">
643
+ <div className="flex items-center gap-2 text-sm text-red-600 dark:text-red-400">
644
+ <AlertCircle className="h-4 w-4 shrink-0" />
645
+ <span>{error}</span>
646
+ </div>
647
+ </div>
648
+ )}
649
+
650
+ {/* Progress */}
651
+ {loading && renderProgress()}
652
+
653
+ {/* Content */}
654
+ <div className="flex-1 min-h-0 flex flex-col">
655
+ {renderEmptyState()}
656
+ {renderDocumentLoaded()}
657
+ {renderResults()}
658
+ </div>
659
+ </div>
660
+ );
661
+ }