@recursica/mui-adapter 0.34.0 → 0.34.3

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 (78) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/index.d.ts +219 -154
  3. package/dist/mui-adapter.cjs +55 -55
  4. package/dist/mui-adapter.cjs.map +1 -1
  5. package/dist/mui-adapter.css +1 -1
  6. package/dist/mui-adapter.js +3383 -3440
  7. package/dist/mui-adapter.js.map +1 -1
  8. package/package.json +9 -4
  9. package/src/OverStyling.tsx +2 -2
  10. package/src/components/Accordion/Accordion.test.tsx +1 -0
  11. package/src/components/AssistiveElement/AssistiveElement.test.tsx +1 -0
  12. package/src/components/Autocomplete/Autocomplete.test.tsx +1 -0
  13. package/src/components/Avatar/Avatar.test.tsx +1 -0
  14. package/src/components/Badge/Badge.test.tsx +1 -0
  15. package/src/components/Box/Box.test.tsx +1 -0
  16. package/src/components/Breadcrumb/Breadcrumb.test.tsx +1 -0
  17. package/src/components/Button/BUTTON_IMPLEMENTATION_NOTES.md +10 -0
  18. package/src/components/Button/Button.module.css +1 -0
  19. package/src/components/Button/Button.styleIsolation.dom.test.tsx +98 -0
  20. package/src/components/Card/Card.test.tsx +1 -0
  21. package/src/components/Checkbox/Checkbox.test.tsx +1 -0
  22. package/src/components/Chip/Chip.test.tsx +1 -0
  23. package/src/components/Container/Container.test.tsx +1 -0
  24. package/src/components/DatePicker/DatePicker.stories.tsx +25 -0
  25. package/src/components/DatePicker/DatePicker.test.tsx +1 -0
  26. package/src/components/Dropdown/Dropdown.test.tsx +1 -0
  27. package/src/components/FileInput/FileInput.test.tsx +1 -0
  28. package/src/components/FileUpload/FileUpload.test.tsx +1 -0
  29. package/src/components/Flex/Flex.test.tsx +1 -0
  30. package/src/components/Flex/Flex.tsx +21 -4
  31. package/src/components/FormControlLayout/FormControlLayout.test.tsx +1 -0
  32. package/src/components/FormControlWrapper/FormControlWrapper.test.tsx +1 -0
  33. package/src/components/Grid/Grid.stories.tsx +38 -62
  34. package/src/components/Grid/Grid.test.tsx +1 -0
  35. package/src/components/Grid/Grid.tsx +59 -144
  36. package/src/components/Grid/IMPLEMENTATION_NOTES.md +11 -11
  37. package/src/components/Grid/USAGE.md +8 -6
  38. package/src/components/Group/Group.test.tsx +1 -0
  39. package/src/components/Group/Group.tsx +24 -2
  40. package/src/components/HoverCard/HoverCard.test.tsx +1 -0
  41. package/src/components/Label/Label.test.tsx +1 -0
  42. package/src/components/Link/Link.test.tsx +1 -0
  43. package/src/components/Loader/Loader.test.tsx +1 -0
  44. package/src/components/Menu/Menu.test.tsx +1 -0
  45. package/src/components/Modal/Modal.module.css +103 -1
  46. package/src/components/Modal/Modal.stories.tsx +28 -2
  47. package/src/components/Modal/Modal.test.tsx +1 -0
  48. package/src/components/NumberInput/NumberInput.test.tsx +1 -0
  49. package/src/components/Pagination/Pagination.test.tsx +1 -0
  50. package/src/components/Panel/Panel.test.tsx +1 -0
  51. package/src/components/Popover/Popover.test.tsx +1 -0
  52. package/src/components/Radio/Radio.test.tsx +1 -0
  53. package/src/components/ReadOnlyField/ReadOnlyField.test.tsx +1 -0
  54. package/src/components/SegmentedControl/SegmentedControl.test.tsx +1 -0
  55. package/src/components/Slider/Slider.test.tsx +1 -0
  56. package/src/components/Stack/Stack.stories.tsx +9 -9
  57. package/src/components/Stack/Stack.test.tsx +1 -0
  58. package/src/components/Stack/Stack.tsx +15 -14
  59. package/src/components/Stack/USAGE.md +1 -1
  60. package/src/components/Stepper/Stepper.test.tsx +1 -0
  61. package/src/components/Switch/Switch.stories.tsx +1 -1
  62. package/src/components/Switch/Switch.test.tsx +1 -0
  63. package/src/components/Table/Table.test.tsx +1 -0
  64. package/src/components/Tabs/Tabs.test.tsx +1 -0
  65. package/src/components/Text/Text.stories.tsx +3 -16
  66. package/src/components/Text/Text.test.tsx +1 -0
  67. package/src/components/TextArea/TextArea.test.tsx +1 -0
  68. package/src/components/TextField/TextField.test.tsx +1 -0
  69. package/src/components/TimePicker/TimePicker.test.tsx +1 -0
  70. package/src/components/Timeline/Timeline.test.tsx +1 -0
  71. package/src/components/Title/Title.stories.tsx +3 -16
  72. package/src/components/Title/Title.test.tsx +1 -0
  73. package/src/components/Toast/Toast.test.tsx +1 -0
  74. package/src/components/Tooltip/Tooltip.test.tsx +1 -0
  75. package/src/components/TransferList/TransferList.test.tsx +1 -0
  76. package/src/components/Tree/Tree.test.tsx +1 -0
  77. package/src/components/Typography/Typography.test.tsx +1 -0
  78. package/src/utils/filterStylingProps.test.ts +85 -0
