@ifc-lite/viewer 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/dist/assets/{Arrow.dom-BGPQieQQ.js → Arrow.dom-CusgkT03.js} +1 -1
  3. package/dist/assets/browser-BXNIkE8a.js +694 -0
  4. package/dist/assets/emscripten-module-BTRCZGcB.wasm +0 -0
  5. package/dist/assets/emscripten-module-CGIn_cMh.wasm +0 -0
  6. package/dist/assets/emscripten-module-DYvzWiHh.wasm +0 -0
  7. package/dist/assets/emscripten-module-NWak2PoB.wasm +0 -0
  8. package/dist/assets/emscripten-module.browser-CY5t0Vfq.js +1 -0
  9. package/dist/assets/esbuild-COv63sf-.js +1 -0
  10. package/dist/assets/esbuild-Cpd5nU_H.wasm +0 -0
  11. package/dist/assets/ffi-DlhRHxHv.js +1 -0
  12. package/dist/assets/index-6Mr3byM-.js +216 -0
  13. package/dist/assets/index-CGbokkQ9.css +1 -0
  14. package/dist/assets/index-huvR-kGC.js +98305 -0
  15. package/dist/assets/module-6F3E5H7Y-tx0BadV3.js +6 -0
  16. package/dist/assets/{native-bridge-DD0SNyQ5.js → native-bridge-DsHOKdgD.js} +1 -1
  17. package/dist/assets/{wasm-bridge-D54YMO7X.js → wasm-bridge-Bd73HXn-.js} +1 -1
  18. package/dist/index.html +12 -3
  19. package/index.html +10 -1
  20. package/package.json +30 -21
  21. package/src/App.tsx +6 -1
  22. package/src/components/ui/dialog.tsx +8 -6
  23. package/src/components/viewer/CodeEditor.tsx +309 -0
  24. package/src/components/viewer/CommandPalette.tsx +597 -0
  25. package/src/components/viewer/Drawing2DCanvas.tsx +364 -1
  26. package/src/components/viewer/EntityContextMenu.tsx +47 -20
  27. package/src/components/viewer/ExportDialog.tsx +166 -17
  28. package/src/components/viewer/HierarchyPanel.tsx +3 -1
  29. package/src/components/viewer/LensPanel.tsx +848 -85
  30. package/src/components/viewer/MainToolbar.tsx +145 -84
  31. package/src/components/viewer/ScriptPanel.tsx +416 -0
  32. package/src/components/viewer/Section2DPanel.tsx +269 -29
  33. package/src/components/viewer/TextAnnotationEditor.tsx +112 -0
  34. package/src/components/viewer/ViewerLayout.tsx +63 -11
  35. package/src/components/viewer/Viewport.tsx +58 -23
  36. package/src/components/viewer/ViewportContainer.tsx +2 -0
  37. package/src/components/viewer/hierarchy/HierarchyNode.tsx +1 -1
  38. package/src/components/viewer/hierarchy/types.ts +1 -1
  39. package/src/components/viewer/lists/ListResultsTable.tsx +53 -19
  40. package/src/components/viewer/tools/cloudPathGenerator.test.ts +118 -0
  41. package/src/components/viewer/tools/cloudPathGenerator.ts +275 -0
  42. package/src/components/viewer/tools/computePolygonArea.test.ts +165 -0
  43. package/src/components/viewer/tools/computePolygonArea.ts +72 -0
  44. package/src/components/viewer/useGeometryStreaming.ts +25 -5
  45. package/src/hooks/ids/idsExportService.ts +1 -1
  46. package/src/hooks/useAnnotation2D.ts +551 -0
  47. package/src/hooks/useDrawingExport.ts +83 -1
  48. package/src/hooks/useKeyboardShortcuts.ts +114 -14
  49. package/src/hooks/useLens.ts +40 -55
  50. package/src/hooks/useLensDiscovery.ts +46 -0
  51. package/src/hooks/useModelSelection.ts +5 -22
  52. package/src/hooks/useSandbox.ts +113 -0
  53. package/src/index.css +7 -1
  54. package/src/lib/lens/adapter.ts +127 -1
  55. package/src/lib/lists/columnToAutoColor.ts +33 -0
  56. package/src/lib/recent-files.ts +122 -0
  57. package/src/lib/scripts/persistence.ts +132 -0
  58. package/src/lib/scripts/templates/bim-globals.d.ts +111 -0
  59. package/src/lib/scripts/templates/data-quality-audit.ts +149 -0
  60. package/src/lib/scripts/templates/envelope-check.ts +164 -0
  61. package/src/lib/scripts/templates/federation-compare.ts +189 -0
  62. package/src/lib/scripts/templates/fire-safety-check.ts +161 -0
  63. package/src/lib/scripts/templates/mep-equipment-schedule.ts +175 -0
  64. package/src/lib/scripts/templates/quantity-takeoff.ts +145 -0
  65. package/src/lib/scripts/templates/reset-view.ts +6 -0
  66. package/src/lib/scripts/templates/space-validation.ts +189 -0
  67. package/src/lib/scripts/templates/tsconfig.json +13 -0
  68. package/src/lib/scripts/templates.ts +86 -0
  69. package/src/sdk/BimProvider.tsx +50 -0
  70. package/src/sdk/adapters/export-adapter.ts +283 -0
  71. package/src/sdk/adapters/lens-adapter.ts +44 -0
  72. package/src/sdk/adapters/model-adapter.ts +32 -0
  73. package/src/sdk/adapters/model-compat.ts +80 -0
  74. package/src/sdk/adapters/mutate-adapter.ts +45 -0
  75. package/src/sdk/adapters/query-adapter.ts +241 -0
  76. package/src/sdk/adapters/selection-adapter.ts +29 -0
  77. package/src/sdk/adapters/spatial-adapter.ts +37 -0
  78. package/src/sdk/adapters/types.ts +11 -0
  79. package/src/sdk/adapters/viewer-adapter.ts +103 -0
  80. package/src/sdk/adapters/visibility-adapter.ts +61 -0
  81. package/src/sdk/local-backend.ts +144 -0
  82. package/src/sdk/useBimHost.ts +69 -0
  83. package/src/store/constants.ts +10 -2
  84. package/src/store/index.ts +28 -2
  85. package/src/store/resolveEntityRef.ts +44 -0
  86. package/src/store/slices/drawing2DSlice.ts +321 -0
  87. package/src/store/slices/lensSlice.ts +46 -4
  88. package/src/store/slices/pinboardSlice.ts +171 -42
  89. package/src/store/slices/scriptSlice.ts +218 -0
  90. package/src/store/slices/uiSlice.ts +2 -0
  91. package/src/store.ts +3 -0
  92. package/tsconfig.json +5 -2
  93. package/vite.config.ts +8 -0
  94. package/dist/assets/index-dgdgiQ9p.js +0 -75456
  95. package/dist/assets/index-yTqs8kgX.css +0 -1
