@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.
- package/CHANGELOG.md +28 -0
- package/README.md +3 -3
- package/dist/mui-adapter.cjs +89 -58
- package/dist/mui-adapter.cjs.map +1 -1
- package/dist/mui-adapter.css +1 -1
- package/dist/mui-adapter.js +26163 -8727
- package/dist/mui-adapter.js.map +1 -1
- package/dist/src/components/Dropdown/BareDropdown.d.ts +41 -0
- package/dist/src/components/TimePicker/TimePicker.d.ts +16 -3
- package/dist/src/components/Tree/Tree.d.ts +5 -0
- package/dist/src/index.d.ts +1 -1
- package/docs/PHILOSOPHY.md +39 -0
- package/package.json +10 -3
- package/src/components/Box/USAGE.md +1 -1
- package/src/components/Button/BUTTON_IMPLEMENTATION_NOTES.md +22 -0
- package/src/components/Button/Button.module.css +10 -7
- package/src/components/Button/Button.tsx +4 -0
- package/src/components/Button/USAGE.md +1 -13
- package/src/components/Card/USAGE.md +1 -13
- package/src/components/Container/USAGE.md +1 -1
- package/src/components/Dropdown/BareDropdown.tsx +135 -0
- package/src/components/Dropdown/Dropdown.tsx +16 -0
- package/src/components/Grid/USAGE.md +6 -10
- package/src/components/Loader/USAGE.md +1 -23
- package/src/components/Menu/USAGE.md +0 -7
- package/src/components/Pagination/USAGE.md +0 -6
- package/src/components/Panel/USAGE.md +1 -58
- package/src/components/Stepper/USAGE.md +0 -7
- package/src/components/Tabs/USAGE.md +0 -7
- package/src/components/TimePicker/TIMEPICKER_IMPLEMENTATION_NOTES.md +68 -0
- package/src/components/TimePicker/TimePicker.module.css +213 -37
- package/src/components/TimePicker/TimePicker.stories.tsx +119 -4
- package/src/components/TimePicker/TimePicker.tsx +257 -8
- package/src/components/TimePicker/USAGE.md +27 -2
- package/src/components/Tree/IMPLEMENTATION_NOTES.md +28 -1
- package/src/components/Tree/Tree.module.css +114 -69
- package/src/components/Tree/Tree.stories.tsx +13 -0
- package/src/components/Tree/Tree.tsx +99 -32
- package/src/components/Tree/USAGE.md +18 -2
- 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 {
|
|
3
|
-
|
|
4
|
-
|
|
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>>;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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))
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
- `
|
|
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
|
-
|
|
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.
|