@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
@@ -46,6 +46,7 @@ import {
46
46
  type SectionClipForGrid,
47
47
  } from '../../hooks/useSymbolicAnnotations.js';
48
48
  import { useAlignmentLines3D } from '../../hooks/useAlignmentLines3D.js';
49
+ import { useGridLines3D } from '../../hooks/useGridLines3D.js';
49
50
 
50
51
  interface ViewportProps {
51
52
  geometry: MeshData[] | null;
@@ -807,6 +808,9 @@ export function Viewport({
807
808
  }
808
809
  setIsInitialized(false);
809
810
  rendererRef.current = null;
811
+ // Free all WebGPU resources held by this renderer instance.
812
+ // destroy() is idempotent, so this is safe even if init() rejected.
813
+ renderer.destroy();
810
814
  // Clear BCF global refs to prevent memory leaks
811
815
  clearGlobalRefs();
812
816
  };
@@ -901,6 +905,20 @@ export function Viewport({
901
905
  }
902
906
  }, [alignmentVertices3D, isInitialized]);
903
907
 
908
+ // Structural-grid (IfcGridAxis) lines, gated by the `ifcGrid` type-visibility
909
+ // toggle (issue #967). Parsed once per source + cached; only the upload/clear
910
+ // is toggled so flipping visibility doesn't re-parse.
911
+ const gridVertices3D = useGridLines3D();
912
+ useEffect(() => {
913
+ const renderer = rendererRef.current;
914
+ if (!renderer || !isInitialized) return;
915
+ if (!ifcGridVisible || gridVertices3D.length === 0) {
916
+ renderer.clearGridLines3D();
917
+ } else {
918
+ renderer.uploadGridLines3D(gridVertices3D);
919
+ }
920
+ }, [gridVertices3D, ifcGridVisible, isInitialized]);
921
+
904
922
  // Upload IfcAnnotation text + fill data for the WebGPU symbolic overlay
905
923
  // pipelines. Map the hook's per-annotation records into the SymbolicFillInput
906
924
  // / SymbolicTextInput shape the renderer expects. Empty arrays clear cleanly.
