@wallarm-org/design-system 1.8.0 → 1.10.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.
@@ -12,9 +12,14 @@ const DropdownMenuContent = ({ className, children, ref, ...props })=>{
12
12
  const testId = useTestId('content');
13
13
  const cascadeBase = useTestId();
14
14
  const childArray = Children.toArray(children);
15
- const inputChildren = childArray.filter((child)=>/*#__PURE__*/ isValidElement(child) && child.type === DropdownMenuInput);
15
+ const isInputChild = (child)=>{
16
+ if (!/*#__PURE__*/ isValidElement(child)) return false;
17
+ if (child.type === DropdownMenuInput) return true;
18
+ return 'function' == typeof child.type && 'input' === child.type.dropdownMenuSlot;
19
+ };
20
+ const inputChildren = childArray.filter(isInputChild);
16
21
  const footerChildren = childArray.filter((child)=>/*#__PURE__*/ isValidElement(child) && child.type === DropdownMenuFooter);
17
- const menuChildren = childArray.filter((child)=>!/*#__PURE__*/ (isValidElement(child) && (child.type === DropdownMenuFooter || child.type === DropdownMenuInput)));
22
+ const menuChildren = childArray.filter((child)=>!isInputChild(child) && !/*#__PURE__*/ (isValidElement(child) && child.type === DropdownMenuFooter));
18
23
  return /*#__PURE__*/ jsx(Portal, {
19
24
  children: /*#__PURE__*/ jsx(Menu.Positioner, {
20
25
  children: /*#__PURE__*/ jsxs(Menu.Content, {
@@ -21,9 +21,11 @@ export interface FilterInputChipProps extends Omit<HTMLAttributes<HTMLDivElement
21
21
  /** When true, the chip cannot be edited or removed (dimmed appearance) */
22
22
  disabled?: boolean;
23
23
  /** Caps the applied value segment at this width (px), truncating with an
24
- * ellipsis; lifts the chip's default max-width so it grows to fit (AS-1064). */
24
+ * ellipsis; lifts the chip's default max-width so it grows to fit. Defaults
25
+ * to 500; a paired chip caps its whole width there instead (AS-1064). */
25
26
  valueMaxWidth?: number;
26
- /** `'end'` places the caret at the end on value inline-edit; omit to select all (AS-1064). */
27
+ /** Caret placement on value inline-edit: `'end'` (default) puts it at the
28
+ * tail — better for long values; pass explicitly to keep the API visible (AS-1064). */
27
29
  caretMode?: 'end';
28
30
  /** Second paired triplet (two-step fields). The paired attribute is fixed. */
29
31
  pair?: FilterInputChipData['pair'];
@@ -9,7 +9,7 @@ import { FilterInputRemoveButton } from "./FilterInputRemoveButton.js";
9
9
  import { PairSeparator } from "./PairSeparator.js";
10
10
  import { Segment } from "./Segment.js";
11
11
  import { SEGMENT_VARIANT } from "./segmentVariant.js";
12
- const FilterInputChip = ({ ref, chipId, attribute, attributeDescription, operator, value, error = false, valueParts, valueSeparator, errorValueIndices, building = false, disabled = false, valueMaxWidth, caretMode, pair, onRemove, onSegmentClick, onPairSegmentClick, className, style, ...props })=>{
12
+ const FilterInputChip = ({ ref, chipId, attribute, attributeDescription, operator, value, error = false, valueParts, valueSeparator, errorValueIndices, building = false, disabled = false, valueMaxWidth = 500, caretMode = 'end', pair, onRemove, onSegmentClick, onPairSegmentClick, className, style, ...props })=>{
13
13
  const interactive = !disabled;
14
14
  const internalRef = useRef(null);
15
15
  const editing = useEditingContext();
@@ -74,11 +74,14 @@ const FilterInputChip = ({ ref, chipId, attribute, attributeDescription, operato
74
74
  interactive,
75
75
  disabled,
76
76
  building
77
- }), pair ? 'max-w-[380px]' : 'max-w-[320px]', className),
78
- style: valueMaxWidth && !pair ? {
77
+ }), pair ? void 0 : 'max-w-[320px]', className),
78
+ style: pair ? {
79
+ maxWidth: valueMaxWidth,
80
+ ...style
81
+ } : {
79
82
  maxWidth: 'none',
80
83
  ...style
81
- } : style,
84
+ },
82
85
  "data-slot": "filter-input-condition-chip",
83
86
  ...building && {
84
87
  'data-building': ''
@@ -144,6 +147,8 @@ const FilterInputChip = ({ ref, chipId, attribute, attributeDescription, operato
144
147
  variant: SEGMENT_VARIANT.value,
145
148
  className: pair.value ? 'min-w-0' : emptyValueHitTarget,
146
149
  error: pairActiveSegment !== SEGMENT_VARIANT.value && (true === effectivePairError || effectivePairError === SEGMENT_VARIANT.value),
150
+ caretMode: caretMode,
151
+ valueMaxWidth: valueMaxWidth,
147
152
  onClick: interactive ? (e)=>handlePairSegmentClick('value', e) : void 0,
148
153
  ...segmentEditProps(SEGMENT_VARIANT.value, 1),
149
154
  children: pair.value ?? ''
@@ -15,7 +15,7 @@ const toTanStackPinning = (p, pinnedLeft)=>({
15
15
  end: p?.right ?? []
16
16
  });
17
17
  const TableProvider = (props)=>{
18
- const { data, columns, isLoading = false, isLoadingPrevious = false, skeletonCount = TABLE_SKELETON_ROWS, children, getRowId, sorting: sortingProp, onSortingChange, manualSorting = false, rowSelection: rowSelectionProp, onRowSelectionChange, columnSizing: columnSizingProp, onColumnSizingChange, columnPinning: columnPinningProp, onColumnPinningChange, columnOrder: columnOrderProp, onColumnOrderChange, grouping: groupingProp, onGroupingChange, expanded: expandedProp, onExpandedChange, renderGroupRow, getSubRows, renderExpandedRow, columnVisibility: columnVisibilityProp, onColumnVisibilityChange, defaultColumnVisibility, defaultColumnOrder, virtualized, estimateRowHeight, overscan = TABLE_VIRTUALIZATION_OVERSCAN, onEndReached, onEndReachedThreshold, onStartReached, onStartReachedThreshold, initialScrollToRowId, onMasterCellClick, activeRowId: activeRowIdProp, onSettingsOpenChange } = props;
18
+ const { data, columns, isLoading = false, isLoadingPrevious = false, skeletonCount = TABLE_SKELETON_ROWS, children, getRowId, sorting: sortingProp, onSortingChange, manualSorting = false, rowSelection: rowSelectionProp, onRowSelectionChange, columnSizing: columnSizingProp, onColumnSizingChange, columnPinning: columnPinningProp, onColumnPinningChange, columnOrder: columnOrderProp, onColumnOrderChange, grouping: groupingProp, onGroupingChange, expanded: expandedProp, onExpandedChange, renderGroupRow, getSubRows, renderExpandedRow, columnVisibility: columnVisibilityProp, onColumnVisibilityChange, defaultColumnVisibility, defaultColumnOrder, columnGroups, virtualized, estimateRowHeight, overscan = TABLE_VIRTUALIZATION_OVERSCAN, onEndReached, onEndReachedThreshold, onStartReached, onStartReachedThreshold, initialScrollToRowId, onMasterCellClick, activeRowId: activeRowIdProp, onSettingsOpenChange } = props;
19
19
  const masterCellActiveRowId = activeRowIdProp ?? null;
20
20
  const sortingEnabled = !!onSortingChange;
21
21
  const selectionEnabled = !!onRowSelectionChange;
@@ -201,6 +201,7 @@ const TableProvider = (props)=>{
201
201
  allLeafColumns,
202
202
  defaultColumnVisibility,
203
203
  defaultColumnOrder,
204
+ columnGroups,
204
205
  setColumnOrder,
205
206
  alwaysPinnedLeft,
206
207
  masterColumnId,
@@ -236,6 +237,7 @@ const TableProvider = (props)=>{
236
237
  allLeafColumns,
237
238
  defaultColumnVisibility,
238
239
  defaultColumnOrder,
240
+ columnGroups,
239
241
  setColumnOrder,
240
242
  alwaysPinnedLeft,
241
243
  masterColumnId,
@@ -2,7 +2,7 @@ import type { ReactNode, RefObject } from 'react';
2
2
  import type { Column, ColumnVisibilityState, ReactTable, Row, RowData } from '@tanstack/react-table';
3
3
  import type { Virtualizer } from '@tanstack/react-virtual';
4
4
  import type { DSTableFeatures } from '../lib';
5
- import type { TableProps, TableVirtualized } from '../types';
5
+ import type { TableColumnGroup, TableProps, TableVirtualized } from '../types';
6
6
  /**
7
7
  * Union covers both virtualizer flavors used by the Table (`useVirtualizer`
8
8
  * with HTMLElement scroll container and `useWindowVirtualizer` with Window).
@@ -29,6 +29,7 @@ export interface TableContextValue<T extends RowData> {
29
29
  allLeafColumns: Column<DSTableFeatures, T, unknown>[];
30
30
  defaultColumnVisibility?: ColumnVisibilityState;
31
31
  defaultColumnOrder?: string[];
32
+ columnGroups?: TableColumnGroup[];
32
33
  setColumnOrder: (order: string[]) => void;
33
34
  lastSelectedRowIndexRef: RefObject<number | null>;
34
35
  alwaysPinnedLeft: string[];
@@ -1,4 +1,4 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
2
  import { cloneElement, useLayoutEffect, useMemo, useRef, useState } from "react";
3
3
  import { createPortal } from "react-dom";
4
4
  import { DndContext, KeyboardSensor, PointerSensor, closestCenter, useSensor, useSensors } from "@dnd-kit/core";
@@ -24,7 +24,7 @@ const TableSettingsMenu = ({ 'data-testid': testIdProp, children, ...rest })=>{
24
24
  const testId = useTestId('settings-menu', testIdProp);
25
25
  const { anchorNode } = useTableSettingsMenuContext();
26
26
  const ctx = useTableContext();
27
- const { table, alwaysPinnedLeft, masterColumnId, onSettingsOpenChange } = ctx;
27
+ const { table, alwaysPinnedLeft, masterColumnId, columnGroups, onSettingsOpenChange } = ctx;
28
28
  const hasTextDescription = table.getAllLeafColumns().some((col)=>col.columnDef.meta?.description?.type === 'text');
29
29
  const [search, setSearch] = useState('');
30
30
  const [menuOpen, setMenuOpen] = useState(false);
@@ -59,6 +59,30 @@ const TableSettingsMenu = ({ 'data-testid': testIdProp, children, ...rest })=>{
59
59
  allColumns,
60
60
  search
61
61
  ]);
62
+ const labeledSections = useMemo(()=>{
63
+ if (!columnGroups) return null;
64
+ const byId = new Map(filteredColumns.map((col)=>[
65
+ col.id,
66
+ col
67
+ ]));
68
+ const claimed = new Set();
69
+ const sections = columnGroups.map((group)=>{
70
+ const cols = group.columns.map((id)=>byId.get(id)).filter((col)=>null != col);
71
+ for (const col of cols)claimed.add(col.id);
72
+ return {
73
+ label: group.label,
74
+ cols
75
+ };
76
+ }).filter((section)=>section.cols.length > 0);
77
+ const ungrouped = filteredColumns.filter((col)=>!claimed.has(col.id));
78
+ return {
79
+ sections,
80
+ ungrouped
81
+ };
82
+ }, [
83
+ columnGroups,
84
+ filteredColumns
85
+ ]);
62
86
  const { pinnedColumns, unpinnedColumns } = useMemo(()=>{
63
87
  const pinned = [];
64
88
  const unpinned = [];
@@ -147,15 +171,30 @@ const TableSettingsMenu = ({ 'data-testid': testIdProp, children, ...rest })=>{
147
171
  }),
148
172
  /*#__PURE__*/ jsxs(DropdownMenuContent, {
149
173
  ref: contentRef,
150
- className: cn('min-w-256 max-h-[430px]'),
151
- style: lockedWidth ? {
174
+ className: cn('min-w-256'),
175
+ style: {
176
+ maxHeight: 'min(430px, var(--available-height))',
152
177
  width: lockedWidth
153
- } : void 0,
178
+ },
154
179
  children: [
155
180
  searchOverride ?? /*#__PURE__*/ jsx(TableSettingsMenuSearch, {}),
156
181
  /*#__PURE__*/ jsx(VStack, {
157
182
  gap: 1,
158
- children: /*#__PURE__*/ jsx(DndContext, {
183
+ children: labeledSections ? /*#__PURE__*/ jsxs(Fragment, {
184
+ children: [
185
+ labeledSections.sections.map((section)=>/*#__PURE__*/ jsxs("div", {
186
+ className: "flex flex-col gap-1",
187
+ children: [
188
+ /*#__PURE__*/ jsx(DropdownMenuLabel, {
189
+ sticky: true,
190
+ children: section.label
191
+ }),
192
+ section.cols.map(renderColumnItem)
193
+ ]
194
+ }, section.label)),
195
+ labeledSections.ungrouped.map(renderColumnItem)
196
+ ]
197
+ }) : /*#__PURE__*/ jsx(DndContext, {
159
198
  sensors: sensors,
160
199
  collisionDetection: closestCenter,
161
200
  onDragEnd: handleDragEnd,
@@ -7,7 +7,8 @@ import { useTestId } from "../../../utils/testId.js";
7
7
  import { Switch, SwitchControl, SwitchLabel } from "../../Switch/index.js";
8
8
  import { useTableContext } from "../TableContext/index.js";
9
9
  const TableSettingsMenuItem = ({ columnId, className, 'data-testid': testIdProp, ...rest })=>{
10
- const { masterColumnId, columnDndEnabled, table } = useTableContext();
10
+ const { masterColumnId, columnDndEnabled, columnGroups, table } = useTableContext();
11
+ const grouped = !!columnGroups;
11
12
  const slotTestId = useTestId('settings-menu-item');
12
13
  const switchTestId = useTestId(`settings-menu-item-${columnId}`, testIdProp);
13
14
  const column = table.getColumn(columnId);
@@ -29,10 +30,10 @@ const TableSettingsMenuItem = ({ columnId, className, 'data-testid': testIdProp,
29
30
  ref: setNodeRef,
30
31
  style: style,
31
32
  "data-testid": slotTestId,
32
- className: cn('relative flex items-center w-full rounded-6 pl-20 pr-8 py-6', 'hover:bg-states-primary-hover transition-colors', 'data-disabled:opacity-50 data-disabled:pointer-events-none'),
33
+ className: cn('relative flex items-center w-full rounded-6 pr-8 py-6', grouped ? 'pl-8' : 'pl-20', 'hover:bg-states-primary-hover transition-colors', 'data-disabled:opacity-50 data-disabled:pointer-events-none'),
33
34
  "data-disabled": isMasterColumn || void 0,
34
35
  children: [
35
- /*#__PURE__*/ jsx("span", {
36
+ !grouped && /*#__PURE__*/ jsx("span", {
36
37
  className: cn('absolute left-4 top-1/2 -translate-y-1/2 cursor-grab active:cursor-grabbing text-icon-secondary hover:text-icon-primary data-disabled:opacity-50 data-disabled:pointer-events-none'),
37
38
  ...attributes,
38
39
  ...listeners,
@@ -14,4 +14,7 @@ const TableSettingsMenuSearch = ({ 'data-testid': testIdProp, placeholder = 'Sea
14
14
  });
15
15
  };
16
16
  TableSettingsMenuSearch.displayName = 'TableSettingsMenuSearch';
17
+ Object.assign(TableSettingsMenuSearch, {
18
+ dropdownMenuSlot: 'input'
19
+ });
17
20
  export { TableSettingsMenuSearch };
@@ -10,4 +10,4 @@ export { TableEmptyState } from './TableEmptyState';
10
10
  export { TableScrollHandler, TableScrollHandlerLeft, type TableScrollHandlerLeftProps, type TableScrollHandlerProps, TableScrollHandlerRight, type TableScrollHandlerRightProps, } from './TableScrollHandler';
11
11
  export { TableSettingsMenu, TableSettingsMenuItem, type TableSettingsMenuItemProps, type TableSettingsMenuProps, TableSettingsMenuReset, type TableSettingsMenuResetProps, TableSettingsMenuSearch, type TableSettingsMenuSearchProps, } from './TableSettingsMenu';
12
12
  export { TableSortTrigger, type TableSortTriggerProps } from './TableSortTrigger';
13
- export type { TableAccessorColumnDef, TableCellContext, TableColumnBase, TableColumnDef, TableColumnMeta, TableColumnPinningState, TableColumnSizingState, TableDisplayColumnDef, TableExpandedState, TableGroupingState, TableHandle, TableOnChangeFn, TableProps, TableRow, TableRowSelectionState, TableScrollToRowOptions, TableSortingState, TableUpdater, TableVirtualized, TableVisibilityState, } from './types';
13
+ export type { TableAccessorColumnDef, TableCellContext, TableColumnBase, TableColumnDef, TableColumnGroup, TableColumnMeta, TableColumnPinningState, TableColumnSizingState, TableDisplayColumnDef, TableExpandedState, TableGroupingState, TableHandle, TableOnChangeFn, TableProps, TableRow, TableRowSelectionState, TableScrollToRowOptions, TableSortingState, TableUpdater, TableVirtualized, TableVisibilityState, } from './types';
@@ -42,6 +42,19 @@ export type TableExpandedState = true | Record<string, boolean>;
42
42
  export type TableGroupingState = string[];
43
43
  /** Column visibility state: `{ [columnId]: boolean }` */
44
44
  export type TableVisibilityState = Record<string, boolean>;
45
+ /**
46
+ * Labeled grouping for the column-settings menu. When `columnGroups` is passed
47
+ * to the Table, the settings menu renders these labeled sections (in the given
48
+ * order) instead of the default pinned/unpinned + drag-reorder list — the same
49
+ * flat, non-reorderable pattern the FilterInput field menu uses. `columns` are
50
+ * column ids; ids absent from every group fall into a trailing headerless
51
+ * section, and empty groups (or groups whose columns are all filtered out by
52
+ * search) render nothing.
53
+ */
54
+ export interface TableColumnGroup {
55
+ label: string;
56
+ columns: string[];
57
+ }
45
58
  /** State updater — value or functional update */
46
59
  export type TableUpdater<T> = T | ((prev: T) => T);
47
60
  /** Options for the imperative `scrollToRow` method */
@@ -190,6 +203,13 @@ export interface TableProps<T> extends TestableProps {
190
203
  onColumnVisibilityChange?: TableOnChangeFn<TableVisibilityState>;
191
204
  defaultColumnVisibility?: TableVisibilityState;
192
205
  defaultColumnOrder?: string[];
206
+ /**
207
+ * Labeled sections for the column-settings menu. When set, the menu renders
208
+ * these groups (flat, non-reorderable — like the FilterInput field menu)
209
+ * instead of the default pinned/unpinned drag-reorder list. See
210
+ * {@link TableColumnGroup}.
211
+ */
212
+ columnGroups?: TableColumnGroup[];
193
213
  /**
194
214
  * Fired when the built-in column-settings menu opens (`true`) or closes
195
215
  * (`false`). The menu's open state otherwise lives inside the DS and is not
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "1.7.0",
3
- "generatedAt": "2026-08-26T12:08:30.982Z",
2
+ "version": "1.9.0",
3
+ "generatedAt": "2026-08-27T14:32:57.789Z",
4
4
  "components": [
5
5
  {
6
6
  "name": "Accordion",
@@ -30385,13 +30385,13 @@
30385
30385
  "name": "valueMaxWidth",
30386
30386
  "type": "number | undefined",
30387
30387
  "required": false,
30388
- "description": "Caps the applied value segment at this width (px), truncating with an\n ellipsis; lifts the chip's default max-width so it grows to fit (AS-1064)."
30388
+ "description": "Caps the applied value segment at this width (px), truncating with an\n ellipsis; lifts the chip's default max-width so it grows to fit. Defaults\n to 500; a paired chip caps its whole width there instead (AS-1064)."
30389
30389
  },
30390
30390
  {
30391
30391
  "name": "caretMode",
30392
30392
  "type": "\"end\" | undefined",
30393
30393
  "required": false,
30394
- "description": "`'end'` places the caret at the end on value inline-edit; omit to select all (AS-1064)."
30394
+ "description": "Caret placement on value inline-edit: `'end'` (default) puts it at the\n tail better for long values; pass explicitly to keep the API visible (AS-1064)."
30395
30395
  },
30396
30396
  {
30397
30397
  "name": "pair",
@@ -76989,6 +76989,12 @@
76989
76989
  "type": "string[] | undefined",
76990
76990
  "required": false
76991
76991
  },
76992
+ {
76993
+ "name": "columnGroups",
76994
+ "type": "TableColumnGroup[] | undefined",
76995
+ "required": false,
76996
+ "description": "Labeled sections for the column-settings menu. When set, the menu renders\nthese groups (flat, non-reorderable — like the FilterInput field menu)\ninstead of the default pinned/unpinned drag-reorder list. See\n{@link TableColumnGroup}."
76997
+ },
76992
76998
  {
76993
76999
  "name": "virtualized",
76994
77000
  "type": "TableVirtualized | undefined",
@@ -83091,6 +83097,11 @@
83091
83097
  "code": "() => {\n const [columnVisibility, setColumnVisibility] = useState<TableVisibilityState>({});\n const [columnOrder, setColumnOrder] = useState<string[]>([]);\n\n return (\n <Table\n data={securityEvents}\n columns={securityColumns}\n getRowId={row => row.id}\n columnVisibility={columnVisibility}\n onColumnVisibilityChange={setColumnVisibility}\n columnOrder={columnOrder}\n onColumnOrderChange={setColumnOrder}\n defaultColumnVisibility={{}}\n defaultColumnOrder={securityColumnIds}\n />\n );\n}",
83092
83098
  "description": "The gear at the end of the header — search the columns, show and hide them, reorder them,\nreset — writing the same visibility and order state as the header menu."
83093
83099
  },
83100
+ {
83101
+ "name": "SettingsMenuGrouped",
83102
+ "code": "() => {\n const [columnVisibility, setColumnVisibility] = useState<TableVisibilityState>({});\n\n const columnGroups: TableColumnGroup[] = [\n {\n label: 'Threat classification',\n columns: ['objectName', 'status', 'firstDetected', 'lastSeen'],\n },\n { label: 'Request features', columns: ['sourceIp', 'parameter', 'endpoint', 'requests'] },\n ];\n\n return (\n <Table\n data={securityEvents}\n columns={securityColumns}\n getRowId={row => row.id}\n columnVisibility={columnVisibility}\n onColumnVisibilityChange={setColumnVisibility}\n columnGroups={columnGroups}\n />\n );\n}",
83103
+ "description": "`columnGroups` turns the settings menu into labeled, non-reorderable sections\n(the FilterInput field-menu pattern): no drag handle, columns rendered under\ntheir group header in the given order. Column ids absent from every group\n(`cweId` here) fall into a trailing headerless section."
83104
+ },
83094
83105
  {
83095
83106
  "name": "Virtualization",
83096
83107
  "code": "() => {\n const largeData = useMemo(() => createLargeSecurityEvents(1000), []);\n\n return (\n <Table\n className='h-500'\n data={largeData}\n columns={securityColumns}\n getRowId={row => row.id}\n virtualized='container'\n />\n );\n}",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wallarm-org/design-system",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
4
4
  "description": "Core design system library with React components and Storybook documentation",
5
5
  "publishConfig": {
6
6
  "access": "public",