@redsift/ds-mcp-server 12.5.4 → 12.5.5-muiv7
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.
- package/consumer-instructions/redsift-design-system.instructions.md +1 -82
- package/data/demos/patterns/_shared/StateDebugPanel.tsx +2 -2
- package/data/demos/patterns/_shared/columns.tsx +3 -3
- package/data/demos/patterns/_shared/defaults.ts +1 -1
- package/data/demos/patterns/_shared/filter-helpers.ts +1 -1
- package/data/demos/patterns/_shared/helpers.tsx +1 -1
- package/data/demos/patterns/_shared/server-logic.ts +1 -1
- package/data/demos/patterns/_shared/story-helpers.ts +36 -106
- package/data/demos/patterns/crossfiltered-datagrid-client-side/CrossfilteredDatagridClientSide.interaction.stories.tsx +19 -4
- package/data/demos/patterns/crossfiltered-datagrid-client-side/example.tsx +2 -2
- package/data/demos/patterns/crossfiltered-datagrid-server-side/CrossfilteredDatagridServerSide.interaction.stories.tsx +3 -3
- package/data/demos/patterns/crossfiltered-datagrid-server-side/example.tsx +5 -5
- package/data/demos/patterns/drilldowned-datagrid-client-side/DrilldownedDatagridClientSide.interaction.stories.tsx +2 -2
- package/data/demos/patterns/drilldowned-datagrid-client-side/example.tsx +1 -1
- package/data/demos/patterns/drilldowned-datagrid-server-side/DrilldownedDatagridServerSide.interaction.stories.tsx +2 -2
- package/data/demos/patterns/drilldowned-datagrid-server-side/example.tsx +19 -5
- package/data/demos/patterns/single-datagrid-client-side/SingleDatagridClientSide.interaction.stories.tsx +3 -3
- package/data/demos/patterns/single-datagrid-client-side/example.tsx +5 -5
- package/data/demos/patterns/single-datagrid-server-side/SingleDatagridServerSide.interaction.stories.tsx +3 -3
- package/data/demos/patterns/single-datagrid-server-side/example.tsx +6 -5
- package/data/demos/patterns/stateful-single-datagrid-client-side/StatefulSingleDatagridClientSide.interaction.stories.tsx +3 -130
- package/data/demos/patterns/stateful-single-datagrid-client-side/example.tsx +6 -6
- package/data/demos/patterns/stateful-single-datagrid-server-side/StatefulSingleDatagridServerSide.interaction.stories.tsx +6 -136
- package/data/demos/patterns/stateful-single-datagrid-server-side/example.tsx +9 -6
- package/data/demos/patterns/summary-dashboard/SummaryDashboard.interaction.stories.tsx +2 -2
- package/data/demos/patterns/tabbed-datagrid-client-side/TabbedDatagridClientSide.interaction.stories.tsx +2 -2
- package/data/demos/patterns/tabbed-datagrid-server-side/TabbedDatagridServerSide.interaction.stories.tsx +2 -2
- package/data/demos/patterns/tabbed-datagrid-server-side/example.tsx +1 -1
- package/data/docs/components/charts/Axis.json +1 -6
- package/data/docs/components/charts/BarChart.json +1 -7
- package/data/docs/components/charts/ChartContainerTitle.json +1 -5
- package/data/docs/components/charts/Legend.json +1 -6
- package/data/docs/components/charts/LineChart.json +1 -7
- package/data/docs/components/charts/PieChart.json +1 -6
- package/data/docs/components/charts/ScatterPlot.json +1 -6
- package/data/docs/components/dashboard/ChartEmptyState.json +1 -8
- package/data/docs/components/dashboard/Dashboard.json +3 -8
- package/data/docs/components/dashboard/DataCard.json +0 -12
- package/data/docs/components/dashboard/DataCardBody.json +0 -4
- package/data/docs/components/dashboard/DataCardHeader.json +0 -4
- package/data/docs/components/dashboard/DataCardListbox.json +0 -5
- package/data/docs/components/dashboard/DataRow.json +1 -7
- package/data/docs/components/dashboard/PdfExportButton.json +1 -6
- package/data/docs/components/dashboard/TimeSeriesBarChart.json +1 -6
- package/data/docs/components/dashboard/WithFilters.json +1 -5
- package/data/docs/components/design-system/Alert.json +1 -8
- package/data/docs/components/design-system/AppBar.json +1 -6
- package/data/docs/components/design-system/AppContent.json +0 -4
- package/data/docs/components/design-system/AppSidePanel.json +0 -5
- package/data/docs/components/design-system/Badge.json +1 -6
- package/data/docs/components/design-system/Breadcrumbs.json +0 -4
- package/data/docs/components/design-system/Button.json +0 -5
- package/data/docs/components/design-system/Card.json +0 -9
- package/data/docs/components/design-system/CardActions.json +0 -4
- package/data/docs/components/design-system/CardBody.json +0 -3
- package/data/docs/components/design-system/CardHeader.json +0 -4
- package/data/docs/components/design-system/DetailedCard.json +0 -6
- package/data/docs/components/design-system/Flexbox.json +1 -14
- package/data/docs/components/design-system/Grid.json +1 -6
- package/data/docs/components/design-system/Heading.json +0 -11
- package/data/docs/components/design-system/Icon.json +1 -6
- package/data/docs/components/design-system/IconButton.json +0 -9
- package/data/docs/components/design-system/Pill.json +0 -10
- package/data/docs/components/design-system/Skeleton.json +1 -10
- package/data/docs/components/design-system/SkeletonCircle.json +1 -6
- package/data/docs/components/design-system/SkeletonText.json +1 -6
- package/data/docs/components/design-system/Tab.json +0 -4
- package/data/docs/components/design-system/TabPanel.json +0 -4
- package/data/docs/components/design-system/Tabs.json +0 -6
- package/data/docs/components/design-system/Text.json +0 -9
- package/data/docs/components/design-system/TextField.json +1 -6
- package/data/docs/components/pickers/Combobox.json +0 -6
- package/data/docs/components/pickers/MenuButton.json +0 -5
- package/data/docs/components/pickers/Select.json +0 -5
- package/data/docs/components/popovers/Dialog.json +0 -6
- package/data/docs/components/popovers/Toggletip.json +0 -5
- package/data/docs/components/popovers/Tooltip.json +0 -4
- package/data/docs/components/products/DmarcSummaryBoxes.json +1 -1
- package/data/docs/components/products/SignalCardAmp.json +16 -0
- package/data/docs/components/products/SignalCardBimi.json +16 -0
- package/data/docs/components/products/SignalCardDkim.json +16 -0
- package/data/docs/components/products/SignalCardDkimDomainAnalyzer.json +16 -0
- package/data/docs/components/products/SignalCardDmarc.json +16 -0
- package/data/docs/components/products/SignalCardDmarcDomain.json +16 -0
- package/data/docs/components/products/SignalCardDnssec.json +16 -0
- package/data/docs/components/products/SignalCardFcrdns.json +16 -0
- package/data/docs/components/products/SignalCardGoogleYahooCompliance.json +16 -0
- package/data/docs/components/products/SignalCardList.json +16 -0
- package/data/docs/components/products/SignalCardMtaSts.json +16 -0
- package/data/docs/components/products/SignalCardSpf.json +16 -0
- package/data/docs/components/products/SignalCardSpfDomain.json +16 -0
- package/data/docs/components/products/SignalCardSpfDomainAnalyzer.json +16 -0
- package/data/docs/components/products/SignalCardSubdo.json +16 -0
- package/data/docs/components/products/SignalCardThreatInv.json +16 -0
- package/data/docs/components/products/SignalCardTls.json +16 -0
- package/data/docs/components/products/SignalCardUrls.json +16 -0
- package/data/docs/components/table/DataGrid.json +46 -14
- package/data/docs/components/table/StatefulDataGrid.json +47 -12
- package/data/docs/components/table/ToolbarWrapper.json +1 -1
- package/data/docs/components-index.json +72 -431
- package/data/docs/components.json +795 -1000
- package/data/docs/llms-full.txt +117 -448
- package/data/docs/llms.txt +26 -88
- package/data/docs/patterns-catalog.md +25 -215
- package/data/docs/patterns.json +31 -369
- package/data/metadata.json +2 -2
- package/data/patterns/crossfiltered-datagrid-server-side.mdx +1 -1
- package/data/patterns/drilldowned-datagrid-client-side.mdx +1 -1
- package/data/patterns/drilldowned-datagrid-server-side.mdx +1 -1
- package/data/patterns/single-datagrid-client-side.mdx +9 -9
- package/data/patterns/single-datagrid-server-side.mdx +4 -4
- package/data/patterns/stateful-single-datagrid-client-side.mdx +20 -36
- package/data/patterns/stateful-single-datagrid-server-side.mdx +18 -46
- package/data/patterns/tabbed-datagrid-server-side.mdx +1 -1
- package/dist/data-store.d.ts +1 -21
- package/dist/data-store.d.ts.map +1 -1
- package/dist/data-store.js +15 -65
- package/dist/data-store.js.map +1 -1
- package/dist/pattern-store.d.ts +1 -18
- package/dist/pattern-store.d.ts.map +1 -1
- package/dist/pattern-store.js +22 -64
- package/dist/pattern-store.js.map +1 -1
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +27 -56
- package/dist/prompts.js.map +1 -1
- package/dist/resources.d.ts.map +1 -1
- package/dist/resources.js +0 -26
- package/dist/resources.js.map +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +0 -12
- package/dist/tools.js.map +1 -1
- package/dist/types.d.ts +0 -11
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +2 -4
- package/data/docs/components/table-pro/ControlledPagination.json +0 -9
- package/data/docs/components/table-pro/DataGrid.json +0 -101
- package/data/docs/components/table-pro/GridToolbarFilterSemanticField.json +0 -69
- package/data/docs/components/table-pro/ServerSideControlledPagination.json +0 -9
- package/data/docs/components/table-pro/StatefulDataGrid.json +0 -122
- package/data/docs/components/table-pro/TextCell.json +0 -118
- package/data/docs/components/table-pro/Toolbar.json +0 -151
- package/data/docs/components/table-pro/ToolbarWrapper.json +0 -9
- package/data/prompts/ds-advisor.md +0 -103
|
@@ -15,7 +15,6 @@ 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
|
|
19
18
|
|
|
20
19
|
## Rule 1 — Mandatory Prop Lookup
|
|
21
20
|
|
|
@@ -55,43 +54,9 @@ When adopting `@redsift/*` components in a new project or migrating from raw HTM
|
|
|
55
54
|
4. Import icons from `@redsift/icons`, not `@mdi/js`.
|
|
56
55
|
5. Remove conflicting Tailwind/utility-CSS classes (`bg-*`, `text-*`, `border-*`, `dark:*`) from elements that render or wrap DS components.
|
|
57
56
|
|
|
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
|
-
|
|
92
57
|
## NEVER Do This
|
|
93
58
|
|
|
94
|
-
These are concrete examples of wrong code. **Every single one will fail
|
|
59
|
+
These are concrete examples of wrong code. **Every single one will fail:**
|
|
95
60
|
|
|
96
61
|
```tsx
|
|
97
62
|
// ❌ WRONG — Button has NO size prop
|
|
@@ -107,46 +72,6 @@ These are concrete examples of wrong code. **Every single one will fail or produ
|
|
|
107
72
|
|
|
108
73
|
// ❌ WRONG — wrong package
|
|
109
74
|
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';
|
|
150
75
|
```
|
|
151
76
|
|
|
152
77
|
## Common Mistakes
|
|
@@ -166,12 +91,6 @@ import { mdiPlus } from '@redsift/icons';
|
|
|
166
91
|
| Missing `@redsift/design-system/style/index.css` | Must import at app entry point | `design-system://getting-started/host-app` |
|
|
167
92
|
| `import { mdiPlus } from '@mdi/js'` | `import { mdiPlus } from '@redsift/icons'` | `get_component_usage` |
|
|
168
93
|
| 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 |
|
|
175
94
|
|
|
176
95
|
## Verification
|
|
177
96
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
2
|
-
import type {
|
|
2
|
+
import type { GridApiPro } from '@mui/x-data-grid-pro/models/gridApiPro';
|
|
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<
|
|
16
|
+
apiRef: React.MutableRefObject<GridApiPro>;
|
|
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-
|
|
5
|
+
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid-pro';
|
|
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:
|
|
113
|
+
valueGetter: (value: unknown) => 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:
|
|
123
|
+
valueGetter: (value: unknown) => 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-
|
|
1
|
+
import { GridFilterModel, GridSortModel } from '@mui/x-data-grid-pro';
|
|
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
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
()
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
},
|
|
378
|
-
{ timeout: 5000 }
|
|
379
|
-
);
|
|
380
|
-
const rows = canvasElement.querySelectorAll('.MuiDataGrid-row');
|
|
381
|
-
const checkbox = rows[rowIndex].querySelector('input[type="checkbox"]')!;
|
|
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;
|
|
382
377
|
await userEvent.click(checkbox);
|
|
383
|
-
// Wait for
|
|
384
|
-
await
|
|
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
|
+
});
|
|
385
385
|
};
|
|
386
386
|
|
|
387
387
|
// ---------------------------------------------------------------------------
|
|
@@ -952,42 +952,29 @@ 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
|
-
*
|
|
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.
|
|
955
|
+
* The dropdown menu renders as a portal on document.body.
|
|
962
956
|
*/
|
|
963
957
|
export const changePageSize = async (canvasElement: HTMLElement, newSize: number) => {
|
|
964
|
-
// Wait for the
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
// Use fireEvent.mouseDown to reliably open MUI v7 Select dropdown
|
|
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
|
|
976
968
|
fireEvent.mouseDown(select);
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
()
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
},
|
|
984
|
-
{ timeout: 5000 }
|
|
985
|
-
);
|
|
986
|
-
|
|
987
|
-
const options = document.querySelectorAll('[role="option"]');
|
|
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"]');
|
|
988
975
|
const target = Array.from(options).find((opt) => opt.textContent === String(newSize));
|
|
989
976
|
if (!target) throw new Error(`Could not find page size option "${newSize}"`);
|
|
990
|
-
|
|
977
|
+
fireEvent.click(target);
|
|
991
978
|
};
|
|
992
979
|
|
|
993
980
|
// ---------------------------------------------------------------------------
|
|
@@ -1001,7 +988,7 @@ export const openColumnsPanel = async (canvasElement: HTMLElement) => {
|
|
|
1001
988
|
) as HTMLElement | null;
|
|
1002
989
|
if (!btn) throw new Error('Could not find columns toolbar button');
|
|
1003
990
|
await userEvent.click(btn);
|
|
1004
|
-
// Wait for the panel to appear on document.body
|
|
991
|
+
// Wait for the panel to appear on document.body (v7: columnsManagement, v6: columnsPanel)
|
|
1005
992
|
await waitFor(() => {
|
|
1006
993
|
const panel = document.querySelector('.MuiDataGrid-columnsManagement');
|
|
1007
994
|
expect(panel).toBeTruthy();
|
|
@@ -1012,9 +999,8 @@ export const openColumnsPanel = async (canvasElement: HTMLElement) => {
|
|
|
1012
999
|
export const toggleColumnInPanel = async (fieldLabel: string) => {
|
|
1013
1000
|
const panel = document.querySelector('.MuiDataGrid-columnsManagement');
|
|
1014
1001
|
if (!panel) throw new Error('Columns panel is not open');
|
|
1015
|
-
|
|
1016
|
-
const
|
|
1017
|
-
const target = rows.find((row) => row.textContent?.includes(fieldLabel));
|
|
1002
|
+
const labels = Array.from(panel.querySelectorAll('.MuiFormControlLabel-root'));
|
|
1003
|
+
const target = labels.find((label) => label.textContent?.includes(fieldLabel));
|
|
1018
1004
|
if (!target) throw new Error(`Could not find column "${fieldLabel}" in columns panel`);
|
|
1019
1005
|
const checkbox = target.querySelector('input[type="checkbox"]') as HTMLElement | null;
|
|
1020
1006
|
if (!checkbox) throw new Error(`Could not find checkbox for column "${fieldLabel}"`);
|
|
@@ -1218,12 +1204,6 @@ interface SyncAssertionOptions {
|
|
|
1218
1204
|
checkDensity?: boolean;
|
|
1219
1205
|
/** Whether to check column order sync */
|
|
1220
1206
|
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;
|
|
1227
1207
|
}
|
|
1228
1208
|
|
|
1229
1209
|
/**
|
|
@@ -1242,9 +1222,6 @@ export const assertAllStatesInSync = async ({
|
|
|
1242
1222
|
checkPagination = true,
|
|
1243
1223
|
checkDensity = true,
|
|
1244
1224
|
checkColumnOrder = false,
|
|
1245
|
-
checkRowGrouping = false,
|
|
1246
|
-
checkAggregation = false,
|
|
1247
|
-
checkPivot = false,
|
|
1248
1225
|
}: SyncAssertionOptions) => {
|
|
1249
1226
|
await waitFor(
|
|
1250
1227
|
() => {
|
|
@@ -1351,53 +1328,6 @@ export const assertAllStatesInSync = async ({
|
|
|
1351
1328
|
expect(urlColumnOrder).toBe(inner);
|
|
1352
1329
|
}
|
|
1353
1330
|
}
|
|
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
|
-
}
|
|
1401
1331
|
},
|
|
1402
1332
|
{ timeout: 5000 }
|
|
1403
1333
|
);
|
|
@@ -92,9 +92,9 @@ import {
|
|
|
92
92
|
RU7_RM_PASTRY,
|
|
93
93
|
} from '../_shared/expected-values';
|
|
94
94
|
|
|
95
|
-
const meta: Meta = {
|
|
95
|
+
const meta: Meta<typeof Example> = {
|
|
96
96
|
title: 'Patterns/Crossfiltered Datagrid (Client)',
|
|
97
|
-
component: Example
|
|
97
|
+
component: Example,
|
|
98
98
|
};
|
|
99
99
|
export default meta;
|
|
100
100
|
type Story = StoryObj;
|
|
@@ -127,7 +127,8 @@ const assertState = async (
|
|
|
127
127
|
};
|
|
128
128
|
|
|
129
129
|
// ---------------------------------------------------------------------------
|
|
130
|
-
// Comprehensive toggle sequence
|
|
130
|
+
// Comprehensive toggle sequence — split into Forward and Reverse halves so
|
|
131
|
+
// each story stays within the Storybook test-runner's timeout.
|
|
131
132
|
//
|
|
132
133
|
// Forward: 11 clicks across all 5 dimensions (Category, InStock, Allergens,
|
|
133
134
|
// Items, Tags). Then uncheck all 8 persistent filters one by one.
|
|
@@ -138,7 +139,7 @@ const assertState = async (
|
|
|
138
139
|
// filtered aggregates exclude the queried field's own filter.
|
|
139
140
|
// ---------------------------------------------------------------------------
|
|
140
141
|
|
|
141
|
-
export const
|
|
142
|
+
export const ComprehensiveToggleForward: Story = {
|
|
142
143
|
render: () => <Example />,
|
|
143
144
|
play: async ({ canvasElement }) => {
|
|
144
145
|
const canvas = within(canvasElement);
|
|
@@ -230,6 +231,20 @@ export const ComprehensiveToggleSequence: Story = {
|
|
|
230
231
|
// -- U8: -Seasonal (bar) → back to full dataset -------------------------
|
|
231
232
|
await clickBarChartBar(canvasElement, 'Seasonal');
|
|
232
233
|
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);
|
|
233
248
|
|
|
234
249
|
// == Reverse sequence (add filters in opposite order) ===================
|
|
235
250
|
|
|
@@ -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:
|
|
21
|
+
dimension={(d: Row) => 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:
|
|
54
|
+
dimension={(d: Row) => 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 = {
|
|
95
|
+
const meta: Meta<typeof Example> = {
|
|
96
96
|
title: 'Patterns/Crossfiltered Datagrid (Server)',
|
|
97
|
-
component: Example
|
|
97
|
+
component: Example,
|
|
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
|
|
136
|
+
// Storybook test-runner's timeout (38 total steps is too many for one story).
|
|
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-
|
|
7
|
+
import { GridFilterModel, GridSortModel } from '@mui/x-data-grid-pro';
|
|
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;
|
|
215
|
+
const key = datum.data.key as string;
|
|
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)
|
|
224
|
+
itemsSelection.length === 0 || itemsSelection.includes(datum.data.key as string)
|
|
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 =
|
|
250
|
+
const key = datum.data.key as string;
|
|
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(
|
|
259
|
+
tagsSelection.length === 0 || tagsSelection.includes(datum.data.key as string)
|
|
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 = {
|
|
38
|
+
const meta: Meta<typeof Example> = {
|
|
39
39
|
title: 'Patterns/Drilldowned Datagrid (Client)',
|
|
40
|
-
component: Example
|
|
40
|
+
component: Example,
|
|
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-
|
|
6
|
+
import { GridFilterModel } from '@mui/x-data-grid-pro';
|
|
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 = {
|
|
40
|
+
const meta: Meta<typeof Example> = {
|
|
41
41
|
title: 'Patterns/Drilldowned Datagrid (Server)',
|
|
42
|
-
component: Example
|
|
42
|
+
component: Example,
|
|
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-
|
|
6
|
+
import { GridFilterModel, GridSortModel } from '@mui/x-data-grid-pro';
|
|
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,15 +101,29 @@ 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
|
+
|
|
104
109
|
const handleFilterModelChange = useCallback((model: GridFilterModel) => {
|
|
105
110
|
if (isInternalUpdate.current) {
|
|
106
111
|
isInternalUpdate.current = false;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
});
|
|
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);
|
|
111
115
|
return;
|
|
112
116
|
}
|
|
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
|
+
|
|
113
127
|
// Panel change — debounce and update cardControlledFields.
|
|
114
128
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
115
129
|
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-
|
|
5
|
+
import { GridFilterModel } from '@mui/x-data-grid-pro';
|
|
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 = {
|
|
55
|
+
const meta: Meta<typeof Example> = {
|
|
56
56
|
title: 'Patterns/Single Datagrid (Client)',
|
|
57
|
-
component: Example
|
|
57
|
+
component: Example,
|
|
58
58
|
};
|
|
59
59
|
export default meta;
|
|
60
60
|
type Story = StoryObj;
|