@ifc-lite/viewer 1.17.4 → 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.
- package/.turbo/turbo-build.log +20 -17
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +630 -0
- package/DESKTOP_CONTRACT_VERSION +1 -1
- package/dist/assets/{basketViewActivator-BmnNtVfZ.js → basketViewActivator-Cm1QEk_R.js} +1 -1
- package/dist/assets/drawing-2d-DoxKMqbO.js +257 -0
- package/dist/assets/{exporters-ChAtBmlj.js → exporters-B_OBqIyD.js} +3479 -2845
- package/dist/assets/{geometry.worker-BQ0rzNo-.js → geometry.worker-xHHy-9DV.js} +1 -1
- package/dist/assets/ids-DQ5jY0E8.js +1 -0
- package/dist/assets/ifc-lite_bg-ADjKXSms.wasm +0 -0
- package/dist/assets/{index-Co8E2-FE.js → index-BKq-M3Mk.js} +55873 -40593
- package/dist/assets/index-COnQRuqY.css +1 -0
- package/dist/assets/{native-bridge-BRvbckFQ.js → native-bridge-SHXiQwFW.js} +104 -104
- package/dist/assets/sandbox-jez21HtV.js +9627 -0
- package/dist/assets/{server-client-BV8zHZ7Y.js → server-client-ncOQVNso.js} +1 -1
- package/dist/assets/{wasm-bridge-g01g7T9b.js → wasm-bridge-DyfBSB8z.js} +1 -1
- package/dist/index.html +8 -7
- package/index.html +1 -0
- package/package.json +13 -13
- package/src/App.tsx +16 -2
- package/src/apache-arrow.d.ts +30 -0
- package/src/components/viewer/AddElementPanel.tsx +758 -0
- package/src/components/viewer/BulkPropertyEditor.tsx +7 -0
- package/src/components/viewer/CesiumOverlay.tsx +62 -19
- package/src/components/viewer/ChatPanel.tsx +259 -93
- package/src/components/viewer/CommandPalette.tsx +56 -7
- package/src/components/viewer/EntityContextMenu.tsx +168 -4
- package/src/components/viewer/ExportChangesButton.tsx +25 -5
- package/src/components/viewer/ExportDialog.tsx +19 -1
- package/src/components/viewer/MainToolbar.tsx +73 -13
- package/src/components/viewer/PropertiesPanel.tsx +237 -23
- package/src/components/viewer/SearchInline.tsx +669 -0
- package/src/components/viewer/SearchModal.filter.builder.tsx +766 -0
- package/src/components/viewer/SearchModal.filter.tsx +514 -0
- package/src/components/viewer/SearchModal.text.tsx +388 -0
- package/src/components/viewer/SearchModal.tsx +235 -0
- package/src/components/viewer/SettingsPage.tsx +252 -101
- package/src/components/viewer/ThemeSwitch.tsx +63 -7
- package/src/components/viewer/ToolOverlays.tsx +5 -0
- package/src/components/viewer/ViewerLayout.tsx +25 -4
- package/src/components/viewer/Viewport.tsx +25 -3
- package/src/components/viewer/ViewportContainer.tsx +51 -64
- package/src/components/viewer/ViewportOverlays.tsx +5 -2
- package/src/components/viewer/annotations/AnnotationDropInput.tsx +203 -0
- package/src/components/viewer/annotations/AnnotationLayer.tsx +287 -0
- package/src/components/viewer/annotations/AnnotationPin.tsx +90 -0
- package/src/components/viewer/annotations/AnnotationPopover.tsx +296 -0
- package/src/components/viewer/bcf/BCFTopicDetail.tsx +4 -4
- package/src/components/viewer/chat/ModelSelector.tsx +90 -54
- package/src/components/viewer/lists/ListPanel.tsx +14 -21
- package/src/components/viewer/properties/GeoreferencingPanel.tsx +113 -51
- package/src/components/viewer/properties/LocationMap.tsx +9 -7
- package/src/components/viewer/properties/ModelMetadataPanel.tsx +1 -1
- package/src/components/viewer/properties/RawStepCard.tsx +332 -0
- package/src/components/viewer/properties/RawStepRow.tsx +261 -0
- package/src/components/viewer/properties/ScheduleCard.tsx +224 -0
- package/src/components/viewer/properties/TaskEditCard.tsx +510 -0
- package/src/components/viewer/properties/raw-step-format.ts +193 -0
- package/src/components/viewer/schedule/AnimationSettingsPopover.tsx +542 -0
- package/src/components/viewer/schedule/GanttDependencyArrows.tsx +89 -0
- package/src/components/viewer/schedule/GanttDragTooltip.tsx +48 -0
- package/src/components/viewer/schedule/GanttEmptyState.tsx +97 -0
- package/src/components/viewer/schedule/GanttPanel.tsx +295 -0
- package/src/components/viewer/schedule/GanttTaskBar.tsx +199 -0
- package/src/components/viewer/schedule/GanttTaskTree.tsx +250 -0
- package/src/components/viewer/schedule/GanttTimeline.tsx +305 -0
- package/src/components/viewer/schedule/GanttToolbar.tsx +406 -0
- package/src/components/viewer/schedule/GenerateAdvancedPanel.tsx +147 -0
- package/src/components/viewer/schedule/GenerateScheduleDialog.tsx +392 -0
- package/src/components/viewer/schedule/HeightStrategyPanel.tsx +120 -0
- package/src/components/viewer/schedule/generate-schedule.test.ts +439 -0
- package/src/components/viewer/schedule/generate-schedule.ts +648 -0
- package/src/components/viewer/schedule/schedule-animator.test.ts +452 -0
- package/src/components/viewer/schedule/schedule-animator.ts +488 -0
- package/src/components/viewer/schedule/schedule-selection.test.ts +148 -0
- package/src/components/viewer/schedule/schedule-selection.ts +163 -0
- package/src/components/viewer/schedule/schedule-utils.ts +223 -0
- package/src/components/viewer/schedule/useConstructionSequence.ts +156 -0
- package/src/components/viewer/schedule/useGanttBarDrag.test.ts +90 -0
- package/src/components/viewer/schedule/useGanttBarDrag.ts +305 -0
- package/src/components/viewer/schedule/useGanttSelection3DHighlight.ts +152 -0
- package/src/components/viewer/schedule/useOverlayCompositor.ts +108 -0
- package/src/components/viewer/selectionHandlers.ts +446 -0
- package/src/components/viewer/tools/AddElementOverlay.tsx +540 -0
- package/src/components/viewer/tools/SectionCapControls.tsx +237 -0
- package/src/components/viewer/tools/SectionPanel.tsx +39 -18
- package/src/components/viewer/useAnimationLoop.ts +9 -1
- package/src/components/viewer/useDuplicateShortcut.ts +77 -0
- package/src/components/viewer/useMouseControls.ts +9 -1
- package/src/components/viewer/useRenderUpdates.ts +1 -1
- package/src/hooks/ids/idsDataAccessor.ts +60 -24
- package/src/hooks/ingest/viewerModelIngest.ts +7 -2
- package/src/hooks/useIfcFederation.ts +326 -71
- package/src/hooks/useIfcLoader.ts +23 -10
- package/src/hooks/useKeyboardShortcuts.ts +25 -0
- package/src/hooks/useSandbox.ts +1 -1
- package/src/hooks/useSearchIndex.ts +125 -0
- package/src/hooks/useViewControls.ts +13 -5
- package/src/index.css +550 -10
- package/src/lib/desktop-entitlement.ts +2 -4
- package/src/lib/geo/cesium-bridge.ts +15 -7
- package/src/lib/geo/effective-georef.test.ts +73 -0
- package/src/lib/geo/effective-georef.ts +111 -0
- package/src/lib/geo/reproject.ts +105 -19
- package/src/lib/llm/byok-guard.test.ts +77 -0
- package/src/lib/llm/byok-guard.ts +39 -0
- package/src/lib/llm/free-models.test.ts +0 -6
- package/src/lib/llm/models.ts +104 -42
- package/src/lib/llm/stream-client.ts +74 -110
- package/src/lib/llm/stream-direct.test.ts +130 -0
- package/src/lib/llm/stream-direct.ts +316 -0
- package/src/lib/llm/system-prompt.test.ts +14 -0
- package/src/lib/llm/system-prompt.ts +102 -1
- package/src/lib/llm/types.ts +20 -2
- package/src/lib/recent-files.ts +38 -4
- package/src/lib/scripts/templates/bim-globals.d.ts +136 -114
- package/src/lib/scripts/templates/construction-schedule.ts +223 -0
- package/src/lib/scripts/templates.ts +7 -0
- package/src/lib/search/common-ifc-types.ts +36 -0
- package/src/lib/search/filter-evaluate.test.ts +537 -0
- package/src/lib/search/filter-evaluate.ts +610 -0
- package/src/lib/search/filter-rules.test.ts +119 -0
- package/src/lib/search/filter-rules.ts +198 -0
- package/src/lib/search/filter-schema.test.ts +233 -0
- package/src/lib/search/filter-schema.ts +146 -0
- package/src/lib/search/recent-searches.test.ts +116 -0
- package/src/lib/search/recent-searches.ts +93 -0
- package/src/lib/search/result-export.test.ts +101 -0
- package/src/lib/search/result-export.ts +104 -0
- package/src/lib/search/saved-filters.test.ts +118 -0
- package/src/lib/search/saved-filters.ts +154 -0
- package/src/lib/search/tier0-scan.test.ts +196 -0
- package/src/lib/search/tier0-scan.ts +237 -0
- package/src/lib/search/tier1-index.test.ts +242 -0
- package/src/lib/search/tier1-index.ts +448 -0
- package/src/main.tsx +1 -10
- package/src/sdk/adapters/export-adapter.test.ts +434 -1
- package/src/sdk/adapters/export-adapter.ts +404 -1
- package/src/sdk/adapters/export-schedule-splice.test.ts +127 -0
- package/src/sdk/adapters/export-schedule-splice.ts +87 -0
- package/src/sdk/adapters/model-compat.ts +8 -2
- package/src/sdk/adapters/schedule-adapter.ts +73 -0
- package/src/sdk/adapters/store-adapter.ts +201 -0
- package/src/sdk/adapters/visibility-adapter.ts +3 -0
- package/src/sdk/local-backend.ts +16 -8
- package/src/services/api-keys.ts +73 -0
- package/src/services/desktop-export.ts +3 -1
- package/src/services/desktop-native-metadata.ts +41 -18
- package/src/services/file-dialog.ts +4 -1
- package/src/services/tauri-modules.d.ts +25 -0
- package/src/store/basketVisibleSet.ts +3 -0
- package/src/store/constants.ts +20 -2
- package/src/store/globalId.ts +4 -1
- package/src/store/index.ts +82 -6
- package/src/store/slices/addElementMeshes.ts +365 -0
- package/src/store/slices/addElementSlice.ts +275 -0
- package/src/store/slices/annotationsSlice.test.ts +133 -0
- package/src/store/slices/annotationsSlice.ts +251 -0
- package/src/store/slices/cesiumSlice.ts +5 -0
- package/src/store/slices/chatSlice.test.ts +6 -76
- package/src/store/slices/chatSlice.ts +17 -58
- package/src/store/slices/dataSlice.test.ts +23 -4
- package/src/store/slices/dataSlice.ts +1 -1
- package/src/store/slices/modelSlice.test.ts +67 -9
- package/src/store/slices/modelSlice.ts +39 -7
- package/src/store/slices/mutationSlice.ts +964 -3
- package/src/store/slices/overlayCompositor.test.ts +164 -0
- package/src/store/slices/overlaySlice.test.ts +93 -0
- package/src/store/slices/overlaySlice.ts +151 -0
- package/src/store/slices/pinboardSlice.test.ts +6 -1
- package/src/store/slices/playbackSlice.ts +128 -0
- package/src/store/slices/schedule-edit-helpers.test.ts +97 -0
- package/src/store/slices/schedule-edit-helpers.ts +179 -0
- package/src/store/slices/scheduleSlice.test.ts +694 -0
- package/src/store/slices/scheduleSlice.ts +1330 -0
- package/src/store/slices/searchSlice.test.ts +342 -0
- package/src/store/slices/searchSlice.ts +341 -0
- package/src/store/slices/sectionSlice.test.ts +87 -7
- package/src/store/slices/sectionSlice.ts +151 -5
- package/src/store/slices/selectionSlice.test.ts +46 -0
- package/src/store/slices/selectionSlice.ts +20 -0
- package/src/store/slices/uiSlice.ts +28 -5
- package/src/store/types.ts +26 -0
- package/src/store.ts +14 -0
- package/src/utils/nativeSpatialDataStore.ts +4 -1
- package/src/utils/viewportUtils.ts +7 -2
- package/src/vite-env.d.ts +0 -4
- package/dist/assets/drawing-2d-gWfpdfYe.js +0 -257
- package/dist/assets/ids-B4jTqB1O.js +0 -1
- package/dist/assets/ifc-lite_bg-BX4E7TX8.wasm +0 -0
- package/dist/assets/index-DckuDqlv.css +0 -1
- package/dist/assets/sandbox-DZiNLNMk.js +0 -5933
- package/src/components/viewer/UpgradePage.tsx +0 -71
- package/src/lib/desktop/ClerkDesktopEntitlementSync.tsx +0 -175
- package/src/lib/llm/ClerkChatSync.tsx +0 -74
- package/src/lib/llm/clerk-auth.ts +0 -62
|
@@ -9,19 +9,23 @@ import { TooltipProvider } from '@/components/ui/tooltip';
|
|
|
9
9
|
import { MainToolbar } from './MainToolbar';
|
|
10
10
|
import { HierarchyPanel } from './HierarchyPanel';
|
|
11
11
|
import { PropertiesPanel } from './PropertiesPanel';
|
|
12
|
+
import { AddElementPanel } from './AddElementPanel';
|
|
12
13
|
import { StatusBar } from './StatusBar';
|
|
13
14
|
import { ViewportContainer } from './ViewportContainer';
|
|
14
15
|
import { KeyboardShortcutsDialog, useKeyboardShortcutsDialog } from './KeyboardShortcutsDialog';
|
|
15
16
|
import { useKeyboardShortcuts } from '@/hooks/useKeyboardShortcuts';
|
|
16
17
|
import { useViewerStore } from '@/store';
|
|
17
18
|
import { EntityContextMenu } from './EntityContextMenu';
|
|
19
|
+
import { useDuplicateShortcut } from './useDuplicateShortcut';
|
|
18
20
|
import { HoverTooltip } from './HoverTooltip';
|
|
19
21
|
import { BCFPanel } from './BCFPanel';
|
|
20
22
|
import { IDSPanel } from './IDSPanel';
|
|
21
23
|
import { LensPanel } from './LensPanel';
|
|
22
24
|
import { ListPanel } from './lists/ListPanel';
|
|
23
25
|
import { ScriptPanel } from './ScriptPanel';
|
|
26
|
+
import { GanttPanel } from './schedule/GanttPanel';
|
|
24
27
|
import { CommandPalette } from './CommandPalette';
|
|
28
|
+
import { SearchModal } from './SearchModal';
|
|
25
29
|
import { DesktopEntitlementBanner } from './DesktopEntitlementBanner';
|
|
26
30
|
import {
|
|
27
31
|
closeActiveAnalysisExtension,
|
|
@@ -37,6 +41,8 @@ const BOTTOM_PANEL_MAX_RATIO = 0.7; // max 70% of container
|
|
|
37
41
|
export function ViewerLayout() {
|
|
38
42
|
// Initialize keyboard shortcuts
|
|
39
43
|
useKeyboardShortcuts();
|
|
44
|
+
// ⌘D / Ctrl+D to duplicate the current selection.
|
|
45
|
+
useDuplicateShortcut();
|
|
40
46
|
const shortcutsDialog = useKeyboardShortcutsDialog();
|
|
41
47
|
|
|
42
48
|
// Command palette state
|
|
@@ -76,6 +82,8 @@ export function ViewerLayout() {
|
|
|
76
82
|
const setRightPanelCollapsed = useViewerStore((s) => s.setRightPanelCollapsed);
|
|
77
83
|
const bcfPanelVisible = useViewerStore((s) => s.bcfPanelVisible);
|
|
78
84
|
const setBcfPanelVisible = useViewerStore((s) => s.setBcfPanelVisible);
|
|
85
|
+
const activeTool = useViewerStore((s) => s.activeTool);
|
|
86
|
+
const setActiveTool = useViewerStore((s) => s.setActiveTool);
|
|
79
87
|
const idsPanelVisible = useViewerStore((s) => s.idsPanelVisible);
|
|
80
88
|
const setIdsPanelVisible = useViewerStore((s) => s.setIdsPanelVisible);
|
|
81
89
|
const listPanelVisible = useViewerStore((s) => s.listPanelVisible);
|
|
@@ -84,6 +92,8 @@ export function ViewerLayout() {
|
|
|
84
92
|
const setLensPanelVisible = useViewerStore((s) => s.setLensPanelVisible);
|
|
85
93
|
const scriptPanelVisible = useViewerStore((s) => s.scriptPanelVisible);
|
|
86
94
|
const setScriptPanelVisible = useViewerStore((s) => s.setScriptPanelVisible);
|
|
95
|
+
const ganttPanelVisible = useViewerStore((s) => s.ganttPanelVisible);
|
|
96
|
+
const setGanttPanelVisible = useViewerStore((s) => s.setGanttPanelVisible);
|
|
87
97
|
const analysisExtensionState = useSyncExternalStore(
|
|
88
98
|
subscribeAnalysisExtensions,
|
|
89
99
|
getAnalysisExtensionsSnapshot,
|
|
@@ -186,6 +196,7 @@ export function ViewerLayout() {
|
|
|
186
196
|
// Keep DOM class in sync when theme changes (initial class is set by inline script in index.html)
|
|
187
197
|
useEffect(() => {
|
|
188
198
|
document.documentElement.classList.toggle('dark', theme === 'dark');
|
|
199
|
+
document.documentElement.classList.toggle('colorful', theme === 'colorful');
|
|
189
200
|
}, [theme]);
|
|
190
201
|
|
|
191
202
|
|
|
@@ -199,6 +210,7 @@ export function ViewerLayout() {
|
|
|
199
210
|
<EntityContextMenu />
|
|
200
211
|
<HoverTooltip />
|
|
201
212
|
<CommandPalette open={commandPaletteOpen} onOpenChange={setCommandPaletteOpen} />
|
|
213
|
+
<SearchModal />
|
|
202
214
|
|
|
203
215
|
{/* Main Toolbar */}
|
|
204
216
|
<MainToolbar onShowShortcuts={shortcutsDialog.toggle} />
|
|
@@ -255,6 +267,8 @@ export function ViewerLayout() {
|
|
|
255
267
|
<div className="h-full w-full overflow-hidden panel-container">
|
|
256
268
|
{activeRightAnalysisExtension ? (
|
|
257
269
|
activeRightAnalysisExtension.renderPanel({ onClose: closeActiveAnalysisExtension })
|
|
270
|
+
) : activeTool === 'addElement' ? (
|
|
271
|
+
<AddElementPanel onClose={() => setActiveTool('select')} />
|
|
258
272
|
) : lensPanelVisible ? (
|
|
259
273
|
<LensPanel onClose={() => setLensPanelVisible(false)} />
|
|
260
274
|
) : idsPanelVisible ? (
|
|
@@ -269,8 +283,8 @@ export function ViewerLayout() {
|
|
|
269
283
|
</PanelGroup>
|
|
270
284
|
</div>
|
|
271
285
|
|
|
272
|
-
{/* Bottom Panel - Lists
|
|
273
|
-
{(listPanelVisible || scriptPanelVisible || !!activeBottomAnalysisExtension) && (
|
|
286
|
+
{/* Bottom Panel - Lists / Script / Gantt / analysis ext (custom resizable) */}
|
|
287
|
+
{(listPanelVisible || scriptPanelVisible || ganttPanelVisible || !!activeBottomAnalysisExtension) && (
|
|
274
288
|
<div style={{ height: bottomHeight, flexShrink: 0 }} className="relative">
|
|
275
289
|
{/* Drag handle */}
|
|
276
290
|
<div
|
|
@@ -280,6 +294,8 @@ export function ViewerLayout() {
|
|
|
280
294
|
<div className="h-full w-full overflow-hidden border-t pt-1.5">
|
|
281
295
|
{activeBottomAnalysisExtension ? (
|
|
282
296
|
activeBottomAnalysisExtension.renderPanel({ onClose: closeActiveAnalysisExtension })
|
|
297
|
+
) : ganttPanelVisible ? (
|
|
298
|
+
<GanttPanel onClose={() => setGanttPanelVisible(false)} />
|
|
283
299
|
) : scriptPanelVisible ? (
|
|
284
300
|
<ScriptPanel onClose={() => setScriptPanelVisible(false)} />
|
|
285
301
|
) : (
|
|
@@ -325,7 +341,7 @@ export function ViewerLayout() {
|
|
|
325
341
|
<div className="absolute inset-x-0 bottom-0 h-[50vh] bg-background border-t rounded-t-xl shadow-xl z-40 animate-in slide-in-from-bottom">
|
|
326
342
|
<div className="flex items-center justify-between p-2 border-b">
|
|
327
343
|
<span className="font-medium text-sm">
|
|
328
|
-
{activeAnalysisExtension ? activeAnalysisExtension.label : scriptPanelVisible ? 'Script' : listPanelVisible ? 'Lists' : lensPanelVisible ? 'Lens' : idsPanelVisible ? 'IDS Validation' : bcfPanelVisible ? 'BCF Issues' : '
|
|
344
|
+
{activeAnalysisExtension ? activeAnalysisExtension.label : ganttPanelVisible ? 'Schedule' : scriptPanelVisible ? 'Script' : listPanelVisible ? 'Lists' : lensPanelVisible ? 'Lens' : idsPanelVisible ? 'IDS Validation' : bcfPanelVisible ? 'BCF Issues' : 'Inspector'}
|
|
329
345
|
</span>
|
|
330
346
|
<button
|
|
331
347
|
className="p-1 hover:bg-muted rounded"
|
|
@@ -333,6 +349,7 @@ export function ViewerLayout() {
|
|
|
333
349
|
setRightPanelCollapsed(true);
|
|
334
350
|
if (scriptPanelVisible) setScriptPanelVisible(false);
|
|
335
351
|
if (listPanelVisible) setListPanelVisible(false);
|
|
352
|
+
if (ganttPanelVisible) setGanttPanelVisible(false);
|
|
336
353
|
if (bcfPanelVisible) setBcfPanelVisible(false);
|
|
337
354
|
if (lensPanelVisible) setLensPanelVisible(false);
|
|
338
355
|
if (idsPanelVisible) setIdsPanelVisible(false);
|
|
@@ -350,10 +367,14 @@ export function ViewerLayout() {
|
|
|
350
367
|
activeBottomAnalysisExtension.renderPanel({ onClose: closeActiveAnalysisExtension })
|
|
351
368
|
) : activeRightAnalysisExtension ? (
|
|
352
369
|
activeRightAnalysisExtension.renderPanel({ onClose: closeActiveAnalysisExtension })
|
|
370
|
+
) : ganttPanelVisible ? (
|
|
371
|
+
<GanttPanel onClose={() => setGanttPanelVisible(false)} />
|
|
353
372
|
) : scriptPanelVisible ? (
|
|
354
373
|
<ScriptPanel onClose={() => setScriptPanelVisible(false)} />
|
|
355
374
|
) : listPanelVisible ? (
|
|
356
375
|
<ListPanel onClose={() => setListPanelVisible(false)} />
|
|
376
|
+
) : activeTool === 'addElement' ? (
|
|
377
|
+
<AddElementPanel onClose={() => setActiveTool('select')} />
|
|
357
378
|
) : lensPanelVisible ? (
|
|
358
379
|
<LensPanel onClose={() => setLensPanelVisible(false)} />
|
|
359
380
|
) : idsPanelVisible ? (
|
|
@@ -385,7 +406,7 @@ export function ViewerLayout() {
|
|
|
385
406
|
setRightPanelCollapsed(!rightPanelCollapsed);
|
|
386
407
|
}}
|
|
387
408
|
>
|
|
388
|
-
|
|
409
|
+
Inspector
|
|
389
410
|
</button>
|
|
390
411
|
</div>
|
|
391
412
|
</div>
|
|
@@ -312,7 +312,7 @@ export function Viewport({
|
|
|
312
312
|
if (cesiumActive) {
|
|
313
313
|
clearColorRef.current = [0, 0, 0, 0]; // fully transparent
|
|
314
314
|
} else {
|
|
315
|
-
clearColorRef.current = getThemeClearColor(theme as 'light' | 'dark');
|
|
315
|
+
clearColorRef.current = getThemeClearColor(theme as 'light' | 'dark' | 'colorful');
|
|
316
316
|
}
|
|
317
317
|
rendererRef.current?.requestRender();
|
|
318
318
|
}, [cesiumActive, theme]);
|
|
@@ -456,11 +456,21 @@ export function Viewport({
|
|
|
456
456
|
}
|
|
457
457
|
|
|
458
458
|
// Set cursor based on active tool
|
|
459
|
-
if (activeTool === 'measure') {
|
|
459
|
+
if (activeTool === 'measure' || activeTool === 'annotate' || activeTool === 'addElement') {
|
|
460
460
|
canvas.style.cursor = 'crosshair';
|
|
461
461
|
} else {
|
|
462
462
|
canvas.style.cursor = 'default';
|
|
463
463
|
}
|
|
464
|
+
|
|
465
|
+
// Clear add-element pending state + hover point when leaving the
|
|
466
|
+
// tool so the SVG overlay doesn't paint stale geometry from a
|
|
467
|
+
// previous session.
|
|
468
|
+
if (activeTool !== 'addElement') {
|
|
469
|
+
const state = useViewerStore.getState();
|
|
470
|
+
if (state.addElementPendingPoints.length > 0 || state.addElementHoverPoint !== null) {
|
|
471
|
+
state.clearAddElementPending();
|
|
472
|
+
}
|
|
473
|
+
}
|
|
464
474
|
}, [activeTool, activeMeasurement, cancelMeasurement]);
|
|
465
475
|
|
|
466
476
|
// Helper: calculate scale bar value (world-space size for 96px scale bar)
|
|
@@ -878,13 +888,25 @@ export function Viewport({
|
|
|
878
888
|
// The model will be rendered by Cesium (as GLB) for correct positioning.
|
|
879
889
|
// Canvas stays in the DOM for picking/interaction.
|
|
880
890
|
|
|
891
|
+
// Colorful mode: transparent WebGPU clear colour + CSS gradient on the
|
|
892
|
+
// canvas element. The gradient is the *CSS background* of the <canvas>;
|
|
893
|
+
// premultiplied-alpha compositing shows it through transparent clear-colour
|
|
894
|
+
// regions while opaque model fragments (alpha=1) stay fully visible.
|
|
895
|
+
const canvasStyle = cesiumActive
|
|
896
|
+
? { opacity: 0 }
|
|
897
|
+
: theme === 'colorful'
|
|
898
|
+
? {
|
|
899
|
+
background: 'linear-gradient(180deg, #4a5a8a 0%, #6272a8 10%, #7e8dba 20%, #9aa3c8 32%, #b5b8d1 44%, #cdc3d4 56%, #dcccc8 68%, #e8d5be 80%, #f0ddb8 92%, #f5e2b6 100%)',
|
|
900
|
+
}
|
|
901
|
+
: undefined;
|
|
902
|
+
|
|
881
903
|
return (
|
|
882
904
|
<canvas
|
|
883
905
|
ref={canvasRef}
|
|
884
906
|
data-viewport="main"
|
|
885
907
|
tabIndex={-1}
|
|
886
908
|
className={`w-full h-full block ${cesiumActive ? 'relative z-[1]' : ''}`}
|
|
887
|
-
style={
|
|
909
|
+
style={canvasStyle}
|
|
888
910
|
onPointerDown={focusViewportForKeyboardShortcuts}
|
|
889
911
|
/>
|
|
890
912
|
);
|
|
@@ -6,6 +6,7 @@ import { useMemo, useRef, useState, useCallback, useEffect, useSyncExternalStore
|
|
|
6
6
|
import { Viewport } from './Viewport';
|
|
7
7
|
import { ViewportOverlays } from './ViewportOverlays';
|
|
8
8
|
import { ToolOverlays } from './ToolOverlays';
|
|
9
|
+
import { AnnotationLayer } from './annotations/AnnotationLayer';
|
|
9
10
|
import { Section2DPanel } from './Section2DPanel';
|
|
10
11
|
import { BasketPresentationDock } from './BasketPresentationDock';
|
|
11
12
|
import { BCFOverlay } from './bcf/BCFOverlay';
|
|
@@ -18,10 +19,11 @@ import { useWebGPU } from '@/hooks/useWebGPU';
|
|
|
18
19
|
import { openIfcFileDialog } from '@/services/file-dialog';
|
|
19
20
|
import { logToDesktopTerminal } from '@/services/desktop-logger';
|
|
20
21
|
import { cacheFileBlobs, formatFileSize, getCachedFile, getRecentFiles, recordRecentFiles, type RecentFileEntry } from '@/lib/recent-files';
|
|
21
|
-
import { isTauri } from '@/
|
|
22
|
+
import { isTauri } from '@/lib/platform';
|
|
22
23
|
import { Upload, MousePointer, Layers, Info, Command, AlertTriangle, ChevronDown, ExternalLink, Plus, Clock3 } from 'lucide-react';
|
|
23
24
|
import type { MeshData, CoordinateInfo, GeometryResult } from '@ifc-lite/geometry';
|
|
24
|
-
import {
|
|
25
|
+
import { type IfcDataStore } from '@ifc-lite/parser';
|
|
26
|
+
import { getEffectiveGeoreference } from '@/lib/geo/effective-georef';
|
|
25
27
|
|
|
26
28
|
const ZERO_VEC3 = { x: 0, y: 0, z: 0 };
|
|
27
29
|
const DEFAULT_COORDINATE_INFO: CoordinateInfo = {
|
|
@@ -43,6 +45,7 @@ export function ViewportContainer() {
|
|
|
43
45
|
const cesiumEnabled = useViewerStore((s) => s.cesiumEnabled);
|
|
44
46
|
const georefMutations = useViewerStore((s) => s.georefMutations);
|
|
45
47
|
const setCesiumSourceModelId = useViewerStore((s) => s.setCesiumSourceModelId);
|
|
48
|
+
const setCesiumAvailable = useViewerStore((s) => s.setCesiumAvailable);
|
|
46
49
|
// Subscribe to mutationVersion so Cesium reacts to georef edits
|
|
47
50
|
const mutationVersion = useViewerStore((s) => s.mutationVersion);
|
|
48
51
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
@@ -176,82 +179,64 @@ export function ViewportContainer() {
|
|
|
176
179
|
const georef = useMemo(() => {
|
|
177
180
|
if (!cesiumEnabled) return null;
|
|
178
181
|
|
|
179
|
-
// Helper: merge original georef with mutations for a model
|
|
180
|
-
function mergeGeoref(
|
|
181
|
-
originalCRS: ProjectedCRS | undefined,
|
|
182
|
-
originalConv: MapConversion | undefined,
|
|
183
|
-
modelId: string,
|
|
184
|
-
): { mapConversion: MapConversion; projectedCRS: ProjectedCRS } | null {
|
|
185
|
-
const muts = georefMutations.get(modelId);
|
|
186
|
-
const mutCRS = muts?.projectedCRS;
|
|
187
|
-
const mutConv = muts?.mapConversion;
|
|
188
|
-
|
|
189
|
-
// Build merged ProjectedCRS — mutation fields override originals
|
|
190
|
-
const hasCRS = originalCRS || mutCRS;
|
|
191
|
-
if (!hasCRS) return null;
|
|
192
|
-
const projectedCRS: ProjectedCRS = {
|
|
193
|
-
id: originalCRS?.id ?? 0,
|
|
194
|
-
name: (mutCRS?.name ?? originalCRS?.name ?? '') as string,
|
|
195
|
-
description: mutCRS?.description ?? originalCRS?.description,
|
|
196
|
-
geodeticDatum: mutCRS?.geodeticDatum ?? originalCRS?.geodeticDatum,
|
|
197
|
-
verticalDatum: mutCRS?.verticalDatum ?? originalCRS?.verticalDatum,
|
|
198
|
-
mapProjection: mutCRS?.mapProjection ?? originalCRS?.mapProjection,
|
|
199
|
-
mapZone: mutCRS?.mapZone ?? originalCRS?.mapZone,
|
|
200
|
-
mapUnit: mutCRS?.mapUnit ?? originalCRS?.mapUnit,
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
// Need at least an EPSG name to resolve projection
|
|
204
|
-
if (!projectedCRS.name) return null;
|
|
205
|
-
|
|
206
|
-
// Build merged MapConversion
|
|
207
|
-
const mapConversion: MapConversion = {
|
|
208
|
-
id: originalConv?.id ?? 0,
|
|
209
|
-
sourceCRS: originalConv?.sourceCRS ?? 0,
|
|
210
|
-
targetCRS: originalConv?.targetCRS ?? 0,
|
|
211
|
-
eastings: (mutConv?.eastings ?? originalConv?.eastings ?? 0) as number,
|
|
212
|
-
northings: (mutConv?.northings ?? originalConv?.northings ?? 0) as number,
|
|
213
|
-
orthogonalHeight: (mutConv?.orthogonalHeight ?? originalConv?.orthogonalHeight ?? 0) as number,
|
|
214
|
-
xAxisAbscissa: mutConv?.xAxisAbscissa ?? originalConv?.xAxisAbscissa,
|
|
215
|
-
xAxisOrdinate: mutConv?.xAxisOrdinate ?? originalConv?.xAxisOrdinate,
|
|
216
|
-
scale: mutConv?.scale ?? originalConv?.scale,
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
return { mapConversion, projectedCRS };
|
|
220
|
-
}
|
|
221
|
-
|
|
222
182
|
// Check federated models first
|
|
223
183
|
for (const [modelId, model] of storeModels) {
|
|
224
184
|
const ds = model.ifcDataStore;
|
|
225
185
|
if (!ds) continue;
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
modelId,
|
|
186
|
+
const effective = getEffectiveGeoreference(
|
|
187
|
+
ds as IfcDataStore,
|
|
188
|
+
model.geometryResult?.coordinateInfo,
|
|
189
|
+
georefMutations.get(modelId),
|
|
231
190
|
);
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
const coordInfo = model.geometryResult?.coordinateInfo;
|
|
235
|
-
return { hasGeoreference: true, ...merged, sourceModelId: modelId, coordinateInfo: coordInfo };
|
|
191
|
+
if (effective?.projectedCRS?.name && effective.mapConversion) {
|
|
192
|
+
return { ...effective, sourceModelId: modelId };
|
|
236
193
|
}
|
|
237
194
|
}
|
|
238
195
|
|
|
239
196
|
// Fallback to legacy single-model
|
|
240
197
|
if (ifcDataStore) {
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
'__legacy__',
|
|
198
|
+
const effective = getEffectiveGeoreference(
|
|
199
|
+
ifcDataStore as IfcDataStore,
|
|
200
|
+
mergedGeometryResult?.coordinateInfo,
|
|
201
|
+
georefMutations.get('__legacy__'),
|
|
246
202
|
);
|
|
247
|
-
if (
|
|
248
|
-
return {
|
|
203
|
+
if (effective?.projectedCRS?.name && effective.mapConversion) {
|
|
204
|
+
return { ...effective, sourceModelId: '__legacy__' };
|
|
249
205
|
}
|
|
250
206
|
}
|
|
251
207
|
|
|
252
208
|
return null;
|
|
253
209
|
}, [cesiumEnabled, storeModels, ifcDataStore, georefMutations, mutationVersion, mergedGeometryResult]);
|
|
254
210
|
|
|
211
|
+
// Determine whether Cesium button should be visible (model has georef or user added it via mutations).
|
|
212
|
+
// Runs independently of cesiumEnabled so the button appears/disappears reactively.
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
function hasGeoref(): boolean {
|
|
215
|
+
// Check federated models
|
|
216
|
+
for (const [modelId, model] of storeModels) {
|
|
217
|
+
const ds = model.ifcDataStore;
|
|
218
|
+
if (!ds) continue;
|
|
219
|
+
const effective = getEffectiveGeoreference(
|
|
220
|
+
ds as IfcDataStore,
|
|
221
|
+
model.geometryResult?.coordinateInfo,
|
|
222
|
+
georefMutations.get(modelId),
|
|
223
|
+
);
|
|
224
|
+
if (effective?.projectedCRS?.name) return true;
|
|
225
|
+
}
|
|
226
|
+
// Fallback to legacy single-model
|
|
227
|
+
if (ifcDataStore) {
|
|
228
|
+
const effective = getEffectiveGeoreference(
|
|
229
|
+
ifcDataStore as IfcDataStore,
|
|
230
|
+
mergedGeometryResult?.coordinateInfo,
|
|
231
|
+
georefMutations.get('__legacy__'),
|
|
232
|
+
);
|
|
233
|
+
if (effective?.projectedCRS?.name) return true;
|
|
234
|
+
}
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
setCesiumAvailable(hasGeoref());
|
|
238
|
+
}, [storeModels, ifcDataStore, georefMutations, mutationVersion, setCesiumAvailable, mergedGeometryResult]);
|
|
239
|
+
|
|
255
240
|
// Sync the active Cesium source model ID so terrain actions are scoped correctly
|
|
256
241
|
useEffect(() => {
|
|
257
242
|
setCesiumSourceModelId(georef?.sourceModelId ?? null);
|
|
@@ -847,13 +832,14 @@ export function ViewportContainer() {
|
|
|
847
832
|
</div>
|
|
848
833
|
)}
|
|
849
834
|
|
|
850
|
-
{/* Cesium 3D world context overlay — rendered behind the WebGPU canvas */}
|
|
851
|
-
{cesiumEnabled && georef && (
|
|
835
|
+
{/* Cesium 3D world context overlay — rendered behind the WebGPU canvas (web only) */}
|
|
836
|
+
{cesiumEnabled && georef && !isTauri() && (
|
|
852
837
|
<CesiumOverlay
|
|
853
838
|
mapConversion={georef.mapConversion}
|
|
854
839
|
projectedCRS={georef.projectedCRS}
|
|
855
840
|
coordinateInfo={georef.coordinateInfo}
|
|
856
841
|
geometryResult={mergedGeometryResult}
|
|
842
|
+
lengthUnitScale={georef.lengthUnitScale}
|
|
857
843
|
/>
|
|
858
844
|
)}
|
|
859
845
|
<Viewport
|
|
@@ -862,10 +848,11 @@ export function ViewportContainer() {
|
|
|
862
848
|
coordinateInfo={mergedGeometryResult?.coordinateInfo}
|
|
863
849
|
computedIsolatedIds={computedIsolatedIds}
|
|
864
850
|
modelIdToIndex={modelIdToIndex}
|
|
865
|
-
cesiumActive={cesiumEnabled && georef !== null}
|
|
851
|
+
cesiumActive={cesiumEnabled && georef !== null && !isTauri()}
|
|
866
852
|
releaseGeometryAfterStream={false}
|
|
867
853
|
onGeometryReleased={releaseGeometryMemory}
|
|
868
854
|
/>
|
|
855
|
+
<AnnotationLayer />
|
|
869
856
|
{bcfOverlayVisible && <BCFOverlay />}
|
|
870
857
|
<ViewportOverlays />
|
|
871
858
|
<ToolOverlays />
|
|
@@ -21,6 +21,9 @@ import type { CesiumDataSource } from '@/store/slices/cesiumSlice';
|
|
|
21
21
|
import { goHomeFromStore } from '@/store/homeView';
|
|
22
22
|
import { useIfc } from '@/hooks/useIfc';
|
|
23
23
|
import { cn } from '@/lib/utils';
|
|
24
|
+
import { isTauri } from '@/lib/platform';
|
|
25
|
+
|
|
26
|
+
const isDesktop = isTauri();
|
|
24
27
|
import { ViewCube, type ViewCubeRef } from './ViewCube';
|
|
25
28
|
import { AxisHelper, type AxisHelperRef } from './AxisHelper';
|
|
26
29
|
|
|
@@ -146,8 +149,8 @@ export function ViewportOverlays({ hideViewCube = false }: { hideViewCube?: bool
|
|
|
146
149
|
|
|
147
150
|
return (
|
|
148
151
|
<>
|
|
149
|
-
{/* Bottom-right: Cesium settings overlay OR Navigation controls */}
|
|
150
|
-
{cesiumEnabled ? (
|
|
152
|
+
{/* Bottom-right: Cesium settings overlay OR Navigation controls (Cesium is web-only) */}
|
|
153
|
+
{cesiumEnabled && !isDesktop ? (
|
|
151
154
|
<CesiumSettingsOverlay
|
|
152
155
|
dataSource={cesiumDataSource}
|
|
153
156
|
onDataSourceChange={setCesiumDataSource}
|
|
@@ -0,0 +1,203 @@
|
|
|
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
|
+
* The inline note input that appears at the click site when the user
|
|
7
|
+
* drops a fresh pin with the Annotate tool. Shape mirrors the popover's
|
|
8
|
+
* edit mode so muscle memory carries over, but the chrome is lighter
|
|
9
|
+
* (a guiding label, no entity-context header) since this is a
|
|
10
|
+
* commit-or-cancel surface.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
14
|
+
import { Check, X } from 'lucide-react';
|
|
15
|
+
import { Button } from '@/components/ui/button';
|
|
16
|
+
import { cn } from '@/lib/utils';
|
|
17
|
+
|
|
18
|
+
const MAX_NOTE_LEN = 2000;
|
|
19
|
+
const SOFT_NOTE_LIMIT = 200;
|
|
20
|
+
const INPUT_WIDTH = 280;
|
|
21
|
+
const INPUT_OFFSET_X = 16;
|
|
22
|
+
|
|
23
|
+
export interface AnnotationDropInputProps {
|
|
24
|
+
anchorX: number;
|
|
25
|
+
anchorY: number;
|
|
26
|
+
canvasWidth: number;
|
|
27
|
+
canvasHeight: number;
|
|
28
|
+
/** Resolved entity type when the drop landed on a known mesh. */
|
|
29
|
+
entityType?: string | null;
|
|
30
|
+
entityExpressId?: number | null;
|
|
31
|
+
onSave: (note: string) => void;
|
|
32
|
+
onCancel: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function AnnotationDropInput({
|
|
36
|
+
anchorX,
|
|
37
|
+
anchorY,
|
|
38
|
+
canvasWidth,
|
|
39
|
+
canvasHeight,
|
|
40
|
+
entityType,
|
|
41
|
+
entityExpressId,
|
|
42
|
+
onSave,
|
|
43
|
+
onCancel,
|
|
44
|
+
}: AnnotationDropInputProps) {
|
|
45
|
+
const [draft, setDraft] = useState('');
|
|
46
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
47
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
textareaRef.current?.focus();
|
|
51
|
+
}, []);
|
|
52
|
+
|
|
53
|
+
// Cancel on outside click, but defer registration so the click that
|
|
54
|
+
// dropped the pin doesn't immediately close the input.
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
const handler = (e: MouseEvent) => {
|
|
57
|
+
const node = containerRef.current;
|
|
58
|
+
if (!node) return;
|
|
59
|
+
if (node.contains(e.target as Node)) return;
|
|
60
|
+
// Empty draft on outside-click → silent cancel; non-empty
|
|
61
|
+
// → commit the draft (matches "blur to save" feel without
|
|
62
|
+
// destroying typed content). An over-limit draft is rejected
|
|
63
|
+
// consistently with the disabled save button.
|
|
64
|
+
if (draft.trim().length === 0 || draft.length > MAX_NOTE_LEN) {
|
|
65
|
+
onCancel();
|
|
66
|
+
} else {
|
|
67
|
+
onSave(draft);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const id = window.setTimeout(() => {
|
|
71
|
+
document.addEventListener('mousedown', handler);
|
|
72
|
+
}, 0);
|
|
73
|
+
return () => {
|
|
74
|
+
window.clearTimeout(id);
|
|
75
|
+
document.removeEventListener('mousedown', handler);
|
|
76
|
+
};
|
|
77
|
+
}, [draft, onSave, onCancel]);
|
|
78
|
+
|
|
79
|
+
const handleKeyDown = useCallback(
|
|
80
|
+
(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
81
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
if (draft.trim().length === 0 || draft.length > MAX_NOTE_LEN) {
|
|
84
|
+
// Over-limit Enter does nothing — match the disabled button.
|
|
85
|
+
if (draft.trim().length === 0) onCancel();
|
|
86
|
+
} else {
|
|
87
|
+
onSave(draft);
|
|
88
|
+
}
|
|
89
|
+
} else if (e.key === 'Escape') {
|
|
90
|
+
e.preventDefault();
|
|
91
|
+
onCancel();
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
[draft, onSave, onCancel],
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const wantsLeft = anchorX + INPUT_OFFSET_X + INPUT_WIDTH > canvasWidth;
|
|
98
|
+
const left = wantsLeft
|
|
99
|
+
? Math.max(8, anchorX - INPUT_OFFSET_X - INPUT_WIDTH)
|
|
100
|
+
: Math.min(anchorX + INPUT_OFFSET_X, canvasWidth - INPUT_WIDTH - 8);
|
|
101
|
+
const top = Math.min(Math.max(8, anchorY - 8), canvasHeight - 140);
|
|
102
|
+
|
|
103
|
+
const charCountVisible = draft.length >= SOFT_NOTE_LIMIT;
|
|
104
|
+
const overSoftLimit = draft.length > SOFT_NOTE_LIMIT;
|
|
105
|
+
const overHardLimit = draft.length > MAX_NOTE_LEN;
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div
|
|
109
|
+
ref={containerRef}
|
|
110
|
+
role="dialog"
|
|
111
|
+
aria-label="New annotation"
|
|
112
|
+
style={{ left, top, width: INPUT_WIDTH }}
|
|
113
|
+
className={cn(
|
|
114
|
+
'absolute z-[60] pointer-events-auto',
|
|
115
|
+
'rounded-md border border-amber-400/70 dark:border-amber-600/40',
|
|
116
|
+
'bg-white/95 dark:bg-zinc-950/95 backdrop-blur-md',
|
|
117
|
+
'shadow-[0_8px_32px_rgba(0,0,0,0.18)]',
|
|
118
|
+
'overflow-hidden',
|
|
119
|
+
'animate-in fade-in-0 zoom-in-95 duration-150',
|
|
120
|
+
)}
|
|
121
|
+
>
|
|
122
|
+
{/* Guiding label — explicit so the user knows what to type and
|
|
123
|
+
establishes "this is for capturing intent, not chat". */}
|
|
124
|
+
<div className="px-3 py-1.5 border-b border-zinc-200 dark:border-zinc-800 bg-amber-50/40 dark:bg-amber-950/20">
|
|
125
|
+
<span className="font-mono text-[10px] uppercase tracking-wider text-amber-700 dark:text-amber-300">
|
|
126
|
+
What's worth noting?
|
|
127
|
+
{entityType && (
|
|
128
|
+
<span className="ml-1.5 text-zinc-500 dark:text-zinc-400">
|
|
129
|
+
· {entityType}
|
|
130
|
+
{entityExpressId !== null && entityExpressId !== undefined && ` #${entityExpressId}`}
|
|
131
|
+
</span>
|
|
132
|
+
)}
|
|
133
|
+
</span>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div className="px-3 py-2.5">
|
|
137
|
+
<textarea
|
|
138
|
+
ref={textareaRef}
|
|
139
|
+
value={draft}
|
|
140
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
141
|
+
onKeyDown={handleKeyDown}
|
|
142
|
+
placeholder="A short note — flag a defect, ask a question, leave context…"
|
|
143
|
+
rows={3}
|
|
144
|
+
maxLength={MAX_NOTE_LEN + 100}
|
|
145
|
+
className={cn(
|
|
146
|
+
'w-full resize-none font-mono text-[11px] leading-relaxed',
|
|
147
|
+
'bg-zinc-50 dark:bg-zinc-900/60 text-zinc-800 dark:text-zinc-200',
|
|
148
|
+
'border border-zinc-200 dark:border-zinc-800 rounded-sm',
|
|
149
|
+
'px-2 py-1.5 outline-none focus:ring-1',
|
|
150
|
+
overHardLimit
|
|
151
|
+
? 'focus:ring-red-400 border-red-300 dark:border-red-700/60'
|
|
152
|
+
: 'focus:ring-amber-400/50 focus:border-amber-300/60',
|
|
153
|
+
)}
|
|
154
|
+
spellCheck
|
|
155
|
+
autoCorrect="on"
|
|
156
|
+
/>
|
|
157
|
+
<div className="mt-1.5 flex items-center justify-between gap-2 text-[10px] font-mono">
|
|
158
|
+
<span className="text-zinc-400 dark:text-zinc-500">
|
|
159
|
+
⏎ save · ⇧⏎ newline · esc cancel
|
|
160
|
+
</span>
|
|
161
|
+
{charCountVisible && (
|
|
162
|
+
<span
|
|
163
|
+
className={cn(
|
|
164
|
+
'tabular-nums',
|
|
165
|
+
overHardLimit
|
|
166
|
+
? 'text-red-500'
|
|
167
|
+
: overSoftLimit
|
|
168
|
+
? 'text-amber-600 dark:text-amber-400'
|
|
169
|
+
: 'text-zinc-400',
|
|
170
|
+
)}
|
|
171
|
+
>
|
|
172
|
+
{draft.length}/{MAX_NOTE_LEN}
|
|
173
|
+
</span>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
<div className="mt-2 flex items-center justify-end gap-1">
|
|
177
|
+
<Button
|
|
178
|
+
variant="ghost"
|
|
179
|
+
size="sm"
|
|
180
|
+
className="h-7 px-2 text-[11px]"
|
|
181
|
+
onClick={onCancel}
|
|
182
|
+
>
|
|
183
|
+
<X className="h-3 w-3 mr-1" />
|
|
184
|
+
Cancel
|
|
185
|
+
</Button>
|
|
186
|
+
<Button
|
|
187
|
+
size="sm"
|
|
188
|
+
className="h-7 px-2 text-[11px] bg-amber-500 hover:bg-amber-500/90 text-white"
|
|
189
|
+
onClick={() => {
|
|
190
|
+
if (overHardLimit) return;
|
|
191
|
+
if (draft.trim().length === 0) onCancel();
|
|
192
|
+
else onSave(draft);
|
|
193
|
+
}}
|
|
194
|
+
disabled={overHardLimit}
|
|
195
|
+
>
|
|
196
|
+
<Check className="h-3 w-3 mr-1" />
|
|
197
|
+
Drop pin
|
|
198
|
+
</Button>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
}
|