@redsift/ds-mcp-server 12.5.3-muiv7 → 12.5.3

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 (116) hide show
  1. package/consumer-instructions/redsift-design-system.instructions.md +82 -1
  2. package/data/demos/patterns/_shared/StateDebugPanel.tsx +2 -2
  3. package/data/demos/patterns/_shared/columns.tsx +3 -3
  4. package/data/demos/patterns/_shared/defaults.ts +1 -1
  5. package/data/demos/patterns/_shared/filter-helpers.ts +1 -1
  6. package/data/demos/patterns/_shared/helpers.tsx +1 -1
  7. package/data/demos/patterns/_shared/server-logic.ts +1 -1
  8. package/data/demos/patterns/_shared/story-helpers.ts +106 -36
  9. package/data/demos/patterns/crossfiltered-datagrid-client-side/CrossfilteredDatagridClientSide.interaction.stories.tsx +4 -19
  10. package/data/demos/patterns/crossfiltered-datagrid-client-side/example.tsx +2 -2
  11. package/data/demos/patterns/crossfiltered-datagrid-server-side/CrossfilteredDatagridServerSide.interaction.stories.tsx +3 -3
  12. package/data/demos/patterns/crossfiltered-datagrid-server-side/example.tsx +5 -5
  13. package/data/demos/patterns/drilldowned-datagrid-client-side/DrilldownedDatagridClientSide.interaction.stories.tsx +2 -2
  14. package/data/demos/patterns/drilldowned-datagrid-client-side/example.tsx +1 -1
  15. package/data/demos/patterns/drilldowned-datagrid-server-side/DrilldownedDatagridServerSide.interaction.stories.tsx +2 -2
  16. package/data/demos/patterns/drilldowned-datagrid-server-side/example.tsx +5 -19
  17. package/data/demos/patterns/single-datagrid-client-side/SingleDatagridClientSide.interaction.stories.tsx +3 -3
  18. package/data/demos/patterns/single-datagrid-client-side/example.tsx +5 -5
  19. package/data/demos/patterns/single-datagrid-server-side/SingleDatagridServerSide.interaction.stories.tsx +3 -3
  20. package/data/demos/patterns/single-datagrid-server-side/example.tsx +5 -6
  21. package/data/demos/patterns/stateful-single-datagrid-client-side/StatefulSingleDatagridClientSide.interaction.stories.tsx +130 -3
  22. package/data/demos/patterns/stateful-single-datagrid-client-side/example.tsx +6 -6
  23. package/data/demos/patterns/stateful-single-datagrid-server-side/StatefulSingleDatagridServerSide.interaction.stories.tsx +136 -6
  24. package/data/demos/patterns/stateful-single-datagrid-server-side/example.tsx +6 -9
  25. package/data/demos/patterns/summary-dashboard/SummaryDashboard.interaction.stories.tsx +2 -2
  26. package/data/demos/patterns/tabbed-datagrid-client-side/TabbedDatagridClientSide.interaction.stories.tsx +2 -2
  27. package/data/demos/patterns/tabbed-datagrid-server-side/TabbedDatagridServerSide.interaction.stories.tsx +2 -2
  28. package/data/demos/patterns/tabbed-datagrid-server-side/example.tsx +1 -1
  29. package/data/docs/components/charts/Axis.json +6 -1
  30. package/data/docs/components/charts/BarChart.json +7 -1
  31. package/data/docs/components/charts/ChartContainerTitle.json +5 -1
  32. package/data/docs/components/charts/Legend.json +6 -1
  33. package/data/docs/components/charts/LineChart.json +7 -1
  34. package/data/docs/components/charts/PieChart.json +6 -1
  35. package/data/docs/components/charts/ScatterPlot.json +6 -1
  36. package/data/docs/components/dashboard/ChartEmptyState.json +8 -1
  37. package/data/docs/components/dashboard/Dashboard.json +8 -3
  38. package/data/docs/components/dashboard/DataCard.json +12 -0
  39. package/data/docs/components/dashboard/DataCardBody.json +4 -0
  40. package/data/docs/components/dashboard/DataCardHeader.json +4 -0
  41. package/data/docs/components/dashboard/DataCardListbox.json +5 -0
  42. package/data/docs/components/dashboard/DataRow.json +7 -1
  43. package/data/docs/components/dashboard/PdfExportButton.json +6 -1
  44. package/data/docs/components/dashboard/TimeSeriesBarChart.json +6 -1
  45. package/data/docs/components/dashboard/WithFilters.json +5 -1
  46. package/data/docs/components/design-system/Alert.json +8 -1
  47. package/data/docs/components/design-system/AppBar.json +6 -1
  48. package/data/docs/components/design-system/AppContent.json +4 -0
  49. package/data/docs/components/design-system/AppSidePanel.json +5 -0
  50. package/data/docs/components/design-system/Badge.json +6 -1
  51. package/data/docs/components/design-system/Breadcrumbs.json +4 -0
  52. package/data/docs/components/design-system/Button.json +5 -0
  53. package/data/docs/components/design-system/Card.json +9 -0
  54. package/data/docs/components/design-system/CardActions.json +4 -0
  55. package/data/docs/components/design-system/CardBody.json +3 -0
  56. package/data/docs/components/design-system/CardHeader.json +4 -0
  57. package/data/docs/components/design-system/DetailedCard.json +6 -0
  58. package/data/docs/components/design-system/Flexbox.json +14 -1
  59. package/data/docs/components/design-system/Grid.json +6 -1
  60. package/data/docs/components/design-system/Heading.json +11 -0
  61. package/data/docs/components/design-system/Icon.json +6 -1
  62. package/data/docs/components/design-system/IconButton.json +9 -0
  63. package/data/docs/components/design-system/Pill.json +10 -0
  64. package/data/docs/components/design-system/Skeleton.json +10 -1
  65. package/data/docs/components/design-system/SkeletonCircle.json +6 -1
  66. package/data/docs/components/design-system/SkeletonText.json +6 -1
  67. package/data/docs/components/design-system/Tab.json +4 -0
  68. package/data/docs/components/design-system/TabPanel.json +4 -0
  69. package/data/docs/components/design-system/Tabs.json +6 -0
  70. package/data/docs/components/design-system/Text.json +9 -0
  71. package/data/docs/components/design-system/TextField.json +6 -1
  72. package/data/docs/components/pickers/Combobox.json +6 -0
  73. package/data/docs/components/pickers/MenuButton.json +5 -0
  74. package/data/docs/components/pickers/Select.json +5 -0
  75. package/data/docs/components/popovers/Dialog.json +6 -0
  76. package/data/docs/components/popovers/Toggletip.json +5 -0
  77. package/data/docs/components/popovers/Tooltip.json +4 -0
  78. package/data/docs/components/table/DataGrid.json +12 -31
  79. package/data/docs/components/table/StatefulDataGrid.json +9 -31
  80. package/data/docs/components-index.json +343 -53
  81. package/data/docs/components.json +10 -64
  82. package/data/docs/llms-full.txt +291 -68
  83. package/data/docs/llms.txt +56 -6
  84. package/data/docs/patterns-catalog.md +215 -25
  85. package/data/docs/patterns.json +369 -31
  86. package/data/metadata.json +2 -2
  87. package/data/patterns/crossfiltered-datagrid-server-side.mdx +1 -1
  88. package/data/patterns/drilldowned-datagrid-client-side.mdx +1 -1
  89. package/data/patterns/drilldowned-datagrid-server-side.mdx +1 -1
  90. package/data/patterns/single-datagrid-client-side.mdx +9 -9
  91. package/data/patterns/single-datagrid-server-side.mdx +4 -4
  92. package/data/patterns/stateful-single-datagrid-client-side.mdx +36 -20
  93. package/data/patterns/stateful-single-datagrid-server-side.mdx +46 -18
  94. package/data/patterns/tabbed-datagrid-server-side.mdx +1 -1
  95. package/data/prompts/ds-advisor.md +103 -0
  96. package/dist/data-store.d.ts +21 -1
  97. package/dist/data-store.d.ts.map +1 -1
  98. package/dist/data-store.js +65 -15
  99. package/dist/data-store.js.map +1 -1
  100. package/dist/pattern-store.d.ts +18 -1
  101. package/dist/pattern-store.d.ts.map +1 -1
  102. package/dist/pattern-store.js +64 -22
  103. package/dist/pattern-store.js.map +1 -1
  104. package/dist/prompts.d.ts.map +1 -1
  105. package/dist/prompts.js +56 -27
  106. package/dist/prompts.js.map +1 -1
  107. package/dist/resources.d.ts.map +1 -1
  108. package/dist/resources.js +26 -0
  109. package/dist/resources.js.map +1 -1
  110. package/dist/tools.d.ts.map +1 -1
  111. package/dist/tools.js +12 -0
  112. package/dist/tools.js.map +1 -1
  113. package/dist/types.d.ts +11 -0
  114. package/dist/types.d.ts.map +1 -1
  115. package/dist/types.js.map +1 -1
  116. package/package.json +4 -2
