@recursica/mui-adapter 0.19.0 → 0.21.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 (40) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +3 -3
  3. package/dist/mui-adapter.cjs +89 -58
  4. package/dist/mui-adapter.cjs.map +1 -1
  5. package/dist/mui-adapter.css +1 -1
  6. package/dist/mui-adapter.js +26163 -8727
  7. package/dist/mui-adapter.js.map +1 -1
  8. package/dist/src/components/Dropdown/BareDropdown.d.ts +41 -0
  9. package/dist/src/components/TimePicker/TimePicker.d.ts +16 -3
  10. package/dist/src/components/Tree/Tree.d.ts +5 -0
  11. package/dist/src/index.d.ts +1 -1
  12. package/docs/PHILOSOPHY.md +39 -0
  13. package/package.json +10 -3
  14. package/src/components/Box/USAGE.md +1 -1
  15. package/src/components/Button/BUTTON_IMPLEMENTATION_NOTES.md +22 -0
  16. package/src/components/Button/Button.module.css +10 -7
  17. package/src/components/Button/Button.tsx +4 -0
  18. package/src/components/Button/USAGE.md +1 -13
  19. package/src/components/Card/USAGE.md +1 -13
  20. package/src/components/Container/USAGE.md +1 -1
  21. package/src/components/Dropdown/BareDropdown.tsx +135 -0
  22. package/src/components/Dropdown/Dropdown.tsx +16 -0
  23. package/src/components/Grid/USAGE.md +6 -10
  24. package/src/components/Loader/USAGE.md +1 -23
  25. package/src/components/Menu/USAGE.md +0 -7
  26. package/src/components/Pagination/USAGE.md +0 -6
  27. package/src/components/Panel/USAGE.md +1 -58
  28. package/src/components/Stepper/USAGE.md +0 -7
  29. package/src/components/Tabs/USAGE.md +0 -7
  30. package/src/components/TimePicker/TIMEPICKER_IMPLEMENTATION_NOTES.md +68 -0
  31. package/src/components/TimePicker/TimePicker.module.css +213 -37
  32. package/src/components/TimePicker/TimePicker.stories.tsx +119 -4
  33. package/src/components/TimePicker/TimePicker.tsx +257 -8
  34. package/src/components/TimePicker/USAGE.md +27 -2
  35. package/src/components/Tree/IMPLEMENTATION_NOTES.md +28 -1
  36. package/src/components/Tree/Tree.module.css +114 -69
  37. package/src/components/Tree/Tree.stories.tsx +13 -0
  38. package/src/components/Tree/Tree.tsx +99 -32
  39. package/src/components/Tree/USAGE.md +18 -2
  40. package/src/index.ts +1 -0
@@ -0,0 +1,41 @@
1
+ import { default as React } from 'react';
2
+ import { SelectProps as MuiSelectProps } from '@mui/material';
3
+ import { RecursicaOverStyled } from '../../utils/filterStylingProps';
4
+ /**
5
+ * Bare, unwrapped Select — no `FormControlWrapper`/`WithReadOnlyWrapper`, no label/assistiveText/
6
+ * error/required. Tied to the same `Dropdown.module.css` variables/classes as the public
7
+ * `Dropdown` component, so it looks identical, but is meant to be embedded inside another
8
+ * component that already owns its own `FormControlWrapper` (e.g. `TimePicker`'s AM/PM control) —
9
+ * nesting the full `Dropdown` there would double up `FormControl`/`FormControlLayout` wrapping.
10
+ *
11
+ * Not exported from this folder's `index.ts` — internal use only. Import it directly:
12
+ * `import { BareDropdown } from "../Dropdown/BareDropdown"`.
13
+ */
14
+ export interface BareDropdownProps extends Omit<MuiSelectProps, "size" | "variant" | "classes" | "error" | "onChange"> {
15
+ data: (string | {
16
+ value: string;
17
+ label: React.ReactNode;
18
+ disabled?: boolean;
19
+ })[];
20
+ /** Normalized to just the selected value, unlike MUI's raw (event, child) Select onChange. */
21
+ onChange?: (value: string | null) => void;
22
+ /** Applies the error visual state (via `data-error`) — no error message is rendered here. */
23
+ error?: boolean;
24
+ }
25
+ export type BareDropdownComponentProps = RecursicaOverStyled<BareDropdownProps>;
26
+ export declare const BareDropdown: React.ForwardRefExoticComponent<(Omit<Omit<import('@recursica/adapter-common').WithRecursicaSpacing<BareDropdownProps>, import('@recursica/adapter-common').BlockedStylingKeys> & import('@recursica/adapter-common').ForbiddenStyles & {
27
+ overStyled?: false | undefined;
28
+ }, "ref"> | Omit<Omit<BareDropdownProps, "m" | "my" | "mx" | "mt" | "mb" | "ml" | "mr" | "gap" | "rowGap" | "columnGap"> & {
29
+ m?: string | number | import('@recursica/adapter-common').RecursicaSpacing;
30
+ mx?: string | number | import('@recursica/adapter-common').RecursicaSpacing;
31
+ my?: string | number | import('@recursica/adapter-common').RecursicaSpacing;
32
+ mt?: string | number | import('@recursica/adapter-common').RecursicaSpacing;
33
+ mb?: string | number | import('@recursica/adapter-common').RecursicaSpacing;
34
+ ml?: string | number | import('@recursica/adapter-common').RecursicaSpacing;
35
+ mr?: string | number | import('@recursica/adapter-common').RecursicaSpacing;
36
+ gap?: string | number | import('@recursica/adapter-common').RecursicaSpacing;
37
+ rowGap?: string | number | import('@recursica/adapter-common').RecursicaSpacing;
38
+ columnGap?: string | number | import('@recursica/adapter-common').RecursicaSpacing;
39
+ } & {
40
+ overStyled: true;
41
+ }, "ref">) & React.RefAttributes<HTMLInputElement>>;
@@ -1,4 +1,17 @@
1
1
  import { default as React } from 'react';