@@ -275,7 +275,7 @@ export function HierarchyNode({
275
275
  {/* Name */}
276
276
  <span className={cn(
277
277
  'flex-1 text-sm truncate ml-1.5',
278
- isSpatialContainer(node.type) || node.type === 'IfcBuildingStorey' || node.type === 'IfcSpace' || node.type === 'unified-storey' || node.type === 'type-group'
278
+ isSpatialContainer(node.type) || node.type === 'IfcBuildingStorey' || node.type === 'IfcSpace' || node.type === 'unified-storey' || node.type === 'type-group' || node.type === 'material-group'
279
279
  ? 'font-medium text-zinc-900 dark:text-zinc-100'
280
280
  : 'text-zinc-700 dark:text-zinc-300',
281
281
  nodeHidden && 'line-through decoration-zinc-400 dark:decoration-zinc-600'
@@ -306,11 +306,11 @@ export function HierarchyNode({
306
306
  <Tooltip>
307
307
  <TooltipTrigger asChild>
308
308
  <span className="text-[10px] font-mono bg-zinc-100 dark:bg-zinc-950 px-1.5 py-0.5 border border-zinc-200 dark:border-zinc-800 text-zinc-500 dark:text-zinc-400 rounded-none">
309
- {node.elementCount}
309
+ {node.elementCount.toLocaleString()}
310
310
  </span>
311
311
  </TooltipTrigger>
312
312
  <TooltipContent>
313
- <p className="text-xs">{node.elementCount} {node.elementCount === 1 ? 'element' : 'elements'}</p>
313
+ <p className="text-xs">{node.elementCount.toLocaleString()} {node.elementCount === 1 ? 'element' : 'elements'}</p>
314
314
  </TooltipContent>
315
315
  </Tooltip>
316
316
  )}
@@ -140,6 +140,15 @@ export const IFC_ICON_CODEPOINTS: Record<string, string> = {
140
140
  IfcTrackElement: '\ue260', // "linear_scale"
141
141
  IfcVehicle: '\ue531', // "directions_car"
142
142
 
143
+ // Materials (Materials hierarchy tab)
144
+ IfcMaterial: '\ue4f4', // "texture"
145
+ IfcMaterialLayerSet: '\ue8fe', // "layers"
146
+ IfcMaterialLayerSetUsage: '\ue8fe',
147
+ IfcMaterialProfileSet: '\ue8fe',
148
+ IfcMaterialProfileSetUsage: '\ue8fe',
149
+ IfcMaterialConstituentSet: '\ue4f4',
150
+ IfcMaterialList: '\ue4f4',
151
+
143
152
  // Proxy / generic fallback
144
153
  IfcProduct: '\ue047',
145
154
  IfcBuildingElementProxy: '\ue047',
@@ -12,6 +12,7 @@ import {
12
12
  type SpatialNode,
13
13
  } from '@ifc-lite/data';
14
14
  import type { IfcDataStore } from '@ifc-lite/parser';
15
+ import { buildMaterialUsageIndex } from '@ifc-lite/parser';
15
16
  import { useViewerStore, type FederatedModel } from '@/store';
16
17
  import { toGlobalIdFromModels } from '@/store/globalId';
17
18
  import type { TreeNode, NodeType, StoreyData, UnifiedStorey } from './types';
@@ -712,6 +713,92 @@ export function buildIfcTypeTree(
712
713
  return nodes;
713
714
  }
714
715
 
716
+ /**
717
+ * Build a flat "By Material" tree: one row per base material (IfcMaterial),
718
+ * grouped by name so the same-named material across federated models merges.
719
+ * Each row carries the using elements' global ids for click-to-isolate and the
720
+ * representative material express id for the properties panel. Mirrors
721
+ * {@link buildIfcTypeTree} but keyed on the parser's material usage index.
722
+ */
723
+ export function buildMaterialTree(
724
+ models: Map<string, FederatedModel>,
725
+ ifcDataStore: IfcDataStore | null | undefined,
726
+ _expandedNodes: Set<string>,
727
+ _isMultiModel: boolean,
728
+ geometricIds?: Set<number>,
729
+ ): TreeNode[] {
730
+ interface MatEntry {
731
+ name: string;
732
+ ifcClass: string;
733
+ materialId: number; // representative material express id
734
+ modelIds: Set<string>; // contributing models (insertion order)
735
+ elements: Map<number, number>; // globalId -> expressId (deduped)
736
+ }
737
+
738
+ const byName = new Map<string, MatEntry>();
739
+ const applyGeomFilter = !!geometricIds && geometricIds.size > 0;
740
+
741
+ const processDataStore = (dataStore: IfcDataStore, modelId: string) => {
742
+ const usage = buildMaterialUsageIndex(dataStore);
743
+ for (const u of usage.values()) {
744
+ let entry = byName.get(u.name);
745
+ if (!entry) {
746
+ // Invariant: the representative `materialId` and the first entry in
747
+ // `modelIds` come from the SAME (first-contributing) model, so the click
748
+ // handler's `node.modelIds[0]` + `node.entityExpressId` always resolve a
749
+ // valid (model, material) pair. Sets preserve insertion order.
750
+ entry = {
751
+ name: u.name,
752
+ ifcClass: u.ifcClass,
753
+ materialId: u.id,
754
+ modelIds: new Set([modelId]),
755
+ elements: new Map(),
756
+ };
757
+ byName.set(u.name, entry);
758
+ } else {
759
+ entry.modelIds.add(modelId);
760
+ }
761
+ for (const { entityId } of u.entries) {
762
+ const globalId = resolveTreeGlobalId(modelId, entityId, models);
763
+ if (applyGeomFilter && !geometricIds!.has(globalId)) continue;
764
+ entry.elements.set(globalId, entityId);
765
+ }
766
+ }
767
+ };
768
+
769
+ if (models.size > 0) {
770
+ for (const [modelId, model] of models) {
771
+ if (model.ifcDataStore) processDataStore(model.ifcDataStore, modelId);
772
+ }
773
+ } else if (ifcDataStore) {
774
+ processDataStore(ifcDataStore, 'legacy');
775
+ }
776
+
777
+ const nodes: TreeNode[] = [];
778
+ const names = Array.from(byName.keys()).sort((a, b) => a.localeCompare(b));
779
+ for (const name of names) {
780
+ const entry = byName.get(name)!;
781
+ if (entry.elements.size === 0) continue; // skip materials with no visible elements (dead clicks)
782
+ nodes.push({
783
+ id: `material-${name}`,
784
+ expressIds: Array.from(entry.elements.values()),
785
+ globalIds: Array.from(entry.elements.keys()),
786
+ entityExpressId: entry.materialId,
787
+ modelIds: Array.from(entry.modelIds),
788
+ name,
789
+ type: 'material-group',
790
+ ifcType: entry.ifcClass,
791
+ depth: 0,
792
+ hasChildren: false,
793
+ isExpanded: false,
794
+ isVisible: true,
795
+ elementCount: entry.elements.size,
796
+ });
797
+ }
798
+
799
+ return nodes;
800
+ }
801
+
715
802
  /** Filter nodes based on search query */
716
803
  export function filterNodes(nodes: TreeNode[], searchQuery: string): TreeNode[] {
717
804
  if (!searchQuery.trim()) return nodes;
@@ -22,6 +22,7 @@ export type NodeType =
22
22
  | 'IfcSpace' // Space node
23
23
  | 'type-group' // IFC class grouping header (e.g., "IfcWall (47)")
24
24
  | 'ifc-type' // IFC type entity node (e.g., "IfcWallType/W01")
25
+ | 'material-group' // Material grouping (e.g., "Concrete (47)") from the Materials tab
25
26
  | 'element'; // Individual element
26
27
 
27
28
  export interface TreeNode {
@@ -13,11 +13,12 @@ import {
13
13
  buildTreeData,
14
14
  buildTypeTree,
15
15
  buildIfcTypeTree,
16
+ buildMaterialTree,
16
17
  filterNodes,
17
18
  splitNodes,
18
19
  } from './treeDataBuilder';
19
20
 
20
- export type GroupingMode = 'spatial' | 'type' | 'ifc-type';
21
+ export type GroupingMode = 'spatial' | 'type' | 'ifc-type' | 'material';
21
22
 
22
23
  interface UseHierarchyTreeParams {
23
24
  models: Map<string, FederatedModel>;
@@ -192,6 +193,9 @@ export function useHierarchyTree({ models, ifcDataStore, isMultiModel, geometryR
192
193
  if (groupingMode === 'ifc-type') {
193
194
  return buildIfcTypeTree(models, ifcDataStore, expandedNodes, isMultiModel, geometricIds);
194
195
  }
196
+ if (groupingMode === 'material') {
197
+ return buildMaterialTree(models, ifcDataStore, expandedNodes, isMultiModel, geometricIds);
198
+ }
195
199
  return buildTreeData(models, ifcDataStore, expandedNodes, isMultiModel, unifiedStoreys);
196
200
  },
197
201
  [models, ifcDataStore, expandedNodes, isMultiModel, unifiedStoreys, groupingMode, geometricIds]
@@ -223,7 +227,7 @@ export function useHierarchyTree({ models, ifcDataStore, isMultiModel, geometryR
223
227
 
224
228
  // Get all elements for a node (handles type groups, ifc-type, unified storeys, single storeys, model contributions, and elements)
225
229
  const getNodeElements = useCallback((node: TreeNode): number[] => {
226
- if (node.type === 'type-group' || node.type === 'ifc-type') {
230
+ if (node.type === 'type-group' || node.type === 'ifc-type' || node.type === 'material-group') {
227
231
  // GlobalIds are pre-stored on the node during tree construction — O(1)
228
232
  return node.globalIds;
229
233
  }
@@ -0,0 +1,84 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+
5
+ /**
6
+ * Per-column actions menu for the Lists results table header. Brings
7
+ * grouping / aggregation / sorting onto the table itself so the user never
8
+ * has to round-trip through the list settings.
9
+ */
10
+
11
+ import { ArrowUp, ArrowDown, Group, Ungroup, Sigma, Palette, MoreVertical } from 'lucide-react';
12
+ import {
13
+ DropdownMenu,
14
+ DropdownMenuTrigger,
15
+ DropdownMenuContent,
16
+ DropdownMenuItem,
17
+ DropdownMenuCheckboxItem,
18
+ DropdownMenuSeparator,
19
+ } from '@/components/ui/dropdown-menu';
20
+ import { cn } from '@/lib/utils';
21
+
22
+ interface ColumnHeaderMenuProps {
23
+ isNumeric: boolean;
24
+ isGroupedBy: boolean;
25
+ isSummed: boolean;
26
+ active: boolean;
27
+ onSort: (dir: 'asc' | 'desc') => void;
28
+ onToggleGroup: () => void;
29
+ onToggleSum: () => void;
30
+ onColorBy: () => void;
31
+ }
32
+
33
+ export function ColumnHeaderMenu({
34
+ isNumeric, isGroupedBy, isSummed, active,
35
+ onSort, onToggleGroup, onToggleSum, onColorBy,
36
+ }: ColumnHeaderMenuProps) {
37
+ return (
38
+ <DropdownMenu>
39
+ <DropdownMenuTrigger asChild>
40
+ <button
41
+ aria-label="Column options"
42
+ onClick={(e) => e.stopPropagation()}
43
+ className={cn(
44
+ 'shrink-0 rounded-sm p-0.5 transition-opacity hover:text-foreground',
45
+ active
46
+ ? 'text-primary opacity-100'
47
+ : 'text-muted-foreground opacity-0 group-hover/col:opacity-100 data-[state=open]:opacity-100',
48
+ )}
49
+ >
50
+ <MoreVertical className="h-3 w-3" />
51
+ </button>
52
+ </DropdownMenuTrigger>
53
+ <DropdownMenuContent align="start" className="w-52">
54
+ <DropdownMenuItem className="gap-2 text-xs" onClick={() => onSort('asc')}>
55
+ <ArrowUp className="h-3.5 w-3.5" /> Sort ascending
56
+ </DropdownMenuItem>
57
+ <DropdownMenuItem className="gap-2 text-xs" onClick={() => onSort('desc')}>
58
+ <ArrowDown className="h-3.5 w-3.5" /> Sort descending
59
+ </DropdownMenuItem>
60
+ <DropdownMenuSeparator />
61
+ <DropdownMenuItem className="gap-2 text-xs" onClick={onToggleGroup}>
62
+ {isGroupedBy
63
+ ? (<><Ungroup className="h-3.5 w-3.5" /> Remove grouping</>)
64
+ : (<><Group className="h-3.5 w-3.5" /> Group by this column</>)}
65
+ </DropdownMenuItem>
66
+ <DropdownMenuCheckboxItem
67
+ className="text-xs"
68
+ checked={isSummed}
69
+ disabled={!isNumeric}
70
+ onCheckedChange={onToggleSum}
71
+ >
72
+ <span className="flex items-center gap-2">
73
+ <Sigma className="h-3.5 w-3.5" />
74
+ {isNumeric ? 'Sum / total this column' : 'Sum (numeric only)'}
75
+ </span>
76
+ </DropdownMenuCheckboxItem>
77
+ <DropdownMenuSeparator />
78
+ <DropdownMenuItem className="gap-2 text-xs" onClick={onColorBy}>
79
+ <Palette className="h-3.5 w-3.5" /> Colour by this column
80
+ </DropdownMenuItem>
81
+ </DropdownMenuContent>
82
+ </DropdownMenu>
83
+ );
84
+ }