@ifc-lite/viewer 1.25.2 → 1.27.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 +40 -30
- package/CHANGELOG.md +110 -0
- package/dist/assets/{basketViewActivator-CTgyKI3U.js → basketViewActivator-B3CdrLsb.js} +7 -7
- package/dist/assets/{bcf-7jQby1qi.js → bcf-QeHK_Aud.js} +5 -5
- package/dist/assets/{browser-DXS29_v9.js → browser-BIoDDfBW.js} +1 -1
- package/dist/assets/{cesium-BoVuJvTC.js → cesium-CzZn5yVA.js} +319 -319
- package/dist/assets/{deflate-Cfp9t1Df.js → deflate-B-d0SYQM.js} +1 -1
- package/dist/assets/exceljs.min-DsuzKYnj.js +29 -0
- package/dist/assets/{exporters-DfSvJPi4.js → exporters-B4LbZFeT.js} +1434 -1179
- package/dist/assets/geometry.worker-BdH-E6NB.js +1 -0
- package/dist/assets/{geotiff-xZoE8BkO.js → geotiff-CrVtDRFq.js} +10 -10
- package/dist/assets/html2canvas.esm-Ge7aVWlp.js +5 -0
- package/dist/assets/{ids-Cu73hD0Y.js → ids-DjsGFN10.js} +21 -21
- package/dist/assets/ifc-lite_bg-DsYUIHm3.wasm +0 -0
- package/dist/assets/{index-WSbA5iy6.js → index-COYokSKc.js} +44122 -38782
- package/dist/assets/index-ajK6D32J.css +1 -0
- package/dist/assets/index.es-CY202jA3.js +6866 -0
- package/dist/assets/{jpeg-DhwFEbqb.js → jpeg-D4wOkf5h.js} +1 -1
- package/dist/assets/jspdf.es.min-DIGb9BHN.js +19571 -0
- package/dist/assets/jspdf.plugin.autotable-BBLUVd7n.js +2 -0
- package/dist/assets/{lerc-Dz6BXOVb.js → lerc-DmW0_tgf.js} +1 -1
- package/dist/assets/{lzw-C9z0fG2o.js → lzw-oWetY-d6.js} +1 -1
- package/dist/assets/{maplibre-gl-Do6O5tDc.js → maplibre-gl-BF3Z0idw.js} +1 -1
- package/dist/assets/{native-bridge-RvDmzO-2.js → native-bridge-BX8_tHXE.js} +1 -1
- package/dist/assets/{packbits-jfwifz7C.js → packbits-F8Nkp4NY.js} +1 -1
- package/dist/assets/{pako.esm-Cram60i4.js → pako.esm-n3Pgozwg.js} +1 -1
- package/dist/assets/{parser.worker-C594dWxH.js → parser.worker-D591Zu_-.js} +3 -3
- package/dist/assets/pdf-Dsh3HPZB.js +135 -0
- package/dist/assets/raw-D9iw0tmc.js +1 -0
- package/dist/assets/{sandbox-DDSZ7rek.js → sandbox-BAC3a-eN.js} +4235 -2716
- package/dist/assets/server-client-Cjwnm7il.js +706 -0
- package/dist/assets/{webimage-XFHVyVtC.js → webimage-BLV1dgmd.js} +1 -1
- package/dist/assets/xlsx-Bc2HTrjC.js +142 -0
- package/dist/assets/{zip-BJqVbRkU.js → zip-DFgP-l20.js} +1 -1
- package/dist/assets/{zstd-3q5qcl5V.js → zstd-C_1HxVrA.js} +1 -1
- package/dist/index.html +8 -8
- package/package.json +13 -9
- package/src/components/extensions/FlavorDialog.tsx +18 -2
- package/src/components/extensions/FlavorListView.tsx +12 -3
- package/src/components/mcp/PlaygroundChat.tsx +1 -0
- package/src/components/mcp/data.ts +6 -0
- package/src/components/mcp/playground-dispatcher.ts +277 -0
- package/src/components/mcp/types.ts +2 -1
- package/src/components/ui/combo-input.tsx +163 -0
- package/src/components/ui/tabs.tsx +1 -1
- package/src/components/viewer/ClashBcfExportDialog.tsx +271 -0
- package/src/components/viewer/ClashPanel.tsx +370 -0
- package/src/components/viewer/ClashSettingsDialog.tsx +407 -0
- package/src/components/viewer/CommandPalette.tsx +14 -15
- package/src/components/viewer/MainToolbar.tsx +155 -175
- package/src/components/viewer/PropertiesPanel.tsx +13 -6
- package/src/components/viewer/SearchInline.tsx +62 -2
- package/src/components/viewer/SearchModal.filter.builder.tsx +24 -393
- package/src/components/viewer/SearchModal.filter.editors.tsx +503 -0
- package/src/components/viewer/SearchModal.filter.tsx +64 -1
- package/src/components/viewer/SearchModal.tsx +19 -6
- package/src/components/viewer/ViewerLayout.tsx +5 -0
- package/src/components/viewer/Viewport.tsx +64 -9
- package/src/components/viewer/ViewportContainer.tsx +45 -3
- package/src/components/viewer/bcf/BCFOverlay.tsx +5 -4
- package/src/components/viewer/lists/ColumnHeaderMenu.tsx +84 -0
- package/src/components/viewer/lists/ListBuilder.tsx +789 -280
- package/src/components/viewer/lists/ListGroupingBar.tsx +72 -0
- package/src/components/viewer/lists/ListPanel.tsx +49 -5
- package/src/components/viewer/lists/ListResultsTable.tsx +270 -176
- package/src/components/viewer/lists/list-table-utils.ts +123 -0
- package/src/components/viewer/useGeometryStreaming.ts +21 -1
- package/src/generated/mcp-catalog.json +4 -0
- package/src/hooks/ingest/streamCleanup.test.ts +41 -0
- package/src/hooks/ingest/streamCleanup.ts +45 -0
- package/src/hooks/ingest/viewerModelIngest.ts +64 -42
- package/src/hooks/ingest/watchedGeometryStream.test.ts +78 -0
- package/src/hooks/ingest/watchedGeometryStream.ts +76 -0
- package/src/hooks/source-key.ts +35 -0
- package/src/hooks/useAlignmentLines3D.ts +139 -0
- package/src/hooks/useClash.ts +420 -0
- package/src/hooks/useGridLines3D.ts +140 -0
- package/src/hooks/useIfcFederation.ts +16 -2
- package/src/hooks/useIfcLoader.ts +5 -7
- package/src/lib/clash/persistence.ts +308 -0
- package/src/lib/geo/effective-georef.test.ts +66 -0
- package/src/lib/length-unit-scale.ts +41 -0
- package/src/lib/lists/adapter.ts +136 -11
- package/src/lib/lists/export/csv.ts +47 -0
- package/src/lib/lists/export/index.ts +49 -0
- package/src/lib/lists/export/model.ts +111 -0
- package/src/lib/lists/export/pdf.ts +67 -0
- package/src/lib/lists/export/xlsx.ts +83 -0
- package/src/lib/lists/index.ts +2 -0
- package/src/lib/search/filter-evaluate.test.ts +81 -0
- package/src/lib/search/filter-evaluate.ts +59 -87
- package/src/lib/search/filter-match.ts +167 -0
- package/src/lib/search/filter-rules.test.ts +25 -0
- package/src/lib/search/filter-rules.ts +75 -2
- package/src/lib/search/filter-schema.ts +0 -0
- package/src/lib/slab-edit.test.ts +72 -0
- package/src/lib/slab-edit.ts +159 -19
- package/src/sdk/adapters/export-adapter.ts +3 -3
- package/src/sdk/adapters/query-adapter.ts +3 -3
- package/src/services/extensions/host.ts +13 -0
- package/src/store/constants.ts +33 -25
- package/src/store/index.ts +29 -8
- package/src/store/slices/clashSlice.ts +251 -0
- package/src/store/slices/listSlice.ts +6 -0
- package/src/store/slices/mutationSlice.ts +14 -6
- package/src/store/slices/searchSlice.ts +29 -3
- package/src/store/slices/visibilitySlice.test.ts +23 -5
- package/src/store/slices/visibilitySlice.ts +18 -8
- package/src/utils/nativeSpatialDataStore.ts +6 -0
- package/src/utils/serverDataModel.test.ts +6 -0
- package/src/utils/serverDataModel.ts +7 -0
- package/dist/assets/geometry.worker-Cyn5BybV.js +0 -1
- package/dist/assets/ifc-lite_bg-ksLBP5cA.wasm +0 -0
- package/dist/assets/index-Bws3UAkj.css +0 -1
- package/dist/assets/raw-R2QfzPAR.js +0 -1
- package/dist/assets/server-client-Ctk8_Bof.js +0 -626
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
Redo2,
|
|
53
53
|
} from 'lucide-react';
|
|
54
54
|
import { Button } from '@/components/ui/button';
|
|
55
|
+
import { Switch } from '@/components/ui/switch';
|
|
55
56
|
import { Separator } from '@/components/ui/separator';
|
|
56
57
|
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
|
57
58
|
import {
|
|
@@ -158,6 +159,39 @@ function ToolButton({
|
|
|
158
159
|
);
|
|
159
160
|
}
|
|
160
161
|
|
|
162
|
+
interface ClassVisibilityRowProps {
|
|
163
|
+
/** Colored class glyph (caller sets the tint). */
|
|
164
|
+
icon: React.ReactNode;
|
|
165
|
+
label: string;
|
|
166
|
+
/** One-line plain-language hint about what the IFC class covers. */
|
|
167
|
+
description: string;
|
|
168
|
+
checked: boolean;
|
|
169
|
+
onChange: (next: boolean) => void;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* One row of the Visibility panel: colored class icon + label/description
|
|
174
|
+
* on the left, a Switch on the right. The whole row is a <label>, so a
|
|
175
|
+
* click anywhere toggles the switch and — because it isn't a menu item —
|
|
176
|
+
* the dropdown stays open for flipping several classes in a row. The left
|
|
177
|
+
* cluster dims when off so on/off reads from saturation as well as the
|
|
178
|
+
* switch position.
|
|
179
|
+
*/
|
|
180
|
+
function ClassVisibilityRow({ icon, label, description, checked, onChange }: ClassVisibilityRowProps) {
|
|
181
|
+
return (
|
|
182
|
+
<label className="group flex items-center justify-between gap-3 rounded-md px-2 py-1.5 cursor-pointer hover:bg-muted/50 transition-colors">
|
|
183
|
+
<span className={cn('flex items-center gap-2.5 min-w-0 transition-opacity', !checked && 'opacity-50')}>
|
|
184
|
+
{icon}
|
|
185
|
+
<span className="grid gap-0.5 min-w-0">
|
|
186
|
+
<span className="text-sm leading-tight truncate">{label}</span>
|
|
187
|
+
<span className="text-[10px] leading-tight text-muted-foreground truncate">{description}</span>
|
|
188
|
+
</span>
|
|
189
|
+
</span>
|
|
190
|
+
<Switch checked={checked} onCheckedChange={onChange} />
|
|
191
|
+
</label>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
161
195
|
/**
|
|
162
196
|
* Stacked / Exploded / Solo level display dropdown. Pinned next
|
|
163
197
|
* to the Quick Floorplan dropdown so storey-related controls
|
|
@@ -505,6 +539,16 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
505
539
|
const toggleHoverTooltips = useViewerStore((state) => state.toggleHoverTooltips);
|
|
506
540
|
const typeVisibility = useViewerStore((state) => state.typeVisibility);
|
|
507
541
|
const toggleTypeVisibility = useViewerStore((state) => state.toggleTypeVisibility);
|
|
542
|
+
const resetTypeVisibility = useViewerStore((state) => state.resetTypeVisibility);
|
|
543
|
+
// How many of the five class toggles are on — surfaced in the menu
|
|
544
|
+
// header so the user sees scene state at a glance.
|
|
545
|
+
const visibleClassCount = [
|
|
546
|
+
typeVisibility.spaces,
|
|
547
|
+
typeVisibility.openings,
|
|
548
|
+
typeVisibility.site,
|
|
549
|
+
typeVisibility.ifcAnnotations,
|
|
550
|
+
typeVisibility.ifcGrid,
|
|
551
|
+
].filter(Boolean).length;
|
|
508
552
|
// Issue #540: load-time toggle that asks the WASM bridge to merge
|
|
509
553
|
// Revit-style multilayer walls. We surface this in the Class
|
|
510
554
|
// Visibility dropdown so users discover it next to the other
|
|
@@ -516,6 +560,8 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
516
560
|
const setBcfPanelVisible = useViewerStore((state) => state.setBcfPanelVisible);
|
|
517
561
|
const idsPanelVisible = useViewerStore((state) => state.idsPanelVisible);
|
|
518
562
|
const setIdsPanelVisible = useViewerStore((state) => state.setIdsPanelVisible);
|
|
563
|
+
const clashPanelVisible = useViewerStore((state) => state.clashPanelVisible);
|
|
564
|
+
const setClashPanelVisible = useViewerStore((state) => state.setClashPanelVisible);
|
|
519
565
|
const listPanelVisible = useViewerStore((state) => state.listPanelVisible);
|
|
520
566
|
const setListPanelVisible = useViewerStore((state) => state.setListPanelVisible);
|
|
521
567
|
const setRightPanelCollapsed = useViewerStore((state) => state.setRightPanelCollapsed);
|
|
@@ -562,107 +608,11 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
562
608
|
);
|
|
563
609
|
const desktopShell = isTauri();
|
|
564
610
|
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
//
|
|
568
|
-
//
|
|
569
|
-
//
|
|
570
|
-
const typeGeomScanRef = useRef({
|
|
571
|
-
spaces: false, openings: false, site: false,
|
|
572
|
-
legacyLastLen: 0,
|
|
573
|
-
modelLastLen: new Map<string | number, number>(),
|
|
574
|
-
});
|
|
575
|
-
const meshLen = geometryResult?.meshes.length ?? 0;
|
|
576
|
-
const typeGeometryExists = useMemo(() => {
|
|
577
|
-
const scan = typeGeomScanRef.current;
|
|
578
|
-
|
|
579
|
-
// Reset if legacy meshes array shrunk (new file loaded)
|
|
580
|
-
if (meshLen < scan.legacyLastLen) {
|
|
581
|
-
scan.spaces = false;
|
|
582
|
-
scan.openings = false;
|
|
583
|
-
scan.site = false;
|
|
584
|
-
scan.legacyLastLen = 0;
|
|
585
|
-
scan.modelLastLen.clear();
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// Already found all types — nothing to do
|
|
589
|
-
if (scan.spaces && scan.openings && scan.site) {
|
|
590
|
-
return { spaces: scan.spaces, openings: scan.openings, site: scan.site };
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// Check federated models (scan only new meshes per model)
|
|
594
|
-
if (models.size > 0) {
|
|
595
|
-
for (const [modelId, model] of models) {
|
|
596
|
-
const meshes = model.geometryResult?.meshes;
|
|
597
|
-
if (!meshes) continue;
|
|
598
|
-
const modelStart = scan.modelLastLen.get(modelId) ?? 0;
|
|
599
|
-
// Reset cursor if model was reloaded (mesh array shrunk)
|
|
600
|
-
const start = meshes.length < modelStart ? 0 : modelStart;
|
|
601
|
-
for (let i = start; i < meshes.length; i++) {
|
|
602
|
-
const t = meshes[i].ifcType;
|
|
603
|
-
if (t === 'IfcSpace') scan.spaces = true;
|
|
604
|
-
else if (t === 'IfcOpeningElement') scan.openings = true;
|
|
605
|
-
else if (t === 'IfcSite') scan.site = true;
|
|
606
|
-
if (scan.spaces && scan.openings && scan.site) break;
|
|
607
|
-
}
|
|
608
|
-
scan.modelLastLen.set(modelId, meshes.length);
|
|
609
|
-
if (scan.spaces && scan.openings && scan.site) break;
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// Legacy single-model path (scan only new meshes)
|
|
614
|
-
if (geometryResult?.meshes) {
|
|
615
|
-
const meshes = geometryResult.meshes;
|
|
616
|
-
for (let i = scan.legacyLastLen; i < meshes.length; i++) {
|
|
617
|
-
const t = meshes[i].ifcType;
|
|
618
|
-
if (t === 'IfcSpace') scan.spaces = true;
|
|
619
|
-
else if (t === 'IfcOpeningElement') scan.openings = true;
|
|
620
|
-
else if (t === 'IfcSite') scan.site = true;
|
|
621
|
-
if (scan.spaces && scan.openings && scan.site) break;
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
scan.legacyLastLen = meshLen;
|
|
626
|
-
return { spaces: scan.spaces, openings: scan.openings, site: scan.site };
|
|
627
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- meshLen is a stable proxy for geometryResult
|
|
628
|
-
}, [models, meshLen]);
|
|
629
|
-
|
|
630
|
-
// IfcAnnotation / IfcGrid have no body mesh, so they can't be detected via
|
|
631
|
-
// the mesh scan. Look up the entity table directly. byType keys are
|
|
632
|
-
// uppercase STEP names but cache loads sometimes preserve PascalCase.
|
|
633
|
-
//
|
|
634
|
-
// Issue #862 split these into separate visibility toggles — files that
|
|
635
|
-
// ship only one of the two need only that menu entry. Some files ship
|
|
636
|
-
// only grids (Snowdon Towers Structural — no IfcAnnotation) so probing
|
|
637
|
-
// each independently is required.
|
|
638
|
-
const hasIfcEntities = useMemo(() => {
|
|
639
|
-
const probe = (store: typeof ifcDataStore | undefined) => {
|
|
640
|
-
const byType = store?.entityIndex?.byType;
|
|
641
|
-
if (!byType) return { annotations: false, grid: false };
|
|
642
|
-
return {
|
|
643
|
-
annotations: (byType.get('IFCANNOTATION')?.length ?? 0) > 0
|
|
644
|
-
|| (byType.get('IfcAnnotation')?.length ?? 0) > 0,
|
|
645
|
-
grid: (byType.get('IFCGRID')?.length ?? 0) > 0
|
|
646
|
-
|| (byType.get('IfcGrid')?.length ?? 0) > 0,
|
|
647
|
-
};
|
|
648
|
-
};
|
|
649
|
-
let annotations = false;
|
|
650
|
-
let grid = false;
|
|
651
|
-
if (models.size > 0) {
|
|
652
|
-
for (const [, m] of models) {
|
|
653
|
-
const p = probe(m.ifcDataStore);
|
|
654
|
-
annotations ||= p.annotations;
|
|
655
|
-
grid ||= p.grid;
|
|
656
|
-
}
|
|
657
|
-
} else {
|
|
658
|
-
const p = probe(ifcDataStore);
|
|
659
|
-
annotations = p.annotations;
|
|
660
|
-
grid = p.grid;
|
|
661
|
-
}
|
|
662
|
-
return { annotations, grid };
|
|
663
|
-
}, [models, ifcDataStore]);
|
|
664
|
-
const hasIfcAnnotations = hasIfcEntities.annotations;
|
|
665
|
-
const hasIfcGrid = hasIfcEntities.grid;
|
|
611
|
+
// NOTE: The Class Visibility dropdown used to gate each toggle on whether
|
|
612
|
+
// the loaded model actually contained that class (scanning meshes for
|
|
613
|
+
// Spaces/Openings/Site and probing the entity table for Annotations/Grids).
|
|
614
|
+
// That gating was removed: the toggles are persisted user preferences, so
|
|
615
|
+
// they now render unconditionally and stay sticky across models and reloads.
|
|
666
616
|
|
|
667
617
|
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
668
618
|
const files = e.target.files;
|
|
@@ -811,7 +761,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
811
761
|
setScriptPanelVisible,
|
|
812
762
|
]);
|
|
813
763
|
|
|
814
|
-
const handleToggleRightPanel = useCallback((panel: 'bcf' | 'ids' | 'lens' | 'addElement' | 'extensions') => {
|
|
764
|
+
const handleToggleRightPanel = useCallback((panel: 'bcf' | 'ids' | 'lens' | 'clash' | 'addElement' | 'extensions') => {
|
|
815
765
|
if (activeAnalysisExtension?.placement !== 'bottom') {
|
|
816
766
|
closeActiveAnalysisExtension();
|
|
817
767
|
}
|
|
@@ -828,6 +778,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
828
778
|
const nextBcfVisible = panel === 'bcf' ? !bcfPanelVisible : false;
|
|
829
779
|
const nextIdsVisible = panel === 'ids' ? !idsPanelVisible : false;
|
|
830
780
|
const nextLensVisible = panel === 'lens' ? !lensPanelVisible : false;
|
|
781
|
+
const nextClashVisible = panel === 'clash' ? !clashPanelVisible : false;
|
|
831
782
|
const nextExtensionsVisible = panel === 'extensions' ? !extensionsPanelVisible : false;
|
|
832
783
|
const isAddElementActive = activeTool === 'addElement';
|
|
833
784
|
const nextAddElementActive = panel === 'addElement' ? !isAddElementActive : false;
|
|
@@ -835,6 +786,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
835
786
|
setBcfPanelVisible(nextBcfVisible);
|
|
836
787
|
setIdsPanelVisible(nextIdsVisible);
|
|
837
788
|
setLensPanelVisible(nextLensVisible);
|
|
789
|
+
setClashPanelVisible(nextClashVisible);
|
|
838
790
|
setExtensionsPanelVisible(nextExtensionsVisible);
|
|
839
791
|
|
|
840
792
|
if (panel === 'addElement') {
|
|
@@ -843,19 +795,21 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
843
795
|
setActiveTool('select');
|
|
844
796
|
}
|
|
845
797
|
|
|
846
|
-
if (nextBcfVisible || nextIdsVisible || nextLensVisible || nextExtensionsVisible || nextAddElementActive) {
|
|
798
|
+
if (nextBcfVisible || nextIdsVisible || nextLensVisible || nextClashVisible || nextExtensionsVisible || nextAddElementActive) {
|
|
847
799
|
setRightPanelCollapsed(false);
|
|
848
800
|
}
|
|
849
801
|
}, [
|
|
850
802
|
activeAnalysisExtension?.placement,
|
|
851
803
|
activeTool,
|
|
852
804
|
bcfPanelVisible,
|
|
805
|
+
clashPanelVisible,
|
|
853
806
|
extensionsPanelVisible,
|
|
854
807
|
idsPanelVisible,
|
|
855
808
|
lensPanelVisible,
|
|
856
809
|
requireDesktopFeature,
|
|
857
810
|
setActiveTool,
|
|
858
811
|
setBcfPanelVisible,
|
|
812
|
+
setClashPanelVisible,
|
|
859
813
|
setExtensionsPanelVisible,
|
|
860
814
|
setIdsPanelVisible,
|
|
861
815
|
setLensPanelVisible,
|
|
@@ -889,6 +843,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
889
843
|
setBcfPanelVisible(false);
|
|
890
844
|
setIdsPanelVisible(false);
|
|
891
845
|
setLensPanelVisible(false);
|
|
846
|
+
setClashPanelVisible(false);
|
|
892
847
|
setExtensionsPanelVisible(false);
|
|
893
848
|
// The right slot is single-tenant: when an analysis extension takes
|
|
894
849
|
// it over, the AddElement tool must release it too, otherwise its 3D
|
|
@@ -903,6 +858,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
903
858
|
analysisExtensionState.extensions,
|
|
904
859
|
setActiveTool,
|
|
905
860
|
setBcfPanelVisible,
|
|
861
|
+
setClashPanelVisible,
|
|
906
862
|
setExtensionsPanelVisible,
|
|
907
863
|
setGanttPanelVisible,
|
|
908
864
|
setIdsPanelVisible,
|
|
@@ -920,6 +876,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
920
876
|
if (bcfPanelVisible) panels.add('bcf');
|
|
921
877
|
if (idsPanelVisible) panels.add('ids');
|
|
922
878
|
if (lensPanelVisible) panels.add('lens');
|
|
879
|
+
if (clashPanelVisible) panels.add('clash');
|
|
923
880
|
if (extensionsPanelVisible) panels.add('extensions');
|
|
924
881
|
if (activeTool === 'addElement') panels.add('addElement');
|
|
925
882
|
if (analysisExtensionState.activeId) panels.add(analysisExtensionState.activeId);
|
|
@@ -928,6 +885,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
928
885
|
activeTool,
|
|
929
886
|
analysisExtensionState.activeId,
|
|
930
887
|
bcfPanelVisible,
|
|
888
|
+
clashPanelVisible,
|
|
931
889
|
extensionsPanelVisible,
|
|
932
890
|
ganttPanelVisible,
|
|
933
891
|
idsPanelVisible,
|
|
@@ -945,6 +903,7 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
945
903
|
if (activeWorkspacePanels.has('bcf')) return 'BCF Issues';
|
|
946
904
|
if (activeWorkspacePanels.has('ids')) return 'IDS Validation';
|
|
947
905
|
if (activeWorkspacePanels.has('lens')) return 'Lens Rules';
|
|
906
|
+
if (activeWorkspacePanels.has('clash')) return 'Clash Detection';
|
|
948
907
|
if (activeWorkspacePanels.has('extensions')) return 'Extensions';
|
|
949
908
|
if (activeWorkspacePanels.has('addElement')) return 'Add Element';
|
|
950
909
|
return activeAnalysisExtension?.label ?? 'Analysis';
|
|
@@ -1299,6 +1258,13 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
1299
1258
|
<Palette className="h-4 w-4 mr-2" />
|
|
1300
1259
|
Lens Rules
|
|
1301
1260
|
</DropdownMenuCheckboxItem>
|
|
1261
|
+
<DropdownMenuCheckboxItem
|
|
1262
|
+
checked={activeWorkspacePanels.has('clash')}
|
|
1263
|
+
onCheckedChange={() => handleToggleRightPanel('clash')}
|
|
1264
|
+
>
|
|
1265
|
+
<Crosshair className="h-4 w-4 mr-2" />
|
|
1266
|
+
Clash Detection
|
|
1267
|
+
</DropdownMenuCheckboxItem>
|
|
1302
1268
|
<DropdownMenuSeparator />
|
|
1303
1269
|
<DropdownMenuLabel className="text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
1304
1270
|
Author
|
|
@@ -1534,9 +1500,9 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
1534
1500
|
// Stay enabled even with no model loaded — the dropdown
|
|
1535
1501
|
// also exposes load-time settings (Merge Multilayer
|
|
1536
1502
|
// Walls) that the user should be able to set BEFORE
|
|
1537
|
-
// opening a file.
|
|
1538
|
-
//
|
|
1539
|
-
aria-label={mergeLayers ? '
|
|
1503
|
+
// opening a file. The class toggles are persisted
|
|
1504
|
+
// preferences, so they always render too.
|
|
1505
|
+
aria-label={mergeLayers ? 'Visibility (Merge Multilayer Walls is on)' : 'Visibility'}
|
|
1540
1506
|
className="relative"
|
|
1541
1507
|
>
|
|
1542
1508
|
<Filter className="h-4 w-4" />
|
|
@@ -1553,79 +1519,93 @@ export function MainToolbar({ onShowShortcuts }: MainToolbarProps = {} as MainTo
|
|
|
1553
1519
|
</DropdownMenuTrigger>
|
|
1554
1520
|
</TooltipTrigger>
|
|
1555
1521
|
<TooltipContent>
|
|
1556
|
-
{mergeLayers ? '
|
|
1522
|
+
{mergeLayers ? 'Visibility · Merge Multilayer Walls is on' : 'Visibility'}
|
|
1557
1523
|
</TooltipContent>
|
|
1558
1524
|
</Tooltip>
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
<
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
>
|
|
1574
|
-
<
|
|
1575
|
-
|
|
1576
|
-
</DropdownMenuCheckboxItem>
|
|
1577
|
-
)}
|
|
1578
|
-
{typeGeometryExists.site && (
|
|
1579
|
-
<DropdownMenuCheckboxItem
|
|
1580
|
-
checked={typeVisibility.site}
|
|
1581
|
-
onCheckedChange={() => toggleTypeVisibility('site')}
|
|
1582
|
-
>
|
|
1583
|
-
<Building2 className="h-4 w-4 mr-2" style={{ color: '#66cc4d' }} />
|
|
1584
|
-
Show Site
|
|
1585
|
-
</DropdownMenuCheckboxItem>
|
|
1586
|
-
)}
|
|
1587
|
-
{hasIfcAnnotations && (
|
|
1588
|
-
<DropdownMenuCheckboxItem
|
|
1589
|
-
checked={typeVisibility.ifcAnnotations}
|
|
1590
|
-
onCheckedChange={() => toggleTypeVisibility('ifcAnnotations')}
|
|
1591
|
-
>
|
|
1592
|
-
<Pencil className="h-4 w-4 mr-2" style={{ color: '#e4b400' }} />
|
|
1593
|
-
Show Annotations
|
|
1594
|
-
</DropdownMenuCheckboxItem>
|
|
1595
|
-
)}
|
|
1596
|
-
{hasIfcGrid && (
|
|
1597
|
-
<DropdownMenuCheckboxItem
|
|
1598
|
-
checked={typeVisibility.ifcGrid}
|
|
1599
|
-
onCheckedChange={() => toggleTypeVisibility('ifcGrid')}
|
|
1600
|
-
>
|
|
1601
|
-
<Pencil className="h-4 w-4 mr-2" style={{ color: '#e4b400' }} />
|
|
1602
|
-
Show Grids
|
|
1603
|
-
</DropdownMenuCheckboxItem>
|
|
1604
|
-
)}
|
|
1605
|
-
|
|
1606
|
-
{/* Load-time toggles live below the runtime visibility
|
|
1607
|
-
switches — they apply on next model open rather than
|
|
1608
|
-
affecting the current scene. The subheader makes that
|
|
1609
|
-
boundary visible at a glance. */}
|
|
1610
|
-
<DropdownMenuSeparator />
|
|
1611
|
-
<DropdownMenuLabel className="px-2 pt-1 pb-0.5 text-[10px] font-semibold uppercase tracking-wider text-muted-foreground">
|
|
1612
|
-
Load Settings
|
|
1613
|
-
</DropdownMenuLabel>
|
|
1614
|
-
<DropdownMenuCheckboxItem
|
|
1615
|
-
checked={mergeLayers}
|
|
1616
|
-
onCheckedChange={(next) => setMergeLayers(next === true)}
|
|
1617
|
-
// Use items-start so the checkmark and icon line up with
|
|
1618
|
-
// the primary label while the description wraps below.
|
|
1619
|
-
className="items-start gap-2 py-2"
|
|
1620
|
-
>
|
|
1621
|
-
<Layers2 className="h-4 w-4 mr-2 mt-0.5 shrink-0 text-primary" />
|
|
1622
|
-
<div className="flex flex-col gap-0.5 min-w-0">
|
|
1623
|
-
<span className="text-sm font-medium leading-tight">Merge Multilayer Walls</span>
|
|
1624
|
-
<span className="text-[11px] leading-tight text-muted-foreground">
|
|
1625
|
-
Render walls as 1 solid · Applies on reload
|
|
1525
|
+
{/*
|
|
1526
|
+
Settings-style panel (not a list of menu-items): each row is a
|
|
1527
|
+
plain <label> wrapping a right-aligned Switch, so toggling does
|
|
1528
|
+
NOT close the menu — users routinely flip several classes in one
|
|
1529
|
+
pass. State reads two ways: the switch position and the row
|
|
1530
|
+
dimming when off. All five render unconditionally (persisted
|
|
1531
|
+
preferences, sticky across models/reloads); toggling a class the
|
|
1532
|
+
model lacks is a no-op.
|
|
1533
|
+
*/}
|
|
1534
|
+
<DropdownMenuContent align="start" className="w-[300px] p-1.5">
|
|
1535
|
+
<div className="flex items-center justify-between gap-2 px-1.5 pb-1 pt-0.5">
|
|
1536
|
+
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
|
|
1537
|
+
Visibility
|
|
1538
|
+
</span>
|
|
1539
|
+
<div className="flex items-center gap-1">
|
|
1540
|
+
<span className="text-[11px] tabular-nums text-muted-foreground/80">
|
|
1541
|
+
{visibleClassCount}/5
|
|
1626
1542
|
</span>
|
|
1543
|
+
<Button
|
|
1544
|
+
variant="ghost"
|
|
1545
|
+
size="sm"
|
|
1546
|
+
className="h-6 px-1.5 text-[11px] font-medium text-muted-foreground hover:text-foreground"
|
|
1547
|
+
onClick={resetTypeVisibility}
|
|
1548
|
+
>
|
|
1549
|
+
Reset
|
|
1550
|
+
</Button>
|
|
1627
1551
|
</div>
|
|
1628
|
-
</
|
|
1552
|
+
</div>
|
|
1553
|
+
|
|
1554
|
+
<ClassVisibilityRow
|
|
1555
|
+
icon={<Box className="h-4 w-4 shrink-0" style={{ color: '#33d9ff' }} />}
|
|
1556
|
+
label="Spaces"
|
|
1557
|
+
description="Room & zone volumes"
|
|
1558
|
+
checked={typeVisibility.spaces}
|
|
1559
|
+
onChange={() => toggleTypeVisibility('spaces')}
|
|
1560
|
+
/>
|
|
1561
|
+
<ClassVisibilityRow
|
|
1562
|
+
icon={<SquareX className="h-4 w-4 shrink-0" style={{ color: '#ff6b4a' }} />}
|
|
1563
|
+
label="Openings"
|
|
1564
|
+
description="Door & window voids"
|
|
1565
|
+
checked={typeVisibility.openings}
|
|
1566
|
+
onChange={() => toggleTypeVisibility('openings')}
|
|
1567
|
+
/>
|
|
1568
|
+
<ClassVisibilityRow
|
|
1569
|
+
icon={<Building2 className="h-4 w-4 shrink-0" style={{ color: '#66cc4d' }} />}
|
|
1570
|
+
label="Site"
|
|
1571
|
+
description="Terrain & context"
|
|
1572
|
+
checked={typeVisibility.site}
|
|
1573
|
+
onChange={() => toggleTypeVisibility('site')}
|
|
1574
|
+
/>
|
|
1575
|
+
<ClassVisibilityRow
|
|
1576
|
+
icon={<Pencil className="h-4 w-4 shrink-0" style={{ color: '#e4b400' }} />}
|
|
1577
|
+
label="Annotations"
|
|
1578
|
+
description="Text, dimensions, leaders"
|
|
1579
|
+
checked={typeVisibility.ifcAnnotations}
|
|
1580
|
+
onChange={() => toggleTypeVisibility('ifcAnnotations')}
|
|
1581
|
+
/>
|
|
1582
|
+
<ClassVisibilityRow
|
|
1583
|
+
icon={<Grid3x3 className="h-4 w-4 shrink-0" style={{ color: '#e4b400' }} />}
|
|
1584
|
+
label="Grids"
|
|
1585
|
+
description="Structural axes"
|
|
1586
|
+
checked={typeVisibility.ifcGrid}
|
|
1587
|
+
onChange={() => toggleTypeVisibility('ifcGrid')}
|
|
1588
|
+
/>
|
|
1589
|
+
|
|
1590
|
+
<DropdownMenuSeparator className="my-1" />
|
|
1591
|
+
|
|
1592
|
+
{/* Merge multilayer walls rebuilds geometry, so unlike the live
|
|
1593
|
+
toggles above it only takes effect on the next model load.
|
|
1594
|
+
The "· on reload" suffix carries that nuance inline — keeps
|
|
1595
|
+
the row identical in shape to the others (no header, no chip
|
|
1596
|
+
crowding the long label). */}
|
|
1597
|
+
<label className="group flex items-center justify-between gap-3 rounded-md px-2 py-1.5 cursor-pointer hover:bg-muted/50 transition-colors">
|
|
1598
|
+
<span className={cn('flex items-center gap-2.5 min-w-0 transition-opacity', !mergeLayers && 'opacity-50')}>
|
|
1599
|
+
<Layers2 className="h-4 w-4 shrink-0 text-primary" />
|
|
1600
|
+
<span className="grid gap-0.5 min-w-0">
|
|
1601
|
+
<span className="text-sm leading-tight truncate">Merge multilayer walls</span>
|
|
1602
|
+
<span className="text-[10px] leading-tight text-muted-foreground truncate">
|
|
1603
|
+
Render walls as one solid · on reload
|
|
1604
|
+
</span>
|
|
1605
|
+
</span>
|
|
1606
|
+
</span>
|
|
1607
|
+
<Switch checked={mergeLayers} onCheckedChange={(next) => setMergeLayers(next === true)} />
|
|
1608
|
+
</label>
|
|
1629
1609
|
</DropdownMenuContent>
|
|
1630
1610
|
</DropdownMenu>
|
|
1631
1611
|
|
|
@@ -512,7 +512,7 @@ export function PropertiesPanel() {
|
|
|
512
512
|
if (!entityNode) return [];
|
|
513
513
|
|
|
514
514
|
const rawProps = entityNode.properties();
|
|
515
|
-
let result = rawProps.map(pset => ({
|
|
515
|
+
let result: DisplayPropertySet[] = rawProps.map(pset => ({
|
|
516
516
|
name: pset.name,
|
|
517
517
|
properties: pset.properties.map(p => ({ name: p.name, value: p.value, isMutated: false })),
|
|
518
518
|
isNewPset: false,
|
|
@@ -970,21 +970,28 @@ export function PropertiesPanel() {
|
|
|
970
970
|
}));
|
|
971
971
|
}, [nativeDetails]);
|
|
972
972
|
|
|
973
|
+
// Overlay (authored) entities — split halves, duplicates, scripted
|
|
974
|
+
// adds — live only in the StoreEditor overlay, NOT the parsed store.
|
|
975
|
+
// `modelQuery.entity()` always returns a node, and its getters fall
|
|
976
|
+
// back to the 'Unknown'/'' sentinels for ids absent from the parsed
|
|
977
|
+
// table (entity-table.ts#getTypeName). Those non-null sentinels would
|
|
978
|
+
// shadow the overlay record in an `entityNode ?? overlay` chain, so
|
|
979
|
+
// when an overlay record exists it MUST take precedence.
|
|
973
980
|
const renderedEntityType = isNativeLazySelection
|
|
974
981
|
? (nativeDetails?.summary.type ?? 'Loading...')
|
|
975
|
-
: (
|
|
982
|
+
: (overlayEntity?.type ?? entityNode?.type ?? 'Unknown');
|
|
976
983
|
const renderedEntityName = isNativeLazySelection
|
|
977
984
|
? (nativeDetails?.summary.name ?? `#${selectedEntity?.expressId ?? ''}`)
|
|
978
|
-
: (
|
|
985
|
+
: (overlayAttr(2) ?? entityNode?.name ?? undefined);
|
|
979
986
|
const renderedEntityGlobalId = isNativeLazySelection
|
|
980
987
|
? (nativeDetails?.summary.globalId ?? null)
|
|
981
|
-
: (
|
|
988
|
+
: (overlayAttr(0) ?? entityNode?.globalId);
|
|
982
989
|
const renderedEntityDescription = isNativeLazySelection
|
|
983
990
|
? undefined
|
|
984
|
-
: (
|
|
991
|
+
: (overlayAttr(3) ?? entityNode?.description ?? undefined);
|
|
985
992
|
const renderedEntityObjectType = isNativeLazySelection
|
|
986
993
|
? undefined
|
|
987
|
-
: (
|
|
994
|
+
: (overlayAttr(4) ?? entityNode?.objectType ?? undefined);
|
|
988
995
|
const renderedSpatialInfo = isNativeLazySelection ? nativeSpatialInfo : spatialInfo;
|
|
989
996
|
const renderedOccurrenceProperties = isNativeLazySelection ? nativeOccurrenceProperties : occurrenceProperties;
|
|
990
997
|
const renderedInheritedTypeProperties = isNativeLazySelection ? [] : inheritedTypeProperties;
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
27
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
28
|
-
import { Search, Clock, X } from 'lucide-react';
|
|
28
|
+
import { Search, Clock, X, SlidersHorizontal } from 'lucide-react';
|
|
29
29
|
import { useShallow } from 'zustand/react/shallow';
|
|
30
30
|
import { Input } from '@/components/ui/input';
|
|
31
31
|
import { useViewerStore } from '@/store';
|
|
@@ -79,6 +79,9 @@ export function SearchInline() {
|
|
|
79
79
|
exitVimCycle,
|
|
80
80
|
stepVimCycle,
|
|
81
81
|
setSearchModalOpen,
|
|
82
|
+
setSearchModalTab,
|
|
83
|
+
activeRuleCount,
|
|
84
|
+
clearFilterRules,
|
|
82
85
|
models,
|
|
83
86
|
setSelectedEntity,
|
|
84
87
|
setSelectedEntityId,
|
|
@@ -99,6 +102,9 @@ export function SearchInline() {
|
|
|
99
102
|
exitVimCycle: s.exitVimCycle,
|
|
100
103
|
stepVimCycle: s.stepVimCycle,
|
|
101
104
|
setSearchModalOpen: s.setSearchModalOpen,
|
|
105
|
+
setSearchModalTab: s.setSearchModalTab,
|
|
106
|
+
activeRuleCount: s.searchFilter.rules.length,
|
|
107
|
+
clearFilterRules: s.clearFilterRules,
|
|
102
108
|
models: s.models,
|
|
103
109
|
setSelectedEntity: s.setSelectedEntity,
|
|
104
110
|
setSelectedEntityId: s.setSelectedEntityId,
|
|
@@ -395,8 +401,10 @@ export function SearchInline() {
|
|
|
395
401
|
e.preventDefault();
|
|
396
402
|
// ⌘↵ / Ctrl+↵ opens the advanced modal instead of committing — the
|
|
397
403
|
// inline query is preserved so the modal opens already populated.
|
|
404
|
+
// Text-search entry point, so land on the Search tab.
|
|
398
405
|
if (e.metaKey || e.ctrlKey) {
|
|
399
406
|
setSearchOpen(false);
|
|
407
|
+
setSearchModalTab('search');
|
|
400
408
|
setSearchModalOpen(true);
|
|
401
409
|
return;
|
|
402
410
|
}
|
|
@@ -416,9 +424,19 @@ export function SearchInline() {
|
|
|
416
424
|
if (target) commitResult(target, idx, e.shiftKey, liveResults, live);
|
|
417
425
|
}
|
|
418
426
|
},
|
|
419
|
-
[commitResult, results, searchHighlightIndex, searchOpen, setSearchHighlightIndex, setSearchModalOpen, setSearchOpen],
|
|
427
|
+
[commitResult, results, searchHighlightIndex, searchOpen, setSearchHighlightIndex, setSearchModalOpen, setSearchModalTab, setSearchOpen],
|
|
420
428
|
);
|
|
421
429
|
|
|
430
|
+
const hasFilters = activeRuleCount > 0;
|
|
431
|
+
|
|
432
|
+
/** Open the advanced modal straight to the Filter builder — the
|
|
433
|
+
* always-visible entry point to structured filtering. */
|
|
434
|
+
const openAdvancedFilter = useCallback(() => {
|
|
435
|
+
setSearchOpen(false);
|
|
436
|
+
setSearchModalTab('filter');
|
|
437
|
+
setSearchModalOpen(true);
|
|
438
|
+
}, [setSearchOpen, setSearchModalTab, setSearchModalOpen]);
|
|
439
|
+
|
|
422
440
|
const queryTrimmedLen = searchQuery.trim().length;
|
|
423
441
|
const showPopover = searchOpen && (results.length > 0 || queryTrimmedLen > 0 || recents.length > 0);
|
|
424
442
|
const showRecents = searchOpen && queryTrimmedLen === 0 && recents.length > 0;
|
|
@@ -437,11 +455,52 @@ export function SearchInline() {
|
|
|
437
455
|
}}
|
|
438
456
|
onFocus={() => setSearchOpen(true)}
|
|
439
457
|
onKeyDown={handleInputKeyDown}
|
|
458
|
+
className={cn(hasFilters ? 'pr-[4.5rem]' : 'pr-9')}
|
|
440
459
|
aria-label="Search entities"
|
|
441
460
|
aria-autocomplete="list"
|
|
442
461
|
aria-expanded={showPopover}
|
|
443
462
|
aria-controls="search-inline-popover"
|
|
444
463
|
/>
|
|
464
|
+
{/* Advanced-filter affordance — always visible so structured
|
|
465
|
+
filtering is discoverable without the ⌘⇧F shortcut. Shows the
|
|
466
|
+
active rule count and a quick-clear when a filter is applied. */}
|
|
467
|
+
<div className="absolute right-1.5 top-1/2 -translate-y-1/2 flex items-center gap-0.5">
|
|
468
|
+
{hasFilters && (
|
|
469
|
+
<button
|
|
470
|
+
type="button"
|
|
471
|
+
aria-label="Clear filters"
|
|
472
|
+
title="Clear filters"
|
|
473
|
+
onMouseDown={(e) => {
|
|
474
|
+
e.preventDefault();
|
|
475
|
+
clearFilterRules();
|
|
476
|
+
}}
|
|
477
|
+
className="rounded p-1 text-muted-foreground transition-colors hover:bg-zinc-100 hover:text-foreground dark:hover:bg-zinc-800"
|
|
478
|
+
>
|
|
479
|
+
<X className="h-3.5 w-3.5" />
|
|
480
|
+
</button>
|
|
481
|
+
)}
|
|
482
|
+
<button
|
|
483
|
+
type="button"
|
|
484
|
+
aria-label={hasFilters ? `Advanced filter — ${activeRuleCount} active` : 'Advanced filter'}
|
|
485
|
+
aria-pressed={hasFilters}
|
|
486
|
+
title="Advanced filter (⌘⇧F)"
|
|
487
|
+
onMouseDown={(e) => {
|
|
488
|
+
e.preventDefault();
|
|
489
|
+
openAdvancedFilter();
|
|
490
|
+
}}
|
|
491
|
+
className={cn(
|
|
492
|
+
'flex items-center gap-1 rounded px-1.5 py-1 text-xs transition-colors',
|
|
493
|
+
hasFilters
|
|
494
|
+
? 'bg-primary/10 text-primary hover:bg-primary/15'
|
|
495
|
+
: 'text-muted-foreground hover:bg-zinc-100 hover:text-foreground dark:hover:bg-zinc-800',
|
|
496
|
+
)}
|
|
497
|
+
>
|
|
498
|
+
<SlidersHorizontal className="h-3.5 w-3.5" />
|
|
499
|
+
{hasFilters && (
|
|
500
|
+
<span className="font-mono text-[10px] font-semibold leading-none">{activeRuleCount}</span>
|
|
501
|
+
)}
|
|
502
|
+
</button>
|
|
503
|
+
</div>
|
|
445
504
|
{/* Vim cycle hint — shows below the input whenever a cycle is active
|
|
446
505
|
and the popover is closed. Clicking it exits the cycle. */}
|
|
447
506
|
{searchVimCycle && !showPopover && (
|
|
@@ -475,6 +534,7 @@ export function SearchInline() {
|
|
|
475
534
|
onHover={(i) => setSearchHighlightIndex(i)}
|
|
476
535
|
onOpenAdvanced={() => {
|
|
477
536
|
setSearchOpen(false);
|
|
537
|
+
setSearchModalTab('search');
|
|
478
538
|
setSearchModalOpen(true);
|
|
479
539
|
}}
|
|
480
540
|
/>
|