@ifc-lite/viewer 1.17.6 → 1.18.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 (143) hide show
  1. package/.turbo/turbo-build.log +17 -14
  2. package/.turbo/turbo-typecheck.log +1 -1
  3. package/CHANGELOG.md +513 -0
  4. package/dist/assets/{basketViewActivator-86rgogji.js → basketViewActivator-Cm1QEk_R.js} +1 -1
  5. package/dist/assets/{exporters-CcPS9MK5.js → exporters-B_OBqIyD.js} +3235 -2648
  6. package/dist/assets/{geometry.worker-BFUYA08u.js → geometry.worker-xHHy-9DV.js} +1 -1
  7. package/dist/assets/{ifc-lite_bg-BINvzoCP.wasm → ifc-lite_bg-ADjKXSms.wasm} +0 -0
  8. package/dist/assets/{index-Bfms9I4A.js → index-BKq-M3Mk.js} +44124 -30920
  9. package/dist/assets/index-COnQRuqY.css +1 -0
  10. package/dist/assets/{native-bridge-DUyLCMZS.js → native-bridge-SHXiQwFW.js} +1 -1
  11. package/dist/assets/sandbox-jez21HtV.js +9627 -0
  12. package/dist/assets/{server-client-BuZK7OST.js → server-client-ncOQVNso.js} +1 -1
  13. package/dist/assets/{wasm-bridge-JsqEGDV8.js → wasm-bridge-DyfBSB8z.js} +1 -1
  14. package/dist/index.html +6 -6
  15. package/package.json +10 -10
  16. package/src/apache-arrow.d.ts +30 -0
  17. package/src/components/viewer/AddElementPanel.tsx +758 -0
  18. package/src/components/viewer/BulkPropertyEditor.tsx +7 -0
  19. package/src/components/viewer/ChatPanel.tsx +64 -2
  20. package/src/components/viewer/CommandPalette.tsx +56 -7
  21. package/src/components/viewer/EntityContextMenu.tsx +168 -4
  22. package/src/components/viewer/ExportChangesButton.tsx +25 -5
  23. package/src/components/viewer/ExportDialog.tsx +19 -1
  24. package/src/components/viewer/MainToolbar.tsx +69 -10
  25. package/src/components/viewer/PropertiesPanel.tsx +222 -22
  26. package/src/components/viewer/SearchInline.tsx +669 -0
  27. package/src/components/viewer/SearchModal.filter.builder.tsx +766 -0
  28. package/src/components/viewer/SearchModal.filter.tsx +514 -0
  29. package/src/components/viewer/SearchModal.text.tsx +388 -0
  30. package/src/components/viewer/SearchModal.tsx +235 -0
  31. package/src/components/viewer/ToolOverlays.tsx +5 -0
  32. package/src/components/viewer/ViewerLayout.tsx +24 -4
  33. package/src/components/viewer/Viewport.tsx +11 -1
  34. package/src/components/viewer/ViewportContainer.tsx +2 -0
  35. package/src/components/viewer/annotations/AnnotationDropInput.tsx +203 -0
  36. package/src/components/viewer/annotations/AnnotationLayer.tsx +287 -0
  37. package/src/components/viewer/annotations/AnnotationPin.tsx +90 -0
  38. package/src/components/viewer/annotations/AnnotationPopover.tsx +296 -0
  39. package/src/components/viewer/bcf/BCFTopicDetail.tsx +1 -1
  40. package/src/components/viewer/lists/ListPanel.tsx +14 -21
  41. package/src/components/viewer/properties/RawStepCard.tsx +332 -0
  42. package/src/components/viewer/properties/RawStepRow.tsx +261 -0
  43. package/src/components/viewer/properties/ScheduleCard.tsx +224 -0
  44. package/src/components/viewer/properties/TaskEditCard.tsx +510 -0
  45. package/src/components/viewer/properties/raw-step-format.ts +193 -0
  46. package/src/components/viewer/schedule/AnimationSettingsPopover.tsx +542 -0
  47. package/src/components/viewer/schedule/GanttDependencyArrows.tsx +89 -0
  48. package/src/components/viewer/schedule/GanttDragTooltip.tsx +48 -0
  49. package/src/components/viewer/schedule/GanttEmptyState.tsx +97 -0
  50. package/src/components/viewer/schedule/GanttPanel.tsx +295 -0
  51. package/src/components/viewer/schedule/GanttTaskBar.tsx +199 -0
  52. package/src/components/viewer/schedule/GanttTaskTree.tsx +250 -0
  53. package/src/components/viewer/schedule/GanttTimeline.tsx +305 -0
  54. package/src/components/viewer/schedule/GanttToolbar.tsx +406 -0
  55. package/src/components/viewer/schedule/GenerateAdvancedPanel.tsx +147 -0
  56. package/src/components/viewer/schedule/GenerateScheduleDialog.tsx +392 -0
  57. package/src/components/viewer/schedule/HeightStrategyPanel.tsx +120 -0
  58. package/src/components/viewer/schedule/generate-schedule.test.ts +439 -0
  59. package/src/components/viewer/schedule/generate-schedule.ts +648 -0
  60. package/src/components/viewer/schedule/schedule-animator.test.ts +452 -0
  61. package/src/components/viewer/schedule/schedule-animator.ts +488 -0
  62. package/src/components/viewer/schedule/schedule-selection.test.ts +148 -0
  63. package/src/components/viewer/schedule/schedule-selection.ts +163 -0
  64. package/src/components/viewer/schedule/schedule-utils.ts +223 -0
  65. package/src/components/viewer/schedule/useConstructionSequence.ts +156 -0
  66. package/src/components/viewer/schedule/useGanttBarDrag.test.ts +90 -0
  67. package/src/components/viewer/schedule/useGanttBarDrag.ts +305 -0
  68. package/src/components/viewer/schedule/useGanttSelection3DHighlight.ts +152 -0
  69. package/src/components/viewer/schedule/useOverlayCompositor.ts +108 -0
  70. package/src/components/viewer/selectionHandlers.ts +446 -0
  71. package/src/components/viewer/tools/AddElementOverlay.tsx +540 -0
  72. package/src/components/viewer/useDuplicateShortcut.ts +77 -0
  73. package/src/components/viewer/useMouseControls.ts +9 -1
  74. package/src/hooks/useIfcLoader.ts +22 -10
  75. package/src/hooks/useKeyboardShortcuts.ts +25 -0
  76. package/src/hooks/useSandbox.ts +1 -1
  77. package/src/hooks/useSearchIndex.ts +125 -0
  78. package/src/index.css +66 -0
  79. package/src/lib/llm/system-prompt.test.ts +14 -0
  80. package/src/lib/llm/system-prompt.ts +102 -1
  81. package/src/lib/llm/types.ts +6 -0
  82. package/src/lib/recent-files.ts +38 -4
  83. package/src/lib/scripts/templates/bim-globals.d.ts +136 -114
  84. package/src/lib/scripts/templates/construction-schedule.ts +223 -0
  85. package/src/lib/scripts/templates.ts +7 -0
  86. package/src/lib/search/common-ifc-types.ts +36 -0
  87. package/src/lib/search/filter-evaluate.test.ts +537 -0
  88. package/src/lib/search/filter-evaluate.ts +610 -0
  89. package/src/lib/search/filter-rules.test.ts +119 -0
  90. package/src/lib/search/filter-rules.ts +198 -0
  91. package/src/lib/search/filter-schema.test.ts +233 -0
  92. package/src/lib/search/filter-schema.ts +146 -0
  93. package/src/lib/search/recent-searches.test.ts +116 -0
  94. package/src/lib/search/recent-searches.ts +93 -0
  95. package/src/lib/search/result-export.test.ts +101 -0
  96. package/src/lib/search/result-export.ts +104 -0
  97. package/src/lib/search/saved-filters.test.ts +118 -0
  98. package/src/lib/search/saved-filters.ts +154 -0
  99. package/src/lib/search/tier0-scan.test.ts +196 -0
  100. package/src/lib/search/tier0-scan.ts +237 -0
  101. package/src/lib/search/tier1-index.test.ts +242 -0
  102. package/src/lib/search/tier1-index.ts +448 -0
  103. package/src/sdk/adapters/export-adapter.test.ts +434 -1
  104. package/src/sdk/adapters/export-adapter.ts +404 -1
  105. package/src/sdk/adapters/export-schedule-splice.test.ts +127 -0
  106. package/src/sdk/adapters/export-schedule-splice.ts +87 -0
  107. package/src/sdk/adapters/model-compat.ts +8 -2
  108. package/src/sdk/adapters/schedule-adapter.ts +73 -0
  109. package/src/sdk/adapters/store-adapter.ts +201 -0
  110. package/src/sdk/adapters/visibility-adapter.ts +3 -0
  111. package/src/sdk/local-backend.ts +16 -8
  112. package/src/services/desktop-export.ts +3 -1
  113. package/src/services/desktop-native-metadata.ts +41 -18
  114. package/src/services/file-dialog.ts +4 -1
  115. package/src/services/tauri-modules.d.ts +25 -0
  116. package/src/store/basketVisibleSet.ts +3 -0
  117. package/src/store/globalId.ts +4 -1
  118. package/src/store/index.ts +70 -1
  119. package/src/store/slices/addElementMeshes.ts +365 -0
  120. package/src/store/slices/addElementSlice.ts +275 -0
  121. package/src/store/slices/annotationsSlice.test.ts +133 -0
  122. package/src/store/slices/annotationsSlice.ts +251 -0
  123. package/src/store/slices/dataSlice.test.ts +23 -4
  124. package/src/store/slices/dataSlice.ts +1 -1
  125. package/src/store/slices/modelSlice.test.ts +67 -9
  126. package/src/store/slices/modelSlice.ts +39 -7
  127. package/src/store/slices/mutationSlice.ts +964 -3
  128. package/src/store/slices/overlayCompositor.test.ts +164 -0
  129. package/src/store/slices/overlaySlice.test.ts +93 -0
  130. package/src/store/slices/overlaySlice.ts +151 -0
  131. package/src/store/slices/pinboardSlice.test.ts +6 -1
  132. package/src/store/slices/playbackSlice.ts +128 -0
  133. package/src/store/slices/schedule-edit-helpers.test.ts +97 -0
  134. package/src/store/slices/schedule-edit-helpers.ts +179 -0
  135. package/src/store/slices/scheduleSlice.test.ts +694 -0
  136. package/src/store/slices/scheduleSlice.ts +1330 -0
  137. package/src/store/slices/searchSlice.test.ts +342 -0
  138. package/src/store/slices/searchSlice.ts +341 -0
  139. package/src/store/slices/selectionSlice.test.ts +46 -0
  140. package/src/store/slices/selectionSlice.ts +20 -0
  141. package/src/store.ts +14 -0
  142. package/dist/assets/index-_bfZsDCC.css +0 -1
  143. package/dist/assets/sandbox-C8575tul.js +0 -5951
