@wallarm-org/design-system 2.4.0 → 2.5.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.
@@ -3,43 +3,48 @@ import { useSortable } from "@dnd-kit/sortable";
3
3
  import { useTableContext } from "../TableContext/useTableContext.js";
4
4
  import { isGroupRow } from "./isGroupRow.js";
5
5
  import { useTableValue } from "./tableReactivity.js";
6
- const useRowDnd = (row)=>{
7
- const $ = c(16);
8
- const { rowDndEnabled, showEmptyGroups } = useTableContext();
6
+ const useRowCanDrag = (row)=>{
7
+ const $ = c(4);
8
+ const { rowDndEnabled, showEmptyGroups, getRowCanDrag } = useTableContext();
9
9
  let t0;
10
- if ($[0] !== row || $[1] !== showEmptyGroups) {
11
- t0 = ()=>isGroupRow(row, showEmptyGroups);
12
- $[0] = row;
13
- $[1] = showEmptyGroups;
14
- $[2] = t0;
15
- } else t0 = $[2];
16
- const isGroup = useTableValue(t0);
17
- const canDnd = rowDndEnabled && !isGroup;
18
- const t1 = !canDnd;
19
- let t2;
20
- if ($[3] !== row.id || $[4] !== t1) {
21
- t2 = {
10
+ if ($[0] !== getRowCanDrag || $[1] !== row || $[2] !== showEmptyGroups) {
11
+ t0 = ()=>!isGroupRow(row, showEmptyGroups) && (getRowCanDrag?.(row) ?? true);
12
+ $[0] = getRowCanDrag;
13
+ $[1] = row;
14
+ $[2] = showEmptyGroups;
15
+ $[3] = t0;
16
+ } else t0 = $[3];
17
+ const canDrag = useTableValue(t0);
18
+ return rowDndEnabled && canDrag;
19
+ };
20
+ const useRowDnd = (row)=>{
21
+ const $ = c(13);
22
+ const canDnd = useRowCanDrag(row);
23
+ const t0 = !canDnd;
24
+ let t1;
25
+ if ($[0] !== row.id || $[1] !== t0) {
26
+ t1 = {
22
27
  id: row.id,
23
- disabled: t1
28
+ disabled: t0
24
29
  };
25
- $[3] = row.id;
26
- $[4] = t1;
30
+ $[0] = row.id;
31
+ $[1] = t0;
32
+ $[2] = t1;
33
+ } else t1 = $[2];
34
+ const { attributes, listeners, setNodeRef, isDragging } = useSortable(t1);
35
+ let t2;
36
+ if ($[3] !== canDnd || $[4] !== isDragging) {
37
+ t2 = canDnd && isDragging ? {
38
+ opacity: 0.4
39
+ } : {};
40
+ $[3] = canDnd;
41
+ $[4] = isDragging;
27
42
  $[5] = t2;
28
43
  } else t2 = $[5];
29
- const { attributes, listeners, setNodeRef, isDragging } = useSortable(t2);
44
+ const style = t2;
30
45
  let t3;
31
- if ($[6] !== canDnd || $[7] !== isDragging) {
32
- t3 = canDnd && isDragging ? {
33
- opacity: 0.4
34
- } : {};
35
- $[6] = canDnd;
36
- $[7] = isDragging;
37
- $[8] = t3;
38
- } else t3 = $[8];
39
- const style = t3;
40
- let t4;
41
- if ($[9] !== attributes || $[10] !== canDnd || $[11] !== isDragging || $[12] !== listeners || $[13] !== setNodeRef || $[14] !== style) {
42
- t4 = {
46
+ if ($[6] !== attributes || $[7] !== canDnd || $[8] !== isDragging || $[9] !== listeners || $[10] !== setNodeRef || $[11] !== style) {
47
+ t3 = {
43
48
  canDnd,
44
49
  isDragging,
45
50
  setNodeRef,
@@ -47,14 +52,14 @@ const useRowDnd = (row)=>{
47
52
  attributes,
48
53
  listeners
49
54
  };
50
- $[9] = attributes;
51
- $[10] = canDnd;
52
- $[11] = isDragging;
53
- $[12] = listeners;
54
- $[13] = setNodeRef;
55
- $[14] = style;
56
- $[15] = t4;
57
- } else t4 = $[15];
58
- return t4;
55
+ $[6] = attributes;
56
+ $[7] = canDnd;
57
+ $[8] = isDragging;
58
+ $[9] = listeners;
59
+ $[10] = setNodeRef;
60
+ $[11] = style;
61
+ $[12] = t3;
62
+ } else t3 = $[12];
63
+ return t3;
59
64
  };
60
- export { useRowDnd };
65
+ export { useRowCanDrag, useRowDnd };
@@ -220,7 +220,27 @@ export interface TableProps<T> extends TestableProps {
220
220
  onColumnOrderChange?: TableOnChangeFn<string[]>;
221
221
  grouping?: TableGroupingState;
222
222
  onGroupingChange?: TableOnChangeFn<TableGroupingState>;
223
+ /**
224
+ * Group row content for `getSubRows` grouping. Passing it switches the table
225
+ * to sub-row grouping mode: the auto-injected expand column is dropped, so
226
+ * the content is responsible for its own expand/collapse toggle
227
+ * (`row.getToggleExpandedHandler()`).
228
+ *
229
+ * Rendered only with `groupRowSpan: 'full'`. With the default `'master'` the
230
+ * group row shows its master (first data) column cell, as before.
231
+ */
223
232
  renderGroupRow?: (row: TableRow<T>) => ReactNode;
233
+ /**
234
+ * How a group row lays out its content.
235
+ *
236
+ * - `'master'` (default): the master column's cell, then empty filler cells —
237
+ * the content is confined to the master column width.
238
+ * - `'full'`: `renderGroupRow(row)` in a single cell spanning every data
239
+ * column (system cells — selection, drag handle — keep their own cells).
240
+ * Use for a group header with a title on the left and actions pinned to the
241
+ * right edge of the table. Requires `renderGroupRow`.
242
+ */
243
+ groupRowSpan?: 'master' | 'full';
224
244
  /** Sub-row accessor for hierarchical/tree data (used with renderGroupRow) */
225
245
  getSubRows?: (row: T) => T[] | undefined;
226
246
  /**
@@ -287,6 +307,25 @@ export interface TableProps<T> extends TestableProps {
287
307
  * among the rows of its own group.
288
308
  */
289
309
  onRowReorder?: (event: TableRowReorderEvent) => void;
310
+ /**
311
+ * Per-row drag control for `onRowReorder`. A row for which it returns
312
+ * `false` shows no grip, cannot be dragged and is not a drop target — the
313
+ * other rows of its group move around it. Group rows never move regardless.
314
+ *
315
+ * Default: every non-group row can be dragged.
316
+ */
317
+ getRowCanDrag?: (row: TableRow<T>) => boolean;
318
+ /**
319
+ * Renders a non-group row as a single cell spanning every data column — a
320
+ * banner or notice row among regular rows. Return `undefined` to render the
321
+ * row normally, with a cell per column. System cells (selection, drag
322
+ * handle) keep their own cells. Pair with `getRowCanDrag` to keep such a row
323
+ * in place in a reorderable table.
324
+ *
325
+ * Called on every render of the row: return elements, do not call hooks in
326
+ * the callback itself (components it returns may).
327
+ */
328
+ renderFullWidthRow?: (row: TableRow<T>) => ReactNode | undefined;
290
329
  /** Callback fired when the master cell is clicked. Receives the row ID. */
291
330
  onMasterCellClick?: (rowId: string) => void;
292
331
  /** ID of the currently active (highlighted) row, or null. Controls row highlighting via data-preview-active attribute. */
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "2.3.3",
3
- "generatedAt": "2026-10-13T07:49:15.152Z",
2
+ "version": "2.4.0",
3
+ "generatedAt": "2026-10-13T09:13:52.244Z",
4
4
  "components": [
5
5
  {
6
6
  "name": "Accordion",
@@ -20392,6 +20392,11 @@
20392
20392
  "name": "LongDocument",
20393
20393
  "code": "() => <LongDocumentDemo />",
20394
20394
  "description": "2 000 read-only lines clamped by `maxLines` — the top of the envelope the colour painter is\nbuilt for, where `readOnly` still lets the text be selected, searched and copied."
20395
+ },
20396
+ {
20397
+ "name": "FullscreenInsideDrawer",
20398
+ "code": "() => {\n const [value, setValue] = useState(jsonCode);\n\n return (\n <Drawer>\n <DrawerTrigger asChild>\n <Button variant='outline' color='neutral'>\n Open Drawer\n </Button>\n </DrawerTrigger>\n <DrawerContent>\n <DrawerHeader>\n <DrawerTitle>Rule</DrawerTitle>\n </DrawerHeader>\n <DrawerBody>\n <CodeEditorRoot value={value} onChange={setValue} language='json'>\n <CodeSnippetHeader>\n <CodeSnippetTitle>rule.json</CodeSnippetTitle>\n <CodeSnippetActions>\n <CodeSnippetFullscreenButton />\n <CodeSnippetCopyButton />\n </CodeSnippetActions>\n </CodeSnippetHeader>\n <CodeEditorContent lineNumbers aria-label='Rule JSON' />\n </CodeEditorRoot>\n </DrawerBody>\n </DrawerContent>\n </Drawer>\n );\n}",
20399
+ "description": "Fullscreen opened from a modal `Drawer` stacks above it: the editor and its buttons take\npointer and keyboard input, and Escape leaves fullscreen while the drawer stays open."
20395
20400
  }
20396
20401
  ]
20397
20402
  },
@@ -24930,6 +24935,11 @@
24930
24935
  "name": "WithFoldingAndShowMore",
24931
24936
  "code": "() => (\n <CodeSnippetAdapterProvider adapter={loadShikiAdapter}>\n <CodeSnippetRoot\n code={foldableResponseCode}\n language='http'\n startingLineNumber={100}\n maxLines={6}\n folds={getHttpFolds(foldableResponseCode, { startingLineNumber: 100 })}\n >\n <CodeSnippetContent>\n <CodeSnippetLineNumbers />\n <CodeSnippetCode />\n </CodeSnippetContent>\n </CodeSnippetRoot>\n </CodeSnippetAdapterProvider>\n)",
24932
24937
  "description": "Folds, `maxLines` and `startingLineNumber` together — fold line numbers are absolute, so\n`getHttpFolds` has to be given the same offset the gutter uses."
24938
+ },
24939
+ {
24940
+ "name": "FullscreenInsideDrawer",
24941
+ "code": "() => (\n <Drawer>\n <DrawerTrigger asChild>\n <Button variant='outline' color='neutral'>\n Open Drawer\n </Button>\n </DrawerTrigger>\n <DrawerContent>\n <DrawerHeader>\n <DrawerTitle>Request</DrawerTitle>\n </DrawerHeader>\n <DrawerBody>\n <CodeSnippetRoot code={jsonCode} language='json'>\n <CodeSnippetHeader>\n <CodeSnippetTitle>package.json</CodeSnippetTitle>\n <CodeSnippetActions>\n <CodeSnippetFullscreenButton />\n <CodeSnippetCopyButton />\n </CodeSnippetActions>\n </CodeSnippetHeader>\n <CodeSnippetContent>\n <CodeSnippetCode />\n </CodeSnippetContent>\n </CodeSnippetRoot>\n </DrawerBody>\n </DrawerContent>\n </Drawer>\n)",
24942
+ "description": "Fullscreen opened from a modal `Drawer` stacks above it as a nested layer: its buttons stay\nclickable, and Escape or a backdrop click leaves fullscreen while the drawer stays open."
24933
24943
  }
24934
24944
  ]
24935
24945
  },
@@ -101518,7 +101528,14 @@
101518
101528
  {
101519
101529
  "name": "renderGroupRow",
101520
101530
  "type": "((row: TableRow<T>) => ReactNode) | undefined",
101521
- "required": false
101531
+ "required": false,
101532
+ "description": "Group row content for `getSubRows` grouping. Passing it switches the table\nto sub-row grouping mode: the auto-injected expand column is dropped, so\nthe content is responsible for its own expand/collapse toggle\n(`row.getToggleExpandedHandler()`).\n\nRendered only with `groupRowSpan: 'full'`. With the default `'master'` the\ngroup row shows its master (first data) column cell, as before."
101533
+ },
101534
+ {
101535
+ "name": "groupRowSpan",
101536
+ "type": "\"master\" | \"full\" | undefined",
101537
+ "required": false,
101538
+ "description": "How a group row lays out its content.\n\n- `'master'` (default): the master column's cell, then empty filler cells —\n the content is confined to the master column width.\n- `'full'`: `renderGroupRow(row)` in a single cell spanning every data\n column (system cells — selection, drag handle — keep their own cells).\n Use for a group header with a title on the left and actions pinned to the\n right edge of the table. Requires `renderGroupRow`."
101522
101539
  },
101523
101540
  {
101524
101541
  "name": "getSubRows",
@@ -101591,6 +101608,18 @@
101591
101608
  "required": false,
101592
101609
  "description": "Row id to anchor the initial scroll position to. The table scrolls this row\ninto view on mount and arms the edge detectors only after that initial\nscroll settles. Use for deep-linking into the middle of a dataset."
101593
101610
  },
101611
+ {
101612
+ "name": "getRowCanDrag",
101613
+ "type": "((row: TableRow<T>) => boolean) | undefined",
101614
+ "required": false,
101615
+ "description": "Per-row drag control for `onRowReorder`. A row for which it returns\n`false` shows no grip, cannot be dragged and is not a drop target — the\nother rows of its group move around it. Group rows never move regardless.\n\nDefault: every non-group row can be dragged."
101616
+ },
101617
+ {
101618
+ "name": "renderFullWidthRow",
101619
+ "type": "((row: TableRow<T>) => ReactNode) | undefined",
101620
+ "required": false,
101621
+ "description": "Renders a non-group row as a single cell spanning every data column — a\nbanner or notice row among regular rows. Return `undefined` to render the\nrow normally, with a cell per column. System cells (selection, drag\nhandle) keep their own cells. Pair with `getRowCanDrag` to keep such a row\nin place in a reorderable table.\n\nCalled on every render of the row: return elements, do not call hooks in\nthe callback itself (components it returns may)."
101622
+ },
101594
101623
  {
101595
101624
  "name": "activeRowId",
101596
101625
  "type": "string | null | undefined",
@@ -107794,6 +107823,11 @@
107794
107823
  "code": "() => {\n const [data, setData] = useState<SecurityHeaderEntry[]>(() => [\n ...groupedHeaderData,\n { ...groupedHeaderData[0]!, id: 'group-empty', objectName: 'Nothing here yet', children: [] },\n ]);\n const [expanded, setExpanded] = useState<TableExpandedState>(true);\n\n const handleRowReorder = useCallback((event: TableRowReorderEvent) => {\n setData(prev =>\n prev.map(group => {\n const children = group.children ?? [];\n const oldIndex = children.findIndex(row => row.id === event.activeRowId);\n const newIndex = children.findIndex(row => row.id === event.overRowId);\n if (oldIndex === -1 || newIndex === -1) return group;\n return { ...group, children: arrayMove(children, oldIndex, newIndex) };\n }),\n );\n }, []);\n\n return (\n <Table<SecurityHeaderEntry>\n data={data}\n columns={headerColumns}\n getRowId={row => row.id}\n getSubRows={row => row.children}\n showEmptyGroups\n expanded={expanded}\n onExpandedChange={setExpanded}\n onRowReorder={handleRowReorder}\n data-testid='row-reorder-groups-table'\n />\n );\n}",
107795
107824
  "description": "With `getSubRows` grouping, a row moves only among the rows of its own group; group rows stay\nput. `showEmptyGroups` keeps a group whose `getSubRows` returns `[]` on screen as a group row."
107796
107825
  },
107826
+ {
107827
+ "name": "GroupedWithFullWidthRows",
107828
+ "code": "() => {\n const [data, setData] = useState<SecurityHeaderEntry[]>(() => [\n {\n ...groupedHeaderData[0]!,\n children: [\n { ...groupedHeaderData[0]!, id: `${BANNER_ROW_PREFIX}csp`, children: undefined },\n ...(groupedHeaderData[0]!.children ?? []),\n ],\n },\n ...groupedHeaderData.slice(1, 3),\n { ...groupedHeaderData[0]!, id: 'group-empty', objectName: 'Nothing here yet', children: [] },\n ]);\n const [expanded, setExpanded] = useState<TableExpandedState>(true);\n\n const handleRowReorder = useCallback((event: TableRowReorderEvent) => {\n setData(prev =>\n prev.map(group => {\n const children = group.children ?? [];\n const oldIndex = children.findIndex(row => row.id === event.activeRowId);\n const newIndex = children.findIndex(row => row.id === event.overRowId);\n if (oldIndex === -1 || newIndex === -1) return group;\n return { ...group, children: arrayMove(children, oldIndex, newIndex) };\n }),\n );\n }, []);\n\n return (\n <Table<SecurityHeaderEntry>\n data={data}\n columns={headerColumns}\n getRowId={row => row.id}\n getSubRows={row => row.children}\n showEmptyGroups\n expanded={expanded}\n onExpandedChange={setExpanded}\n onRowReorder={handleRowReorder}\n groupRowSpan='full'\n renderGroupRow={row => {\n const isExpanded = row.getIsExpanded();\n return (\n <HStack gap={8} align='center'>\n <Button\n variant='ghost'\n color='neutral'\n size='small'\n aria-expanded={isExpanded}\n aria-label={isExpanded ? 'Collapse group' : 'Expand group'}\n onClick={row.getToggleExpandedHandler()}\n >\n {isExpanded ? <ChevronDown /> : <ChevronRight />}\n </Button>\n <Text weight='medium'>{row.original.objectName}</Text>\n <Badge color='gray' type='secondary' size='medium'>\n {row.subRows.filter(r => !r.id.startsWith(BANNER_ROW_PREFIX)).length}\n </Badge>\n <HStack gap={4} className='ml-auto'>\n <Button variant='ghost' color='neutral' size='small'>\n <Plus />\n Add header\n </Button>\n <Button variant='ghost' color='neutral' size='small' aria-label='More actions'>\n <Ellipsis />\n </Button>\n </HStack>\n </HStack>\n );\n }}\n renderFullWidthRow={row =>\n row.id.startsWith(BANNER_ROW_PREFIX) ? (\n <HStack gap={8} align='center'>\n <Shield size='sm' />\n <Text size='sm' weight='medium'>\n Wallarm baseline headers\n </Text>\n <Text size='sm' color='secondary'>\n · applied first · built-in, always on\n </Text>\n <Button variant='ghost' color='neutral' size='small' className='ml-auto'>\n Manage baseline →\n </Button>\n </HStack>\n ) : undefined\n }\n getRowCanDrag={row => !row.id.startsWith(BANNER_ROW_PREFIX) && !LOCKED_ROW_IDS.has(row.id)}\n data-testid='grouped-full-width-table'\n />\n );\n}",
107829
+ "description": "`groupRowSpan: 'full'` draws `renderGroupRow` across every data column — title on the left,\nactions on the right edge; the content owns the collapse toggle. `renderFullWidthRow` turns a\nregular row into a single spanning banner, and `getRowCanDrag` keeps the banner and the locked\nfirst header in place while the other rows of the group reorder around them."
107830
+ },
107797
107831
  {
107798
107832
  "name": "RowReorderingWithSelection",
107799
107833
  "code": "() => {\n const [data, setData] = useState(() => securityEvents.slice(0, 6));\n const [rowSelection, setRowSelection] = useState<TableRowSelectionState>({});\n\n const handleRowReorder = useCallback((event: TableRowReorderEvent) => {\n setData(prev => {\n const oldIndex = prev.findIndex(row => row.id === event.activeRowId);\n const newIndex = prev.findIndex(row => row.id === event.overRowId);\n if (oldIndex === -1 || newIndex === -1) return prev;\n return arrayMove(prev, oldIndex, newIndex);\n });\n }, []);\n\n return (\n <Table\n data={data}\n columns={securityColumns}\n getRowId={row => row.id}\n onRowReorder={handleRowReorder}\n rowSelection={rowSelection}\n onRowSelectionChange={setRowSelection}\n />\n );\n}",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wallarm-org/design-system",
3
- "version": "2.4.0",
3
+ "version": "2.5.0",
4
4
  "description": "Core design system library with React components and Storybook documentation",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -167,7 +167,9 @@
167
167
  "@tanstack/react-table": "9.2.4",
168
168
  "@tanstack/react-virtual": "3.14.13",
169
169
  "@zag-js/cascade-select": "1.43.3",
170
+ "@zag-js/dismissable": "1.43.3",
170
171
  "@zag-js/file-utils": "1.43.3",
172
+ "@zag-js/focus-trap": "1.43.3",
171
173
  "@zag-js/react": "1.43.3",
172
174
  "class-variance-authority": "0.7.1",
173
175
  "clsx": "2.1.1",