2
- import { RecursicaTimePickerProps } from '@recursica/adapter-common';
3
- export type TimePickerProps = React.HTMLAttributes<HTMLDivElement> & RecursicaTimePickerProps;
4
- export declare const TimePicker: React.FC<TimePickerProps>;
2
+ import { TimePickerProps as MuiTimePickerProps } from '@mui/x-date-pickers/TimePicker';
3
+ import { ReadOnlyControlProps, RecursicaTimePickerProps as BaseRecursicaTimePickerProps } from '@recursica/adapter-common';
4
+ import { RecursicaOverStyled } from '../../utils/filterStylingProps';
5
+ import { RecursicaFormControlWrapperProps } from '../FormControlWrapper/FormControlWrapper';
6
+ export interface RecursicaTimePickerProps extends Omit<MuiTimePickerProps, "value" | "defaultValue" | "onChange" | "minTime" | "maxTime" | "views" | "format" | "style">, Pick<RecursicaFormControlWrapperProps, "label" | "error" | "required" | "id" | "assistiveText" | "assistiveWithIcon" | "formLayout" | "labelSize" | "labelAlignment" | "labelOptionalText" | "labelWithEditIcon" | "onLabelEditClick">, ReadOnlyControlProps, BaseRecursicaTimePickerProps {
7
+ /** Selected time as an "HH:mm" (or "HH:mm:ss" with `withSeconds`) string, matching the mantine-adapter convention. */
8
+ value?: string;
9
+ /** Uncontrolled initial time as an "HH:mm" (or "HH:mm:ss" with `withSeconds`) string. */
10
+ defaultValue?: string;
11
+ /** Fires with the new time as an "HH:mm" (or "HH:mm:ss" with `withSeconds`) string, or `null` if cleared. */
12
+ onChange?: (value: string | null) => void;
13
+ /** Caller-provided inline style, passed through to the FormControlWrapper root. */
14
+ style?: React.CSSProperties;
15
+ }
16
+ export type TimePickerProps = RecursicaOverStyled<RecursicaTimePickerProps>;
17
+ export declare const TimePicker: React.ForwardRefExoticComponent<TimePickerProps & React.RefAttributes<HTMLDivElement>>;
@@ -9,5 +9,10 @@ export type TreeProps = RecursicaOverStyled<RecursicaTreeProps & Omit<React.Comp
9
9
  * Wraps `@mui/x-tree-view`'s `RichTreeView` with a fully custom item renderer so every visual
10
10
  * aspect (row box model, selected/unselected colors and typography, indent, item spacing)
11
11
  * comes from Recursica's `tree` design tokens rather than MUI's defaults.
12
+ *
13
+ * **Interaction pattern (fixed, not prop-configurable):** expand/collapse and select are
14
+ * independent — the chevron button toggles a node's subtree only, clicking the rest of a row
15
+ * (or pressing `Enter`/`Space`) selects it only, and `ArrowLeft`/`ArrowRight` toggle expansion
16
+ * only. See `RecursicaTreeProps` for the full breakdown.
12
17
  */
13
18
  export declare const Tree: React.ForwardRefExoticComponent<TreeProps & React.RefAttributes<HTMLUListElement>>;
@@ -88,4 +88,4 @@ export declare const Toast: typeof rawComponents.Toast;
88
88
  export declare const Tooltip: typeof rawComponents.Tooltip;
89
89
  export declare const TransferList: typeof rawComponents.TransferList;
90
90
  export declare const Tree: typeof rawComponents.Tree;
