@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.
Files changed (55) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/OVERSTYLING.md +1 -1
  3. package/SETUP.md +26 -3
  4. package/dist/adapter-mui-v7.cjs +69 -69
  5. package/dist/adapter-mui-v7.cjs.map +1 -1
  6. package/dist/adapter-mui-v7.css +1 -1
  7. package/dist/adapter-mui-v7.js +5955 -5739
  8. package/dist/adapter-mui-v7.js.map +1 -1
  9. package/dist/index.d.ts +69 -51
  10. package/llms.txt +1 -1
  11. package/package.json +4 -4
  12. package/recursica_brand.json +2 -2
  13. package/recursica_tokens.json +2 -2
  14. package/recursica_ui-kit.json +2 -2
  15. package/recursica_variables_scoped.css +7 -3
  16. package/src/components/Checkbox/Checkbox.module.css +1 -3
  17. package/src/components/Container/USAGE.md +0 -1
  18. package/src/components/FileInput/FileInput.module.css +8 -0
  19. package/src/components/FileUpload/FileUpload.module.css +1 -0
  20. package/src/components/FileUpload/FileUpload.tsx +1 -0
  21. package/src/components/Flex/USAGE.md +2 -1
  22. package/src/components/Group/USAGE.md +2 -1
  23. package/src/components/LayoutGrid/LAYOUT_GRID_IMPLEMENTATION_NOTES.md +82 -0
  24. package/src/components/LayoutGrid/LayoutGrid.columns.dom.test.tsx +78 -0
  25. package/src/components/{Grid/Grid.module.css → LayoutGrid/LayoutGrid.module.css} +11 -10
  26. package/src/components/LayoutGrid/LayoutGrid.stories.tsx +61 -0
  27. package/src/components/LayoutGrid/LayoutGrid.test.tsx +1 -0
  28. package/src/components/LayoutGrid/LayoutGrid.tsx +181 -0
  29. package/src/components/LayoutGrid/USAGE.md +79 -0
  30. package/src/components/LayoutGrid/index.ts +1 -0
  31. package/src/components/Menu/Menu.stories.tsx +0 -3
  32. package/src/components/Pagination/IMPLEMENTATION_NOTES.md +42 -38
  33. package/src/components/Pagination/Pagination.icons.tsx +0 -32
  34. package/src/components/Pagination/Pagination.manifest.dom.test.tsx +105 -0
  35. package/src/components/Pagination/Pagination.module.css +25 -164
  36. package/src/components/Pagination/Pagination.tsx +221 -97
  37. package/src/components/Pagination/USAGE.md +8 -1
  38. package/src/components/Panel/Panel.module.css +24 -1
  39. package/src/components/Panel/Panel.stories.tsx +4 -4
  40. package/src/components/Panel/Panel.tsx +10 -1
  41. package/src/components/Radio/Radio.module.css +2 -2
  42. package/src/components/SegmentedControl/SegmentedControl.module.css +18 -0
  43. package/src/components/Stack/USAGE.md +2 -1
  44. package/src/components/Stepper/Stepper.module.css +0 -21
  45. package/src/components/index.ts +1 -1
  46. package/src/index.ts +1 -0
  47. package/src/utils/breakpointsFromRecManifest.test.ts +40 -0
  48. package/src/utils/breakpointsFromRecManifest.ts +72 -0
  49. package/src/utils/index.ts +1 -0
  50. package/src/components/Grid/Grid.stories.tsx +0 -151
  51. package/src/components/Grid/Grid.test.tsx +0 -1
  52. package/src/components/Grid/Grid.tsx +0 -189
  53. package/src/components/Grid/IMPLEMENTATION_NOTES.md +0 -99
  54. package/src/components/Grid/USAGE.md +0 -57
  55. 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
- - **Compositional API Dropped:** Mantine's original `Pagination` component relies heavily on dot-notation sub-components (`Pagination.Root`, `Pagination.Items`, `Pagination.Control`, etc.). MUI's `<Pagination>` is fundamentally monolithic. Following architectural review, we have decided to drop the dot-notation wrappers for `adapter-mui-v7` and rely strictly on MUI's monolithic API. Storybook and visual regression tests have been updated to reflect this divergence while retaining core property mapping compatibility.
4
-
5
- ## `renderItem` was never wired up (source-of-truth audit, 2026-08-30)
6
-
7
- `Pagination.module.css` already had complete, correctly-tokenized rules for `.control` (the
8
- circle/pill for every page/nav item), `.dots` (the ellipsis), `.iconWithLabel`, and `.baseIcon` —
9
- and `Pagination.icons.tsx` already had `PaginationIcon` plus `NextWithLabel`/`PrevWithLabel`/
10
- `FirstWithLabel`/`LastWithLabel` fully built. None of it was ever applied: `Pagination.tsx` only
11
- mapped MUI `Pagination`'s top-level `classes` prop (`root`/`ul` — the _only_ two slots MUI's own
12
- `Pagination` component exposes) and silently discarded the `withLabels` prop entirely. Every
13
- page/nav button rendered as MUI's own completely unstyled default `PaginationItem` — explaining
14
- the whole cluster of reported issues at once: undersized circles (no `.control` height/width),
15
- grey selected state and missing outline (no `.control`/`[data-active]` colors), wrong font color,
16
- default MUI arrow icons in the wrong color (no `PaginationIcon`), default ripple (MUI's
17
- `PaginationItem` uses `ButtonBase` with ripple enabled by default), and no text labels ever
18
- possible (`withLabels` was read and thrown away).
19
-
20
- **Fix:** `Pagination.tsx` now passes a `renderItem` to `MuiPagination` that renders every item as
21
- `<PaginationItem className={styles.control} data-active={item.selected} data-variant={isNavigation
22
- ? "text" : undefined} disableRipple slots={...} />` (ellipsis items get `styles.dots` instead).
23
- `slots.first/last/next/previous` point at `PaginationIcon` (plain) or the `*WithLabel` components
24
- (when `withLabels` is true) — both were already correctly built to work as MUI `PaginationItem`
25
- icon slots (only the icon itself receives MUI's forwarded className/props via `{...props}` spread
26
- onto the inner `PaginationIcon`, not the outer label-wrapping `<div>`, so the label text isn't
27
- constrained by MUI's icon-sized styled wrapper).
28
-
29
- Also added a local `--pagination-control-size` bridge variable (bound to the Button height token
30
- `.control` is already sized from) — `.baseIcon`'s `calc(var(--pagination-control-size) / 1.8)`
31
- was copied verbatim from mantine-adapter, where `--pagination-control-size` is a _native_ Mantine
32
- CSS variable Mantine's own `Pagination` stamps onto the DOM already; MUI has no equivalent, so
33
- the calc silently resolved to `NaN`/`0` here until this bridge was added.
34
-
35
- **Residual, not fixed:** the previous/next chevron control renders ~3.8px wider in mantine
36
- (51.8px vs. mui's exact 48px square) — mantine's own icon-only nav button apparently doesn't
37
- collapse fully to `min-width` the way mui's `PaginationItem` does. Pixel-matched everywhere else
38
- (page-number circle size/color/border, selected state, ellipsis centering, chevron colors, hover
39
- treatment, ripple removal, text labels) — not investigated further given the small, cosmetic
40
- scope of this one remaining gap.
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
+ });