@ifc-lite/viewer 1.1.6 → 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,437 @@
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
+ * TitleBlockEditor - Modal dialog for editing title block fields
7
+ *
8
+ * Allows users to:
9
+ * - Edit field values (project name, drawing number, etc.)
10
+ * - Add/remove custom fields
11
+ * - Configure field properties (label, auto-populate)
12
+ * - Add revision entries
13
+ */
14
+
15
+ import React, { useCallback, useState, useMemo } from 'react';
16
+ import { Plus, Trash2, Upload, Calendar, Hash, FileText, User } from 'lucide-react';
17
+ import { Button } from '@/components/ui/button';
18
+ import { Input } from '@/components/ui/input';
19
+ import { Label } from '@/components/ui/label';
20
+ import {
21
+ Dialog,
22
+ DialogContent,
23
+ DialogHeader,
24
+ DialogTitle,
25
+ DialogFooter,
26
+ } from '@/components/ui/dialog';
27
+ import { useViewerStore } from '@/store';
28
+ import type { TitleBlockField, RevisionEntry } from '@ifc-lite/drawing-2d';
29
+
30
+ interface TitleBlockEditorProps {
31
+ open: boolean;
32
+ onOpenChange: (open: boolean) => void;
33
+ }
34
+
35
+ // Icon mapping for common field types
36
+ const FIELD_ICONS: Record<string, React.ReactNode> = {
37
+ 'project-name': <FileText className="h-4 w-4" />,
38
+ 'drawing-title': <FileText className="h-4 w-4" />,
39
+ 'drawing-number': <Hash className="h-4 w-4" />,
40
+ revision: <Hash className="h-4 w-4" />,
41
+ scale: <Hash className="h-4 w-4" />,
42
+ date: <Calendar className="h-4 w-4" />,
43
+ 'drawn-by': <User className="h-4 w-4" />,
44
+ 'checked-by': <User className="h-4 w-4" />,
45
+ 'sheet-number': <Hash className="h-4 w-4" />,
46
+ };
47
+
48
+ // Standard field IDs
49
+ const STANDARD_FIELD_IDS = [
50
+ 'project-name',
51
+ 'drawing-title',
52
+ 'drawing-number',
53
+ 'revision',
54
+ 'scale',
55
+ 'date',
56
+ 'drawn-by',
57
+ 'checked-by',
58
+ 'sheet-number',
59
+ ];
60
+
61
+ export function TitleBlockEditor({ open, onOpenChange }: TitleBlockEditorProps): React.ReactElement {
62
+ const activeSheet = useViewerStore((s) => s.activeSheet);
63
+ const updateTitleBlockField = useViewerStore((s) => s.updateTitleBlockField);
64
+ const addTitleBlockField = useViewerStore((s) => s.addTitleBlockField);
65
+ const removeTitleBlockField = useViewerStore((s) => s.removeTitleBlockField);
66
+ const setTitleBlockLogo = useViewerStore((s) => s.setTitleBlockLogo);
67
+ const addRevision = useViewerStore((s) => s.addRevision);
68
+ const removeRevision = useViewerStore((s) => s.removeRevision);
69
+
70
+ // Local state for new field form
71
+ const [newFieldLabel, setNewFieldLabel] = useState('');
72
+ const [showNewFieldForm, setShowNewFieldForm] = useState(false);
73
+
74
+ // Local state for new revision form
75
+ const [newRevision, setNewRevision] = useState({
76
+ revision: '',
77
+ date: new Date().toLocaleDateString(),
78
+ description: '',
79
+ author: '',
80
+ });
81
+ const [showRevisionForm, setShowRevisionForm] = useState(false);
82
+
83
+ // Group fields by category
84
+ const fieldGroups = useMemo(() => {
85
+ if (!activeSheet) return { standard: [], custom: [] };
86
+
87
+ const standard: TitleBlockField[] = [];
88
+ const custom: TitleBlockField[] = [];
89
+
90
+ for (const field of activeSheet.titleBlock.fields) {
91
+ if (STANDARD_FIELD_IDS.includes(field.id)) {
92
+ standard.push(field);
93
+ } else {
94
+ custom.push(field);
95
+ }
96
+ }
97
+
98
+ return { standard, custom };
99
+ }, [activeSheet]);
100
+
101
+ // Handle field value change
102
+ const handleFieldChange = useCallback((fieldId: string, value: string) => {
103
+ updateTitleBlockField(fieldId, value);
104
+ }, [updateTitleBlockField]);
105
+
106
+ // Add new custom field
107
+ const handleAddField = useCallback(() => {
108
+ if (!newFieldLabel.trim()) return;
109
+
110
+ // Find max row from existing fields
111
+ const maxRow = activeSheet?.titleBlock.fields.reduce(
112
+ (max, f) => Math.max(max, f.row ?? 0),
113
+ 0
114
+ ) ?? 0;
115
+
116
+ const newField: TitleBlockField = {
117
+ id: `custom-${Date.now()}`,
118
+ label: newFieldLabel.trim(),
119
+ value: '',
120
+ editable: true,
121
+ autoPopulate: false,
122
+ fontSize: 3,
123
+ fontWeight: 'normal',
124
+ row: maxRow + 1,
125
+ col: 0,
126
+ colSpan: 2,
127
+ };
128
+
129
+ addTitleBlockField(newField);
130
+ setNewFieldLabel('');
131
+ setShowNewFieldForm(false);
132
+ }, [newFieldLabel, activeSheet, addTitleBlockField]);
133
+
134
+ // Handle logo upload
135
+ const handleLogoUpload = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
136
+ const file = e.target.files?.[0];
137
+ if (!file) return;
138
+
139
+ const reader = new FileReader();
140
+ reader.onload = (event) => {
141
+ const dataUrl = event.target?.result as string;
142
+ setTitleBlockLogo({
143
+ source: dataUrl,
144
+ widthMm: 30,
145
+ heightMm: 15,
146
+ position: 'top-left',
147
+ });
148
+ };
149
+ reader.readAsDataURL(file);
150
+ }, [setTitleBlockLogo]);
151
+
152
+ // Add revision entry
153
+ const handleAddRevision = useCallback(() => {
154
+ if (!newRevision.revision || !newRevision.description) return;
155
+
156
+ addRevision({
157
+ revision: newRevision.revision,
158
+ date: newRevision.date || new Date().toLocaleDateString(),
159
+ description: newRevision.description,
160
+ author: newRevision.author,
161
+ });
162
+
163
+ setNewRevision({
164
+ revision: '',
165
+ date: new Date().toLocaleDateString(),
166
+ description: '',
167
+ author: '',
168
+ });
169
+ setShowRevisionForm(false);
170
+ }, [newRevision, addRevision]);
171
+
172
+ if (!activeSheet) return <></>;
173
+
174
+ return (
175
+ <Dialog open={open} onOpenChange={onOpenChange}>
176
+ <DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
177
+ <DialogHeader>
178
+ <DialogTitle>Edit Title Block</DialogTitle>
179
+ </DialogHeader>
180
+
181
+ <div className="space-y-6">
182
+ {/* Standard Fields */}
183
+ <div className="space-y-3">
184
+ <h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
185
+ Standard Fields
186
+ </h3>
187
+ <div className="grid gap-3">
188
+ {fieldGroups.standard.map((field) => (
189
+ <div key={field.id} className="grid grid-cols-[120px_1fr] items-center gap-3">
190
+ <div className="flex items-center gap-2 text-sm">
191
+ {FIELD_ICONS[field.id] || <FileText className="h-4 w-4" />}
192
+ <span className="truncate">{field.label}</span>
193
+ </div>
194
+ <Input
195
+ value={field.value}
196
+ onChange={(e) => handleFieldChange(field.id, e.target.value)}
197
+ placeholder={`Enter ${field.label.toLowerCase()}...`}
198
+ className="h-8"
199
+ disabled={!field.editable}
200
+ />
201
+ </div>
202
+ ))}
203
+ </div>
204
+ </div>
205
+
206
+ {/* Custom Fields */}
207
+ <div className="space-y-3">
208
+ <div className="flex items-center justify-between">
209
+ <h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
210
+ Custom Fields
211
+ </h3>
212
+ <Button
213
+ variant="outline"
214
+ size="sm"
215
+ onClick={() => setShowNewFieldForm(true)}
216
+ >
217
+ <Plus className="h-4 w-4 mr-1" />
218
+ Add Field
219
+ </Button>
220
+ </div>
221
+
222
+ {showNewFieldForm && (
223
+ <div className="flex gap-2 p-3 bg-muted/30 rounded-lg">
224
+ <Input
225
+ value={newFieldLabel}
226
+ onChange={(e) => setNewFieldLabel(e.target.value)}
227
+ placeholder="Field label..."
228
+ className="h-8 flex-1"
229
+ autoFocus
230
+ />
231
+ <Button size="sm" onClick={handleAddField} disabled={!newFieldLabel.trim()}>
232
+ Add
233
+ </Button>
234
+ <Button
235
+ size="sm"
236
+ variant="ghost"
237
+ onClick={() => {
238
+ setShowNewFieldForm(false);
239
+ setNewFieldLabel('');
240
+ }}
241
+ >
242
+ Cancel
243
+ </Button>
244
+ </div>
245
+ )}
246
+
247
+ {fieldGroups.custom.length === 0 && !showNewFieldForm ? (
248
+ <div className="text-sm text-muted-foreground text-center py-4">
249
+ No custom fields yet
250
+ </div>
251
+ ) : (
252
+ <div className="grid gap-2">
253
+ {fieldGroups.custom.map((field) => (
254
+ <div key={field.id} className="flex items-center gap-2">
255
+ <div className="grid grid-cols-[120px_1fr] items-center gap-3 flex-1">
256
+ <span className="text-sm truncate">{field.label}</span>
257
+ <Input
258
+ value={field.value}
259
+ onChange={(e) => handleFieldChange(field.id, e.target.value)}
260
+ placeholder={`Enter ${field.label.toLowerCase()}...`}
261
+ className="h-8"
262
+ />
263
+ </div>
264
+ <Button
265
+ variant="ghost"
266
+ size="icon-sm"
267
+ className="text-destructive hover:text-destructive"
268
+ onClick={() => removeTitleBlockField(field.id)}
269
+ >
270
+ <Trash2 className="h-4 w-4" />
271
+ </Button>
272
+ </div>
273
+ ))}
274
+ </div>
275
+ )}
276
+ </div>
277
+
278
+ {/* Logo */}
279
+ <div className="space-y-3">
280
+ <h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
281
+ Company Logo
282
+ </h3>
283
+ <div className="flex items-center gap-4">
284
+ {activeSheet.titleBlock.logo ? (
285
+ <div className="flex items-center gap-3">
286
+ <div className="w-16 h-10 bg-muted rounded flex items-center justify-center overflow-hidden">
287
+ <img
288
+ src={activeSheet.titleBlock.logo.source}
289
+ alt="Logo"
290
+ className="max-w-full max-h-full object-contain"
291
+ />
292
+ </div>
293
+ <Button
294
+ variant="outline"
295
+ size="sm"
296
+ onClick={() => setTitleBlockLogo(null)}
297
+ >
298
+ Remove
299
+ </Button>
300
+ </div>
301
+ ) : (
302
+ <div className="flex items-center gap-2">
303
+ <label className="cursor-pointer">
304
+ <input
305
+ type="file"
306
+ accept="image/*"
307
+ className="hidden"
308
+ onChange={handleLogoUpload}
309
+ />
310
+ <div className="flex items-center gap-2 px-3 py-2 border rounded-md text-sm hover:bg-muted/50 transition-colors">
311
+ <Upload className="h-4 w-4" />
312
+ Upload Logo
313
+ </div>
314
+ </label>
315
+ <span className="text-xs text-muted-foreground">
316
+ PNG, JPG, or SVG
317
+ </span>
318
+ </div>
319
+ )}
320
+ </div>
321
+ </div>
322
+
323
+ {/* Revisions */}
324
+ <div className="space-y-3">
325
+ <div className="flex items-center justify-between">
326
+ <h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wide">
327
+ Revision History
328
+ </h3>
329
+ <Button
330
+ variant="outline"
331
+ size="sm"
332
+ onClick={() => setShowRevisionForm(true)}
333
+ >
334
+ <Plus className="h-4 w-4 mr-1" />
335
+ Add Revision
336
+ </Button>
337
+ </div>
338
+
339
+ {showRevisionForm && (
340
+ <div className="p-3 bg-muted/30 rounded-lg space-y-3">
341
+ <div className="grid grid-cols-2 gap-3">
342
+ <div>
343
+ <Label className="text-xs">Rev #</Label>
344
+ <Input
345
+ value={newRevision.revision}
346
+ onChange={(e) => setNewRevision(prev => ({ ...prev, revision: e.target.value }))}
347
+ placeholder="A, B, 01..."
348
+ className="h-8 mt-1"
349
+ />
350
+ </div>
351
+ <div>
352
+ <Label className="text-xs">Date</Label>
353
+ <Input
354
+ value={newRevision.date}
355
+ onChange={(e) => setNewRevision(prev => ({ ...prev, date: e.target.value }))}
356
+ placeholder="2024-01-15"
357
+ className="h-8 mt-1"
358
+ />
359
+ </div>
360
+ </div>
361
+ <div>
362
+ <Label className="text-xs">Description</Label>
363
+ <Input
364
+ value={newRevision.description}
365
+ onChange={(e) => setNewRevision(prev => ({ ...prev, description: e.target.value }))}
366
+ placeholder="Description of changes..."
367
+ className="h-8 mt-1"
368
+ />
369
+ </div>
370
+ <div>
371
+ <Label className="text-xs">Author</Label>
372
+ <Input
373
+ value={newRevision.author}
374
+ onChange={(e) => setNewRevision(prev => ({ ...prev, author: e.target.value }))}
375
+ placeholder="Initials..."
376
+ className="h-8 mt-1"
377
+ />
378
+ </div>
379
+ <div className="flex gap-2 pt-2">
380
+ <Button size="sm" onClick={handleAddRevision} disabled={!newRevision.revision || !newRevision.description}>
381
+ Add Revision
382
+ </Button>
383
+ <Button
384
+ size="sm"
385
+ variant="ghost"
386
+ onClick={() => {
387
+ setShowRevisionForm(false);
388
+ setNewRevision({ revision: '', date: new Date().toLocaleDateString(), description: '', author: '' });
389
+ }}
390
+ >
391
+ Cancel
392
+ </Button>
393
+ </div>
394
+ </div>
395
+ )}
396
+
397
+ {activeSheet.revisions.length === 0 && !showRevisionForm ? (
398
+ <div className="text-sm text-muted-foreground text-center py-4">
399
+ No revisions yet
400
+ </div>
401
+ ) : (
402
+ <div className="space-y-1">
403
+ {activeSheet.revisions.map((rev, index) => (
404
+ <div
405
+ key={index}
406
+ className="flex items-center justify-between px-3 py-2 bg-muted/30 rounded text-xs"
407
+ >
408
+ <div className="flex gap-4">
409
+ <span className="font-mono font-bold">{rev.revision}</span>
410
+ <span className="text-muted-foreground">{rev.date}</span>
411
+ <span className="truncate">{rev.description}</span>
412
+ {rev.author && <span className="text-muted-foreground">by {rev.author}</span>}
413
+ </div>
414
+ <Button
415
+ variant="ghost"
416
+ size="icon-sm"
417
+ className="h-6 w-6 text-destructive hover:text-destructive"
418
+ onClick={() => removeRevision(index)}
419
+ >
420
+ <Trash2 className="h-3 w-3" />
421
+ </Button>
422
+ </div>
423
+ ))}
424
+ </div>
425
+ )}
426
+ </div>
427
+ </div>
428
+
429
+ <DialogFooter>
430
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
431
+ Done
432
+ </Button>
433
+ </DialogFooter>
434
+ </DialogContent>
435
+ </Dialog>
436
+ );
437
+ }