@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.
Files changed (63) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/OVERSTYLING.md +1 -1
  3. package/SETUP.md +26 -3
  4. package/dist/adapter-mui-v7.cjs +64 -64
  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 +5869 -5645
  8. package/dist/adapter-mui-v7.js.map +1 -1
  9. package/dist/index.d.ts +123 -104
  10. package/llms.txt +1 -1
  11. package/package.json +4 -4
  12. package/recursica_brand.json +172 -235
  13. package/recursica_tokens.json +16 -127
  14. package/recursica_ui-kit.json +2195 -8127
  15. package/recursica_variables_scoped.css +4547 -9194
  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/FormControlLayout/FormControlLayout.module.css +6 -3
  23. package/src/components/Group/USAGE.md +2 -1
  24. package/src/components/Heading/Heading.stories.tsx +62 -0
  25. package/src/components/Heading/Heading.tsx +6 -2
  26. package/src/components/Heading/USAGE.md +19 -1
  27. package/src/components/LayoutGrid/LAYOUT_GRID_IMPLEMENTATION_NOTES.md +82 -0
  28. package/src/components/LayoutGrid/LayoutGrid.columns.dom.test.tsx +78 -0
  29. package/src/components/{Grid/Grid.module.css → LayoutGrid/LayoutGrid.module.css} +13 -1
  30. package/src/components/LayoutGrid/LayoutGrid.stories.tsx +61 -0
  31. package/src/components/LayoutGrid/LayoutGrid.test.tsx +1 -0
  32. package/src/components/LayoutGrid/LayoutGrid.tsx +181 -0
  33. package/src/components/LayoutGrid/USAGE.md +79 -0
  34. package/src/components/LayoutGrid/index.ts +1 -0
  35. package/src/components/Menu/Menu.stories.tsx +0 -3
  36. package/src/components/Pagination/IMPLEMENTATION_NOTES.md +42 -38
  37. package/src/components/Pagination/Pagination.icons.tsx +0 -32
  38. package/src/components/Pagination/Pagination.manifest.dom.test.tsx +105 -0
  39. package/src/components/Pagination/Pagination.module.css +25 -164
  40. package/src/components/Pagination/Pagination.tsx +221 -97
  41. package/src/components/Pagination/USAGE.md +8 -1
  42. package/src/components/Panel/Panel.module.css +24 -1
  43. package/src/components/Panel/Panel.stories.tsx +4 -4
  44. package/src/components/Panel/Panel.tsx +10 -1
  45. package/src/components/Radio/Radio.module.css +2 -2
  46. package/src/components/SegmentedControl/SegmentedControl.module.css +18 -0
  47. package/src/components/Stack/USAGE.md +2 -1
  48. package/src/components/Stepper/Stepper.module.css +0 -21
  49. package/src/components/Text/Text.stories.tsx +52 -3
  50. package/src/components/Text/Text.tsx +13 -3
  51. package/src/components/Text/USAGE.md +20 -2
  52. package/src/components/Typography/Typography.module.css +39 -0
  53. package/src/components/index.ts +1 -1
  54. package/src/index.ts +1 -0
  55. package/src/utils/breakpointsFromRecManifest.test.ts +40 -0
  56. package/src/utils/breakpointsFromRecManifest.ts +72 -0
  57. package/src/utils/index.ts +1 -0
  58. package/src/components/Grid/Grid.stories.tsx +0 -161
  59. package/src/components/Grid/Grid.test.tsx +0 -1
  60. package/src/components/Grid/Grid.tsx +0 -155
  61. package/src/components/Grid/IMPLEMENTATION_NOTES.md +0 -24
  62. package/src/components/Grid/USAGE.md +0 -53
  63. 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: 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
@@ -30,6 +30,7 @@
30
30
  display: flex;
31
31
  flex-direction: column;
32
32
  width: 100%;
33
+ max-width: var(--recursica_ui-kit_globals_form_field_size_max-width);
33
34
  gap: var(--recursica_ui-kit_components_file-upload_properties_list-spacing);
34
35
  }
35
36
 
@@ -264,6 +264,7 @@ export const FileUpload = forwardRef<HTMLDivElement, FileUploadProps>(
264
264
  required={required}
265
265
  disabled={disabled}
266
266
  id={id}
267
+ controlMaxWidth="var(--recursica_ui-kit_globals_form_field_size_max-width)"
267
268
  >
268
269
  <div
269
270
  ref={ref}
@@ -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
- min-width: 0; /* Break boundary preventing internal flex explosion */
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"> & RecursicaHeadingProps
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. Design System Integration
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;