@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.
Files changed (150) hide show
  1. package/.turbo/turbo-build.log +45 -38
  2. package/CHANGELOG.md +93 -0
  3. package/dist/assets/{basketViewActivator-ZpTYWE3K.js → basketViewActivator-BNRDNuUJ.js} +9 -9
  4. package/dist/assets/{bcf-Ctcu_Sc2.js → bcf-DCwCuP7n.js} +56 -56
  5. package/dist/assets/{browser-DXS29_v9.js → browser-BIoDDfBW.js} +1 -1
  6. package/dist/assets/{cesium-BoVuJvTC.js → cesium-CzZn5yVA.js} +319 -319
  7. package/dist/assets/{decode-worker-CgM1iNSK.js → decode-worker-Cjign7Zh.js} +1 -1
  8. package/dist/assets/deflate-DNGgs8Ur.js +1 -0
  9. package/dist/assets/drawing-2d-D0dDf6Lh.js +257 -0
  10. package/dist/assets/e57-source-2wI9jkCA.js +1 -0
  11. package/dist/assets/exceljs.min-DsuzKYnj.js +29 -0
  12. package/dist/assets/{exporters-DSq76AVM.js → exporters-B9v81gi9.js} +1861 -1524
  13. package/dist/assets/geometry.worker-Bpa3115V.js +1 -0
  14. package/dist/assets/{geotiff-A5UjhI6L.js → geotiff-D-YCLS4g.js} +10 -10
  15. package/dist/assets/html2canvas.esm-Ge7aVWlp.js +5 -0
  16. package/dist/assets/{ids-DiLcGTer.js → ids-CCpq-5d3.js} +952 -945
  17. package/dist/assets/ifc-lite_bg-DbgS5EUA.wasm +0 -0
  18. package/dist/assets/{index-BAH8IJVR.js → index-Bgb3_Pu_.js} +47682 -42474
  19. package/dist/assets/index-BtbXFKsX.css +1 -0
  20. package/dist/assets/index.es-CWfqZyyr.js +6866 -0
  21. package/dist/assets/{jpeg-BzSkwo5D.js → jpeg-DGOAeUqU.js} +1 -1
  22. package/dist/assets/jspdf.es.min-XPLU2Wkq.js +19571 -0
  23. package/dist/assets/jspdf.plugin.autotable-BBLUVd7n.js +2 -0
  24. package/dist/assets/lens-C4p1kQ0p.js +1 -0
  25. package/dist/assets/{lerc-Cg2Rz-D5.js → lerc-1PMSCHwX.js} +1 -1
  26. package/dist/assets/{lzw-BBPPLW-0.js → lzw-C65U9lNM.js} +1 -1
  27. package/dist/assets/{maplibre-gl-Do6O5tDc.js → maplibre-gl-BF3Z0idw.js} +1 -1
  28. package/dist/assets/{native-bridge-CPojOeGE.js → native-bridge-XxXos6yI.js} +2 -2
  29. package/dist/assets/{packbits-yLSpjW-V.js → packbits-BdMWXC3m.js} +1 -1
  30. package/dist/assets/{pako.esm-Cram60i4.js → pako.esm-n3Pgozwg.js} +1 -1
  31. package/dist/assets/parser.worker-Ddwo3_06.js +182 -0
  32. package/dist/assets/pdf-CRwaZf3s.js +135 -0
  33. package/dist/assets/raw-CJgQdyuZ.js +1 -0
  34. package/dist/assets/{sandbox-CsRXlgCO.js → sandbox-0sDo3g3m.js} +3037 -2554
  35. package/dist/assets/server-client-cTCJ-853.js +719 -0
  36. package/dist/assets/{webimage-YafxjjGr.js → webimage-BtakWX7W.js} +1 -1
  37. package/dist/assets/xlsx-B1YOg2QB.js +142 -0
  38. package/dist/assets/{zip-BJqVbRkU.js → zip-DFgP-l20.js} +1 -1
  39. package/dist/assets/{zstd-CkSLOiuu.js → zstd-CmwsbxmM.js} +1 -1
  40. package/dist/index.html +10 -10
  41. package/package.json +27 -23
  42. package/src/components/mcp/PlaygroundChat.tsx +1 -0
  43. package/src/components/mcp/data.ts +6 -0
  44. package/src/components/mcp/playground-dispatcher.ts +280 -0
  45. package/src/components/mcp/playground-files.ts +33 -1
  46. package/src/components/mcp/types.ts +2 -1
  47. package/src/components/ui/combo-input.tsx +163 -0
  48. package/src/components/ui/tabs.tsx +1 -1
  49. package/src/components/viewer/CommandPalette.tsx +6 -1
  50. package/src/components/viewer/ComparePanel.tsx +420 -0
  51. package/src/components/viewer/HierarchyPanel.tsx +46 -7
  52. package/src/components/viewer/MainToolbar.tsx +19 -2
  53. package/src/components/viewer/PropertiesPanel.tsx +84 -8
  54. package/src/components/viewer/SearchInline.tsx +62 -2
  55. package/src/components/viewer/SearchModal.filter.builder.tsx +24 -393
  56. package/src/components/viewer/SearchModal.filter.editors.tsx +503 -0
  57. package/src/components/viewer/SearchModal.filter.tsx +64 -1
  58. package/src/components/viewer/SearchModal.tsx +19 -6
  59. package/src/components/viewer/ViewerLayout.tsx +5 -0
  60. package/src/components/viewer/Viewport.tsx +18 -0
  61. package/src/components/viewer/hierarchy/HierarchyNode.tsx +3 -3
  62. package/src/components/viewer/hierarchy/ifc-icons.ts +9 -0
  63. package/src/components/viewer/hierarchy/treeDataBuilder.ts +87 -0
  64. package/src/components/viewer/hierarchy/types.ts +1 -0
  65. package/src/components/viewer/hierarchy/useHierarchyTree.ts +6 -2
  66. package/src/components/viewer/lists/ColumnHeaderMenu.tsx +84 -0
  67. package/src/components/viewer/lists/ListBuilder.tsx +789 -280
  68. package/src/components/viewer/lists/ListGroupingBar.tsx +72 -0
  69. package/src/components/viewer/lists/ListPanel.tsx +49 -5
  70. package/src/components/viewer/lists/ListResultsTable.tsx +270 -176
  71. package/src/components/viewer/lists/list-table-utils.ts +123 -0
  72. package/src/components/viewer/properties/MaterialTotalsPanel.tsx +283 -0
  73. package/src/generated/mcp-catalog.json +4 -0
  74. package/src/hooks/federationLoadGate.test.ts +12 -2
  75. package/src/hooks/federationLoadGate.ts +9 -2
  76. package/src/hooks/ingest/federationAlign.ts +481 -0
  77. package/src/hooks/ingest/viewerModelIngest.ts +3 -212
  78. package/src/hooks/source-key.ts +35 -0
  79. package/src/hooks/useAlignmentLines3D.ts +1 -26
  80. package/src/hooks/useCompare.ts +0 -0
  81. package/src/hooks/useCompareOverlay.ts +119 -0
  82. package/src/hooks/useDrawingGeneration.ts +23 -1
  83. package/src/hooks/useGridLines3D.ts +140 -0
  84. package/src/hooks/useIfc.ts +1 -1
  85. package/src/hooks/useIfcCache.ts +32 -9
  86. package/src/hooks/useIfcFederation.ts +42 -810
  87. package/src/hooks/useIfcLoader.ts +361 -488
  88. package/src/hooks/useIfcServer.ts +3 -0
  89. package/src/hooks/useLens.ts +5 -1
  90. package/src/hooks/useSymbolicAnnotations.ts +70 -38
  91. package/src/lib/compare/buildFingerprints.ts +173 -0
  92. package/src/lib/compare/describeChange.ts +0 -0
  93. package/src/lib/compare/geometricData.test.ts +54 -0
  94. package/src/lib/compare/geometricData.ts +37 -0
  95. package/src/lib/compare/overlay.test.ts +99 -0
  96. package/src/lib/compare/overlay.ts +91 -0
  97. package/src/lib/geo/cesium-placement.ts +1 -1
  98. package/src/lib/geo/reproject.ts +4 -1
  99. package/src/lib/length-unit-scale.ts +41 -0
  100. package/src/lib/lists/adapter.ts +136 -11
  101. package/src/lib/lists/export/csv.ts +47 -0
  102. package/src/lib/lists/export/index.ts +49 -0
  103. package/src/lib/lists/export/model.ts +111 -0
  104. package/src/lib/lists/export/pdf.ts +67 -0
  105. package/src/lib/lists/export/xlsx.ts +83 -0
  106. package/src/lib/lists/index.ts +2 -0
  107. package/src/lib/llm/script-edit-ops.ts +23 -0
  108. package/src/lib/llm/stream-client.ts +8 -1
  109. package/src/lib/search/filter-evaluate.test.ts +81 -0
  110. package/src/lib/search/filter-evaluate.ts +59 -87
  111. package/src/lib/search/filter-match.ts +167 -0
  112. package/src/lib/search/filter-rules.test.ts +25 -0
  113. package/src/lib/search/filter-rules.ts +75 -2
  114. package/src/lib/search/filter-schema.ts +0 -0
  115. package/src/lib/search/result-export.ts +7 -1
  116. package/src/lib/slab-edit.test.ts +72 -0
  117. package/src/lib/slab-edit.ts +159 -19
  118. package/src/sdk/adapters/export-adapter.ts +9 -4
  119. package/src/sdk/adapters/query-adapter.ts +3 -3
  120. package/src/store/globalId.ts +15 -13
  121. package/src/store/index.ts +16 -1
  122. package/src/store/slices/cesiumSlice.ts +8 -1
  123. package/src/store/slices/compareSlice.ts +96 -0
  124. package/src/store/slices/lensSlice.ts +8 -0
  125. package/src/store/slices/listSlice.ts +6 -0
  126. package/src/store/slices/mutationSlice.ts +14 -6
  127. package/src/store/slices/searchSlice.ts +29 -3
  128. package/src/utils/acquireFileBuffer.test.ts +12 -4
  129. package/src/utils/desktopModelSnapshot.ts +2 -1
  130. package/src/utils/loadingUtils.ts +32 -0
  131. package/src/utils/nativeSpatialDataStore.ts +6 -0
  132. package/src/utils/serverDataModel.test.ts +6 -0
  133. package/src/utils/serverDataModel.ts +7 -0
  134. package/src/utils/spatialHierarchy.test.ts +53 -1
  135. package/src/utils/spatialHierarchy.ts +42 -2
  136. package/src/vite-env.d.ts +2 -0
  137. package/dist/assets/deflate-Cnx0il6E.js +0 -1
  138. package/dist/assets/drawing-2d-C71b8Ugx.js +0 -257
  139. package/dist/assets/e57-source-CQHxE8n3.js +0 -1
  140. package/dist/assets/geometry.worker-0Q9qEa6p.js +0 -1
  141. package/dist/assets/ifc-lite_bg-CEZnhM2e.wasm +0 -0
  142. package/dist/assets/index-B9Ug2EqU.css +0 -1
  143. package/dist/assets/lens-PYsLu_MA.js +0 -1
  144. package/dist/assets/parser.worker-8md211IW.js +0 -182
  145. package/dist/assets/raw-BQrAgxwT.js +0 -1
  146. package/dist/assets/server-client-Bk4c1CPO.js +0 -626
  147. package/src/hooks/ingest/resolveDataStoreOrAbort.test.ts +0 -61
  148. package/src/hooks/ingest/resolveDataStoreOrAbort.ts +0 -28
  149. package/src/hooks/ingest/watchedGeometryStream.test.ts +0 -78
  150. 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
- : (entityNode?.type ?? overlayEntity?.type ?? 'Unknown');
1015
+ : (overlayEntity?.type ?? entityNode?.type ?? 'Unknown');
976
1016
  const renderedEntityName = isNativeLazySelection
977
1017
  ? (nativeDetails?.summary.name ?? `#${selectedEntity?.expressId ?? ''}`)
978
- : (entityNode?.name ?? overlayAttr(2) ?? undefined);
1018
+ : (overlayAttr(2) ?? entityNode?.name ?? undefined);
979
1019
  const renderedEntityGlobalId = isNativeLazySelection
980
1020
  ? (nativeDetails?.summary.globalId ?? null)
981
- : (entityNode?.globalId ?? overlayAttr(0));
1021
+ : (overlayAttr(0) ?? entityNode?.globalId);
982
1022
  const renderedEntityDescription = isNativeLazySelection
983
1023
  ? undefined
984
- : (entityNode?.description ?? overlayAttr(3) ?? undefined);
1024
+ : (overlayAttr(3) ?? entityNode?.description ?? undefined);
985
1025
  const renderedEntityObjectType = isNativeLazySelection
986
1026
  ? undefined
987
- : (entityNode?.objectType ?? overlayAttr(4) ?? undefined);
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
  />