@@ -2,7 +2,7 @@
2
2
  * License, v. 2.0. If a copy of the MPL was not distributed with this
3
3
  * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
4
 
5
- import React, { useRef, useCallback, useMemo } from 'react';
5
+ import React, { useRef, useCallback, useEffect, useMemo } from 'react';
6
6
  import {
7
7
  FolderOpen,
8
8
  Download,
@@ -14,7 +14,7 @@ import {
14
14
  Scissors,
15
15
  Eye,
16
16
  EyeOff,
17
- Focus,
17
+ Equal,
18
18
  Crosshair,
19
19
  Home,
20
20
  Maximize2,
@@ -34,13 +34,11 @@ import {
34
34
  SquareX,
35
35
  Building2,
36
36
  Plus,
37
+ Minus,
37
38
  MessageSquare,
38
39
  ClipboardCheck,
39
- Pin,
40
- PinOff,
41
40
  Palette,
42
41
  Orbit,
43
- Trash2,
44
42
  } from 'lucide-react';
45
43
  import { Button } from '@/components/ui/button';
46
44
  import { Separator } from '@/components/ui/separator';
@@ -57,7 +55,8 @@ import {
57
55
  DropdownMenuSubContent,
58
56
  } from '@/components/ui/dropdown-menu';
59
57
  import { Progress } from '@/components/ui/progress';
60
- import { useViewerStore, isIfcxDataStore } from '@/store';
58
+ import { useViewerStore, isIfcxDataStore, stringToEntityRef } from '@/store';
59
+ import type { EntityRef } from '@/store';
61
60
  import { useIfc } from '@/hooks/useIfc';
62
61
  import { cn } from '@/lib/utils';
63
62
  import { GLTFExporter, CSVExporter } from '@ifc-lite/export';
@@ -67,6 +66,7 @@ import { BulkPropertyEditor } from './BulkPropertyEditor';
67
66
  import { DataConnector } from './DataConnector';
68
67
  import { ExportChangesButton } from './ExportChangesButton';
69
68
  import { useFloorplanView } from '@/hooks/useFloorplanView';
69
+ import { recordRecentFiles, cacheFileBlobs } from '@/lib/recent-files';
70
70
 
71
71
  type Tool = 'select' | 'pan' | 'orbit' | 'walk' | 'measure' | 'section';
72
72
 
@@ -148,6 +148,16 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
148
148
  const addModelInputRef = useRef<HTMLInputElement>(null);
149
149
  const { loadFile, loading, progress, geometryResult, ifcDataStore, models, clearAllModels, loadFilesSequentially, loadFederatedIfcx, addIfcxOverlays, addModel } = useIfc();
150
150
 
151
+ // Listen for programmatic file-load requests (from command palette recent files)
152
+ useEffect(() => {
153
+ const handler = (e: Event) => {
154
+ const file = (e as CustomEvent<File>).detail;
155
+ if (file) loadFile(file);
156
+ };
157
+ window.addEventListener('ifc-lite:load-file', handler);
158
+ return () => window.removeEventListener('ifc-lite:load-file', handler);
159
+ }, [loadFile]);
160
+
151
161
  // Floorplan view
152
162
  const { availableStoreys, activateFloorplan } = useFloorplanView();
153
163
 
@@ -158,8 +168,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
158
168
  const theme = useViewerStore((state) => state.theme);
159
169
  const toggleTheme = useViewerStore((state) => state.toggleTheme);
160
170
  const selectedEntityId = useViewerStore((state) => state.selectedEntityId);
161
- const isolateEntity = useViewerStore((state) => state.isolateEntity);
162
- const hideEntity = useViewerStore((state) => state.hideEntity);
171
+ const hideEntities = useViewerStore((state) => state.hideEntities);
163
172
  const showAll = useViewerStore((state) => state.showAll);
164
173
  const clearStoreySelection = useViewerStore((state) => state.clearStoreySelection);
165
174
  const error = useViewerStore((state) => state.error);
@@ -171,23 +180,27 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
171
180
  const resetViewerState = useViewerStore((state) => state.resetViewerState);
172
181
  const bcfPanelVisible = useViewerStore((state) => state.bcfPanelVisible);
173
182
  const toggleBcfPanel = useViewerStore((state) => state.toggleBcfPanel);
183
+ const setBcfPanelVisible = useViewerStore((state) => state.setBcfPanelVisible);
174
184
  const idsPanelVisible = useViewerStore((state) => state.idsPanelVisible);
175
185
  const toggleIdsPanel = useViewerStore((state) => state.toggleIdsPanel);
186
+ const setIdsPanelVisible = useViewerStore((state) => state.setIdsPanelVisible);
176
187
  const listPanelVisible = useViewerStore((state) => state.listPanelVisible);
177
188
  const toggleListPanel = useViewerStore((state) => state.toggleListPanel);
178
189
  const setRightPanelCollapsed = useViewerStore((state) => state.setRightPanelCollapsed);
179
190
  const projectionMode = useViewerStore((state) => state.projectionMode);
180
191
  const toggleProjectionMode = useViewerStore((state) => state.toggleProjectionMode);
181
- // Pinboard state
192
+ // Basket state (= + − isolation basket)
182
193
  const pinboardEntities = useViewerStore((state) => state.pinboardEntities);
183
- const addToPinboard = useViewerStore((state) => state.addToPinboard);
184
- const removeFromPinboard = useViewerStore((state) => state.removeFromPinboard);
185
- const showPinboard = useViewerStore((state) => state.showPinboard);
186
- const clearPinboard = useViewerStore((state) => state.clearPinboard);
187
- const selectedEntity = useViewerStore((state) => state.selectedEntity);
194
+ const setBasket = useViewerStore((state) => state.setBasket);
195
+ const addToBasket = useViewerStore((state) => state.addToBasket);
196
+ const removeFromBasket = useViewerStore((state) => state.removeFromBasket);
197
+ const clearBasket = useViewerStore((state) => state.clearBasket);
198
+ // NOTE: selectedEntity and selectedEntitiesSet accessed via getState() in callbacks
199
+ // to avoid re-rendering MainToolbar on every Cmd+Click selection change.
188
200
  // Lens state
189
201
  const lensPanelVisible = useViewerStore((state) => state.lensPanelVisible);
190
202
  const toggleLensPanel = useViewerStore((state) => state.toggleLensPanel);
203
+ const setLensPanelVisible = useViewerStore((state) => state.setLensPanelVisible);
191
204
 
192
205
  // Check which type geometries exist across ALL loaded models (federation-aware)
193
206
  const typeGeometryExists = useMemo(() => {
@@ -232,6 +245,10 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
232
245
 
233
246
  if (supportedFiles.length === 0) return;
234
247
 
248
+ // Track recently opened files (metadata + blob cache for instant reload)
249
+ recordRecentFiles(supportedFiles.map(f => ({ name: f.name, size: f.size })));
250
+ cacheFileBlobs(supportedFiles);
251
+
235
252
  if (supportedFiles.length === 1) {
236
253
  // Single file - use loadFile (simpler single-model path)
237
254
  loadFile(supportedFiles[0]);
@@ -289,25 +306,85 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
289
306
  e.target.value = '';
290
307
  }, [loadFilesSequentially, addIfcxOverlays, ifcDataStore]);
291
308
 
292
- const handleIsolate = useCallback(() => {
293
- if (selectedEntityId) {
294
- isolateEntity(selectedEntityId);
309
+ /** Get current selection as EntityRef[] — uses getState() to avoid reactive subscriptions */
310
+ const getSelectionRefs = useCallback((): EntityRef[] => {
311
+ const state = useViewerStore.getState();
312
+ if (state.selectedEntitiesSet.size > 0) {
313
+ const refs: EntityRef[] = [];
314
+ for (const str of state.selectedEntitiesSet) {
315
+ refs.push(stringToEntityRef(str));
316
+ }
317
+ return refs;
318
+ }
319
+ if (state.selectedEntity) {
320
+ return [state.selectedEntity];
321
+ }
322
+ return [];
323
+ }, []);
324
+
325
+ const hasSelection = selectedEntityId !== null;
326
+
327
+ // Basket state
328
+ const showPinboard = useViewerStore((state) => state.showPinboard);
329
+
330
+ // Clear multi-select state after basket operations so subsequent − targets a single entity
331
+ const clearMultiSelect = useCallback(() => {
332
+ const state = useViewerStore.getState();
333
+ if (state.selectedEntitiesSet.size > 0) {
334
+ useViewerStore.setState({ selectedEntitiesSet: new Set(), selectedEntityIds: new Set() });
335
+ }
336
+ }, []);
337
+
338
+ // Basket operations
339
+ const handleSetBasket = useCallback(() => {
340
+ const state = useViewerStore.getState();
341
+ // If basket already exists and user hasn't explicitly multi-selected,
342
+ // re-apply the basket instead of replacing it with a stale single selection.
343
+ // Only an explicit multi-selection (Ctrl+Click) should replace an existing basket.
344
+ if (state.pinboardEntities.size > 0 && state.selectedEntitiesSet.size === 0) {
345
+ showPinboard();
346
+ return;
347
+ }
348
+ const refs = getSelectionRefs();
349
+ if (refs.length > 0) {
350
+ setBasket(refs);
351
+ clearMultiSelect();
295
352
  }
296
- }, [selectedEntityId, isolateEntity]);
353
+ }, [getSelectionRefs, setBasket, showPinboard, clearMultiSelect]);
354
+
355
+ const handleAddToBasket = useCallback(() => {
356
+ const refs = getSelectionRefs();
357
+ if (refs.length > 0) {
358
+ addToBasket(refs);
359
+ clearMultiSelect();
360
+ }
361
+ }, [getSelectionRefs, addToBasket, clearMultiSelect]);
362
+
363
+ const handleRemoveFromBasket = useCallback(() => {
364
+ const refs = getSelectionRefs();
365
+ if (refs.length > 0) {
366
+ removeFromBasket(refs);
367
+ clearMultiSelect();
368
+ }
369
+ }, [getSelectionRefs, removeFromBasket, clearMultiSelect]);
297
370
 
298
371
  const clearSelection = useViewerStore((state) => state.clearSelection);
299
372
 
300
373
  const handleHide = useCallback(() => {
301
- if (selectedEntityId) {
302
- hideEntity(selectedEntityId);
303
- // Clear selection after hiding - element is no longer visible
374
+ // Hide ALL selected entities (multi-select or single)
375
+ const state = useViewerStore.getState();
376
+ const ids: number[] = state.selectedEntityIds.size > 0
377
+ ? Array.from(state.selectedEntityIds)
378
+ : selectedEntityId !== null ? [selectedEntityId] : [];
379
+ if (ids.length > 0) {
380
+ hideEntities(ids);
304
381
  clearSelection();
305
382
  }
306
- }, [selectedEntityId, hideEntity, clearSelection]);
383
+ }, [selectedEntityId, hideEntities, clearSelection]);
307
384
 
308
385
  const handleShowAll = useCallback(() => {
309
- showAll();
310
- clearStoreySelection(); // Also clear storey filtering (matches 'A' keyboard shortcut)
386
+ showAll(); // Clear hiddenEntities + isolatedEntities (basket contents preserved)
387
+ clearStoreySelection(); // Also clear storey filtering
311
388
  }, [showAll, clearStoreySelection]);
312
389
 
313
390
  const handleExportGLB = useCallback(() => {
@@ -413,6 +490,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
413
490
  <div className="flex items-center gap-1 px-2 h-12 border-b bg-white dark:bg-black border-zinc-200 dark:border-zinc-800 relative z-50">
414
491
  {/* ── File Operations ── */}
415
492
  <input
493
+ id="file-input-open"
416
494
  ref={fileInputRef}
417
495
  type="file"
418
496
  accept=".ifc,.ifcx,.glb"
@@ -574,8 +652,10 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
574
652
  size="icon-sm"
575
653
  onClick={(e) => {
576
654
  (e.currentTarget as HTMLButtonElement).blur();
577
- // If BCF is being shown, also expand the right panel
578
655
  if (!bcfPanelVisible) {
656
+ // Close other right-panel content first, then expand
657
+ setIdsPanelVisible(false);
658
+ setLensPanelVisible(false);
579
659
  setRightPanelCollapsed(false);
580
660
  }
581
661
  toggleBcfPanel();
@@ -596,8 +676,10 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
596
676
  size="icon-sm"
597
677
  onClick={(e) => {
598
678
  (e.currentTarget as HTMLButtonElement).blur();
599
- // If IDS is being shown, also expand the right panel
600
679
  if (!idsPanelVisible) {
680
+ // Close other right-panel content first, then expand
681
+ setBcfPanelVisible(false);
682
+ setLensPanelVisible(false);
601
683
  setRightPanelCollapsed(false);
602
684
  }
603
685
  toggleIdsPanel();
@@ -618,6 +700,8 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
618
700
  size="icon-sm"
619
701
  onClick={(e) => {
620
702
  (e.currentTarget as HTMLButtonElement).blur();
703
+ // Close script panel (bottom-panel exclusivity)
704
+ useViewerStore.getState().setScriptPanelVisible(false);
621
705
  if (!listPanelVisible) {
622
706
  setRightPanelCollapsed(false);
623
707
  }
@@ -675,9 +759,37 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
675
759
 
676
760
  <Separator orientation="vertical" className="h-6 mx-1" />
677
761
 
678
- {/* ── Visibility & Filtering (all together) ── */}
679
- <ActionButton icon={Focus} label="Isolate Selection" onClick={handleIsolate} shortcut="I" disabled={!selectedEntityId} />
680
- <ActionButton icon={EyeOff} label="Hide Selection" onClick={handleHide} shortcut="Del" disabled={!selectedEntityId} />
762
+ {/* ── Basket Isolation (= + −) ── */}
763
+ <Tooltip>
764
+ <TooltipTrigger asChild>
765
+ <Button
766
+ variant={pinboardEntities.size > 0 ? 'default' : 'ghost'}
767
+ size="icon-sm"
768
+ onClick={(e) => {
769
+ (e.currentTarget as HTMLButtonElement).blur();
770
+ handleSetBasket();
771
+ }}
772
+ disabled={!hasSelection && pinboardEntities.size === 0}
773
+ className={cn(
774
+ pinboardEntities.size > 0 && 'bg-primary text-primary-foreground relative',
775
+ )}
776
+ >
777
+ <Equal className="h-4 w-4" />
778
+ {pinboardEntities.size > 0 && (
779
+ <span className="absolute -top-1 -right-1 bg-primary text-primary-foreground text-[9px] font-bold rounded-full min-w-[14px] h-[14px] flex items-center justify-center px-0.5 border border-background">
780
+ {pinboardEntities.size}
781
+ </span>
782
+ )}
783
+ </Button>
784
+ </TooltipTrigger>
785
+ <TooltipContent>
786
+ Set Basket — isolate selection <span className="ml-2 text-xs opacity-60">(I)</span>
787
+ </TooltipContent>
788
+ </Tooltip>
789
+ <ActionButton icon={Plus} label="Add to Basket" onClick={handleAddToBasket} shortcut="+" disabled={!hasSelection} />
790
+ <ActionButton icon={Minus} label="Remove from Basket" onClick={handleRemoveFromBasket} shortcut="−" disabled={!hasSelection} />
791
+
792
+ <ActionButton icon={EyeOff} label="Hide Selection" onClick={handleHide} shortcut="Del / Space" disabled={!hasSelection} />
681
793
  <ActionButton icon={Eye} label="Show All (Reset Filters)" onClick={handleShowAll} shortcut="A" />
682
794
  <ActionButton icon={Maximize2} label="Fit All" onClick={() => cameraCallbacks.fitAll?.()} shortcut="Z" />
683
795
  <ActionButton
@@ -685,7 +797,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
685
797
  label="Frame Selection"
686
798
  onClick={() => cameraCallbacks.frameSelection?.()}
687
799
  shortcut="F"
688
- disabled={!selectedEntityId}
800
+ disabled={!hasSelection}
689
801
  />
690
802
 
691
803
  <DropdownMenu>
@@ -730,60 +842,6 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
730
842
  </DropdownMenuContent>
731
843
  </DropdownMenu>
732
844
 
733
- {/* Pinboard dropdown */}
734
- <DropdownMenu>
735
- <Tooltip>
736
- <TooltipTrigger asChild>
737
- <DropdownMenuTrigger asChild>
738
- <Button
739
- variant={pinboardEntities.size > 0 ? 'default' : 'ghost'}
740
- size="icon-sm"
741
- className={cn(pinboardEntities.size > 0 && 'bg-primary text-primary-foreground relative')}
742
- >
743
- <Pin className="h-4 w-4" />
744
- {pinboardEntities.size > 0 && (
745
- <span className="absolute -top-1 -right-1 bg-primary text-primary-foreground text-[9px] font-bold rounded-full min-w-[14px] h-[14px] flex items-center justify-center px-0.5 border border-background">
746
- {pinboardEntities.size}
747
- </span>
748
- )}
749
- </Button>
750
- </DropdownMenuTrigger>
751
- </TooltipTrigger>
752
- <TooltipContent>Pinboard ({pinboardEntities.size})</TooltipContent>
753
- </Tooltip>
754
- <DropdownMenuContent>
755
- <DropdownMenuItem
756
- onClick={() => { if (selectedEntity) addToPinboard([selectedEntity]); }}
757
- disabled={!selectedEntity}
758
- >
759
- <Pin className="h-4 w-4 mr-2" />
760
- Pin Selection
761
- </DropdownMenuItem>
762
- <DropdownMenuItem
763
- onClick={() => { if (selectedEntity) removeFromPinboard([selectedEntity]); }}
764
- disabled={!selectedEntity}
765
- >
766
- <PinOff className="h-4 w-4 mr-2" />
767
- Unpin Selection
768
- </DropdownMenuItem>
769
- <DropdownMenuSeparator />
770
- <DropdownMenuItem
771
- onClick={() => showPinboard()}
772
- disabled={pinboardEntities.size === 0}
773
- >
774
- <Eye className="h-4 w-4 mr-2" />
775
- Show Pinboard
776
- </DropdownMenuItem>
777
- <DropdownMenuItem
778
- onClick={() => clearPinboard()}
779
- disabled={pinboardEntities.size === 0}
780
- >
781
- <Trash2 className="h-4 w-4 mr-2" />
782
- Clear Pinboard
783
- </DropdownMenuItem>
784
- </DropdownMenuContent>
785
- </DropdownMenu>
786
-
787
845
  {/* Lens (rule-based filtering) */}
788
846
  <Tooltip>
789
847
  <TooltipTrigger asChild>
@@ -793,6 +851,9 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
793
851
  onClick={(e) => {
794
852
  (e.currentTarget as HTMLButtonElement).blur();
795
853
  if (!lensPanelVisible) {
854
+ // Close other right-panel content first, then expand
855
+ setBcfPanelVisible(false);
856
+ setIdsPanelVisible(false);
796
857
  setRightPanelCollapsed(false);
797
858
  }
798
859
  toggleLensPanel();