91
- export type { RecursicaAutocompleteProps, RecursicaDropdownProps, RecursicaFormControlWrapperProps, RecursicaNumberInputProps, RecursicaSliderProps, RecursicaTextAreaProps, RecursicaTextFieldProps, RecursicaToastProps, } from './components';
91
+ export type { RecursicaAutocompleteProps, RecursicaDropdownProps, RecursicaFormControlWrapperProps, RecursicaNumberInputProps, RecursicaSliderProps, RecursicaTextAreaProps, RecursicaTextFieldProps, RecursicaTimePickerProps, RecursicaToastProps, } from './components';
@@ -0,0 +1,39 @@
1
+ # Recursica MUI Adapter: Core Philosophy
2
+
3
+ Recursica's component architecture isn't just a wrapper; it's a strict enforcing layer over MUI's massive API surface. Our primary goal is to ensure consistency, eliminate "design system rot," and provide clear boundaries for application developers using the UI Kit.
4
+
5
+ This document serves as the governing framework for why the `mui-adapter` components are built the way they are.
6
+
7
+ ## 1. Strict Separation of Props (The Unified Recursica Prop Layer)
8
+
9
+ Recursica has a **single universal API surface** internally regardless of whether we use MUI or another underlying UI library.
10
+
11
+ - We decouple our visual properties natively. Instead of mapping perfectly to MUI's native variants `(contained, outlined, text)`, we intentionally use Recursica's semantic and behavioral structures (e.g., `<Badge variant="alert" />`).
12
+ - We intentionally omit and strip complex underlying parameters if they collide with or circumvent our UI tokens (like stripping `--size` or raw MUI size properties when Recursica enforces a universal scale).
13
+
14
+ ## 2. Component Wrappers (Leaving MUI Alone)
15
+
16
+ We actively avoid mutating or patching MUI source code or deeply hooking into the MUI `createTheme` Theme object to apply our token system.
17
+
18
+ - We rely on standard DOM `module.css` bridging with strictly targeted `className`/`classes` overrides whenever possible, instead of heavy Emotion/CSS-in-JS logic.
19
+ - This creates total decoupled isolation: updating MUI natively will not fracture our styles, and we avoid dealing with deep CSS-in-JS theme clashing logic.
20
+
21
+ ## 3. The `overStyled` Property
22
+
23
+ MUI encourages deep styling access by injecting the `sx` prop, system props like `bgcolor`, `color`, `typography`, or nested `classes` directly into component tags.
24
+
25
+ - By default, **Recursica components block all arbitrary styling vectors**. `sx` maps, `classes` overrides, system styles, and inline logic are proactively stripped before they hit MUI using central utility functions.
26
+ - **Why?** To prevent the design system from deteriorating over time as developers write one-off hotfixes into their TSX rendering blocks.
27
+ - **The Caveat:** We allow _external DOM layout positioning props_ (e.g., margin `m`, `mt`, `p`, `px`, etc.) to pass through and natively intercept Recursica Spacing Tokens (`rec-sm`, `rec-default`) so developers can structure components organically within their parent layouts.
28
+
29
+ ### Escape Hatches
30
+
31
+ If a developer _strictly must_ heavily alter a component, they are required to explicitly declare `<Component overStyled={true} />`. This immediately raises a visible red flag during code reviews.
32
+
33
+ ## 4. Expectations for External Developers (Modifying Recursica)
34
+
35
+ If a developer finds that a component does not fit their needs and styling must be modified, their path of execution should follow these principles sequentially:
36
+
37
+ 1. **Leverage Native MUI First:** If a Recursica component lacks the functionality or styling variant needed for a highly custom edge case (e.g., a massive marketing hero button), do not try to forcibly hack the Recursica component. Instead, import the raw underlying `Button` component directly from `@mui/material` and style it manually. Use Recursica for standard systematic needs, and native libraries for isolated custom one-offs.
38
+ 2. **Accept `overStyled` as Technical Debt:** If you must override the Recursica component immediately but intend to roll it back, use `overStyled={true}`. The expectation is that `overStyled` uses will eventually be replaced once the actual Recursica Figma variants are natively updated to accommodate your usecase, at which point `overStyled={true}` can be safely removed.
39
+ 3. **Contribute to the Kit:** Avoid building private custom wrappers around Recursica components. If the system is missing a variant, that is a shared project deficit—raise a concern and have the variant integrated directly into the universal token libraries!
package/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "url": "git+https://github.com/borderux/recursica.git",
14
14
  "directory": "packages/mui-adapter"
15
15
  },
16
- "version": "0.19.0",
16
+ "version": "0.21.0",
17
17
  "publishConfig": {
18
18
  "access": "public"
19
19
  },
@@ -38,7 +38,8 @@
38
38
  "USAGE.md",
39
39
  "ARCHITECTURE.md",
40
40
  "SETUP.md",
41
- "OVERSTYLING.md"
41
+ "OVERSTYLING.md",
42
+ "docs/PHILOSOPHY.md"
42
43
  ],
43
44
  "keywords": [
44
45
  "react",
@@ -64,6 +65,7 @@
64
65
  "@chromatic-com/storybook": "^5.1.1",
65
66
  "@eslint/js": "^9.25.0",
66
67
  "@mui/lab": "^7.0.1-beta.25",
68
+ "@mui/x-date-pickers": "^9.11.0",
67
69
  "@mui/x-tree-view": "^9.11.0",
68
70
  "@recursica/recursica-postcss-vars": "*",
69
71
  "@recursica/storybook-template": "*",
@@ -101,13 +103,15 @@
101
103
  },
102
104
  "dependencies": {
103
105
  "@recursica/adapter-common": "*",
104
- "@recursica/official-release": "*"
106
+ "@recursica/official-release": "*",
107
+ "dayjs": "^1.11.21"
105
108
  },
