@recursica/adapter-mui-v7 1.0.4 → 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 +30 -0
- package/OVERSTYLING.md +1 -1
- package/SETUP.md +26 -3
- package/dist/adapter-mui-v7.cjs +64 -64
- package/dist/adapter-mui-v7.cjs.map +1 -1
- package/dist/adapter-mui-v7.css +1 -1
- package/dist/adapter-mui-v7.js +5869 -5645
- package/dist/adapter-mui-v7.js.map +1 -1
- package/dist/index.d.ts +123 -104
- package/llms.txt +1 -1
- package/package.json +4 -4
- package/recursica_brand.json +172 -235
- package/recursica_tokens.json +16 -127
- package/recursica_ui-kit.json +2195 -8127
- package/recursica_variables_scoped.css +4547 -9194
- 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/FormControlLayout/FormControlLayout.module.css +6 -3
- package/src/components/Group/USAGE.md +2 -1
- package/src/components/Heading/Heading.stories.tsx +62 -0
- package/src/components/Heading/Heading.tsx +6 -2
- package/src/components/Heading/USAGE.md +19 -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} +13 -1
- 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/Text/Text.stories.tsx +52 -3
- package/src/components/Text/Text.tsx +13 -3
- package/src/components/Text/USAGE.md +20 -2
- package/src/components/Typography/Typography.module.css +39 -0
- 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 -161
- package/src/components/Grid/Grid.test.tsx +0 -1
- package/src/components/Grid/Grid.tsx +0 -155
- package/src/components/Grid/IMPLEMENTATION_NOTES.md +0 -24
- package/src/components/Grid/USAGE.md +0 -53
- package/src/components/Grid/index.ts +0 -1
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
/* SideBySide Variant */
|
|
22
22
|
.groupRoot[data-layout="side-by-side"] {
|
|
23
23
|
flex-direction: column;
|
|
24
|
-
flex-wrap:
|
|
24
|
+
flex-wrap: nowrap;
|
|
25
25
|
padding-top: var(
|
|
26
26
|
--recursica_ui-kit_components_checkbox-group_variants_layouts_side-by-side_properties_vertical-padding
|
|
27
27
|
);
|
|
@@ -278,8 +278,6 @@
|
|
|
278
278
|
);
|
|
279
279
|
}
|
|
280
280
|
.groupRoot[data-layout="side-by-side"] {
|
|
281
|
-
flex-direction: row;
|
|
282
|
-
flex-wrap: wrap;
|
|
283
281
|
gap: var(
|
|
284
282
|
--recursica_ui-kit_components_checkbox-group_variants_layouts_side-by-side_properties_gutter
|
|
285
283
|
);
|
|
@@ -35,7 +35,6 @@ All Recursica components in the `@recursica/adapter-mui-v7` package adhere stric
|
|
|
35
35
|
|
|
36
36
|
> [!IMPORTANT]
|
|
37
37
|
>
|
|
38
|
-
> - **Anti-override protection**: Rogues style injections (like inline `style` or arbitrary `className`) are automatically blocked by our prop layer unless `overStyled={true}` is explicitly provided.
|
|
39
38
|
> - **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.
|
|
40
39
|
> - **Variables and Theming**: Styling is entirely determined by local CSS variables defined in `recursica_variables_scoped.css` and mapped in the component's CSS module.
|
|
41
40
|
|
|
@@ -52,6 +52,14 @@
|
|
|
52
52
|
min-height: var(
|
|
53
53
|
--recursica_ui-kit_components_file-input_properties_min-height
|
|
54
54
|
);
|
|
55
|
+
/* Fixed height (icon + padding + border) so the nested clear Button overflows the row instead of growing the control. */
|
|
56
|
+
height: calc(
|
|
57
|
+
var(--recursica_ui-kit_components_file-input_properties_icon-size) + 2 *
|
|
58
|
+
var(
|
|
59
|
+
--recursica_ui-kit_components_file-input_properties_vertical-padding
|
|
60
|
+
) +
|
|
61
|
+
2 * var(--recursica_ui-kit_components_file-input_properties_border-size)
|
|
62
|
+
);
|
|
55
63
|
gap: var(--recursica_ui-kit_components_file-input_properties_icon-text-gap);
|
|
56
64
|
padding-top: var(
|
|
57
65
|
--recursica_ui-kit_components_file-input_properties_vertical-padding
|
|
@@ -36,6 +36,7 @@ All Recursica components in the `@recursica/adapter-mui-v7` package adhere stric
|
|
|
36
36
|
|
|
37
37
|
> [!IMPORTANT]
|
|
38
38
|
>
|
|
39
|
-
> - **Anti-override protection**: Rogues style injections (like inline `style` or arbitrary `className`) are automatically blocked by our prop layer unless `overStyled={true}` is explicitly provided.
|
|
40
39
|
> - **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.
|
|
41
40
|
> - **Variables and Theming**: Styling is entirely determined by local CSS variables defined in `recursica_variables_scoped.css` and mapped in the component's CSS module.
|
|
41
|
+
|
|
42
|
+
> [!NOTE] > **Responsive props use your MUI theme's breakpoints, not Forge's.** Keys such as `{ xs, sm, md }` come from `theme.breakpoints`; Forge's layout grids switch via plain CSS `@media` and never edit the MUI theme. Build your theme with `breakpointsFromRecManifest` so the two agree (see [LayoutGrid](../LayoutGrid/USAGE.md)).
|
|
@@ -7,8 +7,6 @@
|
|
|
7
7
|
|
|
8
8
|
/* Recursica Layout Overrides mapped down dynamically from components */
|
|
9
9
|
padding: 0; /* Hardcoded structural reset */
|
|
10
|
-
max-width: var(--form-control-max-width);
|
|
11
|
-
min-width: var(--form-control-min-width);
|
|
12
10
|
}
|
|
13
11
|
|
|
14
12
|
/* =========================================
|
|
@@ -88,5 +86,10 @@
|
|
|
88
86
|
display: flex; /* Groups internal field alongside subsequent errors/descriptions */
|
|
89
87
|
flex-direction: column;
|
|
90
88
|
flex: 1; /* Consumes the remaining x-axis in grid columns natively */
|
|
91
|
-
|
|
89
|
+
/* Constrains only the control itself, never the label in .leftSection. */
|
|
90
|
+
max-width: var(--form-control-max-width, 100%);
|
|
91
|
+
min-width: var(
|
|
92
|
+
--form-control-min-width,
|
|
93
|
+
0
|
|
94
|
+
); /* Break boundary preventing internal flex explosion */
|
|
92
95
|
}
|
|
@@ -36,6 +36,7 @@ All Recursica components in the `@recursica/adapter-mui-v7` package adhere stric
|
|
|
36
36
|
|
|
37
37
|
> [!IMPORTANT]
|
|
38
38
|
>
|
|
39
|
-
> - **Anti-override protection**: Rogues style injections (like inline `style` or arbitrary `className`) are automatically blocked by our prop layer unless `overStyled={true}` is explicitly provided.
|
|
40
39
|
> - **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.
|
|
41
40
|
> - **Variables and Theming**: Styling is entirely determined by local CSS variables defined in `recursica_variables_scoped.css` and mapped in the component's CSS module.
|
|
41
|
+
|
|
42
|
+
> [!NOTE] > **Responsive props use your MUI theme's breakpoints, not Forge's.** Keys such as `{ xs, sm, md }` come from `theme.breakpoints`; Forge's layout grids switch via plain CSS `@media` and never edit the MUI theme. Build your theme with `breakpointsFromRecManifest` so the two agree (see [LayoutGrid](../LayoutGrid/USAGE.md)).
|
|
@@ -20,6 +20,18 @@ const meta: Meta<typeof Heading> = {
|
|
|
20
20
|
description:
|
|
21
21
|
"Controls the `h` tag and the resultant typographical weighting natively mapped to Recursica.",
|
|
22
22
|
},
|
|
23
|
+
color: {
|
|
24
|
+
control: "select",
|
|
25
|
+
options: ["default", "warning", "alert", "success"],
|
|
26
|
+
description:
|
|
27
|
+
"Semantic text color, bound to the active layer's text-element tokens via `data-color`.",
|
|
28
|
+
},
|
|
29
|
+
emphasis: {
|
|
30
|
+
control: "inline-radio",
|
|
31
|
+
options: ["high", "low"],
|
|
32
|
+
description:
|
|
33
|
+
"Emphasis level, bound to the theme's text-emphasis opacity tokens via `data-emphasis`.",
|
|
34
|
+
},
|
|
23
35
|
},
|
|
24
36
|
};
|
|
25
37
|
|
|
@@ -48,3 +60,53 @@ export const StaticVariations: Story = {
|
|
|
48
60
|
</div>
|
|
49
61
|
),
|
|
50
62
|
};
|
|
63
|
+
|
|
64
|
+
export const Colors: Story = {
|
|
65
|
+
args: {},
|
|
66
|
+
parameters: {
|
|
67
|
+
docs: {
|
|
68
|
+
description: {
|
|
69
|
+
story:
|
|
70
|
+
"Semantic colors map to the active layer's text-element tokens. `default` follows the layer's base text color; `warning`, `alert`, and `success` pull their respective semantic tones.",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
render: () => (
|
|
75
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
|
|
76
|
+
<Heading order={2} color="default">
|
|
77
|
+
Default heading
|
|
78
|
+
</Heading>
|
|
79
|
+
<Heading order={2} color="warning">
|
|
80
|
+
Warning heading
|
|
81
|
+
</Heading>
|
|
82
|
+
<Heading order={2} color="alert">
|
|
83
|
+
Alert heading
|
|
84
|
+
</Heading>
|
|
85
|
+
<Heading order={2} color="success">
|
|
86
|
+
Success heading
|
|
87
|
+
</Heading>
|
|
88
|
+
</div>
|
|
89
|
+
),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const Emphasis: Story = {
|
|
93
|
+
args: {},
|
|
94
|
+
parameters: {
|
|
95
|
+
docs: {
|
|
96
|
+
description: {
|
|
97
|
+
story:
|
|
98
|
+
"Emphasis controls text opacity via the theme's text-emphasis tokens. `high` (the default) is solid; `low` dims the heading for secondary content.",
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
render: () => (
|
|
103
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
|
|
104
|
+
<Heading order={2} emphasis="high">
|
|
105
|
+
High emphasis heading
|
|
106
|
+
</Heading>
|
|
107
|
+
<Heading order={2} emphasis="low">
|
|
108
|
+
Low emphasis heading
|
|
109
|
+
</Heading>
|
|
110
|
+
</div>
|
|
111
|
+
),
|
|
112
|
+
};
|
|
@@ -5,11 +5,13 @@ import { type RecursicaOverStyled } from "../../utils/filterStylingProps";
|
|
|
5
5
|
import { type RecursicaHeadingProps } from "@recursica/adapter-common";
|
|
6
6
|
|
|
7
7
|
export type HeadingProps = RecursicaOverStyled<
|
|
8
|
-
Omit<MuiTypographyProps, "variant" | "classes"> &
|
|
8
|
+
Omit<MuiTypographyProps, "variant" | "classes" | "color"> &
|
|
9
|
+
RecursicaHeadingProps,
|
|
10
|
+
"color"
|
|
9
11
|
>;
|
|
10
12
|
|
|
11
13
|
export const Heading = forwardRef<HTMLElement, HeadingProps>(function Heading(
|
|
12
|
-
{ order = 1, component, ...rest },
|
|
14
|
+
{ order = 1, component, emphasis = "high", color = "default", ...rest },
|
|
13
15
|
ref,
|
|
14
16
|
) {
|
|
15
17
|
const typographyClass = `recursica_brand_typography_h${order}`;
|
|
@@ -20,6 +22,8 @@ export const Heading = forwardRef<HTMLElement, HeadingProps>(function Heading(
|
|
|
20
22
|
ref={ref}
|
|
21
23
|
typographyClass={typographyClass}
|
|
22
24
|
component={component || defaultComponent}
|
|
25
|
+
data-color={color}
|
|
26
|
+
data-emphasis={emphasis}
|
|
23
27
|
{...rest}
|
|
24
28
|
/>
|
|
25
29
|
);
|
|
@@ -25,7 +25,25 @@ export default function Demo() {
|
|
|
25
25
|
|
|
26
26
|
---
|
|
27
27
|
|
|
28
|
-
## 3.
|
|
28
|
+
## 3. Semantic `color` and `emphasis`
|
|
29
|
+
|
|
30
|
+
`Heading` exposes two Recursica-native, token-bound props (not MUI's freeform `color`):
|
|
31
|
+
|
|
32
|
+
- **`color`** — semantic text color: `"default"` (the active layer's base text color), `"warning"`, `"alert"`, or `"success"`. Applied as a `data-color` attribute mapped to the layer's text-element tokens.
|
|
33
|
+
- **`emphasis`** — `"high"` (default, solid) or `"low"` (dimmed for secondary content). Applied as a `data-emphasis` attribute mapped to the theme's text-emphasis opacity tokens.
|
|
34
|
+
|
|
35
|
+
```tsx
|
|
36
|
+
<Heading order={2} color="alert" emphasis="high">
|
|
37
|
+
Something went wrong.
|
|
38
|
+
</Heading>
|
|
39
|
+
<Heading order={3} color="default" emphasis="low">
|
|
40
|
+
Secondary section header.
|
|
41
|
+
</Heading>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 4. Design System Integration
|
|
29
47
|
|
|
30
48
|
All Recursica components in the `@recursica/adapter-mui-v7` package adhere strictly to design system spacing, scaling, and behavior patterns.
|
|
31
49
|
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# LayoutGrid Implementation Notes
|
|
2
|
+
|
|
3
|
+
`LayoutGrid` is the Recursica layout grid. MUI has no native Grid/Grid.Col split: its `Grid`
|
|
4
|
+
merges "container" and "item" behavior into one component (toggled by `container` and a `size`
|
|
5
|
+
prop). `LayoutGrid` always renders `<MuiGrid container>` and `LayoutGrid.Col` renders `<MuiGrid>`
|
|
6
|
+
in item mode, to keep the same dot-notation shape as the Mantine adapter. It was named `Grid`
|
|
7
|
+
before 2026-10-05.
|
|
8
|
+
|
|
9
|
+
Unlike the other primitive layout components (`Flex`, `Stack`, `Group`, `Container`), it has a
|
|
10
|
+
formal Recursica prop contract (`RecursicaLayoutGridProps` in `adapter-common`) and wires the
|
|
11
|
+
design system's `layout-grids` tokens. Its other props are MUI's own vocabulary, not a shared
|
|
12
|
+
contract:
|
|
13
|
+
|
|
14
|
+
- **`size`** (not Mantine's `span`): a column count, `"auto"`, `"grow"`, or a responsive object keyed by
|
|
15
|
+
MUI's breakpoints (`xs`/`sm`/`md`/`lg`/`xl`, no `"base"`). It renames to `span` when
|
|
16
|
+
`RecursicaLayoutGridColProps` picks `span` up (queued in `adapter-common`).
|
|
17
|
+
- **`offset`**: MUI's own prop, used as is.
|
|
18
|
+
- **`grow` (container)**: not implemented; use `size="grow"` per column.
|
|
19
|
+
- **`order`, `justifyContent`, `alignItems`**: typed by MUI but never read by its Grid generator
|
|
20
|
+
(`@mui/system` `gridGenerator`), so they're applied via inline `style`.
|
|
21
|
+
- **`visibleFrom` / `hiddenFrom`**: no MUI mechanism, so `LayoutGrid.module.css` has media-query
|
|
22
|
+
classes hardcoded to MUI's default breakpoints (0/600/900/1200/1536). They do not follow a
|
|
23
|
+
customized `theme.breakpoints`.
|
|
24
|
+
|
|
25
|
+
## Forge contract
|
|
26
|
+
|
|
27
|
+
Forge always emits a default grid with columns (default 6, editable), column-gutter, row-gutter and
|
|
28
|
+
side margin, in the CSS and in `recursica_manifest.json`. Additional breakpoints are overrides, so
|
|
29
|
+
nothing here has a fallback value.
|
|
30
|
+
|
|
31
|
+
## No integrator-facing layout props
|
|
32
|
+
|
|
33
|
+
Columns, column-gutter, row-gutter and margin are design-system-managed, not per-instance settings.
|
|
34
|
+
`columns`, `spacing`, `columnSpacing` and `rowSpacing` were removed from the public type and are
|
|
35
|
+
deleted at runtime so they can't shadow the token-driven values. For an arbitrary N-column grid,
|
|
36
|
+
use MUI's `Grid` directly.
|
|
37
|
+
|
|
38
|
+
## How each token is applied
|
|
39
|
+
|
|
40
|
+
Forge emits a responsive alias per grid var (`--recursica_brand_layout-grids_{columns,row-gutter,column-gutter,margin}`),
|
|
41
|
+
set from `_default_*` at `:root` and redefined inside Forge's `@media` blocks per non-default grid.
|
|
42
|
+
`LayoutGrid` reads only the alias, so tokens follow breakpoints with no JS. Overlapping ranges are
|
|
43
|
+
not guarded by Forge; the later `@media` block wins.
|
|
44
|
+
|
|
45
|
+
- **Columns**: MUI's Grid computes widths in CSS from `--Grid-parent-columns`, so unlike Mantine no
|
|
46
|
+
per-column style rule is needed. `LayoutGrid` passes the alias as `columns`
|
|
47
|
+
(`var(--recursica_brand_layout-grids_columns)`); MUI types it as a number but writes it
|
|
48
|
+
verbatim into the custom property, which the width `calc()` then reads. Verified in
|
|
49
|
+
`@mui/system`'s `gridGenerator`.
|
|
50
|
+
- **Column gutter / row gutter**: passed to MUI's own `columnSpacing`/`rowSpacing` as the
|
|
51
|
+
responsive aliases. MUI applies each independently (strings are not multiplied by `theme.spacing`).
|
|
52
|
+
- **Margin**: no MUI Grid prop equivalent; `.root` in `LayoutGrid.module.css` reads it directly.
|
|
53
|
+
- **Clamping**: MUI's width calc does not clamp, so `size={6}` at 3 columns would overflow.
|
|
54
|
+
`.col { max-width: 100% }` makes it fill the row instead, matching Mantine's `min(span, columns)`.
|
|
55
|
+
|
|
56
|
+
## Breakpoints
|
|
57
|
+
|
|
58
|
+
Responsive `size`/`offset` switch at the MUI theme's breakpoints, which Forge never edits.
|
|
59
|
+
Integrators should merge `breakpointsFromRecManifest(manifest)` into `theme.breakpoints.values`
|
|
60
|
+
so both switch at the same widths (MUI's `values` replaces its defaults, so spread them first;
|
|
61
|
+
see SETUP.md). `RecursicaThemeProvider` is UI-kit agnostic and does not compare the manifest
|
|
62
|
+
against the theme. The helper returns numbers (MUI's unit), not the `"Npx"` strings the Mantine
|
|
63
|
+
one returns.
|
|
64
|
+
|
|
65
|
+
## `LayoutGrid.Col` contract is scaffolded
|
|
66
|
+
|
|
67
|
+
`RecursicaLayoutGridColProps` currently only contributes `children`; `span`, `order`,
|
|
68
|
+
`visibleFrom` and `hiddenFrom` are drafted and commented out in `adapter-common`. No runtime
|
|
69
|
+
effect here.
|
|
70
|
+
|
|
71
|
+
## Brand-layer exemption
|
|
72
|
+
|
|
73
|
+
`layout-grids` tokens have no `ui-kit` layer in Forge's model, so `LayoutGrid.module.css` reads
|
|
74
|
+
`brand_layout-grids_margin` directly with a `recursica-allow-brand:` header. The other three are
|
|
75
|
+
read in `LayoutGrid.tsx` (a TSX prop value), which the analyzer does not scan.
|
|
76
|
+
|
|
77
|
+
## Stories
|
|
78
|
+
|
|
79
|
+
A single `Default` story reads `brand.layout-grids.default.columns` from the manifest (via
|
|
80
|
+
`useRecursicaManifest()`) and fills two rows of that many single-column cells. Same story id as the
|
|
81
|
+
Mantine adapter's, so it diffs against its golden. Only `size` is used, so the story stays valid
|
|
82
|
+
for every adapter.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { createRoot } from "react-dom/client";
|
|
3
|
+
import { flushSync } from "react-dom";
|
|
4
|
+
import { StyledEngineProvider } from "@mui/material/styles";
|
|
5
|
+
import { LayoutGrid } from "./LayoutGrid";
|
|
6
|
+
|
|
7
|
+
function mount(node: React.ReactElement) {
|
|
8
|
+
const container = document.createElement("div");
|
|
9
|
+
document.body.appendChild(container);
|
|
10
|
+
const root = createRoot(container);
|
|
11
|
+
// injectFirst, as RecursicaThemeProvider does, so the component CSS beats MUI's.
|
|
12
|
+
flushSync(() =>
|
|
13
|
+
root.render(
|
|
14
|
+
<StyledEngineProvider injectFirst>{node}</StyledEngineProvider>,
|
|
15
|
+
),
|
|
16
|
+
);
|
|
17
|
+
return {
|
|
18
|
+
container,
|
|
19
|
+
done: () => {
|
|
20
|
+
root.unmount();
|
|
21
|
+
container.remove();
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("LayoutGrid tokens", () => {
|
|
27
|
+
it("takes columns and gutters from the responsive Forge variables", () => {
|
|
28
|
+
document.documentElement.style.setProperty(
|
|
29
|
+
"--recursica_brand_layout-grids_columns",
|
|
30
|
+
"3",
|
|
31
|
+
);
|
|
32
|
+
document.documentElement.style.setProperty(
|
|
33
|
+
"--recursica_brand_layout-grids_column-gutter",
|
|
34
|
+
"10px",
|
|
35
|
+
);
|
|
36
|
+
document.documentElement.style.setProperty(
|
|
37
|
+
"--recursica_brand_layout-grids_row-gutter",
|
|
38
|
+
"20px",
|
|
39
|
+
);
|
|
40
|
+
const { container, done } = mount(
|
|
41
|
+
<LayoutGrid>
|
|
42
|
+
<LayoutGrid.Col size={1}>a</LayoutGrid.Col>
|
|
43
|
+
</LayoutGrid>,
|
|
44
|
+
);
|
|
45
|
+
const grid = container.querySelector(".MuiGrid-container") as HTMLElement;
|
|
46
|
+
const style = getComputedStyle(grid);
|
|
47
|
+
expect(style.getPropertyValue("--Grid-columns").trim()).toBe("3");
|
|
48
|
+
expect(style.getPropertyValue("--Grid-columnSpacing").trim()).toBe("10px");
|
|
49
|
+
expect(style.getPropertyValue("--Grid-rowSpacing").trim()).toBe("20px");
|
|
50
|
+
done();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("drops columns and spacing props passed at runtime", () => {
|
|
54
|
+
const { container, done } = mount(
|
|
55
|
+
<LayoutGrid {...({ columns: 12, spacing: 8 } as Record<string, unknown>)}>
|
|
56
|
+
<LayoutGrid.Col size={1}>a</LayoutGrid.Col>
|
|
57
|
+
</LayoutGrid>,
|
|
58
|
+
);
|
|
59
|
+
const grid = container.querySelector(".MuiGrid-container") as HTMLElement;
|
|
60
|
+
expect(
|
|
61
|
+
getComputedStyle(grid).getPropertyValue("--Grid-columns").trim(),
|
|
62
|
+
).toBe("3");
|
|
63
|
+
done();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("clamps a column wider than the grid to the row", () => {
|
|
67
|
+
const { container, done } = mount(
|
|
68
|
+
<LayoutGrid>
|
|
69
|
+
<LayoutGrid.Col size={6}>a</LayoutGrid.Col>
|
|
70
|
+
</LayoutGrid>,
|
|
71
|
+
);
|
|
72
|
+
const col = container.querySelector(
|
|
73
|
+
".MuiGrid-container > div",
|
|
74
|
+
) as HTMLElement;
|
|
75
|
+
expect(getComputedStyle(col).maxWidth).toBe("100%");
|
|
76
|
+
done();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -3,14 +3,26 @@
|
|
|
3
3
|
* - Breakpoint pixel thresholds below (600/900/1200/1536) match MUI's own default
|
|
4
4
|
* theme.breakpoints (xs/sm/md/lg/xl). They drive `visibleFrom`/`hiddenFrom`, which
|
|
5
5
|
* have no native MUI Grid equivalent to hook into. If the consuming app customizes
|
|
6
|
-
* its MUI theme breakpoints, these thresholds will not follow that change
|
|
6
|
+
* its MUI theme breakpoints, these thresholds will not follow that change:
|
|
7
7
|
* structural layout mechanics, not a design token.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
/* Brand-layer exemption (recursica-allow-brand): layout-grid tokens have no ui-kit-layer
|
|
11
|
+
* representation in Forge's model (a grid is a page-layout primitive, not a "component"), so
|
|
12
|
+
* LayoutGrid reads the responsive brand_layout-grids_* alias directly (Forge redefines it per
|
|
13
|
+
* breakpoint inside @media). Columns, column-gutter and row-gutter are applied in
|
|
14
|
+
* LayoutGrid.tsx through MUI's own props; `margin` has no MUI Grid prop equivalent, so it's
|
|
15
|
+
* applied here. See LAYOUT_GRID_IMPLEMENTATION_NOTES.md.
|
|
16
|
+
* recursica-allow-brand: --recursica_brand_layout-grids_margin
|
|
17
|
+
*/
|
|
18
|
+
|
|
10
19
|
.root {
|
|
20
|
+
margin: var(--recursica_brand_layout-grids_margin);
|
|
11
21
|
}
|
|
12
22
|
|
|
23
|
+
/* A numeric size larger than the current column count fills the row instead of overflowing. */
|
|
13
24
|
.col {
|
|
25
|
+
max-width: 100%;
|
|
14
26
|
}
|
|
15
27
|
|
|
16
28
|
.hiddenFromXs {
|
|
@@ -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;
|