@recursica/adapter-mui-v7 1.1.0 → 1.3.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.
- package/CHANGELOG.md +17 -0
- package/OVERSTYLING.md +1 -1
- package/SETUP.md +26 -3
- package/dist/adapter-mui-v7.cjs +69 -69
- package/dist/adapter-mui-v7.cjs.map +1 -1
- package/dist/adapter-mui-v7.css +1 -1
- package/dist/adapter-mui-v7.js +5955 -5739
- package/dist/adapter-mui-v7.js.map +1 -1
- package/dist/index.d.ts +69 -51
- package/llms.txt +1 -1
- package/package.json +4 -4
- package/recursica_brand.json +2 -2
- package/recursica_tokens.json +2 -2
- package/recursica_ui-kit.json +2 -2
- package/recursica_variables_scoped.css +7 -3
- package/src/components/Checkbox/Checkbox.module.css +1 -3
- package/src/components/Container/USAGE.md +0 -1
- package/src/components/FileInput/FileInput.module.css +8 -0
- package/src/components/FileUpload/FileUpload.module.css +1 -0
- package/src/components/FileUpload/FileUpload.tsx +1 -0
- package/src/components/Flex/USAGE.md +2 -1
- package/src/components/Group/USAGE.md +2 -1
- package/src/components/LayoutGrid/LAYOUT_GRID_IMPLEMENTATION_NOTES.md +82 -0
- package/src/components/LayoutGrid/LayoutGrid.columns.dom.test.tsx +78 -0
- package/src/components/{Grid/Grid.module.css → LayoutGrid/LayoutGrid.module.css} +11 -10
- package/src/components/LayoutGrid/LayoutGrid.stories.tsx +61 -0
- package/src/components/LayoutGrid/LayoutGrid.test.tsx +1 -0
- package/src/components/LayoutGrid/LayoutGrid.tsx +181 -0
- package/src/components/LayoutGrid/USAGE.md +79 -0
- package/src/components/LayoutGrid/index.ts +1 -0
- package/src/components/Menu/Menu.stories.tsx +0 -3
- package/src/components/Pagination/IMPLEMENTATION_NOTES.md +42 -38
- package/src/components/Pagination/Pagination.icons.tsx +0 -32
- package/src/components/Pagination/Pagination.manifest.dom.test.tsx +105 -0
- package/src/components/Pagination/Pagination.module.css +25 -164
- package/src/components/Pagination/Pagination.tsx +221 -97
- package/src/components/Pagination/USAGE.md +8 -1
- package/src/components/Panel/Panel.module.css +24 -1
- package/src/components/Panel/Panel.stories.tsx +4 -4
- package/src/components/Panel/Panel.tsx +10 -1
- package/src/components/Radio/Radio.module.css +2 -2
- package/src/components/SegmentedControl/SegmentedControl.module.css +18 -0
- package/src/components/Stack/USAGE.md +2 -1
- package/src/components/Stepper/Stepper.module.css +0 -21
- package/src/components/index.ts +1 -1
- package/src/index.ts +1 -0
- package/src/utils/breakpointsFromRecManifest.test.ts +40 -0
- package/src/utils/breakpointsFromRecManifest.ts +72 -0
- package/src/utils/index.ts +1 -0
- package/src/components/Grid/Grid.stories.tsx +0 -151
- package/src/components/Grid/Grid.test.tsx +0 -1
- package/src/components/Grid/Grid.tsx +0 -189
- package/src/components/Grid/IMPLEMENTATION_NOTES.md +0 -99
- package/src/components/Grid/USAGE.md +0 -57
- package/src/components/Grid/index.ts +0 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
+
import { useRecursicaManifest } from "@recursica/adapter-common";
|
|
4
|
+
import { LayoutGrid } from "./LayoutGrid";
|
|
5
|
+
|
|
6
|
+
type LayoutGridStoryProps = React.ComponentProps<typeof LayoutGrid>;
|
|
7
|
+
|
|
8
|
+
const meta: Meta<LayoutGridStoryProps> = {
|
|
9
|
+
title: "UI-Kit/LayoutGrid",
|
|
10
|
+
component: LayoutGrid,
|
|
11
|
+
tags: ["autodocs"],
|
|
12
|
+
parameters: {
|
|
13
|
+
docs: {
|
|
14
|
+
description: {
|
|
15
|
+
component:
|
|
16
|
+
"LayoutGrid is the Recursica layout grid. Its column count, column-gutter, row-gutter and margin come from the design system's layout-grid tokens, which Forge defines per breakpoint, so they respond to the viewport and are not integrator-configurable.",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
23
|
+
|
|
24
|
+
type Story = StoryObj<LayoutGridStoryProps>;
|
|
25
|
+
|
|
26
|
+
// Plain bordered cell: Card has a token min-width wider than one column, which would overflow it.
|
|
27
|
+
const Swatch = ({ children }: { children: React.ReactNode }) => (
|
|
28
|
+
<div
|
|
29
|
+
style={{
|
|
30
|
+
border: "1px dashed currentColor",
|
|
31
|
+
padding: "8px",
|
|
32
|
+
textAlign: "center",
|
|
33
|
+
}}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// Reads the default layout grid from the manifest (brand.layout-grids.default) and fills two full
|
|
40
|
+
// rows of single-column cells, so the story shows exactly the design system's column count.
|
|
41
|
+
const DefaultGrid = () => {
|
|
42
|
+
const manifest = useRecursicaManifest() as {
|
|
43
|
+
brand: {
|
|
44
|
+
"layout-grids": { default: { columns: { $value: number } } };
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
const columns = manifest.brand["layout-grids"].default.columns.$value;
|
|
48
|
+
return (
|
|
49
|
+
<LayoutGrid>
|
|
50
|
+
{Array.from({ length: columns * 2 }, (_, i) => (
|
|
51
|
+
<LayoutGrid.Col key={i} size={1}>
|
|
52
|
+
<Swatch>{(i % columns) + 1}</Swatch>
|
|
53
|
+
</LayoutGrid.Col>
|
|
54
|
+
))}
|
|
55
|
+
</LayoutGrid>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const Default: Story = {
|
|
60
|
+
render: () => <DefaultGrid />,
|
|
61
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// Placeholder for future LayoutGrid component tests. Add tests here if/when needed.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LayoutGrid: the Recursica layout grid.
|
|
3
|
+
*
|
|
4
|
+
* MUI merges "container" and "item" into a single `Grid` component (no separate Grid.Col,
|
|
5
|
+
* unlike Mantine). To keep the LayoutGrid/LayoutGrid.Col dot-notation shape across adapters,
|
|
6
|
+
* LayoutGrid always renders MUI's `<Grid container>` and LayoutGrid.Col a plain MUI `<Grid>`.
|
|
7
|
+
*
|
|
8
|
+
* Recursica's `layout-grids` tokens (columns, column-gutter, row-gutter, margin) are
|
|
9
|
+
* design-system-managed and breakpoint-aware (Forge redefines them inside `@media` blocks), so
|
|
10
|
+
* LayoutGrid applies them itself through the responsive CSS variables, which MUI's own
|
|
11
|
+
* `columns`/`columnSpacing`/`rowSpacing` accept as strings. None of them are props. For a fixed
|
|
12
|
+
* N-column grid use MUI's Grid directly. See `LAYOUT_GRID_IMPLEMENTATION_NOTES.md`.
|
|
13
|
+
*
|
|
14
|
+
* MUI has no per-item "grow to fill remaining space" container flag like Mantine's `grow`;
|
|
15
|
+
* use MUI's own `size="grow"` on individual columns instead. `visibleFrom`/`hiddenFrom`
|
|
16
|
+
* don't exist in MUI at all, so they're added here via CSS classes keyed on MUI's default
|
|
17
|
+
* breakpoint scale (`xs`/`sm`/`md`/`lg`/`xl`).
|
|
18
|
+
*
|
|
19
|
+
* `justifyContent`/`alignItems` (container) and `order` (item) are typed on `MuiGridProps`
|
|
20
|
+
* but MUI's Grid style generator never reads them, so they're applied via inline `style`.
|
|
21
|
+
*
|
|
22
|
+
* Like Flex, Stack, Group, and Container, this is a primitive layout component and
|
|
23
|
+
* does not use the `RecursicaOverStyled` gatekeeper; only the `sx` prop is stripped.
|
|
24
|
+
*/
|
|
25
|
+
import { forwardRef, type CSSProperties } from "react";
|
|
26
|
+
import { Grid as MuiGrid, type GridProps as MuiGridProps } from "@mui/material";
|
|
27
|
+
import {
|
|
28
|
+
type OmitSx,
|
|
29
|
+
filterSxProp,
|
|
30
|
+
type WithRecursicaSpacing,
|
|
31
|
+
} from "../../utils/filterStylingProps";
|
|
32
|
+
import {
|
|
33
|
+
type RecursicaLayoutGridColProps,
|
|
34
|
+
type RecursicaLayoutGridProps,
|
|
35
|
+
} from "@recursica/adapter-common";
|
|
36
|
+
import styles from "./LayoutGrid.module.css";
|
|
37
|
+
|
|
38
|
+
type Breakpoint = "xs" | "sm" | "md" | "lg" | "xl";
|
|
39
|
+
|
|
40
|
+
const HIDDEN_FROM_CLASS: Record<Breakpoint, string> = {
|
|
41
|
+
xs: styles.hiddenFromXs,
|
|
42
|
+
sm: styles.hiddenFromSm,
|
|
43
|
+
md: styles.hiddenFromMd,
|
|
44
|
+
lg: styles.hiddenFromLg,
|
|
45
|
+
xl: styles.hiddenFromXl,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// "xs" intentionally absent: visible from the smallest breakpoint means never hidden.
|
|
49
|
+
const VISIBLE_FROM_CLASS: Partial<Record<Breakpoint, string>> = {
|
|
50
|
+
sm: styles.visibleFromSm,
|
|
51
|
+
md: styles.visibleFromMd,
|
|
52
|
+
lg: styles.visibleFromLg,
|
|
53
|
+
xl: styles.visibleFromXl,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// ============================================================
|
|
57
|
+
// LAYOUT GRID
|
|
58
|
+
// ============================================================
|
|
59
|
+
|
|
60
|
+
export type LayoutGridProps = WithRecursicaSpacing<
|
|
61
|
+
OmitSx<
|
|
62
|
+
Omit<
|
|
63
|
+
MuiGridProps,
|
|
64
|
+
| "container"
|
|
65
|
+
| "justifyContent"
|
|
66
|
+
| "alignItems"
|
|
67
|
+
| "spacing"
|
|
68
|
+
| "columnSpacing"
|
|
69
|
+
| "rowSpacing"
|
|
70
|
+
| "columns"
|
|
71
|
+
>
|
|
72
|
+
>
|
|
73
|
+
> &
|
|
74
|
+
RecursicaLayoutGridProps & {
|
|
75
|
+
/** Sets `justify-content` on the container. Applied via inline style. */
|
|
76
|
+
justifyContent?: CSSProperties["justifyContent"];
|
|
77
|
+
/** Sets `align-items` on the container. Applied via inline style. */
|
|
78
|
+
alignItems?: CSSProperties["alignItems"];
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// MUI types these as numbers, but its generator writes strings straight into CSS custom
|
|
82
|
+
// properties (verified in @mui/system gridGenerator), so the responsive var aliases work.
|
|
83
|
+
const COLUMNS =
|
|
84
|
+
"var(--recursica_brand_layout-grids_columns)" as unknown as number;
|
|
85
|
+
const COLUMN_GUTTER = "var(--recursica_brand_layout-grids_column-gutter)";
|
|
86
|
+
const ROW_GUTTER = "var(--recursica_brand_layout-grids_row-gutter)";
|
|
87
|
+
|
|
88
|
+
const LayoutGridBase = forwardRef<HTMLDivElement, LayoutGridProps>(
|
|
89
|
+
function LayoutGrid(
|
|
90
|
+
{ children, justifyContent, alignItems, style, className, ...rest },
|
|
91
|
+
ref,
|
|
92
|
+
) {
|
|
93
|
+
// Spacing/columns props are not supported; dropped so a caller still passing one at runtime
|
|
94
|
+
// can't shadow the token-driven values below.
|
|
95
|
+
const restWithoutLayout = { ...rest } as Record<string, unknown>;
|
|
96
|
+
delete restWithoutLayout.spacing;
|
|
97
|
+
delete restWithoutLayout.columnSpacing;
|
|
98
|
+
delete restWithoutLayout.rowSpacing;
|
|
99
|
+
delete restWithoutLayout.columns;
|
|
100
|
+
|
|
101
|
+
const safeProps = filterSxProp(restWithoutLayout);
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<MuiGrid
|
|
105
|
+
ref={ref}
|
|
106
|
+
{...(safeProps as unknown as MuiGridProps)}
|
|
107
|
+
container
|
|
108
|
+
columns={COLUMNS}
|
|
109
|
+
columnSpacing={COLUMN_GUTTER}
|
|
110
|
+
rowSpacing={ROW_GUTTER}
|
|
111
|
+
className={className ? `${styles.root} ${className}` : styles.root}
|
|
112
|
+
style={{ justifyContent, alignItems, ...(style as CSSProperties) }}
|
|
113
|
+
>
|
|
114
|
+
{children}
|
|
115
|
+
</MuiGrid>
|
|
116
|
+
);
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
LayoutGridBase.displayName = "LayoutGrid";
|
|
120
|
+
|
|
121
|
+
// ============================================================
|
|
122
|
+
// LAYOUT GRID.COL
|
|
123
|
+
// ============================================================
|
|
124
|
+
|
|
125
|
+
// TODO(grid-col-contract): `RecursicaLayoutGridColProps` currently only contributes `children`;
|
|
126
|
+
// `span`, `order`, `visibleFrom` and `hiddenFrom` are drafted but commented out in adapter-common.
|
|
127
|
+
// This component's own fields stay MUI-native until that lands, when `size` renames to `span`.
|
|
128
|
+
// See LAYOUT_GRID_IMPLEMENTATION_NOTES.md.
|
|
129
|
+
export type LayoutGridColProps = WithRecursicaSpacing<
|
|
130
|
+
OmitSx<Omit<MuiGridProps, "container" | "order">>
|
|
131
|
+
> &
|
|
132
|
+
RecursicaLayoutGridColProps & {
|
|
133
|
+
/** Sets the CSS `order` property. Applied via inline style. */
|
|
134
|
+
order?: number;
|
|
135
|
+
/** Hides the column below the given breakpoint. MUI has no native equivalent. */
|
|
136
|
+
visibleFrom?: Breakpoint;
|
|
137
|
+
/** Hides the column above the given breakpoint. MUI has no native equivalent. */
|
|
138
|
+
hiddenFrom?: Breakpoint;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export const LayoutGridCol = forwardRef<HTMLDivElement, LayoutGridColProps>(
|
|
142
|
+
function LayoutGridCol(
|
|
143
|
+
{ children, order, visibleFrom, hiddenFrom, style, className, ...rest },
|
|
144
|
+
ref,
|
|
145
|
+
) {
|
|
146
|
+
const safeProps = filterSxProp(rest as Record<string, unknown>);
|
|
147
|
+
|
|
148
|
+
const visibilityClass = visibleFrom
|
|
149
|
+
? VISIBLE_FROM_CLASS[visibleFrom]
|
|
150
|
+
: hiddenFrom
|
|
151
|
+
? HIDDEN_FROM_CLASS[hiddenFrom]
|
|
152
|
+
: undefined;
|
|
153
|
+
|
|
154
|
+
const finalClassName = [styles.col, visibilityClass, className]
|
|
155
|
+
.filter(Boolean)
|
|
156
|
+
.join(" ");
|
|
157
|
+
|
|
158
|
+
return (
|
|
159
|
+
<MuiGrid
|
|
160
|
+
ref={ref}
|
|
161
|
+
className={finalClassName}
|
|
162
|
+
{...(safeProps as unknown as MuiGridProps)}
|
|
163
|
+
style={{ order, ...(style as CSSProperties) }}
|
|
164
|
+
>
|
|
165
|
+
{children}
|
|
166
|
+
</MuiGrid>
|
|
167
|
+
);
|
|
168
|
+
},
|
|
169
|
+
);
|
|
170
|
+
LayoutGridCol.displayName = "LayoutGridCol";
|
|
171
|
+
|
|
172
|
+
// ============================================================
|
|
173
|
+
// DOT NOTATION EXPORT
|
|
174
|
+
// ============================================================
|
|
175
|
+
|
|
176
|
+
type LayoutGridComponent = typeof LayoutGridBase & {
|
|
177
|
+
Col: typeof LayoutGridCol;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export const LayoutGrid = LayoutGridBase as LayoutGridComponent;
|
|
181
|
+
LayoutGrid.Col = LayoutGridCol;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# LayoutGrid - Usage Guide
|
|
2
|
+
|
|
3
|
+
This document describes how to integrate and use the `LayoutGrid` component in your projects using `@recursica/adapter-mui-v7`.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Import Reference
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { LayoutGrid } from "@recursica/adapter-mui-v7";
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 2. Basic Example
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import React from "react";
|
|
19
|
+
import { LayoutGrid } from "@recursica/adapter-mui-v7";
|
|
20
|
+
|
|
21
|
+
export default function Demo() {
|
|
22
|
+
return (
|
|
23
|
+
// No layout props needed: columns, column-gutter, row-gutter and margin come from the design
|
|
24
|
+
// system's layout-grids tokens and change per breakpoint.
|
|
25
|
+
<LayoutGrid>
|
|
26
|
+
<LayoutGrid.Col size={3}>Half width at 6 columns</LayoutGrid.Col>
|
|
27
|
+
<LayoutGrid.Col size={{ xs: 6, sm: 3, md: 2 }}>
|
|
28
|
+
Responsive width
|
|
29
|
+
</LayoutGrid.Col>
|
|
30
|
+
</LayoutGrid>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 3. Design System Integration
|
|
38
|
+
|
|
39
|
+
All Recursica components in the `@recursica/adapter-mui-v7` package adhere strictly to design system spacing, scaling, and behavior patterns.
|
|
40
|
+
|
|
41
|
+
> [!IMPORTANT]
|
|
42
|
+
>
|
|
43
|
+
> - **Variables and Theming**: LayoutGrid follows the design system's own `layout-grids` tokens (columns, column-gutter, row-gutter and margin), applied automatically. Forge can define these per breakpoint, and the grid follows. None of them are configurable via props.
|
|
44
|
+
> - **Primitive layout component**: see [OVERSTYLING.md](../../../OVERSTYLING.md); only the `sx` prop is stripped, everything else passes through without `overStyled`.
|
|
45
|
+
> - **No Direct Layers**: Do not pass a `layer` prop to this component. To place it on a specific visual layer, wrap it in a `<Layer layer={0|1|2|3}>` component natively.
|
|
46
|
+
|
|
47
|
+
> [!NOTE] > **Recommended: build your MUI theme's breakpoints from the Forge manifest with `breakpointsFromRecManifest`.** Responsive keys such as `{ xs, sm, md }` on `LayoutGrid.Col` (and on Flex, Stack, Group) come from `theme.breakpoints`, while Forge's layout grids switch via plain CSS `@media`. If the two differ, tokens flip at Forge's width and props at the theme's. See [SETUP.md](../../../SETUP.md):
|
|
48
|
+
>
|
|
49
|
+
> ```tsx
|
|
50
|
+
> import { createTheme } from "@mui/material/styles";
|
|
51
|
+
> import { breakpointsFromRecManifest } from "@recursica/adapter-mui-v7";
|
|
52
|
+
> import manifest from "./recursica_manifest.json";
|
|
53
|
+
>
|
|
54
|
+
> const theme = createTheme({
|
|
55
|
+
> breakpoints: {
|
|
56
|
+
> values: {
|
|
57
|
+
> ...createTheme().breakpoints.values,
|
|
58
|
+
> ...breakpointsFromRecManifest(manifest),
|
|
59
|
+
> },
|
|
60
|
+
> },
|
|
61
|
+
> });
|
|
62
|
+
> ```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 4. Key Integration Features & Constraints
|
|
67
|
+
|
|
68
|
+
`LayoutGrid.Col` uses MUI's own prop vocabulary (see [LAYOUT_GRID_IMPLEMENTATION_NOTES.md](./LAYOUT_GRID_IMPLEMENTATION_NOTES.md)), not name-for-name identical to the Mantine adapter's.
|
|
69
|
+
|
|
70
|
+
- `LayoutGrid` does not accept `columns`, `spacing`, `columnSpacing` or `rowSpacing`: column count, column-gutter, row-gutter and margin are design-system-managed (Forge-controlled, breakpoint-aware, applied via CSS variables). For a fixed N-column grid unrelated to page layout, use MUI's `Grid` directly.
|
|
71
|
+
- `size` (on `LayoutGrid.Col`) accepts a column count, `"auto"`, `"grow"`, or a responsive object (`{ xs, sm, md, ... }`). A numeric size is relative to Forge's current column count and a column never grows wider than the row.
|
|
72
|
+
- `offset` shifts a column by a number of columns.
|
|
73
|
+
- `order` accepts a fixed number to control a column's visual order (single value only, no responsive object).
|
|
74
|
+
- `visibleFrom`/`hiddenFrom` show or hide a column at MUI's default breakpoints (600/900/1200/1536px).
|
|
75
|
+
- `justifyContent`/`alignItems` on `LayoutGrid` set the container's flex alignment.
|
|
76
|
+
|
|
77
|
+
> [!WARNING]
|
|
78
|
+
>
|
|
79
|
+
> **Breaking change:** `Grid` is now `LayoutGrid` (`Grid.Col` is `LayoutGrid.Col`), and its `columns` prop was removed.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./LayoutGrid";
|
|
@@ -294,9 +294,6 @@ export const WithSubmenus: Story = {
|
|
|
294
294
|
),
|
|
295
295
|
args: {
|
|
296
296
|
opened: true,
|
|
297
|
-
// Match mantine's explicit `width: 200` for this story so the row has room for the
|
|
298
|
-
// "Products"/"Orders" label plus the submenu chevron without clipping it.
|
|
299
|
-
slotProps: { paper: { style: { minWidth: 200 } } },
|
|
300
297
|
},
|
|
301
298
|
};
|
|
302
299
|
|
|
@@ -1,40 +1,44 @@
|
|
|
1
1
|
# Pagination Implementation Notes
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
`
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
`
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
3
|
+
## Architecture: Recursica Buttons driven by the manifest
|
|
4
|
+
|
|
5
|
+
Forge's `ui-kit.components.pagination` defines its page and navigation controls as `Button`
|
|
6
|
+
variants (`active-pages`, `inactive-pages`, `navigation-controls`, each with a `selected-variants`
|
|
7
|
+
style and size). Pagination therefore renders Recursica `Button`s with those props and does no
|
|
8
|
+
button styling of its own: radius, padding, colors, hover and disabled all come from `Button`.
|
|
9
|
+
(Before 2026-10-05 it restyled MUI's `PaginationItem` by hand with copies of the Button tokens.)
|
|
10
|
+
|
|
11
|
+
- `Pagination` reads the selected `style` and `size` per role from the manifest with
|
|
12
|
+
`useRecursicaManifest()` (`adapter-common`), provided by `RecursicaThemeProvider`'s `manifest`
|
|
13
|
+
prop. It throws if there is no manifest or if a role has no `selected-variants`. The values are
|
|
14
|
+
passed to `Button` as is, with no validation and no fallbacks.
|
|
15
|
+
- `content` (`label`, `icon-label`, `icon-only`) is not read: `Button` derives it from its own
|
|
16
|
+
children and icon, so page numbers are `label` and the navigation buttons are `icon-only`
|
|
17
|
+
(`icon-label` with `withLabels`).
|
|
18
|
+
|
|
19
|
+
## Why `usePagination`, not MUI's `Pagination`
|
|
20
|
+
|
|
21
|
+
MUI's `Pagination`/`PaginationItem` render their own `ButtonBase`, which can't be swapped for our
|
|
22
|
+
`Button`. The component is built on MUI's `usePagination` hook instead (page state, ranges,
|
|
23
|
+
siblings/boundaries, first/previous/next/last items and their disabled state) and maps each item to
|
|
24
|
+
a `Button`. The MUI-vocabulary props `page`, `defaultPage`, `onChange(event, page)`, `siblingCount`,
|
|
25
|
+
`boundaryCount` and `disabled` are kept; other `PaginationProps` (`shape`, `variant`, `color`,
|
|
26
|
+
`size`, `renderItem`, ...) are no longer accepted.
|
|
27
|
+
|
|
28
|
+
There are no dot-notation parts (`Pagination.Root`, `Items`, ...); the Mantine adapter has them.
|
|
29
|
+
`getItemProps(page)` and `getControlProps(control)` are the override hooks for page and
|
|
30
|
+
navigation buttons, and `dotsIcon` replaces the ellipsis.
|
|
31
|
+
|
|
32
|
+
## Labels
|
|
33
|
+
|
|
34
|
+
`withLabels` adds a text label to each navigation button. `First`/`Previous` put the icon first
|
|
35
|
+
(`Button`'s `icon`); `Next`/`Last` put it after the label (`endIcon`, wrapped in `.rightIcon` and
|
|
36
|
+
sized to the Button's icon token via `[data-size]` in `Pagination.module.css`).
|
|
37
|
+
|
|
38
|
+
## Styling
|
|
39
|
+
|
|
40
|
+
`Pagination.module.css` only lays out the row (`item-gap`) and styles the dots (`dots-color`).
|
|
41
|
+
|
|
42
|
+
- The root is a `<nav aria-label="Pagination">` landmark, and each page button has `aria-label="Page N"`
|
|
43
|
+
(`Previous page`, `Next page`, etc. for the navigation buttons). Both can be overridden by props.
|
|
44
|
+
- The ref is an `HTMLElement` (the `<nav>`).
|
|
@@ -32,35 +32,3 @@ export const PaginationIcon = ({
|
|
|
32
32
|
<path d={paths[type as PaginationIconType]} fill="currentColor" />
|
|
33
33
|
</svg>
|
|
34
34
|
);
|
|
35
|
-
|
|
36
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
|
-
export const NextWithLabel = (props: any) => (
|
|
38
|
-
<div className={styles.iconWithLabel}>
|
|
39
|
-
<span>Next</span>
|
|
40
|
-
<PaginationIcon type="next" {...props} />
|
|
41
|
-
</div>
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
-
export const PrevWithLabel = (props: any) => (
|
|
46
|
-
<div className={styles.iconWithLabel}>
|
|
47
|
-
<PaginationIcon type="prev" {...props} />
|
|
48
|
-
<span>Prev</span>
|
|
49
|
-
</div>
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
53
|
-
export const FirstWithLabel = (props: any) => (
|
|
54
|
-
<div className={styles.iconWithLabel}>
|
|
55
|
-
<PaginationIcon type="first" {...props} />
|
|
56
|
-
<span>First</span>
|
|
57
|
-
</div>
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
|
-
export const LastWithLabel = (props: any) => (
|
|
62
|
-
<div className={styles.iconWithLabel}>
|
|
63
|
-
<span>Last</span>
|
|
64
|
-
<PaginationIcon type="last" {...props} />
|
|
65
|
-
</div>
|
|
66
|
-
);
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { describe, it, expect } from "vitest";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { flushSync } from "react-dom";
|
|
5
|
+
import { RecursicaManifestContext } from "@recursica/adapter-common";
|
|
6
|
+
import { Pagination } from "./Pagination";
|
|
7
|
+
|
|
8
|
+
const role = (style: string, size: string) => ({
|
|
9
|
+
$extensions: {
|
|
10
|
+
"recursica.component": { "selected-variants": { style, size } },
|
|
11
|
+
},
|
|
12
|
+
});
|
|
13
|
+
const manifest = (props: Record<string, unknown>) => ({
|
|
14
|
+
"ui-kit": { components: { pagination: { properties: props } } },
|
|
15
|
+
});
|
|
16
|
+
const full = manifest({
|
|
17
|
+
"active-pages": role("solid", "small"),
|
|
18
|
+
"inactive-pages": role("outline", "small"),
|
|
19
|
+
"navigation-controls": role("text", "small"),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
class Catch extends React.Component<
|
|
23
|
+
{ children: React.ReactNode },
|
|
24
|
+
{ error?: Error }
|
|
25
|
+
> {
|
|
26
|
+
state: { error?: Error } = {};
|
|
27
|
+
static getDerivedStateFromError(error: Error) {
|
|
28
|
+
return { error };
|
|
29
|
+
}
|
|
30
|
+
render() {
|
|
31
|
+
return this.state.error ? (
|
|
32
|
+
<span data-error>{this.state.error.message}</span>
|
|
33
|
+
) : (
|
|
34
|
+
this.props.children
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function mount(m: Record<string, unknown> | undefined) {
|
|
40
|
+
const container = document.createElement("div");
|
|
41
|
+
document.body.appendChild(container);
|
|
42
|
+
const root = createRoot(container);
|
|
43
|
+
flushSync(() =>
|
|
44
|
+
root.render(
|
|
45
|
+
<Catch>
|
|
46
|
+
<RecursicaManifestContext.Provider value={m}>
|
|
47
|
+
<Pagination total={5} defaultPage={2} />
|
|
48
|
+
</RecursicaManifestContext.Provider>
|
|
49
|
+
</Catch>,
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
return {
|
|
53
|
+
container,
|
|
54
|
+
error: container.querySelector("[data-error]")?.textContent,
|
|
55
|
+
done: () => {
|
|
56
|
+
root.unmount();
|
|
57
|
+
container.remove();
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe("Pagination manifest variants", () => {
|
|
63
|
+
it("renders Buttons with the style and size the manifest selects per role", () => {
|
|
64
|
+
const { container, error, done } = mount(full);
|
|
65
|
+
expect(error).toBeUndefined();
|
|
66
|
+
const active = container.querySelector('[aria-current="page"]');
|
|
67
|
+
expect(active?.getAttribute("data-variant")).toBe("solid");
|
|
68
|
+
expect(active?.getAttribute("data-size")).toBe("small");
|
|
69
|
+
const inactive = [...container.querySelectorAll("button")].find(
|
|
70
|
+
(b) => b.textContent === "3",
|
|
71
|
+
);
|
|
72
|
+
expect(inactive?.getAttribute("data-variant")).toBe("outline");
|
|
73
|
+
const next = container.querySelector('[aria-label="Next page"]');
|
|
74
|
+
expect(next?.getAttribute("data-variant")).toBe("text");
|
|
75
|
+
expect(next?.getAttribute("data-content")).toBe("icon-only");
|
|
76
|
+
done();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("renders a labelled nav with a labelled button per page", () => {
|
|
80
|
+
const { container, error, done } = mount(full);
|
|
81
|
+
expect(error).toBeUndefined();
|
|
82
|
+
expect(container.querySelector("nav")?.getAttribute("aria-label")).toBe(
|
|
83
|
+
"Pagination",
|
|
84
|
+
);
|
|
85
|
+
const three = [...container.querySelectorAll("button")].find(
|
|
86
|
+
(b) => b.textContent === "3",
|
|
87
|
+
);
|
|
88
|
+
expect(three?.getAttribute("aria-label")).toBe("Page 3");
|
|
89
|
+
done();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("throws without a manifest", () => {
|
|
93
|
+
const { error, done } = mount(undefined);
|
|
94
|
+
expect(error).toMatch(/Recursica manifest/);
|
|
95
|
+
done();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("throws when a role has no selected variants", () => {
|
|
99
|
+
const { error, done } = mount(
|
|
100
|
+
manifest({ "active-pages": role("solid", "small") }),
|
|
101
|
+
);
|
|
102
|
+
expect(error).toMatch(/selected-variants/);
|
|
103
|
+
done();
|
|
104
|
+
});
|
|
105
|
+
});
|