106
109
  "peerDependencies": {
107
110
  "@emotion/react": "^11.14.0",
108
111
  "@emotion/styled": "^11.14.0",
109
112
  "@mui/lab": "^7.0.1-beta.25",
110
113
  "@mui/material": "^7.3.0",
114
+ "@mui/x-date-pickers": "^9.11.0",
111
115
  "@mui/x-tree-view": "^9.11.0",
112
116
  "react": ">=16.8.0",
113
117
  "react-dom": ">=16.8.0"
@@ -116,6 +120,9 @@
116
120
  "@mui/lab": {
117
121
  "optional": true
118
122
  },
123
+ "@mui/x-date-pickers": {
124
+ "optional": true
125
+ },
119
126
  "@mui/x-tree-view": {
120
127
  "optional": true
121
128
  }
@@ -45,4 +45,4 @@ All Recursica components in the `@recursica/mui-adapter` package adhere strictly
45
45
 
46
46
  ## `sx` Prop Exemption
47
47
 
48
- By design, the `Box` component is the most permissive primitive in the UI kit. It explicitly allows the `sx` prop to pass through to the underlying MUI `Box`. It does not use any strict styling gatekeepers (`RecursicaOverStyled`, `filterSxProp`). It is intended to be used as a final escape hatch when the standard layout primitives or design system tokens cannot fulfill a unique layout requirement.
48
+ By design, the `Box` component is the most permissive primitive in the UI kit: it always allows the `sx` prop to pass through, without requiring `overStyled`. It is intended to be used as a final escape hatch when the standard layout primitives or design system tokens cannot fulfill a unique layout requirement.
@@ -31,3 +31,25 @@ We explicitly pass `disableRipple` and `disableElevation` to block MUI's dynamic
31
31
  **Decision:** When `loading={true}` is passed to the Button, the component explicitly forces `disabled={true}` natively on the underlying element.
32
32
 
33
33
  **Implementation:** This ensures that loading buttons automatically inherit the brand theme disabled opacities (via the `:disabled` CSS pseudo-class) rather than relying solely on MUI's internal loading opacity adjustments.
34
+
35
+ ---
36
+
37
+ ## `className` overwrite bug (Matt Massey, 2026-08-08)
38
+
39
+ **Bug:** with `overStyled` and a custom `className` (e.g. Tree embedding a `Button` for its expand chevron), the component's own `styles.root` class silently disappeared from the rendered `<button>` — every `[data-variant]`/`[data-size]` CSS rule stopped applying, and MUI's own default styling (including its default blue "primary" color) showed through instead.
40
+
41
+ **Root cause:** `className={finalClass}` (`` `${styles.root} ${classNameProp}` ``) was set explicitly on `<MuiButton>`, but `{...sanitizedProps}` was spread _after_ it — and `sanitizedProps` still contained the original, unmodified `className` key, since it had only been _read_ to compute `finalClass`, never deleted. The later spread silently overwrote the merged class with just the caller's own class. Same bug class as `Dropdown.tsx`/`BareDropdown.tsx` had.
42
+
43
+ **Fix:** delete `className` from the sanitized props record right after reading it, before it reaches the JSX spread. mantine-adapter's `Button.tsx` had the identical mistake — masked there by a separate `classNames={{root: ...}}` object prop unaffected by the bug, but fixed there too for correctness.
44
+
45
+ ---
46
+
47
+ ## `.MuiButton-startIcon`/`.MuiButton-endIcon` selectors never matched anything (Matt Massey, 2026-08-10)
48
+
49
+ **Bug:** icon-only buttons rendered with the icon visibly off-center — shifted left, with extra empty space on the right. Not a regression from any recent change; `Button.module.css` itself was untouched, and the same unwrapped selectors already existed at `HEAD`. It had just gone unnoticed until the Tree work put an icon-only Button (the chevron) under closer visual scrutiny next to Mantine's (correctly centered) version.
50
+
51
+ **Root cause:** `.MuiButton-startIcon`/`.MuiButton-endIcon` are real global class names MUI's `Button` applies directly in the DOM — not local CSS Modules classes generated from this file. Referencing them as plain `.MuiButton-startIcon` (rather than `:global(.MuiButton-startIcon)`, the pattern already used correctly for `.Mui-disabled` elsewhere in this same file) meant Vite's CSS Modules silently hashed them into scoped names — `.Button-module__MuiButton-startIcon___<hash>` — that never matched anything real in the DOM. Every rule targeting them (the icon↔label gap margin, and the icon-only margin reset) was a total no-op; icon-only buttons were left with MUI's own unreset default `margin-right` on the icon, which is what visibly pushed the icon off-center.
52
+
53
+ **Fix:** wrapped every `.MuiButton-startIcon`/`.MuiButton-endIcon` reference in `:global(...)`.
54
+
55
+ **Not otherwise fixed, flagged separately:** the same unwrapped-global-class pattern shows up in at least `Stepper.module.css` (`.Mui-active`/`.Mui-completed`/`.MuiStepLabel-root`, fully unwrapped) and `SegmentedControl.module.css`, and partially in `Label.module.css`/`Accordion.module.css` — meaning some of those components' MUI-state-driven styling may also be silently no-op'ing. Out of scope for this fix (Button only, per what was asked); worth a dedicated sweep.
@@ -202,26 +202,29 @@
202
202
  );
203
203
  }
204
204
 
