@ifc-lite/viewer 1.26.0 → 1.28.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 +45 -38
- package/CHANGELOG.md +93 -0
- package/dist/assets/{basketViewActivator-ZpTYWE3K.js → basketViewActivator-BNRDNuUJ.js} +9 -9
- package/dist/assets/{bcf-Ctcu_Sc2.js → bcf-DCwCuP7n.js} +56 -56
- 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/{decode-worker-CgM1iNSK.js → decode-worker-Cjign7Zh.js} +1 -1
- package/dist/assets/deflate-DNGgs8Ur.js +1 -0
- package/dist/assets/drawing-2d-D0dDf6Lh.js +257 -0
- package/dist/assets/e57-source-2wI9jkCA.js +1 -0
- package/dist/assets/exceljs.min-DsuzKYnj.js +29 -0
- package/dist/assets/{exporters-DSq76AVM.js → exporters-B9v81gi9.js} +1861 -1524
- package/dist/assets/geometry.worker-Bpa3115V.js +1 -0
- package/dist/assets/{geotiff-A5UjhI6L.js → geotiff-D-YCLS4g.js} +10 -10
- package/dist/assets/html2canvas.esm-Ge7aVWlp.js +5 -0
- package/dist/assets/{ids-DiLcGTer.js → ids-CCpq-5d3.js} +952 -945
- package/dist/assets/ifc-lite_bg-DbgS5EUA.wasm +0 -0
- package/dist/assets/{index-BAH8IJVR.js → index-Bgb3_Pu_.js} +47682 -42474
- package/dist/assets/index-BtbXFKsX.css +1 -0
- package/dist/assets/index.es-CWfqZyyr.js +6866 -0
- package/dist/assets/{jpeg-BzSkwo5D.js → jpeg-DGOAeUqU.js} +1 -1
- package/dist/assets/jspdf.es.min-XPLU2Wkq.js +19571 -0
- package/dist/assets/jspdf.plugin.autotable-BBLUVd7n.js +2 -0
- package/dist/assets/lens-C4p1kQ0p.js +1 -0
- package/dist/assets/{lerc-Cg2Rz-D5.js → lerc-1PMSCHwX.js} +1 -1
- package/dist/assets/{lzw-BBPPLW-0.js → lzw-C65U9lNM.js} +1 -1
- package/dist/assets/{maplibre-gl-Do6O5tDc.js → maplibre-gl-BF3Z0idw.js} +1 -1
- package/dist/assets/{native-bridge-CPojOeGE.js → native-bridge-XxXos6yI.js} +2 -2
- package/dist/assets/{packbits-yLSpjW-V.js → packbits-BdMWXC3m.js} +1 -1
- package/dist/assets/{pako.esm-Cram60i4.js → pako.esm-n3Pgozwg.js} +1 -1
- package/dist/assets/parser.worker-Ddwo3_06.js +182 -0
- package/dist/assets/pdf-CRwaZf3s.js +135 -0
- package/dist/assets/raw-CJgQdyuZ.js +1 -0
- package/dist/assets/{sandbox-CsRXlgCO.js → sandbox-0sDo3g3m.js} +3037 -2554
- package/dist/assets/server-client-cTCJ-853.js +719 -0
- package/dist/assets/{webimage-YafxjjGr.js → webimage-BtakWX7W.js} +1 -1
- package/dist/assets/xlsx-B1YOg2QB.js +142 -0
- package/dist/assets/{zip-BJqVbRkU.js → zip-DFgP-l20.js} +1 -1
- package/dist/assets/{zstd-CkSLOiuu.js → zstd-CmwsbxmM.js} +1 -1
- package/dist/index.html +10 -10
- package/package.json +27 -23
- package/src/components/mcp/PlaygroundChat.tsx +1 -0
- package/src/components/mcp/data.ts +6 -0
- package/src/components/mcp/playground-dispatcher.ts +280 -0
- package/src/components/mcp/playground-files.ts +33 -1
- 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/CommandPalette.tsx +6 -1
- package/src/components/viewer/ComparePanel.tsx +420 -0
- package/src/components/viewer/HierarchyPanel.tsx +46 -7
- package/src/components/viewer/MainToolbar.tsx +19 -2
- package/src/components/viewer/PropertiesPanel.tsx +84 -8
- 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 +18 -0
- package/src/components/viewer/hierarchy/HierarchyNode.tsx +3 -3
- package/src/components/viewer/hierarchy/ifc-icons.ts +9 -0
- package/src/components/viewer/hierarchy/treeDataBuilder.ts +87 -0
- package/src/components/viewer/hierarchy/types.ts +1 -0
- package/src/components/viewer/hierarchy/useHierarchyTree.ts +6 -2
- 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/properties/MaterialTotalsPanel.tsx +283 -0
- package/src/generated/mcp-catalog.json +4 -0
- package/src/hooks/federationLoadGate.test.ts +12 -2
- package/src/hooks/federationLoadGate.ts +9 -2
- package/src/hooks/ingest/federationAlign.ts +481 -0
- package/src/hooks/ingest/viewerModelIngest.ts +3 -212
- package/src/hooks/source-key.ts +35 -0
- package/src/hooks/useAlignmentLines3D.ts +1 -26
- package/src/hooks/useCompare.ts +0 -0
- package/src/hooks/useCompareOverlay.ts +119 -0
- package/src/hooks/useDrawingGeneration.ts +23 -1
- package/src/hooks/useGridLines3D.ts +140 -0
- package/src/hooks/useIfc.ts +1 -1
- package/src/hooks/useIfcCache.ts +32 -9
- package/src/hooks/useIfcFederation.ts +42 -810
- package/src/hooks/useIfcLoader.ts +361 -488
- package/src/hooks/useIfcServer.ts +3 -0
- package/src/hooks/useLens.ts +5 -1
- package/src/hooks/useSymbolicAnnotations.ts +70 -38
- package/src/lib/compare/buildFingerprints.ts +173 -0
- package/src/lib/compare/describeChange.ts +0 -0
- package/src/lib/compare/geometricData.test.ts +54 -0
- package/src/lib/compare/geometricData.ts +37 -0
- package/src/lib/compare/overlay.test.ts +99 -0
- package/src/lib/compare/overlay.ts +91 -0
- package/src/lib/geo/cesium-placement.ts +1 -1
- package/src/lib/geo/reproject.ts +4 -1
- 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/llm/script-edit-ops.ts +23 -0
- package/src/lib/llm/stream-client.ts +8 -1
- 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/search/result-export.ts +7 -1
- package/src/lib/slab-edit.test.ts +72 -0
- package/src/lib/slab-edit.ts +159 -19
- package/src/sdk/adapters/export-adapter.ts +9 -4
- package/src/sdk/adapters/query-adapter.ts +3 -3
- package/src/store/globalId.ts +15 -13
- package/src/store/index.ts +16 -1
- package/src/store/slices/cesiumSlice.ts +8 -1
- package/src/store/slices/compareSlice.ts +96 -0
- package/src/store/slices/lensSlice.ts +8 -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/utils/acquireFileBuffer.test.ts +12 -4
- package/src/utils/desktopModelSnapshot.ts +2 -1
- package/src/utils/loadingUtils.ts +32 -0
- package/src/utils/nativeSpatialDataStore.ts +6 -0
- package/src/utils/serverDataModel.test.ts +6 -0
- package/src/utils/serverDataModel.ts +7 -0
- package/src/utils/spatialHierarchy.test.ts +53 -1
- package/src/utils/spatialHierarchy.ts +42 -2
- package/src/vite-env.d.ts +2 -0
- package/dist/assets/deflate-Cnx0il6E.js +0 -1
- package/dist/assets/drawing-2d-C71b8Ugx.js +0 -257
- package/dist/assets/e57-source-CQHxE8n3.js +0 -1
- package/dist/assets/geometry.worker-0Q9qEa6p.js +0 -1
- package/dist/assets/ifc-lite_bg-CEZnhM2e.wasm +0 -0
- package/dist/assets/index-B9Ug2EqU.css +0 -1
- package/dist/assets/lens-PYsLu_MA.js +0 -1
- package/dist/assets/parser.worker-8md211IW.js +0 -182
- package/dist/assets/raw-BQrAgxwT.js +0 -1
- package/dist/assets/server-client-Bk4c1CPO.js +0 -626
- package/src/hooks/ingest/resolveDataStoreOrAbort.test.ts +0 -61
- package/src/hooks/ingest/resolveDataStoreOrAbort.ts +0 -28
- package/src/hooks/ingest/watchedGeometryStream.test.ts +0 -78
- package/src/hooks/ingest/watchedGeometryStream.ts +0 -76
|
@@ -36,7 +36,7 @@ import { getNativeEntityDetails } from '@/services/desktop-native-metadata';
|
|
|
36
36
|
import { configureMutationView } from '@/utils/configureMutationView';
|
|
37
37
|
import { IfcQuery } from '@ifc-lite/query';
|
|
38
38
|
import { MutablePropertyView } from '@ifc-lite/mutations';
|
|
39
|
-
import { extractClassificationsOnDemand, extractMaterialsOnDemand, extractTypePropertiesOnDemand, extractTypeEntityOwnProperties, extractDocumentsOnDemand, extractRelationshipsOnDemand, extractGeoreferencingOnDemand, extractLengthUnitScale, getAttributeNames, type IfcDataStore } from '@ifc-lite/parser';
|
|
39
|
+
import { extractClassificationsOnDemand, extractMaterialsOnDemand, extractMaterialPropertiesOnDemand, extractTypePropertiesOnDemand, extractTypeEntityOwnProperties, extractDocumentsOnDemand, extractRelationshipsOnDemand, extractGeoreferencingOnDemand, extractLengthUnitScale, getAttributeNames, type IfcDataStore, type MaterialPsetGroup } from '@ifc-lite/parser';
|
|
40
40
|
import type { NewEntity } from '@ifc-lite/mutations';
|
|
41
41
|
import { EntityFlags, RelationshipType, isSpatialStructureTypeName, isStoreyLikeSpatialTypeName } from '@ifc-lite/data';
|
|
42
42
|
import type { EntityRef, FederatedModel } from '@/store/types';
|
|
@@ -47,6 +47,7 @@ import { QuantitySetCard } from './properties/QuantitySetCard';
|
|
|
47
47
|
import { ModelMetadataPanel } from './properties/ModelMetadataPanel';
|
|
48
48
|
import { ClassificationCard } from './properties/ClassificationCard';
|
|
49
49
|
import { MaterialCard } from './properties/MaterialCard';
|
|
50
|
+
import { MaterialTotalsPanel } from './properties/MaterialTotalsPanel';
|
|
50
51
|
import { ScheduleCard } from './properties/ScheduleCard';
|
|
51
52
|
import { TaskEditCard } from './properties/TaskEditCard';
|
|
52
53
|
import { DocumentCard } from './properties/DocumentCard';
|
|
@@ -56,6 +57,17 @@ import { BsddCard } from './properties/BsddCard';
|
|
|
56
57
|
import { GeoreferencingPanel } from './properties/GeoreferencingPanel';
|
|
57
58
|
import { RawStepCard } from './properties/RawStepCard';
|
|
58
59
|
|
|
60
|
+
/** IFC material *definition* classes selectable from the Materials tab. */
|
|
61
|
+
const MATERIAL_DEF_TYPES = new Set([
|
|
62
|
+
'IFCMATERIAL',
|
|
63
|
+
'IFCMATERIALLAYERSET',
|
|
64
|
+
'IFCMATERIALLAYERSETUSAGE',
|
|
65
|
+
'IFCMATERIALPROFILESET',
|
|
66
|
+
'IFCMATERIALPROFILESETUSAGE',
|
|
67
|
+
'IFCMATERIALCONSTITUENTSET',
|
|
68
|
+
'IFCMATERIALLIST',
|
|
69
|
+
]);
|
|
70
|
+
|
|
59
71
|
type DisplayProperty = { name: string; value: unknown; isMutated: boolean };
|
|
60
72
|
type DisplayPropertySet = {
|
|
61
73
|
name: string;
|
|
@@ -450,6 +462,16 @@ export function PropertiesPanel() {
|
|
|
450
462
|
return typeName.endsWith('Type');
|
|
451
463
|
}, [selectedEntity, model, ifcDataStore]);
|
|
452
464
|
|
|
465
|
+
// Detect a material definition selected from the "Materials" hierarchy tab.
|
|
466
|
+
// Materials aren't products, so the EntityTable's getTypeName doesn't cover
|
|
467
|
+
// them — read the raw class from the entity index instead.
|
|
468
|
+
const selectedMaterialId = useMemo(() => {
|
|
469
|
+
if (!selectedEntity) return null;
|
|
470
|
+
const dataStore = model?.ifcDataStore ?? ifcDataStore;
|
|
471
|
+
const rawType = (dataStore as IfcDataStore | null)?.entityIndex?.byId?.get(selectedEntity.expressId)?.type;
|
|
472
|
+
return rawType && MATERIAL_DEF_TYPES.has(rawType.toUpperCase()) ? selectedEntity.expressId : null;
|
|
473
|
+
}, [selectedEntity, model, ifcDataStore]);
|
|
474
|
+
|
|
453
475
|
// Unified property/quantity access - EntityNode handles on-demand extraction automatically
|
|
454
476
|
// These hooks must be called before any early return to maintain hook order
|
|
455
477
|
// Use MutablePropertyView as primary source when available (it handles base + mutations)
|
|
@@ -512,7 +534,7 @@ export function PropertiesPanel() {
|
|
|
512
534
|
if (!entityNode) return [];
|
|
513
535
|
|
|
514
536
|
const rawProps = entityNode.properties();
|
|
515
|
-
let result = rawProps.map(pset => ({
|
|
537
|
+
let result: DisplayPropertySet[] = rawProps.map(pset => ({
|
|
516
538
|
name: pset.name,
|
|
517
539
|
properties: pset.properties.map(p => ({ name: p.name, value: p.value, isMutated: false })),
|
|
518
540
|
isNewPset: false,
|
|
@@ -627,6 +649,17 @@ export function PropertiesPanel() {
|
|
|
627
649
|
return extractMaterialsOnDemand(dataStore as IfcDataStore, lookupExpressId);
|
|
628
650
|
}, [selectedEntity, lookupExpressId, model, ifcDataStore]);
|
|
629
651
|
|
|
652
|
+
// Property sets attached to the selected entity's material(s) via
|
|
653
|
+
// IfcMaterialProperties (e.g. Pset_MaterialConcrete). These live on the
|
|
654
|
+
// IfcMaterial — not on the object — so they never surface through the
|
|
655
|
+
// occurrence/type pset paths; resolve them through the material association.
|
|
656
|
+
const materialProperties: MaterialPsetGroup[] = useMemo(() => {
|
|
657
|
+
if (!selectedEntity || lookupExpressId === null) return [];
|
|
658
|
+
const dataStore = model?.ifcDataStore ?? ifcDataStore;
|
|
659
|
+
if (!dataStore) return [];
|
|
660
|
+
return extractMaterialPropertiesOnDemand(dataStore as IfcDataStore, lookupExpressId);
|
|
661
|
+
}, [selectedEntity, lookupExpressId, model, ifcDataStore]);
|
|
662
|
+
|
|
630
663
|
// Extract documents for the selected entity from the IFC data store
|
|
631
664
|
const documents = useMemo(() => {
|
|
632
665
|
if (!selectedEntity || lookupExpressId === null) return [];
|
|
@@ -970,21 +1003,28 @@ export function PropertiesPanel() {
|
|
|
970
1003
|
}));
|
|
971
1004
|
}, [nativeDetails]);
|
|
972
1005
|
|
|
1006
|
+
// Overlay (authored) entities — split halves, duplicates, scripted
|
|
1007
|
+
// adds — live only in the StoreEditor overlay, NOT the parsed store.
|
|
1008
|
+
// `modelQuery.entity()` always returns a node, and its getters fall
|
|
1009
|
+
// back to the 'Unknown'/'' sentinels for ids absent from the parsed
|
|
1010
|
+
// table (entity-table.ts#getTypeName). Those non-null sentinels would
|
|
1011
|
+
// shadow the overlay record in an `entityNode ?? overlay` chain, so
|
|
1012
|
+
// when an overlay record exists it MUST take precedence.
|
|
973
1013
|
const renderedEntityType = isNativeLazySelection
|
|
974
1014
|
? (nativeDetails?.summary.type ?? 'Loading...')
|
|
975
|
-
: (
|
|
1015
|
+
: (overlayEntity?.type ?? entityNode?.type ?? 'Unknown');
|
|
976
1016
|
const renderedEntityName = isNativeLazySelection
|
|
977
1017
|
? (nativeDetails?.summary.name ?? `#${selectedEntity?.expressId ?? ''}`)
|
|
978
|
-
: (
|
|
1018
|
+
: (overlayAttr(2) ?? entityNode?.name ?? undefined);
|
|
979
1019
|
const renderedEntityGlobalId = isNativeLazySelection
|
|
980
1020
|
? (nativeDetails?.summary.globalId ?? null)
|
|
981
|
-
: (
|
|
1021
|
+
: (overlayAttr(0) ?? entityNode?.globalId);
|
|
982
1022
|
const renderedEntityDescription = isNativeLazySelection
|
|
983
1023
|
? undefined
|
|
984
|
-
: (
|
|
1024
|
+
: (overlayAttr(3) ?? entityNode?.description ?? undefined);
|
|
985
1025
|
const renderedEntityObjectType = isNativeLazySelection
|
|
986
1026
|
? undefined
|
|
987
|
-
: (
|
|
1027
|
+
: (overlayAttr(4) ?? entityNode?.objectType ?? undefined);
|
|
988
1028
|
const renderedSpatialInfo = isNativeLazySelection ? nativeSpatialInfo : spatialInfo;
|
|
989
1029
|
const renderedOccurrenceProperties = isNativeLazySelection ? nativeOccurrenceProperties : occurrenceProperties;
|
|
990
1030
|
const renderedInheritedTypeProperties = isNativeLazySelection ? [] : inheritedTypeProperties;
|
|
@@ -995,6 +1035,7 @@ export function PropertiesPanel() {
|
|
|
995
1035
|
const renderedAttributes = isNativeLazySelection ? [] : attributes;
|
|
996
1036
|
const renderedClassifications = isNativeLazySelection ? [] : classifications;
|
|
997
1037
|
const renderedMaterialInfo = isNativeLazySelection ? null : materialInfo;
|
|
1038
|
+
const renderedMaterialProperties = isNativeLazySelection ? [] : materialProperties;
|
|
998
1039
|
const renderedDocuments = isNativeLazySelection ? [] : documents;
|
|
999
1040
|
const renderedEntityRelationships = isNativeLazySelection ? null : entityRelationships;
|
|
1000
1041
|
const renderedGeoref = isNativeLazySelection ? null : georef;
|
|
@@ -1046,6 +1087,12 @@ export function PropertiesPanel() {
|
|
|
1046
1087
|
}
|
|
1047
1088
|
}
|
|
1048
1089
|
|
|
1090
|
+
// Material selected from the "Materials" hierarchy tab — show the material's
|
|
1091
|
+
// own property sets plus quantities aggregated across all using elements.
|
|
1092
|
+
if (selectedMaterialId !== null && selectedEntity) {
|
|
1093
|
+
return <MaterialTotalsPanel materialId={selectedMaterialId} modelId={selectedEntity.modelId} />;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1049
1096
|
// Multi-entity selection (unified storeys) - render combined view
|
|
1050
1097
|
if (selectedEntities.length > 1) {
|
|
1051
1098
|
return (
|
|
@@ -1474,6 +1521,7 @@ export function PropertiesPanel() {
|
|
|
1474
1521
|
{renderedMergedProperties.length === 0
|
|
1475
1522
|
&& renderedClassifications.length === 0
|
|
1476
1523
|
&& !renderedMaterialInfo
|
|
1524
|
+
&& renderedMaterialProperties.length === 0
|
|
1477
1525
|
&& renderedDocuments.length === 0
|
|
1478
1526
|
&& !renderedEntityRelationships
|
|
1479
1527
|
&& !hasScheduleForSelection ? (
|
|
@@ -1553,10 +1601,38 @@ export function PropertiesPanel() {
|
|
|
1553
1601
|
</>
|
|
1554
1602
|
)}
|
|
1555
1603
|
|
|
1604
|
+
{/* Material Property Sets (Pset_Material* attached to the
|
|
1605
|
+
IfcMaterial via IfcMaterialProperties). Grouped per material,
|
|
1606
|
+
mirroring the Type Properties block. */}
|
|
1607
|
+
{renderedMaterialProperties.length > 0 && (
|
|
1608
|
+
<>
|
|
1609
|
+
{(renderedMergedProperties.length > 0 || renderedClassifications.length > 0 || renderedMaterialInfo) && (
|
|
1610
|
+
<div className="border-t border-amber-200 dark:border-amber-800/50 pt-2 mt-2" />
|
|
1611
|
+
)}
|
|
1612
|
+
{renderedMaterialProperties.map((group) => (
|
|
1613
|
+
<div key={`matpset-${group.materialId}`} className="space-y-3">
|
|
1614
|
+
<div className="flex items-center gap-2 px-1 pb-0.5 text-[11px] text-amber-600/70 dark:text-amber-400/60 uppercase tracking-wider font-semibold">
|
|
1615
|
+
<Layers className="h-3 w-3 shrink-0" />
|
|
1616
|
+
<span className="truncate">Material Properties ({group.materialName})</span>
|
|
1617
|
+
</div>
|
|
1618
|
+
{group.psets.map((pset) => (
|
|
1619
|
+
<PropertySetCard
|
|
1620
|
+
key={`matpset-${group.materialId}-${pset.name}`}
|
|
1621
|
+
pset={{
|
|
1622
|
+
name: pset.name,
|
|
1623
|
+
properties: pset.properties.map((p) => ({ name: p.name, value: p.value, isMutated: false })),
|
|
1624
|
+
}}
|
|
1625
|
+
/>
|
|
1626
|
+
))}
|
|
1627
|
+
</div>
|
|
1628
|
+
))}
|
|
1629
|
+
</>
|
|
1630
|
+
)}
|
|
1631
|
+
|
|
1556
1632
|
{/* Documents */}
|
|
1557
1633
|
{renderedDocuments.length > 0 && (
|
|
1558
1634
|
<>
|
|
1559
|
-
{(renderedMergedProperties.length > 0 || renderedClassifications.length > 0 || renderedMaterialInfo) && (
|
|
1635
|
+
{(renderedMergedProperties.length > 0 || renderedClassifications.length > 0 || renderedMaterialInfo || renderedMaterialProperties.length > 0) && (
|
|
1560
1636
|
<div className="border-t border-zinc-200 dark:border-zinc-800 pt-2 mt-2" />
|
|
1561
1637
|
)}
|
|
1562
1638
|
{renderedDocuments.map((doc, i) => (
|
|
@@ -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
|
/>
|