@@ -1,22 +1,30 @@
1
1
  /**
2
2
  * Grid layout wrapper.
3
3
  *
4
- * NOTE: MUI merges "container" and "item" into a single Grid component (no separate
5
- * Grid.Col in the underlying library, and no native `order`, `justifyContent`,
6
- * `alignItems`, or breakpoint-visibility support). To preserve API parity with the
7
- * mantine-adapter (Grid + Grid.Col dot notation, matching Mantine's own Grid/Grid.Col),
8
- * this file splits MUI's single component back into two wrappers: Grid always renders
9
- * MUI's `<Grid container>`, GridCol always renders MUI's `<Grid>` in item mode.
4
+ * MUI merges "container" and "item" into a single `Grid` component (no separate Grid.Col,
5
+ * unlike Mantine). To preserve the Grid/Grid.Col dot-notation shape across adapters, this
6
+ * file splits MUI's single component back into two thin wrappers: Grid always renders MUI's
7
+ * `<Grid container>`, GridCol always renders a plain MUI `<Grid>` (item mode).
8
+ *
9
+ * Per the layout-components rule, both wrappers simply pass through MUI's own Grid props
10
+ * (`size`, `offset`, `spacing`, `columns`, `direction`, `wrap`) with no Recursica renaming.
11
+ * MUI has no per-item "grow to fill remaining space" container flag like Mantine's `grow`;
12
+ * use MUI's own `size="grow"` on individual columns instead. `visibleFrom`/`hiddenFrom`
13
+ * don't exist in MUI at all, so — per the rule that a missing kit feature is built following
14
+ * Mantine's own shape — they're added here via CSS classes, keyed on MUI's own breakpoint
15
+ * scale (`xs`/`sm`/`md`/`lg`/`xl`).
16
+ *
17
+ * `justifyContent`/`alignItems` (container) and `order` (item) are typed on `MuiGridProps`
18
+ * (inherited generically from `SystemProps`) but MUI's Grid style generator — verified by
19
+ * reading `@mui/system`'s `gridGenerator.js`/`createGrid.js` — only wires up `size`, `offset`,
20
+ * `columns`, `spacing`/`rowSpacing`/`columnSpacing`, `direction`, and `wrap`; passing these
21
+ * three straight through would silently no-op. They're pulled out and applied via inline
22
+ * `style` instead, still using MUI's own native names.
10
23
  *
11
24
  * Like Flex, Stack, Group, and Container, this is a primitive layout component and
12
25
  * does not use the `RecursicaOverStyled` gatekeeper — only the `sx` prop is stripped.
13
26
  */
14
- import {
15
- createContext,
16
- forwardRef,
17
- useContext,
18
- type CSSProperties,
19
- } from "react";
27
+ import { forwardRef, type CSSProperties } from "react";
20
28
  import { Grid as MuiGrid, type GridProps as MuiGridProps } from "@mui/material";