205
- /* MUI uses .MuiButton-startIcon and .MuiButton-endIcon */
206
- .root[data-size="default"] .MuiButton-startIcon {
205
+ /* MUI uses .MuiButton-startIcon and .MuiButton-endIcon — real global classes MUI applies
206
+ directly, not local CSS Modules classes, so they must be wrapped in :global() (same as
207
+ .Mui-disabled below) or Vite's CSS Modules hashes them into scoped names that never match
208
+ anything in the actual DOM, silently no-op'ing every rule below. */
209
+ .root[data-size="default"] :global(.MuiButton-startIcon) {
207
210
  margin-left: 0;
208
211
  margin-right: var(
209
212
  --recursica_ui-kit_components_button_variants_sizes_default_properties_icon-text-gap
210
213
  );
211
214
  }
212
- .root[data-size="default"] .MuiButton-endIcon {
215
+ .root[data-size="default"] :global(.MuiButton-endIcon) {
213
216
  margin-right: 0;
214
217
  margin-left: var(
215
218
  --recursica_ui-kit_components_button_variants_sizes_default_properties_icon-text-gap
216
219
  );
217
220
  }
218
- .root[data-size="small"] .MuiButton-startIcon {
221
+ .root[data-size="small"] :global(.MuiButton-startIcon) {
219
222
  margin-left: 0;
220
223
  margin-right: var(
221
224
  --recursica_ui-kit_components_button_variants_sizes_small_properties_icon-text-gap
222
225
  );
223
226
  }
224
- .root[data-size="small"] .MuiButton-endIcon {
227
+ .root[data-size="small"] :global(.MuiButton-endIcon) {
225
228
  margin-right: 0;
226
229
  margin-left: var(
227
230
  --recursica_ui-kit_components_button_variants_sizes_small_properties_icon-text-gap
@@ -232,8 +235,8 @@
232
235
  .root[data-content="icon-only"] .labelText {
233
236
  display: none;
234
237
  }
235
- .root[data-content="icon-only"] .MuiButton-startIcon,
236
- .root[data-content="icon-only"] .MuiButton-endIcon {
238
+ .root[data-content="icon-only"] :global(.MuiButton-startIcon),
239
+ .root[data-content="icon-only"] :global(.MuiButton-endIcon) {
237
240
  margin-right: 0;
238
241
  margin-left: 0;
239
242
  }
@@ -87,6 +87,10 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
87
87
  const finalClass = classNameProp
88
88
  ? `${styles.root} ${classNameProp}`
89
89
  : styles.root;
90
+ // className is merged explicitly above — don't let the {...sanitizedProps} spread below
91
+ // silently overwrite finalClass with just the caller's own class (same bug class as
92
+ // mui-adapter's Dropdown/BareDropdown.tsx had).
93
+ delete restRecord["className"];
90
94
 
91
95
  // We don't map Recursica variant/size to MUI's because we want to completely disable MUI's native
92
96
  // variant logic (e.g., elevation, shadows) and style everything strictly through our CSS Modules.
@@ -43,16 +43,4 @@ All Recursica components in the `@recursica/mui-adapter` package adhere strictly
43
43
 
44
44
  ## 4. Key Integration Features & Constraints
45
45
 
46
- ## Loader color contrast
47
-
48
- **Decision:** When a Button is in a loading state, the `Recursica Loader` component is injected via the `loadingIndicator` prop. The `Loader` component strictly defines its own colors and styles per variant, meaning it does not automatically inherit the text color (`currentColor`) from the Button.
49
-
50
- **Constraint:** This can lead to contrast issues (e.g., a blue dots loader inside a solid blue button). Design has explicitly decided not to address this at the moment. As such, developers using the `loading` prop must be aware that the loader's color is fixed by its internal tokens, not by the button's context.
51
-
52
- ---
53
-
54
- ## Loading state enforces disabled state
55
-
56
- **Decision:** When `loading={true}` is passed to the Button, the component explicitly forces `disabled={true}` natively on the underlying element.
57
-
58
- **Implementation:** This ensures that loading buttons automatically inherit the brand theme disabled opacities (via the `:disabled` CSS pseudo-class) rather than relying solely on MUI's internal loading opacity adjustments.
46
+ When `loading={true}` is passed to the Button, the button is also automatically disabled, and its loading indicator's color may not always match the button's text color, which can affect contrast in some variants.
@@ -54,16 +54,4 @@ All Recursica components in the `@recursica/mui-adapter` package adhere strictly
54
54
 
55
55
  ## 4. Key Integration Features & Constraints
56
56
 
57
- ## Architecture Overrides
58
-
59
- Because MUI natively constructs `Card` bounding boxes using `<Paper>` components (which lack the precise edge-to-edge layouts native to Mantine's sections), we implemented custom margins for edge-to-edge section components:
60
-
61
- - `<Card.Header>` explicitly hooks `--recursica_ui-kit_components_card_properties_header-background` and corresponding padding variables, stretching edge-to-edge via negative margin resets.
62
- - `<Card.Footer>` explicitly hooks `--recursica_ui-kit_components_card_properties_footer-background` and corresponding padding variables.
63
-
64
- ## Layout Alignment Exceptions
65
-
66
- To allow Cards to fit cleanly inside dynamic/flex layouts (like dashboard panels, grid tracks, or sidebar layout segments), the Card wrapper implements a custom gatekeeper bypass for outer styling properties:
67
-
68
- - Exposes a safe subset of flexbox/dimensions styling properties (`flex`, `flexGrow`, `flexShrink`, `flexBasis`, `grow`, `h`, `height`) on the root `<Card>` component to allow proper sizing alongside layout siblings.
69
- - Sets `<Card.Content>` to `flex-grow: 1;` by default via CSS modules. Since the root `<Card>` has `display: flex; flex-direction: column;`, this makes the content area expand to fill all vertical space, pushing `<Card.Footer>` to align at the absolute bottom of the bounding box.
57
+ `Card.Header` and `Card.Footer` stretch edge-to-edge within the card. The root `Card` component also accepts a safe subset of flexbox/dimension props (`flex`, `flexGrow`, `flexShrink`, `flexBasis`, `grow`, `h`, `height`) so it can be sized properly alongside other elements in dynamic/flex layouts (such as dashboard panels, grid tracks, or sidebar layout segments). `Card.Content` grows to fill the available vertical space, keeping `Card.Footer` aligned to the bottom of the card.
@@ -45,4 +45,4 @@ All Recursica components in the `@recursica/mui-adapter` package adhere strictly
45
45
 
46
46
  ## `sx` Prop Exemption
47
47
 
48
- By design, the `Container` component explicitly allows the `sx` prop to pass through to the underlying MUI `Container`. Unlike standard UI kit components (which use the `RecursicaOverStyled` gatekeeper) or flex layout primitives (which strip `sx` via `OmitSx` and `filterSxProp`), `Container` acts as a structural boundary where advanced, one-off positioning adjustments may be required by the consuming application.
48
+ By design, the `Container` component always allows the `sx` prop to pass through, without requiring `overStyled`. This makes it useful as a structural boundary where advanced, one-off positioning adjustments may be required by the consuming application.
@@ -0,0 +1,135 @@
1
+ import React, { forwardRef } from "react";
2
+ import {
3
+ Select as MuiSelect,
4
+ type SelectProps as MuiSelectProps,
5
+ MenuItem,
6
+ } from "@mui/material";
7
+ import {
8
+ filterStylingProps,
9
+ type RecursicaOverStyled,
10
+ } from "../../utils/filterStylingProps";
11
+ import styles from "./Dropdown.module.css";
12
+
13
+ /**
14
+ * Bare, unwrapped Select — no `FormControlWrapper`/`WithReadOnlyWrapper`, no label/assistiveText/
15
+ * error/required. Tied to the same `Dropdown.module.css` variables/classes as the public
16
+ * `Dropdown` component, so it looks identical, but is meant to be embedded inside another
17
+ * component that already owns its own `FormControlWrapper` (e.g. `TimePicker`'s AM/PM control) —
18
+ * nesting the full `Dropdown` there would double up `FormControl`/`FormControlLayout` wrapping.
19
+ *
20
+ * Not exported from this folder's `index.ts` — internal use only. Import it directly:
21
+ * `import { BareDropdown } from "../Dropdown/BareDropdown"`.
22
+ */
23
+ export interface BareDropdownProps
24
+ extends Omit<
25
+ MuiSelectProps,
26
+ "size" | "variant" | "classes" | "error" | "onChange"
27
+ > {
28
+ data: (
29
+ | string
30
+ | { value: string; label: React.ReactNode; disabled?: boolean }
31
+ )[];
32
+ /** Normalized to just the selected value, unlike MUI's raw (event, child) Select onChange. */
33
+ onChange?: (value: string | null) => void;
34
+ /** Applies the error visual state (via `data-error`) — no error message is rendered here. */
35
+ error?: boolean;
36
+ }
37
+
38
+ export type BareDropdownComponentProps = RecursicaOverStyled<BareDropdownProps>;
39
+
40
+ export const BareDropdown = forwardRef<
41
+ HTMLInputElement,
42
+ BareDropdownComponentProps
43
+ >(function BareDropdown(props, ref) {
44
+ const {
45
+ overStyled = false,
46
+ disabled,
47
+ data,
48
+ onChange,
49
+ className,
50
+ value,
51
+ defaultValue,
52
+ error,
53
+ ...rest
54
+ } = props;
55
+ const sanitizedProps = filterStylingProps(rest, overStyled);
56
+ const restRecord = sanitizedProps as Record<string, unknown>;
57
+
58
+ delete restRecord["size"];
59
+ delete restRecord["variant"];
60
+ // className is merged explicitly below — don't let the spread further down silently overwrite
61
+ // styles.root with just the caller's own class.
62
+ delete restRecord["className"];
63
+
64
+ const mergedClassName = className
65
+ ? `${styles.root} ${className}`
66
+ : styles.root;
67
+
68
+ const selectedValue = value ?? defaultValue;
69
+
70
+ const renderOptions = () =>
71
+ data.map((item, index) => {
72
+ if (typeof item === "string") {
73
+ return (
74
+ <MenuItem
75
+ key={`${item}-${index}`}
76
+ value={item}
77
+ className={styles.option}
78
+ // Dropdown.module.css's own selected-state tint (`.option[data-selected="true"]`) needs
79
+ // this explicitly — MUI's own `Mui-selected` class carries its default primary-color
80
+ // tint instead, which is what shows through without it.
81
+ data-selected={item === selectedValue ? "true" : undefined}
82
+ >
83
+ {item}
84
+ </MenuItem>
85
+ );
86
+ }
87
+ return (
88
+ <MenuItem
89
+ key={`${item.value}-${index}`}
90
+ value={item.value}
91
+ disabled={item.disabled}
92
+ className={styles.option}
93
+ data-selected={item.value === selectedValue ? "true" : undefined}
94
+ >
95
+ {item.label}
96
+ </MenuItem>
97
+ );
98
+ });
99
+
100
+ return (
101
+ <MuiSelect
102
+ ref={ref}
103
+ disabled={disabled}
104
+ value={value}
105
+ defaultValue={defaultValue}
106
+ onChange={(event) => onChange?.((event.target.value as string) ?? null)}
107
+ displayEmpty
108
+ error={!!error}
109
+ className={mergedClassName}
110
+ classes={{
111
+ select: styles.input,
112
+ icon: styles.icon,
113
+ }}
114
+ MenuProps={{
115
+ classes: { paper: styles.dropdown },
116
+ }}
117
+ // Dropdown.module.css's error/disabled state rules key off `.root[data-error]`/
118
+ // `[data-disabled]` (the outer Select element, matching mergedClassName above) —
119
+ // `inputProps` only reaches the nested accessibility <input>, which that selector never
120
+ // matches, so these need to be set here too (mirrors the same fix in Dropdown.tsx).
121
+ data-disabled={disabled ? "true" : undefined}
122
+ data-error={error ? "true" : undefined}
123
+ inputProps={{
124
+ "data-disabled": disabled ? "true" : undefined,
125
+ "data-error": error ? "true" : undefined,
126
+ ...(restRecord.inputProps as Record<string, unknown>),
127
+ }}
128
+ {...(sanitizedProps as unknown as MuiSelectProps)}
129
+ >
130
+ {renderOptions()}
131
+ </MuiSelect>
132
+ );
133
+ });
134
+
135
+ BareDropdown.displayName = "BareDropdown";
@@ -98,6 +98,12 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
98
98
  key={`${item}-${index}`}
99
99
  value={item}
100
100
  className={styles.option}
101
+ // Dropdown.module.css's own selected-state tint (`.option[data-selected="true"]`)
102
+ // needs this explicitly — MUI's own `Mui-selected` class carries its default primary-
103
+ // color tint instead, which is what shows through without it.
104
+ data-selected={
105
+ item === (value ?? defaultValue) ? "true" : undefined
106
+ }
101
107
  >
102
108
  {item}
103
109
  </MenuItem>
@@ -109,6 +115,9 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
109
115
  value={item.value}
110
116
  disabled={item.disabled}
111
117
  className={styles.option}
118
+ data-selected={
119
+ item.value === (value ?? defaultValue) ? "true" : undefined
120
+ }
112
121
  >
113
122
  {item.label}
114
123
  </MenuItem>
@@ -164,6 +173,13 @@ export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
164
173
  MenuProps={{
165
174
  classes: { paper: styles.dropdown },
166
175
  }}
176
+ // Dropdown.module.css's error/disabled state rules key off `.root[data-error]`/
177
+ // `[data-disabled]` (the outer Select element, matching the `className={styles.root}`
178
+ // above) — `inputProps` below only reaches the nested accessibility <input>, which that
179
+ // selector never matches, so these need to be set here too. (Previously only set via
180
+ // inputProps, which meant the error border never actually appeared on the Dropdown.)
181
+ data-disabled={disabled ? "true" : undefined}
182
+ data-error={error ? "true" : undefined}
167
183
  inputProps={{
168
184
  "data-disabled": disabled ? "true" : undefined,
169
185
  "data-error": error ? "true" : undefined,
@@ -36,7 +36,7 @@ All Recursica components in the `@recursica/mui-adapter` package adhere strictly
36
36
 
37
37
  > [!IMPORTANT]
38
38
  >
39
- > - **Anti-override protection**: `Grid` is a primitive layout component (see [OVERSTYLING.md](../../../OVERSTYLING.md)) and is exempt from the `RecursicaOverStyled` gatekeeper — only the `sx` prop is stripped, everything else passes through freely.
39
+ > - **Anti-override protection**: `Grid` is a primitive layout component (see [OVERSTYLING.md](../../../OVERSTYLING.md)) — only the `sx` prop is stripped, everything else passes through freely without needing `overStyled`.
40
40
  > - **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
41
  > - **Variables and Theming**: Spacing is entirely determined by the `rec-*` token scale, mapped transparently to MUI's `spacing` value.
42
42
 
@@ -44,12 +44,8 @@ All Recursica components in the `@recursica/mui-adapter` package adhere strictly
44
44
 
45
45
  ## 4. Key Integration Features & Constraints
46
46
 
47
- `Grid` and `Grid.Col` expose the same public API as the mantine-adapter's `Grid`/`Grid.Col` (same prop names and shapes), so code written against one adapter ports to the other without changes. Internally, MUI has no native `AppShell`-style Grid/Col split — MUI merges "container" and "item" into a single `Grid` component — so this adapter hand-composes `Grid` (always `container`) and `Grid.Col` (always item mode) from that single underlying component.
48
-
49
- Notable mapping details:
50
-
51
- - `gap` maps to MUI's own `spacing` prop (same concept as `gutter` on the mantine side).
52
- - `span`'s `"auto"`/`"content"` keywords are the inverse of MUI's own `"grow"`/`"auto"` keywords — this adapter translates between them internally, so the public `span` values match Mantine's semantics exactly regardless of adapter.
53
- - `offset` maps directly to MUI's own `offset` prop (same name, same shape).
54
- - `order` is applied directly for a fixed number. A responsive object (`{ base, sm, md, ... }`) is **not** fully supported yet — the smallest specified breakpoint's value is applied as a single static order, since MUI's Grid has no native per-breakpoint `order` mechanism. See `IMPLEMENTATION_NOTES.md`.
55
- - `visibleFrom`/`hiddenFrom` are implemented via a small CSS module using MUI's own default breakpoint pixel values (600/900/1200/1536), since MUI's Grid has no built-in breakpoint-visibility mechanism.
47
+ - `gap` controls the spacing between grid items.
48
+ - `span` accepts a column count, `"auto"`, `"content"`, or a responsive object (`{ base, sm, md, ... }`).
49
+ - `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.
51
+ - `visibleFrom`/`hiddenFrom` show or hide a column at the standard breakpoints (600/900/1200/1536px).
@@ -39,26 +39,4 @@ All Recursica components in the `@recursica/mui-adapter` package adhere strictly
39
39
 
40
40
  ## 4. Key Integration Features & Constraints
41
41
 
42
- ## Architecture & Integration
43
-
44
- The `Loader` component for the MUI adapter has been completely hand-coded from scratch using pure CSS and basic HTML `<span>` elements. It explicitly **does not** use MUI's native `<CircularProgress>` component.
45
-
46
- This was a deliberate architectural decision to ensure 100% feature and visual parity with the `mantine-adapter`.
47
-
48
- ### Key Decisions:
49
-
50
- - **Bypassing Native Components:** MUI's native loaders (like `<CircularProgress>`) are built using complex animated SVGs and only support a circular "oval" shape. Since the Recursica design system mandates `oval`, `bars`, and `dots` variants, relying on MUI's primitives would have forced a fragmented architecture where `oval` used MUI but `bars` and `dots` were hand-coded.
51
- - **Parity with Mantine:** To guarantee identical animation timing, easing curves, and DOM structures across frameworks, the CSS keyframes and layout strategies used internally by Mantine's `<Loader>` were extracted and directly replicated in this adapter's `Loader.module.css`.
52
-
53
- ### Token Mapping:
54
-
55
- Sizes are bound through `data-size` attributes (`sm`, `md`, `lg` parsing to target `<div data-size="small">`, etc.).
56
-
57
- - **Oval Variant:** Uses a CSS spinning `::after` pseudo-element. To avoid CSS border inheritance bugs when computing tokenized border-widths, a custom `--loader-thickness` CSS variable is used to bridge the token into the spinning element.
58
- - **Bars & Dots Variants:** Render three internal `<span />` elements sequentially, styled via `Loader.module.css` to handle individual keyframe delays for bouncing or fading animations.
59
-
60
- ### Color Contrast Rules:
61
-
62
- Loaders are hardcoded to map to their explicitly defined design tokens (e.g., `--recursica_ui-kit_components_loader_properties_indicator-color`). By default, they do **not** inherit `currentColor`.
63
-
64
- When injected into components like the `Button` (where contrast issues may arise against solid backgrounds), it is the responsibility of the parent component (e.g., `Button.module.css`) to use contextual CSS overrides to force `--loader-color: currentColor !important` if necessary.
42
+ Loader colors are determined by their own design tokens by default and do not automatically inherit `currentColor` from a parent component, which can occasionally affect contrast when a loader is placed on a colored background.
@@ -45,10 +45,3 @@ All Recursica components in the `@recursica/mui-adapter` package adhere strictly
45
45
  > - **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.
46
46
  > - **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.
47
47
  > - **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.
48
-
49
- ---
50
-
51
- ## 4. Key Integration Features & Constraints
52
-
53
- - **Compositional API Dropped:** Mantine uses `<Menu.Target>`, `<Menu.Dropdown>`, `<Menu.Item>`, etc., and manages state natively via React context within `<Menu>`. MUI's API is fully monolithic.
54
- - **Monolithic API Adopted:** Following architectural review, we have abandoned the fabricated context wrappers for `mui-adapter`. We now natively export `Menu`, `MenuItem`, and `MenuDivider` wrapping their `@mui/material` counterparts. Developers are expected to manage `anchorEl` state themselves, just like native MUI. Storybook tests have been updated to simulate this open state so visual regressions still cover the dropdown menu visually.
@@ -34,9 +34,3 @@ All Recursica components in the `@recursica/mui-adapter` package adhere strictly
34
34
  > - **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.
35
35
  > - **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.
36
36
  > - **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.
37
-
38
- ---
39
-
40
- ## 4. Key Integration Features & Constraints
41
-
42
- - **Compositional API Dropped:** Mantine's original `Pagination` component relies heavily on dot-notation sub-components (`Pagination.Root`, `Pagination.Items`, `Pagination.Control`, etc.). MUI's `<Pagination>` is fundamentally monolithic. Following architectural review, we have decided to drop the dot-notation wrappers for `mui-adapter` and rely strictly on MUI's monolithic API. Storybook and visual regression tests have been updated to reflect this divergence while retaining core property mapping compatibility.