@@ -15,6 +15,7 @@ This project uses the **Red Sift Design System** (`@redsift/*` packages). The de
15
15
  1. Configure the MCP server in `.vscode/mcp.json` — see [MCP Server docs](https://design-system.redsift.io/introduction/mcp-server)
16
16
  2. Copy this file to `.github/instructions/` in your project
17
17
  3. Verify: ask the AI _"What props does Button accept?"_ — it should call `get_component_props`, not answer from memory
18
+ 4. **For automated code generation (Software Factory / agent workflows):** also load the `design-system://prompts/ds-advisor` MCP resource into the system prompt — it contains the component selection cheat sheet, anti-patterns, and disambiguation rules
18
19
 
19
20
  ## Rule 1 — Mandatory Prop Lookup
20
21
 
@@ -54,9 +55,43 @@ When adopting `@redsift/*` components in a new project or migrating from raw HTM
54
55
  4. Import icons from `@redsift/icons`, not `@mdi/js`.
55
56
  5. Remove conflicting Tailwind/utility-CSS classes (`bg-*`, `text-*`, `border-*`, `dark:*`) from elements that render or wrap DS components.
56
57
 
58
+ ## Rule 6 — Semantic Colors Only
59
+
60
+ When setting colors on DS components:
61
+
62
+ 1. **Never use hex/rgb literals** (`#22c55e`, `rgb(34, 197, 94)`) on DS component color props.
63
+ 2. **Never use CSS variable fallbacks** like `color="var(--rs-color-green-500, #22c55e)"`.
64
+ 3. **Always use semantic color values:** `"success"`, `"warning"`, `"error"`, `"info"`, `"no-data"`, `"question"`, or palette names from `NeutralColorPalette` / `PresentationColorPalette`.
65
+ 4. If asked which hex color to use, **refuse** — return the semantic token name instead.
66
+
67
+ ## Rule 7 — Icons from @redsift/icons Only
68
+
69
+ 1. **Never use unicode glyphs** (✓, ✕, ⚠, ›, •) as visual indicators — import the equivalent `mdi*` icon from `@redsift/icons`.
70
+ 2. **Never import from `@mdi/js`** — always use `@redsift/icons` (the DS re-exports the mdi icon set).
71
+ 3. Render icons with the `<Icon>` component from `@redsift/design-system`, e.g. `<Icon path={mdiCheck} />`.
72
+
73
+ ## Rule 8 — Component Selection
74
+
75
+ When choosing which component to use, follow this cheat sheet:
76
+
77
+ | UI Intent | Correct Component | Package | Common Mistake |
78
+ | ------------------------ | -------------------------------------- | ------------------------ | ------------------------------------------------- |
79
+ | KPI / metric tile | `DataCard` + `DataRow` | `@redsift/dashboard` | Using `Card` with inline styles |
80
+ | Status badge | `<Pill color="success">Label</Pill>` | `@redsift/design-system` | `<Pill label="..." />` (label prop doesn't exist) |
81
+ | Loading placeholder | `Skeleton` / `SkeletonText` | `@redsift/design-system` | `Skeleton` from `@mui/material` |
82
+ | Collapsible detail panel | `DetailedCard` + `DetailedCardSection` | `@redsift/design-system` | Custom accordion with Card |
83
+ | Generic container | `Card` | `@redsift/design-system` | Using Card for KPI tiles (use DataCard) |
84
+ | Modal / dialog | `Dialog` compound component | `@redsift/popovers` | Building custom overlay |
85
+
86
+ ## Rule 9 — Refusal Rules
87
+
88
+ 1. If asked to provide a hex color for a DS component → **refuse** and return the semantic token name.
89
+ 2. If asked to wrap a DS component in `styled-components` to change its visual appearance → **refuse** and propose using the component's built-in props, composition, or filing a DS feature request.
90
+ 3. If the requested component does not exist in any `@redsift/*` package → say so and propose the closest existing primitive or composition.
91
+
57
92
  ## NEVER Do This
58
93
 
59
- These are concrete examples of wrong code. **Every single one will fail:**
94
+ These are concrete examples of wrong code. **Every single one will fail or produce incorrect results:**
60
95
 
61
96
  ```tsx
62
97
  // ❌ WRONG — Button has NO size prop
@@ -72,6 +107,46 @@ These are concrete examples of wrong code. **Every single one will fail:**
72
107
 
73
108
  // ❌ WRONG — wrong package
74
109
  import { Select } from '@redsift/design-system';
110
+
111
+ // ❌ WRONG — Use DataCard for KPI tiles, not Card with inline styles
112
+ <Card style={{ borderLeft: '4px solid green' }}>
113
+ // ✅ RIGHT
114
+ <DataCard color="success">
115
+
116
+ // ❌ WRONG — Pill uses children, not label
117
+ <Pill label="Active" />
118
+ // ✅ RIGHT
119
+ <Pill color="success">Active</Pill>
120
+
121
+ // ❌ WRONG — gap takes a string
122
+ <Flexbox gap={4}>
123
+ // ✅ RIGHT
124
+ <Flexbox gap="4px">
125
+
126
+ // ❌ WRONG — use mdi icons, not unicode characters
127
+ <Text>✓ Trusted</Text>
128
+ // ✅ RIGHT
129
+ <Icon path={mdiCheck} /> <Text>Trusted</Text>
130
+
131
+ // ❌ WRONG — use DS Skeleton, not MUI
132
+ import { Skeleton } from '@mui/material';
133
+ // ✅ RIGHT
134
+ import { Skeleton } from '@redsift/design-system';
135
+
136
+ // ❌ WRONG — hex color on DS component
137
+ <Heading color="#22c55e">Title</Heading>
138
+ // ✅ RIGHT
139
+ <Heading color="success">Title</Heading>
140
+
141
+ // ❌ WRONG — CSS variable fallback on DS component
142
+ <Text color="var(--rs-color-green-500, #22c55e)">Status</Text>
143
+ // ✅ RIGHT
144
+ <Text color="success">Status</Text>
145
+
146
+ // ❌ WRONG — import from @mdi/js
147
+ import { mdiPlus } from '@mdi/js';
148
+ // ✅ RIGHT
149
+ import { mdiPlus } from '@redsift/icons';
75
150
  ```
76
151
 
77
152
  ## Common Mistakes
@@ -91,6 +166,12 @@ import { Select } from '@redsift/design-system';
91
166
  | Missing `@redsift/design-system/style/index.css` | Must import at app entry point | `design-system://getting-started/host-app` |
92
167
  | `import { mdiPlus } from '@mdi/js'` | `import { mdiPlus } from '@redsift/icons'` | `get_component_usage` |
93
168
  | Tailwind `bg-*`/`text-*`/`dark:*` on DS wrappers | Use DS tokens or layout primitives | `design-system://getting-started/host-app` |
169
+ | `<Pill label="Active" />` | `<Pill color="success">Active</Pill>` (children) | `get_component_props` for Pill |
170
+ | `<Flexbox gap={4}>` | `<Flexbox gap="4px">` (string, not number) | `get_component_props` for Flexbox |
171
+ | `<Card>` for KPI tiles | `<DataCard>` from `@redsift/dashboard` | `get_component_props` for DataCard |
172
+ | `import { Skeleton } from '@mui/material'` | `import { Skeleton } from '@redsift/design-system'` | `get_component_usage` for Skeleton |
173
+ | Hex/rgb colors on DS components | Use semantic colors: `"success"`, `"error"`, etc. | `get_component_props` for the component |
174
+ | Unicode glyphs (✓, ✕, ⚠) as indicators | `<Icon path={mdiCheck} />` from `@redsift/icons` | `get_component_usage` for Icon |
94
175
 
95
176
  ## Verification
96
177
 
@@ -1,5 +1,5 @@
1
1
  import React, { useCallback, useEffect, useState } from 'react';
2
- import type { GridApiPro } from '@mui/x-data-grid-pro/models/gridApiPro';
2
+ import type { GridApiPremium } from '@mui/x-data-grid-premium';
3
3
 
4
4
  // localStorage key categories — must match @redsift/table internals
5
5
  const LS_CATEGORIES = [
@@ -13,7 +13,7 @@ const LS_CATEGORIES = [
13
13
  ];
14
14
 
15
15
  interface StateDebugPanelProps {
16
- apiRef: React.MutableRefObject<GridApiPro>;
16
+ apiRef: React.MutableRefObject<GridApiPremium | null>;
17
17
  useRouter: () => { pathname: string; search: string; historyReplace: (newSearch: string) => void };
18
18
  localStorageVersion?: number;
19
19
  }
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  import { createColumn, TextCell } from '@redsift/table';
3
3
  import { Flexbox, Icon, IconButtonLink, Pill } from '@redsift/design-system';
4
4
  import { mdiArrowRight, mdiCheck, mdiClose } from '@redsift/icons';
5
- import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid-pro';
5
+ import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid-premium';
6
6
  import { Row } from './data';
7
7
 
8
8
  // -- Option constants -------------------------------------------------------
@@ -110,7 +110,7 @@ export const columns: GridColDef<Row>[] = [
110
110
  width: 140,
111
111
  display: 'flex',
112
112
  ...createColumn('date'),
113
- valueGetter: (value: unknown) => parseDate(value),
113
+ valueGetter: (value: string) => parseDate(value),
114
114
  renderCell: ({ value }: GridRenderCellParams) => <TextCell>{value ? formatDate(value as Date) : '—'}</TextCell>,
115
115
  },
116
116
  // DateTime
@@ -120,7 +120,7 @@ export const columns: GridColDef<Row>[] = [
120
120
  width: 180,
121
121
  display: 'flex',
122
122
  ...createColumn('dateTime'),
123
- valueGetter: (value: unknown) => parseDate(value),
123
+ valueGetter: (value: string) => parseDate(value),
124
124
  renderCell: ({ value }: GridRenderCellParams) => <TextCell>{value ? formatDateTime(value as Date) : '—'}</TextCell>,
125
125
  },
126
126
  // Boolean — in stock
@@ -1,4 +1,4 @@
1
- import { GridFilterModel, GridSortModel } from '@mui/x-data-grid-pro';
1
+ import { GridFilterModel, GridSortModel } from '@mui/x-data-grid-premium';
2
2
 
3
3
  /**
4
4
  * Default filter model applied to all pattern examples.
@@ -1,4 +1,4 @@
1
- import { GridFilterModel } from '@mui/x-data-grid-pro';
1
+ import { GridFilterModel } from '@mui/x-data-grid-premium';
2
2
  import { Row } from './data';
3
3
 
4
4
  // -- Types ------------------------------------------------------------------
@@ -8,7 +8,7 @@ import {
8
8
  GridToolbarExport,
9
9
  GridToolbarFilterButton,
10
10
  GridToolbarQuickFilter,
11
- } from '@mui/x-data-grid-pro';
11
+ } from '@mui/x-data-grid-premium';
12
12
 
13
13
  // -- Toolbar ----------------------------------------------------------------
14
14
 
@@ -1,4 +1,4 @@
1
- import { GridFilterModel, GridSortModel } from '@mui/x-data-grid-pro';
1
+ import { GridFilterModel, GridSortModel } from '@mui/x-data-grid-premium';
2
2
  import { Row, allRows } from './data';
3
3
  import { Aggregates, computeAggregates, applyFilters } from './filter-helpers';
4
4
 
@@ -366,22 +366,22 @@ export const clickHeaderCheckbox = async (canvasElement: HTMLElement) => {
366
366
 
367
367
  /** Click a row checkbox by row index (0-based, within the visible page). */
368
368
  export const clickRowCheckbox = async (canvasElement: HTMLElement, rowIndex: number) => {
369
- let checkbox!: HTMLInputElement;
370
- await waitFor(() => {
371
- const row = canvasElement.querySelector(`.MuiDataGrid-row[data-rowindex="${rowIndex}"]`);
372
- expect(row).toBeTruthy();
373
- checkbox = row!.querySelector('input[type="checkbox"]') as HTMLInputElement;
374
- expect(checkbox).toBeTruthy();
375
- });
376
- const wasChecked = checkbox.checked;
369
+ // Wait for BOTH the row AND its checkbox to be present.
370
+ // Server-side grids may render rows before checkboxes are mounted.
371
+ await waitFor(
372
+ () => {
373
+ const rows = canvasElement.querySelectorAll('.MuiDataGrid-row');
374
+ expect(rows.length).toBeGreaterThan(rowIndex);
375
+ const cb = rows[rowIndex].querySelector('input[type="checkbox"]');
376
+ expect(cb).toBeTruthy();
377
+ },
378
+ { timeout: 5000 }
379
+ );
380
+ const rows = canvasElement.querySelectorAll('.MuiDataGrid-row');
381
+ const checkbox = rows[rowIndex].querySelector('input[type="checkbox"]')!;
377
382
  await userEvent.click(checkbox);
378
- // Wait for the click to actually toggle the checkbox (needed for MUI v7 re-render timing)
379
- await waitFor(() => {
380
- const freshRow = canvasElement.querySelector(`.MuiDataGrid-row[data-rowindex="${rowIndex}"]`);
381
- const freshCheckbox = freshRow?.querySelector('input[type="checkbox"]') as HTMLInputElement | null;
382
- expect(freshCheckbox).toBeTruthy();
383
- expect(freshCheckbox!.checked).toBe(!wasChecked);
384
- });
383
+ // Wait for React and MUI DataGrid to process the selection change
384
+ await new Promise((resolve) => setTimeout(resolve, 200));
385
385
  };
386
386
 
387
387
  // ---------------------------------------------------------------------------
@@ -952,29 +952,42 @@ export const waitForPaginationEnabled = async (canvasElement: HTMLElement, direc
952
952
 
953
953
  /**
954
954
  * Change the page size via the MUI pagination "Rows per page" select.
955
- * The dropdown menu renders as a portal on document.body.
955
+ * MUI Material v7's `useSlot()` applies `.MuiTablePagination-select` to the
956
+ * Select wrapper, NOT the inner display div that has the `onMouseDown` handler.
957
+ * We target `[role="combobox"]` inside the pagination toolbar instead, which is
958
+ * the actual interactive element rendered by `SelectInput`.
959
+ * `userEvent.click` in the Storybook Playwright runner doesn't reliably
960
+ * trigger mouseDown on MUI Select, so we use `fireEvent.mouseDown` directly,
961
+ * then `userEvent.click` to select the option in the dropdown.
956
962
  */
957
963
  export const changePageSize = async (canvasElement: HTMLElement, newSize: number) => {
958
- // Wait for the rows-per-page select to be available
959
- let select!: HTMLElement;
960
- await waitFor(() => {
961
- // The bottom pagination renders a MUI Select with role="combobox"
962
- const selects = canvasElement.querySelectorAll<HTMLElement>('[role="combobox"]');
963
- // Pick the last one (bottom pagination) — top pagination may not have one
964
- select = selects[selects.length - 1];
965
- expect(select).toBeTruthy();
966
- });
967
- // MUI Select listens on mouseDown to open its menu
964
+ // Wait for the pagination combobox to be present
965
+ await waitFor(
966
+ () => {
967
+ const el = canvasElement.querySelector('.MuiTablePagination-toolbar [role="combobox"]');
968
+ expect(el).toBeTruthy();
969
+ },
970
+ { timeout: 5000 }
971
+ );
972
+
973
+ const select = canvasElement.querySelector('.MuiTablePagination-toolbar [role="combobox"]') as HTMLElement;
974
+
975
+ // Use fireEvent.mouseDown to reliably open MUI v7 Select dropdown
968
976
  fireEvent.mouseDown(select);
969
- // The menu renders as a MUI portal on document.body — wait for options
970
- await waitFor(() => {
971
- const options = document.querySelectorAll('li[role="option"]');
972
- expect(options.length).toBeGreaterThan(0);
973
- });
974
- const options = document.querySelectorAll('li[role="option"]');
977
+
978
+ // Wait for the dropdown to render (MUI uses a portal on document.body)
979
+ await waitFor(
980
+ () => {
981
+ const options = document.querySelectorAll('[role="option"]');
982
+ expect(options.length).toBeGreaterThan(0);
983
+ },
984
+ { timeout: 5000 }
985
+ );
986
+
987
+ const options = document.querySelectorAll('[role="option"]');
975
988
  const target = Array.from(options).find((opt) => opt.textContent === String(newSize));
976
989
  if (!target) throw new Error(`Could not find page size option "${newSize}"`);
977
- fireEvent.click(target);
990
+ await userEvent.click(target);
978
991
  };
979
992
 
980
993
  // ---------------------------------------------------------------------------
@@ -988,7 +1001,7 @@ export const openColumnsPanel = async (canvasElement: HTMLElement) => {
988
1001
  ) as HTMLElement | null;
989
1002
  if (!btn) throw new Error('Could not find columns toolbar button');
990
1003
  await userEvent.click(btn);
991
- // Wait for the panel to appear on document.body (v7: columnsManagement, v6: columnsPanel)
1004
+ // Wait for the panel to appear on document.body
992
1005
  await waitFor(() => {
993
1006
  const panel = document.querySelector('.MuiDataGrid-columnsManagement');
994
1007
  expect(panel).toBeTruthy();
@@ -999,8 +1012,9 @@ export const openColumnsPanel = async (canvasElement: HTMLElement) => {
999
1012
  export const toggleColumnInPanel = async (fieldLabel: string) => {
1000
1013
  const panel = document.querySelector('.MuiDataGrid-columnsManagement');
1001
1014
  if (!panel) throw new Error('Columns panel is not open');
1002
- const labels = Array.from(panel.querySelectorAll('.MuiFormControlLabel-root'));
1003
- const target = labels.find((label) => label.textContent?.includes(fieldLabel));
1015
+ // MUI v8 renders each column toggle as a baseCheckbox slot with class columnsManagementRow
1016
+ const rows = Array.from(panel.querySelectorAll('.MuiDataGrid-columnsManagementRow'));
1017
+ const target = rows.find((row) => row.textContent?.includes(fieldLabel));
1004
1018
  if (!target) throw new Error(`Could not find column "${fieldLabel}" in columns panel`);
1005
1019
  const checkbox = target.querySelector('input[type="checkbox"]') as HTMLElement | null;
1006
1020
  if (!checkbox) throw new Error(`Could not find checkbox for column "${fieldLabel}"`);
@@ -1204,6 +1218,12 @@ interface SyncAssertionOptions {
1204
1218
  checkDensity?: boolean;
1205
1219
  /** Whether to check column order sync */
1206
1220
  checkColumnOrder?: boolean;
1221
+ /** Whether to check row grouping sync */
1222
+ checkRowGrouping?: boolean;
1223
+ /** Whether to check aggregation sync */
1224
+ checkAggregation?: boolean;
1225
+ /** Whether to check pivot sync */
1226
+ checkPivot?: boolean;
1207
1227
  }
1208
1228
 
1209
1229
  /**
@@ -1222,6 +1242,9 @@ export const assertAllStatesInSync = async ({
1222
1242
  checkPagination = true,
1223
1243
  checkDensity = true,
1224
1244
  checkColumnOrder = false,
1245
+ checkRowGrouping = false,
1246
+ checkAggregation = false,
1247
+ checkPivot = false,
1225
1248
  }: SyncAssertionOptions) => {
1226
1249
  await waitFor(
1227
1250
  () => {
@@ -1328,6 +1351,53 @@ export const assertAllStatesInSync = async ({
1328
1351
  expect(urlColumnOrder).toBe(inner);
1329
1352
  }
1330
1353
  }
1354
+
1355
+ // --- Row Grouping ---
1356
+ if (checkRowGrouping) {
1357
+ const lsKey = `${pathname}:${localStorageVersion}:rowGroupingModel`;
1358
+ const lsRaw = localStorage.getItem(lsKey);
1359
+ const lsRowGrouping = lsRaw ? JSON.parse(lsRaw) : '';
1360
+
1361
+ const urlRowGrouping = url.get('_rowGrouping');
1362
+ if (lsRowGrouping && urlRowGrouping) {
1363
+ // localStorage: `_rowGrouping=[a,b,c]`
1364
+ // URL: `_rowGrouping=a,b,c`
1365
+ const lsParams = new URLSearchParams(lsRowGrouping);
1366
+ const lsValue = lsParams.get('_rowGrouping') ?? '';
1367
+ const inner = lsValue.startsWith('[') && lsValue.endsWith(']') ? lsValue.slice(1, -1) : lsValue;
1368
+ expect(urlRowGrouping).toBe(inner);
1369
+ }
1370
+ }
1371
+
1372
+ // --- Aggregation ---
1373
+ if (checkAggregation) {
1374
+ const lsKey = `${pathname}:${localStorageVersion}:aggregationModel`;
1375
+ const lsRaw = localStorage.getItem(lsKey);
1376
+ const lsAggregation = lsRaw ? JSON.parse(lsRaw) : '';
1377
+
1378
+ const urlAggregation = url.get('_aggregation');
1379
+ if (lsAggregation && urlAggregation) {
1380
+ // Both use same format: `_aggregation=field.func,...`
1381
+ const lsParams = new URLSearchParams(lsAggregation);
1382
+ const lsValue = lsParams.get('_aggregation') ?? '';
1383
+ expect(urlAggregation).toBe(lsValue);
1384
+ }
1385
+ }
1386
+
1387
+ // --- Pivot ---
1388
+ if (checkPivot) {
1389
+ const lsKey = `${pathname}:${localStorageVersion}:pivotModel`;
1390
+ const lsRaw = localStorage.getItem(lsKey);
1391
+ const lsPivot = lsRaw ? JSON.parse(lsRaw) : '';
1392
+
1393
+ const urlPivot = url.get('_pivot');
1394
+ if (lsPivot && urlPivot) {
1395
+ // Both use same format: `_pivot=cols:f1;rows:f2;vals:f3.sum`
1396
+ const lsParams = new URLSearchParams(lsPivot);
1397
+ const lsValue = lsParams.get('_pivot') ?? '';
1398
+ expect(urlPivot).toBe(lsValue);
1399
+ }
1400
+ }
1331
1401
  },
1332
1402
  { timeout: 5000 }
1333
1403
  );
@@ -92,9 +92,9 @@ import {
92
92
  RU7_RM_PASTRY,
93
93
  } from '../_shared/expected-values';
94
94
 
95
- const meta: Meta<typeof Example> = {
95
+ const meta: Meta = {
96
96
  title: 'Patterns/Crossfiltered Datagrid (Client)',
97
- component: Example,
97
+ component: Example as any,
98
98
  };
99
99
  export default meta;
100
100
  type Story = StoryObj;
@@ -127,8 +127,7 @@ const assertState = async (
127
127
  };
128
128
 
129
129
  // ---------------------------------------------------------------------------
130
- // Comprehensive toggle sequence — split into Forward and Reverse halves so
131
- // each story stays within the Storybook test-runner's timeout.
130
+ // Comprehensive toggle sequence.
132
131
  //
133
132
  // Forward: 11 clicks across all 5 dimensions (Category, InStock, Allergens,
134
133
  // Items, Tags). Then uncheck all 8 persistent filters one by one.
@@ -139,7 +138,7 @@ const assertState = async (
139
138
  // filtered aggregates exclude the queried field's own filter.
140
139
  // ---------------------------------------------------------------------------
141
140
 
142
- export const ComprehensiveToggleForward: Story = {
141
+ export const ComprehensiveToggleSequence: Story = {
143
142
  render: () => <Example />,
144
143
  play: async ({ canvasElement }) => {
145
144
  const canvas = within(canvasElement);
@@ -231,20 +230,6 @@ export const ComprehensiveToggleForward: Story = {
231
230
  // -- U8: -Seasonal (bar) → back to full dataset -------------------------
232
231
  await clickBarChartBar(canvasElement, 'Seasonal');
233
232
  await assertState(canvasElement, S0_NONE, categoryListbox, allergenListbox);
234
- },
235
- };
236
-
237
- export const ComprehensiveToggleReverse: Story = {
238
- render: () => <Example />,
239
- play: async ({ canvasElement }) => {
240
- const canvas = within(canvasElement);
241
- await waitForGridToLoad(canvas);
242
-
243
- const categoryListbox = getListbox(canvas, /filter by category/i);
244
- const allergenListbox = getListbox(canvas, /filter by allergen/i);
245
-
246
- // == Step 0: No filters — full dataset ==================================
247
- await assertState(canvasElement, S0_NONE, categoryListbox, allergenListbox);
248
233
 
249
234
  // == Reverse sequence (add filters in opposite order) ===================
250
235
 
@@ -18,7 +18,7 @@ export default () => (
18
18
  {/* Category card — two-way: counts update when other cards filter */}
19
19
  <WithFilters
20
20
  field="Category"
21
- dimension={(d: Row) => d.Category}
21
+ dimension={(d: any) => d.Category}
22
22
  datagridCategoryDimFilter={{ field: 'Category', operator: 'isAnyOf' }}
23
23
  syncMode="two-way"
24
24
  >
@@ -51,7 +51,7 @@ export default () => (
51
51
  {/* Allergens card — two-way */}
52
52
  <WithFilters
53
53
  field="Allergens"
54
- dimension={(d: Row) => d.Allergens}
54
+ dimension={(d: any) => d.Allergens}
55
55
  isDimensionArray
56
56
  datagridCategoryDimFilter={{ field: 'Allergens', operator: 'hasAnyOf' }}
57
57
  syncMode="two-way"
@@ -92,9 +92,9 @@ import {
92
92
  RU7_RM_PASTRY,
93
93
  } from '../_shared/expected-values';
94
94
 
95
- const meta: Meta<typeof Example> = {
95
+ const meta: Meta = {
96
96
  title: 'Patterns/Crossfiltered Datagrid (Server)',
97
- component: Example,
97
+ component: Example as any,
98
98
  parameters: {
99
99
  msw: { handlers: bakeryHandlers },
100
100
  },
@@ -133,7 +133,7 @@ const assertState = async (
133
133
  // Comprehensive toggle sequence — identical to client but with server waits.
134
134
  //
135
135
  // Split into Forward and Reverse halves so each story stays within the
136
- // Storybook test-runner's timeout (38 total steps is too many for one story).
136
+ // Storybook test-runner's 15-second timeout (38 total steps is too many).
137
137
  // ---------------------------------------------------------------------------
138
138
 
139
139
  export const ComprehensiveToggleForward: Story = {
@@ -4,7 +4,7 @@ import { Flexbox } from '@redsift/design-system';
4
4
  import { DataCard, DataRow } from '@redsift/dashboard';
5
5
  import { BarChart, PieChart, ArcDatum, BarDatum } from '@redsift/charts';
6
6
  import { mdiShapeOutline, mdiToggleSwitch, mdiFoodOff } from '@redsift/icons';
7
- import { GridFilterModel, GridSortModel } from '@mui/x-data-grid-pro';
7
+ import { GridFilterModel, GridSortModel } from '@mui/x-data-grid-premium';
8
8
  import { Row } from '../_shared/data';
9
9
  import { columns, CATEGORY_OPTIONS, ALLERGEN_OPTIONS } from '../_shared/columns';
10
10
  import { fetchBakeryData, FetchResult } from '../_shared/api-client';
@@ -212,7 +212,7 @@ export default () => {
212
212
  data={itemsChartData}
213
213
  sliceRole="option"
214
214
  onSliceClick={(datum: ArcDatum) => {
215
- const key = datum.data.key as string;
215
+ const key = datum.data.key;
216
216
  setFilterModel((prev) => {
217
217
  const selected = getSelectedFromFilterModel(prev, 'Items');
218
218
  const next = selected.includes(key) ? selected.filter((v) => v !== key) : [...selected, key];
@@ -221,7 +221,7 @@ export default () => {
221
221
  setPage(0);
222
222
  }}
223
223
  isSliceSelected={(datum: ArcDatum) =>
224
- itemsSelection.length === 0 || itemsSelection.includes(datum.data.key as string)
224
+ itemsSelection.length === 0 || itemsSelection.includes(datum.data.key)
225
225
  }
226
226
  />
227
227
 
@@ -247,7 +247,7 @@ export default () => {
247
247
  data={tagsChartData}
248
248
  barRole="option"
249
249
  onBarClick={(datum: BarDatum) => {
250
- const key = datum.data.key as string;
250
+ const key = String(datum.data.key);
251
251
  setFilterModel((prev) => {
252
252
  const selected = getSelectedFromFilterModel(prev, 'Tags');
253
253
  const next = selected.includes(key) ? selected.filter((v) => v !== key) : [...selected, key];
@@ -256,7 +256,7 @@ export default () => {
256
256
  setPage(0);
257
257
  }}
258
258
  isBarSelected={(datum: BarDatum) =>
259
- tagsSelection.length === 0 || tagsSelection.includes(datum.data.key as string)
259
+ tagsSelection.length === 0 || tagsSelection.includes(String(datum.data.key))
260
260
  }
261
261
  />
262
262
  </Flexbox>
@@ -35,9 +35,9 @@ import {
35
35
  ALLERGENS_HASANYOF_GLUTEN,
36
36
  } from '../_shared/expected-values';
37
37
 
38
- const meta: Meta<typeof Example> = {
38
+ const meta: Meta = {
39
39
  title: 'Patterns/Drilldowned Datagrid (Client)',
40
- component: Example,
40
+ component: Example as any,
41
41
  };
42
42
  export default meta;
43
43
  type Story = StoryObj;
@@ -3,7 +3,7 @@ import { DataGrid } from '@redsift/table';
3
3
  import { Flexbox } from '@redsift/design-system';
4
4
  import { DataCard, DataRow } from '@redsift/dashboard';
5
5
  import { mdiShapeOutline, mdiToggleSwitch, mdiFoodOff } from '@redsift/icons';
6
- import { GridFilterModel } from '@mui/x-data-grid-pro';
6
+ import { GridFilterModel } from '@mui/x-data-grid-premium';
7
7
  import { rows } from '../_shared/data';
8
8
  import { columns, CATEGORY_OPTIONS, ALLERGEN_OPTIONS } from '../_shared/columns';
9
9
  import { CustomToolbar } from '../_shared/helpers';
@@ -37,9 +37,9 @@ import {
37
37
  ALLERGENS_HASANYOF_GLUTEN,
38
38
  } from '../_shared/expected-values';
39
39
 
40
- const meta: Meta<typeof Example> = {
40
+ const meta: Meta = {
41
41
  title: 'Patterns/Drilldowned Datagrid (Server)',
42
- component: Example,
42
+ component: Example as any,
43
43
  parameters: {
44
44
  msw: { handlers: bakeryHandlers },
45
45
  },
@@ -3,7 +3,7 @@ import { DataGrid } from '@redsift/table';
3
3
  import { Flexbox } from '@redsift/design-system';
4
4
  import { DataCard, DataRow } from '@redsift/dashboard';
5
5
  import { mdiShapeOutline, mdiToggleSwitch, mdiFoodOff } from '@redsift/icons';
6
- import { GridFilterModel, GridSortModel } from '@mui/x-data-grid-pro';
6
+ import { GridFilterModel, GridSortModel } from '@mui/x-data-grid-premium';
7
7
  import { Row } from '../_shared/data';
8
8
  import { columns, CATEGORY_OPTIONS, ALLERGEN_OPTIONS } from '../_shared/columns';
9
9
  import { fetchBakeryData, FetchResult } from '../_shared/api-client';
@@ -101,29 +101,15 @@ export default () => {
101
101
  });
102
102
  }, []);
103
103
 
104
- // Track the latest filterModel in a ref so handleFilterModelChange can compare
105
- // incoming models without needing filterModel in its dependency array.
106
- const filterModelRef = useRef(filterModel);
107
- filterModelRef.current = filterModel;
108
-
109
104
  const handleFilterModelChange = useCallback((model: GridFilterModel) => {
110
105
  if (isInternalUpdate.current) {
111
106
  isInternalUpdate.current = false;
112
- // DataCard change is authoritative — cancel any pending debounce so a
113
- // stale panel echo doesn't override the next card click.
114
- if (debounceRef.current) clearTimeout(debounceRef.current);
107
+ setFilterModel((prev) => {
108
+ if (JSON.stringify(prev.items) === JSON.stringify(model.items)) return prev;
109
+ return model;
110
+ });
115
111
  return;
116
112
  }
117
- // MUI v7 may fire additional onFilterModelChange echoes (with new IDs) after
118
- // a DataCard update. If the incoming model is semantically identical to current
119
- // state (same fields, operators, values — ignoring IDs), skip it.
120
- const normalize = (items: GridFilterModel['items']) =>
121
- items
122
- .map(({ field, operator, value }) => `${field}|${operator}|${JSON.stringify(value)}`)
123
- .sort()
124
- .join(';;');
125
- if (normalize(model.items) === normalize(filterModelRef.current.items)) return;
126
-
127
113
  // Panel change — debounce and update cardControlledFields.
128
114
  if (debounceRef.current) clearTimeout(debounceRef.current);
129
115
  debounceRef.current = setTimeout(() => {
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  import type { Meta, StoryObj } from '@storybook/react';
3
3
  import { within, userEvent } from '@storybook/testing-library';
4
4
  import { expect } from '@storybook/jest';
5
- import { GridFilterModel } from '@mui/x-data-grid-pro';
5
+ import { GridFilterModel } from '@mui/x-data-grid-premium';
6
6
 
7
7
  import Example from './example';
8
8
  import WithLoadingExample from './with-loading';
@@ -52,9 +52,9 @@ import {
52
52
  TAGS_HASANYOF_LOCAL_NEW,
53
53
  } from '../_shared/expected-values';
54
54
 
55
- const meta: Meta<typeof Example> = {
55
+ const meta: Meta = {
56
56
  title: 'Patterns/Single Datagrid (Client)',
57
- component: Example,
57
+ component: Example as any,
58
58
  };
59
59
  export default meta;
60
60
  type Story = StoryObj;