21
29
  import {
22
30
  SPACING_MAP,
@@ -26,81 +34,9 @@ import {
26
34
  } from "../../utils/filterStylingProps";
27
35
  import styles from "./Grid.module.css";
28
36
 
29
- import {
30
- type RecursicaGridProps,
31
- type RecursicaGridColProps,
32
- } from "@recursica/adapter-common";
33
-
34
37
  type Breakpoint = "xs" | "sm" | "md" | "lg" | "xl";
35
- type RecursicaBreakpoint = "base" | Breakpoint;
36
-
37
- const GridContext = createContext<{ grow: boolean }>({ grow: false });
38
-
39
- /** Remaps the Recursica-only "base" breakpoint key to MUI's "xs" (its smallest breakpoint). */
40
- function remapBase<T>(
41
- value: T | Partial<Record<RecursicaBreakpoint, T>> | undefined,
42
- ): T | Partial<Record<Breakpoint, T>> | undefined {
43
- if (value === undefined || typeof value !== "object") {
44
- return value as T | undefined;
45
- }
46
- const { base, ...rest } = value as Partial<Record<RecursicaBreakpoint, T>>;
47
- return base !== undefined
48
- ? ({ xs: base, ...rest } as Partial<Record<Breakpoint, T>>)
49
- : (rest as Partial<Record<Breakpoint, T>>);
50
- }
51
-
52
- /**
53
- * Recursica's "auto" (grow to fill remaining space) and "content" (size to content)
54
- * are the inverse of MUI's own "auto" (size to content) and "grow" (fill remaining
55
- * space) keywords.
56
- */
57
- function mapSpanValue(
58
- value: number | "auto" | "content" | undefined,
59
- ): number | "auto" | "grow" | undefined {
60
- if (value === "auto") return "grow";
61
- if (value === "content") return "auto";
62
- return value;
63
- }
64
-
65
- function mapSpan(span: RecursicaGridColProps["span"]): MuiGridProps["size"] {
66
- const remapped = remapBase(span);
67
- if (remapped === undefined || typeof remapped !== "object") {
68
- return mapSpanValue(remapped as number | "auto" | "content" | undefined);
69
- }
70
- const mapped: Partial<Record<Breakpoint, number | "auto" | "grow">> = {};
71
- (
72
- Object.entries(remapped) as [Breakpoint, number | "auto" | "content"][]
73
- ).forEach(([key, value]) => {
74
- mapped[key] = mapSpanValue(value);
75
- });
76
- return mapped as MuiGridProps["size"];
77
- }
78
-
79
- function mapOffset(
80
- offset: RecursicaGridColProps["offset"],
81
- ): MuiGridProps["offset"] {
82
- return remapBase(offset) as MuiGridProps["offset"];
83
- }
84
-
85
- /**
86
- * `order` has no native MUI Grid equivalent. A fixed number is applied directly.
87
- * A responsive object is NOT fully supported in this v1 — the smallest specified
88
- * breakpoint's value is applied as a single static order (documented in
89
- * IMPLEMENTATION_NOTES.md), rather than building bespoke per-breakpoint CSS for it.
90
- */
91
- function resolveOrder(
92
- order: RecursicaGridColProps["order"],
93
- ): number | undefined {
94
- if (order === undefined || typeof order === "number") {
95
- return order;
96
- }
97
- const remapped = remapBase(order) as Partial<Record<Breakpoint, number>>;
98
- return (
99
- remapped.xs ?? remapped.sm ?? remapped.md ?? remapped.lg ?? remapped.xl
100
- );
101
- }
102
38
 
103
- const HIDDEN_FROM_CLASS: Partial<Record<Breakpoint, string>> = {
39
+ const HIDDEN_FROM_CLASS: Record<Breakpoint, string> = {
104
40
  xs: styles.hiddenFromXs,
105
41
  sm: styles.hiddenFromSm,
106
42
  md: styles.hiddenFromMd,
@@ -108,7 +44,7 @@ const HIDDEN_FROM_CLASS: Partial<Record<Breakpoint, string>> = {
108
44
  xl: styles.hiddenFromXl,
109
45
  };
110
46
 
111
- // "xs"/"base" intentionally absent: visible from the smallest breakpoint means never hidden.
47
+ // "xs" intentionally absent: visible from the smallest breakpoint means never hidden.
112
48
  const VISIBLE_FROM_CLASS: Partial<Record<Breakpoint, string>> = {
113
49
  sm: styles.visibleFromSm,
114
50
  md: styles.visibleFromMd,
@@ -116,56 +52,47 @@ const VISIBLE_FROM_CLASS: Partial<Record<Breakpoint, string>> = {
116
52
  xl: styles.visibleFromXl,
117
53
  };
118
54
 
119
- function normalizeBreakpoint(bp: RecursicaBreakpoint): Breakpoint {
120
- return bp === "base" ? "xs" : bp;
121
- }
122
-
123
55
  // ============================================================
124
56
  // GRID
125
57
  // ============================================================
126
58
 
127
59
  export type GridProps = WithRecursicaSpacing<
128
- OmitSx<Omit<MuiGridProps, "container" | "size" | "offset">> &
129
- RecursicaGridProps
130
- >;
60
+ OmitSx<Omit<MuiGridProps, "container" | "justifyContent" | "alignItems">>
61
+ > & {
62
+ /** Sets `justify-content` on the container. Applied via inline style — see notes above. */
63
+ justifyContent?: CSSProperties["justifyContent"];
64
+ /** Sets `align-items` on the container. Applied via inline style — see notes above. */
65
+ alignItems?: CSSProperties["alignItems"];
66
+ };
131
67
 
132
68
  const GridBase = forwardRef<HTMLDivElement, GridProps>(function Grid(
133
69
  {
134
70
  children,
135
- gap = "rec-default",
136
- columns = 12,
137
- grow = false,
138
- justify,
139
- align,
71
+ spacing = "rec-default",
72
+ justifyContent,
73
+ alignItems,
140
74
  style,
141
75
  ...rest
142
76
  },
143
77
  ref,
144
78
  ) {
145
79
  const safeProps = filterSxProp(rest as Record<string, unknown>);
146
- const resolvedGap =
147
- typeof gap === "string" && gap in SPACING_MAP
148
- ? SPACING_MAP[gap as keyof typeof SPACING_MAP]
149
- : gap;
80
+ const resolvedSpacing =
81
+ typeof spacing === "string" && spacing in SPACING_MAP
82
+ ? SPACING_MAP[spacing as keyof typeof SPACING_MAP]
83
+ : spacing;
150
84
 
151
85
  return (
152
- <GridContext.Provider value={{ grow }}>
153
- <MuiGrid
154
- ref={ref}
155
- {...(safeProps as unknown as MuiGridProps)}
156
- container
157
- columns={columns}
158
- spacing={resolvedGap}
159
- className={styles.root}
160
- style={{
161
- justifyContent: justify,
162
- alignItems: align,
163
- ...(style as CSSProperties),
164
- }}
165
- >
166
- {children}
167
- </MuiGrid>
168
- </GridContext.Provider>
86
+ <MuiGrid
87
+ ref={ref}
88
+ {...(safeProps as unknown as MuiGridProps)}
89
+ container
90
+ spacing={resolvedSpacing}
91
+ className={styles.root}
92
+ style={{ justifyContent, alignItems, ...(style as CSSProperties) }}
93
+ >
94
+ {children}
95
+ </MuiGrid>
169
96
  );
170
97
  });
171
98
  GridBase.displayName = "Grid";
@@ -175,33 +102,27 @@ GridBase.displayName = "Grid";
175
102
  // ============================================================
176
103
 
177
104
  export type GridColProps = WithRecursicaSpacing<
178
- OmitSx<Omit<MuiGridProps, "container" | "size" | "offset">> &
179
- RecursicaGridColProps
180
- >;
105
+ OmitSx<Omit<MuiGridProps, "container" | "order">>
106
+ > & {
107
+ /** Sets the CSS `order` property. Applied via inline style — see notes above. */
108
+ order?: number;
109
+ /** Hides the column below the given breakpoint. MUI has no native equivalent. */
110
+ visibleFrom?: Breakpoint;
111
+ /** Hides the column above the given breakpoint. MUI has no native equivalent. */
112
+ hiddenFrom?: Breakpoint;
113
+ };
181
114
 
182
115
  export const GridCol = forwardRef<HTMLDivElement, GridColProps>(
183
116
  function GridCol(
184
- {
185
- children,
186
- span,
187
- offset,
188
- order,
189
- visibleFrom,
190
- hiddenFrom,
191
- style,
192
- className,
193
- ...rest
194
- },
117
+ { children, order, visibleFrom, hiddenFrom, style, className, ...rest },
195
118
  ref,
196
119
  ) {
197
- const { grow } = useContext(GridContext);
198
120
  const safeProps = filterSxProp(rest as Record<string, unknown>);
199
- const resolvedOrder = resolveOrder(order);
200
121
 
201
122
  const visibilityClass = visibleFrom
202
- ? VISIBLE_FROM_CLASS[normalizeBreakpoint(visibleFrom)]
123
+ ? VISIBLE_FROM_CLASS[visibleFrom]
203
124
  : hiddenFrom
204
- ? HIDDEN_FROM_CLASS[normalizeBreakpoint(hiddenFrom)]
125
+ ? HIDDEN_FROM_CLASS[hiddenFrom]
205
126
  : undefined;
206
127
 
207
128
  const finalClassName = [styles.col, visibilityClass, className]
@@ -211,15 +132,9 @@ export const GridCol = forwardRef<HTMLDivElement, GridColProps>(
211
132
  return (
212
133
  <MuiGrid
213
134
  ref={ref}
214
- size={mapSpan(span)}
215
- offset={mapOffset(offset)}
216
135
  className={finalClassName}
217
136
  {...(safeProps as unknown as MuiGridProps)}
218
- style={{
219
- order: resolvedOrder,
220
- ...(grow ? { flexGrow: 1 } : {}),
221
- ...(style as CSSProperties),
222
- }}
137
+ style={{ order, ...(style as CSSProperties) }}
223
138
  >
224
139
  {children}
225
140
  </MuiGrid>
@@ -1,16 +1,16 @@
1
1
  # Grid Implementation Notes
2
2
 
3
- MUI has no native Grid/Grid.Col split — MUI's own `Grid` component merges "container" and "item" behavior into one component (toggled by the `container` boolean and a `size` prop). To keep the exact same public API as the mantine-adapter (`Grid` + `Grid.Col` dot notation, matching Mantine's own `Grid`/`Grid.Col`), this file hand-composes two wrappers around that single underlying component: `Grid` always renders `<MuiGrid container>`, `Grid.Col` always renders `<MuiGrid>` in item mode. A `GridContext` (internal, not exported) carries the parent's `grow` flag down to `Grid.Col`, since MUI has no equivalent React context of its own for this and siblings can't share props directly.
3
+ MUI has no native Grid/Grid.Col split — MUI's own `Grid` component merges "container" and "item" behavior into one component (toggled by the `container` boolean and a `size` prop). To keep the same `Grid` + `Grid.Col` dot-notation shape as the mantine-adapter, this file hand-composes two thin wrappers around that single underlying component: `Grid` always renders `<MuiGrid container>`, `Grid.Col` always renders `<MuiGrid>` in item mode. This is a structural convenience only the props on each wrapper are MUI's own, not a Recursica-defined contract.
4
4
 
5
- Prop mapping details and divergences from a literal 1:1 MUI wrap:
5
+ Per the layout-components rule (no formal Recursica common props definition — layout components simply pass through the underlying kit's own props), this Grid does **not** attempt prop-name parity with the mantine-adapter's Grid. Its public API is MUI's own vocabulary:
6
6
 
7
- - **`gap` `spacing`**: MUI's own container-spacing prop is `spacing` (not `gutter`, which is Mantine's term). Resolved through the same `rec-*` → `SPACING_MAP` lookup used elsewhere in this adapter.
8
- - **`span` keyword inversion**: Recursica's `"auto"` (fill remaining space) and `"content"` (size to content) are the _inverse_ of MUI's own `size` keywords MUI's `"auto"` means size-to-content and `"grow"` means fill-remaining-space. `mapSpanValue()` translates between them so the public `span` prop matches Mantine's semantics exactly. Verified against `@mui/system`'s `generateGridSizeStyles` (`grow` → `flexGrow:1`, `auto` → `flexBasis:auto`).
9
- - **`offset`**: Maps directly to MUI's own `offset` prop — same name, same responsive shape and semantics. No translation needed.
10
- - **`"base"` breakpoint key**: Recursica's breakpoint objects (matching Mantine's `Grid.Col` shape) use `base` for the default/smallest value; MUI has no `"base"` key, only `xs` as its smallest breakpoint (`min-width: 0`). `remapBase()` renames `base` → `xs` (an explicit `xs` in the same object wins over `base` if both are present).
11
- - **`order`**: MUI's Grid has no `order` prop at all. A fixed number is applied via inline `style.order` (inline style always wins the cascade, so this works regardless of MUI's generated styles). A responsive object is **not** fully implemented building full per-breakpoint CSS custom-property cascading for a rarely-used prop wasn't judged worth the added CSS surface for v1. `resolveOrder()` currently just takes the smallest specified breakpoint's value and applies it as a single static order. This is a known, documented gap, not a silent one — revisit if a real per-breakpoint ordering need comes up.
12
- - **`grow`**: MUI's Grid has no equivalent. Mantine's own implementation (and this one) relies on a CSS quirk: setting `flex-grow: 1` on every column in a row only visibly affects the _last, incomplete_ row — fully-filled rows have zero remaining space to grow into. Applied via inline `style.flexGrow` on `Grid.Col`, read from `GridContext`.
13
- - **`visibleFrom` / `hiddenFrom`**: No native MUI Grid mechanism. Implemented via `Grid.module.css` media-query classes, hardcoded to MUI's own default `theme.breakpoints` pixel values (`xs:0, sm:600, md:900, lg:1200, xl:1536`). If the consuming app customizes its MUI theme breakpoints, these thresholds won't follow structural layout mechanics, not a design token, so this doesn't go through the token pipeline.
14
- - **`justifyContent` / `alignItems`**: MUI's `Grid` does not register these as recognized style-system props the way `Box` does (confirmed by reading `@mui/system`'s `gridGenerator.js` no generator function reads them), so passing them as bare props would silently do nothing. They're applied via inline `style` instead.
7
+ - **`spacing`** (not Mantine's `gutter`/the old Recursica `gap` alias) — MUI's own container-spacing prop. Still accepts `rec-*` tokens, resolved through the same `SPACING_MAP` lookup used elsewhere in this adapter.
8
+ - **`size`** (not Mantine's `span`) MUI's own item-sizing prop on `Grid.Col`: a column count, `"auto"` (size to content), `"grow"` (fill remaining space), or a responsive object keyed by MUI's own breakpoints. No inversion/translation against Mantine's `span` semantics.
9
+ - **`offset`** MUI's own prop, used as-is.
10
+ - **Breakpoint objects** use MUI's own scale (`xs`/`sm`/`md`/`lg`/`xl`) directly. There is no `"base"` alias `xs` is MUI's smallest breakpoint, same role Mantine's own smallest breakpoint plays.
11
+ - **`grow` (container-level)**: not implemented. Mantine's container `grow` flag ("last row's columns expand to fill available space") has no MUI equivalent, but MUI's own `size="grow"` on individual `Grid.Col`s already covers the same real-world need set it per-column instead of on the container.
12
+ - **`order`**: MUI types `order` generically (via `SystemProps`) but its Grid style generator (`gridGenerator.js`/`createGrid.js`, read directly to confirm) never wires it up passing it straight through silently no-ops. Applied here via inline `style.order` instead, single value only (no per-breakpoint object).
13
+ - **`justifyContent` / `alignItems`** (container only): same issue as `order` typed but not read by MUI's Grid generator (confirmed by reading `gridGenerator.js`: only `size`, `offset`, `columns`, spacing, `direction`, and `wrap` have generator functions). Applied via inline `style` instead, using MUI's own prop names (not Mantine's `justify`/`align`).
14
+ - **`visibleFrom` / `hiddenFrom`**: no native MUI Grid mechanism at all, so per the rule that a missing kit feature is built following Mantine's own shape — implemented via `Grid.module.css` media-query classes, hardcoded to MUI's own default `theme.breakpoints` pixel values (`xs:0, sm:600, md:900, lg:1200, xl:1536`). If the consuming app customizes its MUI theme breakpoints, these thresholds won't follow structural layout mechanics, not a design token.
15
15
 
16
- **Cross-adapter parity caveat**: `visibleFrom`/`hiddenFrom` (and the eventual full responsive `order`) are pinned to MUI's default breakpoints here, while the mantine-adapter's equivalent behavior is driven by Mantine's own default breakpoints (576/768/992/1200/1408). The two adapters' Grid stories may show different columns at the exact same viewport width in the `adapter-tester` visual-regression suite as a result — a real, structural difference between the two underlying libraries' breakpoint scales, not a bug in either adapter.
16
+ **Cross-adapter parity caveat**: `visibleFrom`/`hiddenFrom` are pinned to MUI's default breakpoints here, while the mantine-adapter's equivalent behavior is driven by Mantine's own default breakpoints (576/768/992/1200/1408). The two adapters' Grid stories may show different columns at the exact same viewport width in the `adapter-tester` visual-regression suite as a result — a real, structural difference between the two underlying libraries' breakpoint scales, not a bug in either adapter. The two adapters' Grid APIs are also no longer prop-name-identical (`spacing`/`size` here vs. `gutter`/`span` in mantine-adapter) — this is intentional per the layout-components rule; each adapter's Grid speaks its own kit's vocabulary.
@@ -20,9 +20,9 @@ import { Grid } from "@recursica/mui-adapter";
20
20
 
21
21
  export default function Demo() {
22
22
  return (
23
- <Grid gap="rec-default">
24
- <Grid.Col span={6}>Half width</Grid.Col>
25
- <Grid.Col span={{ base: 12, sm: 6, md: 3 }}>Responsive width</Grid.Col>
23
+ <Grid spacing="rec-default">
24
+ <Grid.Col size={6}>Half width</Grid.Col>
25
+ <Grid.Col size={{ xs: 12, sm: 6, md: 3 }}>Responsive width</Grid.Col>
26
26
  </Grid>
27
27
  );
28
28
  }
@@ -44,8 +44,10 @@ All Recursica components in the `@recursica/mui-adapter` package adhere strictly
44
44
 
45
45
  ## 4. Key Integration Features & Constraints
46
46
 
47
- - `gap` controls the spacing between grid items.
48
- - `span` accepts a column count, `"auto"`, `"content"`, or a responsive object (`{ base, sm, md, ... }`).
47
+ This Grid uses MUI's own prop vocabulary directly (see [IMPLEMENTATION_NOTES.md](./IMPLEMENTATION_NOTES.md)) — it is not name-for-name identical to the mantine-adapter's Grid.
48
+
49
+ - `spacing` controls the space between grid items (accepts `rec-*` tokens).
50
+ - `size` accepts a column count, `"auto"`, `"grow"`, or a responsive object (`{ xs, sm, md, ... }`).
49
51
  - `offset` shifts a column by a number of columns.
50
- - `order` accepts a fixed number to control a column's visual order. A responsive object (`{ base, sm, md, ... }`) is **not** fully supported yet — only the smallest specified breakpoint's value is applied.
52
+ - `order` accepts a fixed number to control a column's visual order (single value only, no responsive object).
51
53
  - `visibleFrom`/`hiddenFrom` show or hide a column at the standard breakpoints (600/900/1200/1536px).
@@ -0,0 +1 @@
1
+ // Placeholder for future Group component tests. Add tests here if/when needed.
@@ -1,3 +1,12 @@
1
+ /**
2
+ * Group layout wrapper.
3
+ *
4
+ * NOTE: MUI has no native `<Group>` component. Per the layout-components rule, when a UI kit
5
+ * lacks an equivalent component it's built following a prop definition similar to Mantine's
6
+ * own `Group` (gap/rowGap/columnGap/justify/align/wrap) — this shape is owned locally here,
7
+ * not shared as a formal cross-adapter Recursica contract. It's implemented as a row-direction,
8
+ * wrapping MUI `Stack`.
9
+ */
1
10
  import { forwardRef } from "react";
2
11
  import {
3
12
  Stack as MUIStack,
@@ -10,10 +19,23 @@ import {
10
19
  type WithRecursicaSpacing,
11
20
  } from "../../utils/filterStylingProps";
12
21
 
13
- import { type RecursicaGroupProps } from "@recursica/adapter-common";
22
+ interface GroupOwnProps {
23
+ /** Gap spacing */
24
+ gap?: string | number;
25
+ /** Row gap spacing */
26
+ rowGap?: string | number;
27
+ /** Column gap spacing */
28
+ columnGap?: string | number;
29
+ /** Align items justify properties */
30
+ justify?: React.CSSProperties["justifyContent"];
31
+ /** Align items vertical alignment */
32
+ align?: React.CSSProperties["alignItems"];
33
+ /** Flex-wrap settings */
34
+ wrap?: React.CSSProperties["flexWrap"];
35
+ }
14
36
 
15
37
  export type GroupProps = WithRecursicaSpacing<
16
- OmitSx<Omit<MUIStackProps, "spacing" | "direction"> & RecursicaGroupProps>
38
+ OmitSx<Omit<MUIStackProps, "spacing" | "direction"> & GroupOwnProps>
17
39
  >;
18
40
 
19
41
  export const Group = forwardRef<HTMLDivElement, GroupProps>(function Group(
@@ -0,0 +1 @@
1
+ // Placeholder for future HoverCard component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future Label component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future Link component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future Loader component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future Menu component tests. Add tests here if/when needed.
@@ -1,3 +1,14 @@
1
+ /* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
2
+ * Close button is styled to match the Button component's text/icon-only variant, so it reuses
3
+ * the same global hover/focus state tokens Button.module.css exempts.
4
+ * recursica-allow-brand: --recursica_brand_states_focus_blur
5
+ * recursica-allow-brand: --recursica_brand_states_focus_border-size
6
+ * recursica-allow-brand: --recursica_brand_states_focus_color
7
+ * recursica-allow-brand: --recursica_brand_states_focus_margin
8
+ * recursica-allow-brand: --recursica_brand_states_hover_color
9
+ * recursica-allow-brand: --recursica_brand_states_hover_opacity
10
+ */
11
+
1
12
  .root {
2
13
  }
3
14
 
@@ -40,6 +51,9 @@
40
51
  }
41
52
 
42
53
  .header {
54
+ display: flex; /* HARDCODE: puts the title and close button side-by-side so the title has a bounded width to truncate against */
55
+ align-items: center;
56
+ justify-content: space-between;
43
57
  padding: var(
44
58
  --recursica_ui-kit_components_modal_properties_header-footer-vertical-padding
45
59
  )
@@ -50,6 +64,12 @@
50
64
  }
51
65
 
52
66
  .title {
67
+ flex: 1 1 auto; /* HARDCODE: let the title claim the space between the header edge and the close button */
68
+ min-width: 0; /* HARDCODE: required for text-overflow ellipsis to take effect on a flex child */
69
+ overflow: hidden;
70
+ white-space: nowrap;
71
+ text-overflow: ellipsis;
72
+
53
73
  color: var(--recursica_ui-kit_components_modal_properties_colors_title);
54
74
 
55
75
  /* Direct Figma Typography Mapping */
@@ -173,5 +193,87 @@
173
193
  }
174
194
 
175
195
  .close {
176
- /* Inherits standard icon button tokens natively, but we can override if required */
196
+ /* Matches the Button component's text-variant, icon-only, small-size visual treatment
197
+ (see Button.module.css) so the modal close control looks like a Recursica Button
198
+ rather than MUI's native IconButton. */
199
+ box-sizing: border-box;
200
+ display: flex;
201
+ align-items: center;
202
+ justify-content: center;
203
+ position: relative;
204
+ overflow: hidden;
205
+ transition: all 0.2s ease;
206
+
207
+ height: var(
208
+ --recursica_ui-kit_components_button_variants_sizes_small_properties_height
209
+ );
210
+ min-width: var(
211
+ --recursica_ui-kit_components_button_variants_content_icon-only_variants_sizes_small_properties_min-width
212
+ );
213
+ padding: 0
214
+ var(
215
+ --recursica_ui-kit_components_button_variants_content_icon-only_variants_sizes_small_properties_horizontal-padding
216
+ );
217
+ border-radius: var(
218
+ --recursica_ui-kit_components_button_variants_content_icon-only_variants_sizes_small_properties_border-radius
219
+ );
220
+
221
+ border-style: solid;
222
+ border-width: var(
223
+ --recursica_ui-kit_components_button_variants_styles_text_properties_border-size
224
+ );
225
+ border-color: var(
226
+ --recursica_ui-kit_components_button_variants_styles_text_properties_colors_border-color
227
+ );
228
+ background-color: var(
229
+ --recursica_ui-kit_components_button_variants_styles_text_properties_colors_background-color
230
+ );
231
+ color: var(
232
+ --recursica_ui-kit_components_button_variants_styles_text_properties_colors_icon-color
233
+ );
234
+ }
235
+
236
+ .close svg {
237
+ position: relative;
238
+ z-index: 1;
239
+ width: var(
240
+ --recursica_ui-kit_components_button_variants_sizes_small_properties_icon
241
+ );
242
+ height: var(
243
+ --recursica_ui-kit_components_button_variants_sizes_small_properties_icon
244
+ );
245
+ }
246
+
247
+ .close::after {
248
+ content: "";
249
+ position: absolute;
250
+ inset: 0;
251
+ border-radius: inherit;
252
+ z-index: 0;
253
+ pointer-events: none;
254
+ transition: opacity 150ms ease;
255
+ opacity: 0;
256
+ background-color: var(--recursica_brand_states_hover_color);
257
+ }
258
+ .close:hover:not(:disabled)::after {
259
+ opacity: var(--recursica_brand_states_hover_opacity);
260
+ }
261
+
262
+ /* MuiIconButton injects its own `:hover` background via emotion at render time, which lands in
263
+ the DOM after this stylesheet and wins the tie at equal specificity — same class of conflict
264
+ documented in DatePicker.module.css's error/disabled state overrides. `!important` keeps the
265
+ button's own background transparent so only the `::after` overlay above renders the hover
266
+ feedback, matching Button's single-overlay hover treatment exactly. */
267
+ .close:hover {
268
+ background-color: transparent !important;
269
+ }
270
+
271
+ .close:focus-visible {
272
+ outline: none;
273
+ box-shadow:
274
+ 0 0 0 var(--recursica_brand_states_focus_border-size)
275
+ var(--recursica_brand_states_focus_color),
276
+ 0 0 var(--recursica_brand_states_focus_blur)
277
+ var(--recursica_brand_states_focus_margin)
278
+ var(--recursica_brand_states_focus_color);
177
279
  }
@@ -19,7 +19,8 @@ export default meta;
19
19
  type Story = StoryObj<typeof Modal>;
20
20
 
21
21
  const DefaultWrapper = (args: ModalProps) => {
22
- const [opened, setOpened] = useState(false);
22
+ // Starts opened so the modal is visible without pressing a button first.
23
+ const [opened, setOpened] = useState(true);
23
24
  return (
24
25
  <>
25
26
  <Modal {...args} opened={opened} onClose={() => setOpened(false)}>
@@ -43,8 +44,33 @@ export const Default: Story = {
43
44
  render: (args) => <DefaultWrapper {...args} />,
44
45
  };
45
46
 
47
+ const LongTitleWrapper = (args: ModalProps) => {
48
+ const [opened, setOpened] = useState(true);
49
+ return (
50
+ <>
51
+ <Modal {...args} opened={opened} onClose={() => setOpened(false)}>
52
+ The title above is longer than the header can display, so it truncates
53
+ with an ellipsis instead of wrapping onto a second line.
54
+ <Modal.Footer>
55
+ <Button onClick={() => setOpened(false)}>Got it</Button>
56
+ </Modal.Footer>
57
+ </Modal>
58
+ <Button onClick={() => setOpened(true)}>Open Modal</Button>
59
+ </>
60
+ );
61
+ };
62
+
63
+ export const LongTitle: Story = {
64
+ args: {
65
+ title:
66
+ "This Modal Title Is Deliberately Long Enough To Exceed The Available Header Width",
67
+ },
68
+ render: (args) => <LongTitleWrapper {...args} />,
69
+ };
70
+
46
71
  const ScrollingWrapper = (args: ModalProps) => {
47
- const [opened, setOpened] = useState(false);
72
+ // Starts opened so the modal is visible without pressing a button first.
73
+ const [opened, setOpened] = useState(true);
48
74
  return (
49
75
  <>
50
76
  <Modal {...args} opened={opened} onClose={() => setOpened(false)}>
@@ -0,0 +1 @@
1
+ // Placeholder for future Modal component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future NumberInput component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future Pagination component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future Panel component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future Popover component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future Radio component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future ReadOnlyField component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future SegmentedControl component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future Slider component tests. Add tests here if/when needed.