@@ -0,0 +1,406 @@
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
+ * GanttToolbar — play/pause, timeline scrubber, speed control,
7
+ * work-schedule selector, and animation toggle.
8
+ */
9
+
10
+ import { useCallback, useMemo } from 'react';
11
+ import {
12
+ Play,
13
+ Pause,
14
+ SkipBack,
15
+ SkipForward,
16
+ Repeat,
17
+ Repeat2,
18
+ Gauge,
19
+ Calendar,
20
+ CalendarPlus,
21
+ Plus,
22
+ X,
23
+ Trash2,
24
+ Undo2,
25
+ Redo2,
26
+ } from 'lucide-react';
27
+ import { Button } from '@/components/ui/button';
28
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
29
+ import {
30
+ Select,
31
+ SelectContent,
32
+ SelectItem,
33
+ SelectTrigger,
34
+ SelectValue,
35
+ } from '@/components/ui/select';
36
+ import { useViewerStore, countGeneratedTasks } from '@/store';
37
+ import type { GanttTimeScale } from '@/store';
38
+ import { toast } from '@/components/ui/toast';
39
+ import { formatDateTime } from './schedule-utils';
40
+ import { AnimationSettingsPopover } from './AnimationSettingsPopover';
41
+
42
+ interface GanttToolbarProps {
43
+ onClose?: () => void;
44
+ onOpenGenerate?: () => void;
45
+ canGenerate?: boolean;
46
+ }
47
+
48
+ const SPEED_OPTIONS: Array<{ value: number; label: string }> = [
49
+ { value: 0.5, label: '0.5 d/s' },
50
+ { value: 1, label: '1 d/s' },
51
+ { value: 3, label: '3 d/s' },
52
+ { value: 7, label: '1 w/s' },
53
+ { value: 30, label: '1 mo/s' },
54
+ { value: 90, label: '3 mo/s' },
55
+ ];
56
+
57
+ const SCALE_OPTIONS: Array<{ value: GanttTimeScale; label: string }> = [
58
+ { value: 'hour', label: 'Hour' },
59
+ { value: 'day', label: 'Day' },
60
+ { value: 'week', label: 'Week' },
61
+ { value: 'month', label: 'Month' },
62
+ { value: 'year', label: 'Year' },
63
+ ];
64
+
65
+ // Radix Select rejects '' as a SelectItem value — use a sentinel for the
66
+ // "All tasks" option and translate at the API boundary.
67
+ const ALL_SCHEDULES_SENTINEL = '__all__';
68
+
69
+ export function GanttToolbar({ onClose, onOpenGenerate, canGenerate }: GanttToolbarProps) {
70
+ const scheduleData = useViewerStore(s => s.scheduleData);
71
+ const scheduleRange = useViewerStore(s => s.scheduleRange);
72
+ const activeWorkScheduleId = useViewerStore(s => s.activeWorkScheduleId);
73
+ const setActiveWorkScheduleId = useViewerStore(s => s.setActiveWorkScheduleId);
74
+ const isPlaying = useViewerStore(s => s.playbackIsPlaying);
75
+ const playbackTime = useViewerStore(s => s.playbackTime);
76
+ const playbackSpeed = useViewerStore(s => s.playbackSpeed);
77
+ const playbackLoop = useViewerStore(s => s.playbackLoop);
78
+ const animationEnabled = useViewerStore(s => s.animationEnabled);
79
+ const pendingGeneratedCount = useViewerStore(s => countGeneratedTasks(s.scheduleData));
80
+ const clearGeneratedSchedule = useViewerStore(s => s.clearGeneratedSchedule);
81
+ const undoDepth = useViewerStore(s => s.scheduleUndoStack.length);
82
+ const redoDepth = useViewerStore(s => s.scheduleRedoStack.length);
83
+ const undoScheduleEdit = useViewerStore(s => s.undoScheduleEdit);
84
+ const redoScheduleEdit = useViewerStore(s => s.redoScheduleEdit);
85
+ const addTaskAction = useViewerStore(s => s.addTask);
86
+ const selectedTaskGlobalIds = useViewerStore(s => s.selectedTaskGlobalIds);
87
+ const scale = useViewerStore(s => s.ganttTimeScale);
88
+ const togglePlay = useViewerStore(s => s.togglePlaySchedule);
89
+ const pause = useViewerStore(s => s.pauseSchedule);
90
+ const seek = useViewerStore(s => s.seekSchedule);
91
+ const setSpeed = useViewerStore(s => s.setPlaybackSpeed);
92
+ const setLoop = useViewerStore(s => s.setPlaybackLoop);
93
+ const setAnimationEnabled = useViewerStore(s => s.setAnimationEnabled);
94
+ const setScale = useViewerStore(s => s.setGanttTimeScale);
95
+
96
+ const hasData = !!scheduleData && scheduleData.tasks.length > 0;
97
+ const hasDates = !!scheduleRange && !scheduleRange.synthetic;
98
+
99
+ const scheduleOptions = useMemo(() => {
100
+ if (!scheduleData) return [];
101
+ return [
102
+ { value: ALL_SCHEDULES_SENTINEL, label: 'All tasks' },
103
+ ...scheduleData.workSchedules.map(s => ({
104
+ value: s.globalId,
105
+ label: s.name || s.globalId,
106
+ })),
107
+ ];
108
+ }, [scheduleData]);
109
+
110
+ const selectedScheduleValue = activeWorkScheduleId || ALL_SCHEDULES_SENTINEL;
111
+ const handleScheduleChange = useCallback((value: string) => {
112
+ setActiveWorkScheduleId(value === ALL_SCHEDULES_SENTINEL ? '' : value);
113
+ }, [setActiveWorkScheduleId]);
114
+
115
+ const scrubPercent = useMemo(() => {
116
+ if (!scheduleRange) return 0;
117
+ const span = scheduleRange.end - scheduleRange.start;
118
+ if (span <= 0) return 0;
119
+ return Math.min(100, Math.max(0, ((playbackTime - scheduleRange.start) / span) * 100));
120
+ }, [scheduleRange, playbackTime]);
121
+
122
+ const onScrubInput = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
123
+ if (!scheduleRange) return;
124
+ const pct = parseFloat(e.target.value) / 100;
125
+ seek(scheduleRange.start + pct * (scheduleRange.end - scheduleRange.start));
126
+ }, [scheduleRange, seek]);
127
+
128
+ const onScrubPointerDown = useCallback(() => {
129
+ if (isPlaying) pause();
130
+ }, [isPlaying, pause]);
131
+
132
+ const goStart = useCallback(() => {
133
+ if (scheduleRange) seek(scheduleRange.start);
134
+ }, [scheduleRange, seek]);
135
+
136
+ const goEnd = useCallback(() => {
137
+ if (scheduleRange) seek(scheduleRange.end);
138
+ }, [scheduleRange, seek]);
139
+
140
+ return (
141
+ <div className="flex items-center gap-2 px-3 py-2 border-b bg-card/40 text-sm">
142
+ <div className="flex items-center gap-1">
143
+ <Tooltip>
144
+ <TooltipTrigger asChild>
145
+ <Button
146
+ size="icon-sm"
147
+ variant="ghost"
148
+ onClick={goStart}
149
+ disabled={!hasData}
150
+ aria-label="Jump to start"
151
+ >
152
+ <SkipBack className="h-4 w-4" />
153
+ </Button>
154
+ </TooltipTrigger>
155
+ <TooltipContent>Jump to start</TooltipContent>
156
+ </Tooltip>
157
+
158
+ <Tooltip>
159
+ <TooltipTrigger asChild>
160
+ <Button
161
+ size="icon-sm"
162
+ variant={isPlaying ? 'default' : 'ghost'}
163
+ onClick={togglePlay}
164
+ disabled={!hasData}
165
+ aria-label={isPlaying ? 'Pause' : 'Play'}
166
+ >
167
+ {isPlaying ? <Pause className="h-4 w-4" /> : <Play className="h-4 w-4" />}
168
+ </Button>
169
+ </TooltipTrigger>
170
+ <TooltipContent>{isPlaying ? 'Pause' : 'Play'} construction sequence</TooltipContent>
171
+ </Tooltip>
172
+
173
+ <Tooltip>
174
+ <TooltipTrigger asChild>
175
+ <Button
176
+ size="icon-sm"
177
+ variant="ghost"
178
+ onClick={goEnd}
179
+ disabled={!hasData}
180
+ aria-label="Jump to finish"
181
+ >
182
+ <SkipForward className="h-4 w-4" />
183
+ </Button>
184
+ </TooltipTrigger>
185
+ <TooltipContent>Jump to finish</TooltipContent>
186
+ </Tooltip>
187
+
188
+ <Tooltip>
189
+ <TooltipTrigger asChild>
190
+ <Button
191
+ size="icon-sm"
192
+ variant={playbackLoop ? 'default' : 'ghost'}
193
+ onClick={() => setLoop(!playbackLoop)}
194
+ aria-label={playbackLoop ? 'Disable loop' : 'Enable loop'}
195
+ >
196
+ {playbackLoop ? <Repeat className="h-4 w-4" /> : <Repeat2 className="h-4 w-4" />}
197
+ </Button>
198
+ </TooltipTrigger>
199
+ <TooltipContent>{playbackLoop ? 'Looping' : 'One-shot'}</TooltipContent>
200
+ </Tooltip>
201
+ </div>
202
+
203
+ {/* Scrub bar */}
204
+ <div className="flex-1 flex items-center gap-2 min-w-[240px]">
205
+ <input
206
+ type="range"
207
+ min={0}
208
+ max={100}
209
+ step={0.01}
210
+ value={scrubPercent}
211
+ onChange={onScrubInput}
212
+ onPointerDown={onScrubPointerDown}
213
+ disabled={!hasData}
214
+ className="flex-1 accent-primary cursor-pointer h-1 appearance-none bg-muted rounded-full"
215
+ aria-label="Playback position"
216
+ />
217
+ <span className="text-xs text-muted-foreground font-mono whitespace-nowrap">
218
+ {hasData ? formatDateTime(playbackTime) : '—'}
219
+ </span>
220
+ </div>
221
+
222
+ {/* Work schedule dropdown */}
223
+ <div className="flex items-center gap-1">
224
+ <Calendar className="h-4 w-4 text-muted-foreground" />
225
+ <Select value={selectedScheduleValue} onValueChange={handleScheduleChange}>
226
+ <SelectTrigger className="h-8 w-[180px] text-xs">
227
+ <SelectValue placeholder="All tasks" />
228
+ </SelectTrigger>
229
+ <SelectContent>
230
+ {scheduleOptions.map(opt => (
231
+ <SelectItem key={opt.value} value={opt.value}>
232
+ {opt.label}
233
+ </SelectItem>
234
+ ))}
235
+ </SelectContent>
236
+ </Select>
237
+ </div>
238
+
239
+ {/* Speed */}
240
+ <div className="flex items-center gap-1">
241
+ <Tooltip>
242
+ <TooltipTrigger asChild>
243
+ <Gauge className="h-4 w-4 text-muted-foreground" />
244
+ </TooltipTrigger>
245
+ <TooltipContent>Simulation speed</TooltipContent>
246
+ </Tooltip>
247
+ <Select
248
+ value={String(playbackSpeed)}
249
+ onValueChange={(v) => setSpeed(parseFloat(v))}
250
+ >
251
+ <SelectTrigger className="h-8 w-[110px] text-xs">
252
+ <SelectValue />
253
+ </SelectTrigger>
254
+ <SelectContent>
255
+ {SPEED_OPTIONS.map(opt => (
256
+ <SelectItem key={opt.value} value={String(opt.value)}>
257
+ {opt.label}
258
+ </SelectItem>
259
+ ))}
260
+ </SelectContent>
261
+ </Select>
262
+ </div>
263
+
264
+ {/* Scale */}
265
+ <Select value={scale} onValueChange={(v) => setScale(v as GanttTimeScale)}>
266
+ <SelectTrigger className="h-8 w-[90px] text-xs">
267
+ <SelectValue />
268
+ </SelectTrigger>
269
+ <SelectContent>
270
+ {SCALE_OPTIONS.map(opt => (
271
+ <SelectItem key={opt.value} value={opt.value}>
272
+ {opt.label}
273
+ </SelectItem>
274
+ ))}
275
+ </SelectContent>
276
+ </Select>
277
+
278
+ {/* Generate from spatial hierarchy */}
279
+ {onOpenGenerate && (
280
+ <Tooltip>
281
+ <TooltipTrigger asChild>
282
+ <Button
283
+ size="icon-sm"
284
+ variant="ghost"
285
+ onClick={onOpenGenerate}
286
+ disabled={!canGenerate}
287
+ aria-label="Generate construction schedule"
288
+ >
289
+ <CalendarPlus className="h-4 w-4" />
290
+ </Button>
291
+ </TooltipTrigger>
292
+ <TooltipContent>
293
+ {canGenerate ? 'Generate schedule…' : 'No spatial hierarchy or geometry to generate from'}
294
+ </TooltipContent>
295
+ </Tooltip>
296
+ )}
297
+
298
+ {/* + Task — insert a new task after the currently-selected row
299
+ (or at the end when none is selected). Auto-selects the new
300
+ task so the Inspector's Task card lights up for rename. */}
301
+ {hasData && (
302
+ <Tooltip>
303
+ <TooltipTrigger asChild>
304
+ <Button
305
+ size="icon-sm"
306
+ variant="ghost"
307
+ onClick={() => {
308
+ const afterGlobalId = selectedTaskGlobalIds.size === 1
309
+ ? selectedTaskGlobalIds.values().next().value
310
+ : undefined;
311
+ addTaskAction({ afterGlobalId });
312
+ }}
313
+ aria-label="Add task"
314
+ >
315
+ <Plus className="h-4 w-4" />
316
+ </Button>
317
+ </TooltipTrigger>
318
+ <TooltipContent>Add task (after selection or at end)</TooltipContent>
319
+ </Tooltip>
320
+ )}
321
+
322
+ {/* Undo / Redo for schedule edits. Gated on stack depth so the
323
+ buttons only appear when there's actually something to undo —
324
+ avoids a persistent greyed-out pair on clean schedules. */}
325
+ {(undoDepth > 0 || redoDepth > 0) && (
326
+ <div className="flex items-center gap-1">
327
+ <Tooltip>
328
+ <TooltipTrigger asChild>
329
+ <Button
330
+ size="icon-sm"
331
+ variant="ghost"
332
+ onClick={undoScheduleEdit}
333
+ disabled={undoDepth === 0}
334
+ aria-label="Undo schedule edit"
335
+ >
336
+ <Undo2 className="h-4 w-4" />
337
+ </Button>
338
+ </TooltipTrigger>
339
+ <TooltipContent>Undo (Ctrl+Z)</TooltipContent>
340
+ </Tooltip>
341
+ <Tooltip>
342
+ <TooltipTrigger asChild>
343
+ <Button
344
+ size="icon-sm"
345
+ variant="ghost"
346
+ onClick={redoScheduleEdit}
347
+ disabled={redoDepth === 0}
348
+ aria-label="Redo schedule edit"
349
+ >
350
+ <Redo2 className="h-4 w-4" />
351
+ </Button>
352
+ </TooltipTrigger>
353
+ <TooltipContent>Redo (Ctrl+Shift+Z)</TooltipContent>
354
+ </Tooltip>
355
+ </div>
356
+ )}
357
+
358
+ {/* Discard pending generated schedule — only visible when at least
359
+ one locally-generated task exists. Keeps extracted tasks intact
360
+ so partial-authoring workflows can still revert just the
361
+ pending tail. */}
362
+ {pendingGeneratedCount > 0 && (
363
+ <Tooltip>
364
+ <TooltipTrigger asChild>
365
+ <Button
366
+ size="icon-sm"
367
+ variant="ghost"
368
+ onClick={() => {
369
+ const removed = clearGeneratedSchedule();
370
+ if (removed > 0) {
371
+ toast.success(`Discarded ${removed} pending task${removed === 1 ? '' : 's'}.`);
372
+ }
373
+ }}
374
+ aria-label={`Discard ${pendingGeneratedCount} pending generated task${pendingGeneratedCount === 1 ? '' : 's'}`}
375
+ className="text-amber-600 dark:text-amber-400 hover:text-amber-700 dark:hover:text-amber-300"
376
+ >
377
+ <Trash2 className="h-4 w-4" />
378
+ </Button>
379
+ </TooltipTrigger>
380
+ <TooltipContent>
381
+ Discard {pendingGeneratedCount} pending schedule task{pendingGeneratedCount === 1 ? '' : 's'}
382
+ </TooltipContent>
383
+ </Tooltip>
384
+ )}
385
+
386
+ {/* Animation settings popover (replaces the bare toggle — gives the
387
+ user access to lifecycle colour / palette / preparation window). */}
388
+ <AnimationSettingsPopover
389
+ animationEnabled={animationEnabled}
390
+ onToggleAnimation={() => setAnimationEnabled(!animationEnabled)}
391
+ />
392
+
393
+ {onClose && (
394
+ <Button size="icon-sm" variant="ghost" onClick={onClose} aria-label="Close Gantt panel">
395
+ <X className="h-4 w-4" />
396
+ </Button>
397
+ )}
398
+
399
+ {hasData && !hasDates && (
400
+ <span className="text-xs text-amber-500 whitespace-nowrap" title="No real dates — using synthetic range">
401
+ No dates
402
+ </span>
403
+ )}
404
+ </div>
405
+ );
406
+ }
@@ -0,0 +1,147 @@
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
+ * GenerateAdvancedPanel — disclosure-expanded "Advanced" section of the
7
+ * Generate dialog. Holds rarely-touched fields (lag, PredefinedType,
8
+ * schedule name, sequence linking toggle, skip-empty toggle). Extracted
9
+ * so the main dialog stays readable.
10
+ */
11
+
12
+ import { ChevronDown, ChevronRight } from 'lucide-react';
13
+ import { Input } from '@/components/ui/input';
14
+ import { Label } from '@/components/ui/label';
15
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
16
+ import { Switch } from '@/components/ui/switch';
17
+ import type { GenerateScheduleOptions, SpatialGroupStrategy } from './generate-schedule';
18
+
19
+ const TASK_TYPES = [
20
+ 'CONSTRUCTION', 'INSTALLATION', 'DEMOLITION', 'DISMANTLE',
21
+ 'DISPOSAL', 'LOGISTIC', 'MAINTENANCE', 'MOVE',
22
+ 'OPERATION', 'REMOVAL', 'RENOVATION', 'ATTENDANCE',
23
+ 'USERDEFINED', 'NOTDEFINED',
24
+ ] as const;
25
+
26
+ export interface GenerateAdvancedPanelProps {
27
+ open: boolean;
28
+ onOpenChange: (next: boolean) => void;
29
+ strategy: SpatialGroupStrategy;
30
+ lagDays: number;
31
+ predefinedType: string;
32
+ scheduleName: string;
33
+ linkSequences: boolean;
34
+ skipEmptyGroups: boolean;
35
+ onChange: <K extends keyof GenerateScheduleOptions>(
36
+ key: K,
37
+ value: GenerateScheduleOptions[K],
38
+ ) => void;
39
+ }
40
+
41
+ export function GenerateAdvancedPanel({
42
+ open,
43
+ onOpenChange,
44
+ strategy,
45
+ lagDays,
46
+ predefinedType,
47
+ scheduleName,
48
+ linkSequences,
49
+ skipEmptyGroups,
50
+ onChange,
51
+ }: GenerateAdvancedPanelProps) {
52
+ return (
53
+ <div className="rounded-md border">
54
+ <button
55
+ type="button"
56
+ className="flex w-full items-center gap-2 px-3 py-2 text-sm font-medium text-left hover:bg-muted/40 transition-colors"
57
+ onClick={() => onOpenChange(!open)}
58
+ aria-expanded={open}
59
+ >
60
+ {open ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
61
+ Advanced
62
+ </button>
63
+ {open && (
64
+ <div className="grid gap-3 border-t p-3">
65
+ <div className="grid grid-cols-2 gap-3">
66
+ <div className="grid gap-1.5">
67
+ <Label htmlFor="gen-lag">Lag days (between groups)</Label>
68
+ <Input
69
+ id="gen-lag"
70
+ type="number"
71
+ min={0}
72
+ step={1}
73
+ value={lagDays}
74
+ onChange={(e) => {
75
+ const v = parseFloat(e.target.value);
76
+ onChange('lagDays', Number.isFinite(v) && v >= 0 ? v : 0);
77
+ }}
78
+ />
79
+ </div>
80
+ <div className="grid gap-1.5">
81
+ <Label htmlFor="gen-type">PredefinedType</Label>
82
+ <Select
83
+ value={predefinedType}
84
+ onValueChange={(v) => onChange('predefinedType', v)}
85
+ >
86
+ <SelectTrigger id="gen-type">
87
+ <SelectValue />
88
+ </SelectTrigger>
89
+ <SelectContent>
90
+ {TASK_TYPES.map(t => (
91
+ <SelectItem key={t} value={t}>{t}</SelectItem>
92
+ ))}
93
+ </SelectContent>
94
+ </Select>
95
+ </div>
96
+ </div>
97
+
98
+ <div className="grid gap-1.5">
99
+ <Label htmlFor="gen-name">Work schedule name</Label>
100
+ <Input
101
+ id="gen-name"
102
+ value={scheduleName}
103
+ onChange={(e) => onChange('scheduleName', e.target.value)}
104
+ placeholder="Construction schedule"
105
+ />
106
+ </div>
107
+
108
+ <ToggleRow
109
+ label="Link tasks with FS dependencies"
110
+ description="Adds IfcRelSequence edges between consecutive groups."
111
+ checked={linkSequences}
112
+ onChange={(v) => onChange('linkSequences', v)}
113
+ />
114
+ <ToggleRow
115
+ label="Skip empty groups"
116
+ description={
117
+ strategy === 'IfcElement'
118
+ ? 'Ignore Z slices with no elements.'
119
+ : 'Ignore storeys or buildings with no contained products.'
120
+ }
121
+ checked={skipEmptyGroups}
122
+ onChange={(v) => onChange('skipEmptyGroups', v)}
123
+ />
124
+ </div>
125
+ )}
126
+ </div>
127
+ );
128
+ }
129
+
130
+ interface ToggleRowProps {
131
+ label: string;
132
+ description: string;
133
+ checked: boolean;
134
+ onChange: (next: boolean) => void;
135
+ }
136
+
137
+ function ToggleRow({ label, description, checked, onChange }: ToggleRowProps) {
138
+ return (
139
+ <label className="flex items-center justify-between gap-3 cursor-pointer">
140
+ <span className="grid gap-0.5">
141
+ <span className="text-sm font-medium">{label}</span>
142
+ <span className="text-xs text-muted-foreground">{description}</span>
143
+ </span>
144
+ <Switch checked={checked} onCheckedChange={onChange} />
145
+ </label>
146
+ );
147
+ }