@recursica/mui-adapter 0.21.0 → 0.22.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 (187) hide show
  1. package/ARCHITECTURE.md +3 -0
  2. package/CHANGELOG.md +58 -0
  3. package/dist/index.d.ts +2929 -2
  4. package/dist/mui-adapter.cjs +68 -68
  5. package/dist/mui-adapter.cjs.map +1 -1
  6. package/dist/mui-adapter.css +1 -1
  7. package/dist/mui-adapter.js +5660 -5582
  8. package/dist/mui-adapter.js.map +1 -1
  9. package/package.json +1 -1
  10. package/src/components/Accordion/ACCORDION_IMPLEMENTATION_NOTES.md +63 -0
  11. package/src/components/Accordion/Accordion.module.css +21 -2
  12. package/src/components/Accordion/Accordion.stories.tsx +38 -0
  13. package/src/components/Accordion/Accordion.tsx +27 -12
  14. package/src/components/AssistiveElement/ASSISTIVEELEMENT_IMPLEMENTATION_NOTES.md +59 -0
  15. package/src/components/AssistiveElement/AssistiveElement.module.css +8 -1
  16. package/src/components/AssistiveElement/AssistiveElement.tsx +17 -7
  17. package/src/components/Autocomplete/Autocomplete.tsx +18 -2
  18. package/src/components/Avatar/AVATAR_IMPLEMENTATION_NOTES.md +36 -0
  19. package/src/components/Avatar/Avatar.tsx +6 -9
  20. package/src/components/Badge/Badge.module.css +0 -12
  21. package/src/components/Badge/Badge.tsx +0 -4
  22. package/src/components/Card/Card.tsx +5 -5
  23. package/src/components/Checkbox/Checkbox.module.css +17 -21
  24. package/src/components/Checkbox/Checkbox.tsx +24 -18
  25. package/src/components/Chip/Chip.module.css +16 -7
  26. package/src/components/Chip/Chip.tsx +1 -1
  27. package/src/components/Flex/Flex.tsx +1 -1
  28. package/src/components/FormControlWrapper/FORMCONTROLWRAPPER_IMPLEMENTATION_NOTES.md +35 -0
  29. package/src/components/FormControlWrapper/FormControlWrapper.tsx +24 -8
  30. package/src/components/Grid/Grid.tsx +1 -1
  31. package/src/components/Group/Group.tsx +1 -1
  32. package/src/components/HoverCard/HoverCard.tsx +1 -1
  33. package/src/components/Label/Label.module.css +15 -2
  34. package/src/components/Label/Label.tsx +7 -6
  35. package/src/components/Menu/Menu.tsx +1 -1
  36. package/src/components/Modal/Modal.tsx +1 -1
  37. package/src/components/NumberInput/NumberInput.tsx +1 -1
  38. package/src/components/Pagination/Pagination.tsx +1 -1
  39. package/src/components/Panel/Panel.tsx +1 -1
  40. package/src/components/Radio/Radio.module.css +33 -38
  41. package/src/components/Radio/Radio.tsx +58 -44
  42. package/src/components/Radio/RadioGroup.tsx +5 -0
  43. package/src/components/SegmentedControl/SegmentedControl.module.css +0 -7
  44. package/src/components/SegmentedControl/SegmentedControl.tsx +4 -8
  45. package/src/components/Slider/Slider.tsx +1 -1
  46. package/src/components/Stack/Stack.tsx +1 -1
  47. package/src/components/Stepper/Stepper.tsx +3 -3
  48. package/src/components/Switch/SWITCH_IMPLEMENTATION_NOTES.md +123 -0
  49. package/src/components/Switch/Switch.module.css +152 -89
  50. package/src/components/Switch/Switch.tsx +140 -33
  51. package/src/components/Switch/SwitchGroup.tsx +37 -9
  52. package/src/components/Table/Table.tsx +1 -1
  53. package/src/components/Tabs/IMPLEMENTATION_NOTES.md +10 -0
  54. package/src/components/Tabs/Tabs.module.css +120 -43
  55. package/src/components/Tabs/Tabs.stories.tsx +5 -2
  56. package/src/components/Tabs/Tabs.tsx +1 -1
  57. package/src/components/Tabs/USAGE.md +27 -11
  58. package/src/components/TextField/TextField.tsx +1 -1
  59. package/src/components/TimePicker/TimePicker.tsx +1 -1
  60. package/src/components/Timeline/Timeline.tsx +1 -1
  61. package/src/components/Timeline/TimelineItem.tsx +2 -2
  62. package/src/components/Toast/Toast.tsx +4 -4
  63. package/src/components/Tooltip/Tooltip.tsx +1 -1
  64. package/src/components/Tree/Tree.tsx +1 -1
  65. package/dist/src/OverStyling.d.ts +0 -1
  66. package/dist/src/Version.d.ts +0 -1
  67. package/dist/src/components/Accordion/Accordion.d.ts +0 -77
  68. package/dist/src/components/Accordion/index.d.ts +0 -1
  69. package/dist/src/components/AssistiveElement/AssistiveElement.d.ts +0 -7
  70. package/dist/src/components/AssistiveElement/index.d.ts +0 -1
  71. package/dist/src/components/Autocomplete/Autocomplete.d.ts +0 -24
  72. package/dist/src/components/Autocomplete/index.d.ts +0 -1
  73. package/dist/src/components/Avatar/Avatar.d.ts +0 -25
  74. package/dist/src/components/Avatar/index.d.ts +0 -1
  75. package/dist/src/components/Badge/Badge.d.ts +0 -25
  76. package/dist/src/components/Badge/index.d.ts +0 -1
  77. package/dist/src/components/Box/Box.d.ts +0 -8
  78. package/dist/src/components/Box/index.d.ts +0 -1
  79. package/dist/src/components/Breadcrumb/Breadcrumb.d.ts +0 -20
  80. package/dist/src/components/Breadcrumb/index.d.ts +0 -1
  81. package/dist/src/components/Button/Button.d.ts +0 -37
  82. package/dist/src/components/Button/index.d.ts +0 -1
  83. package/dist/src/components/Card/Card.d.ts +0 -81
  84. package/dist/src/components/Card/index.d.ts +0 -1
  85. package/dist/src/components/Checkbox/Checkbox.d.ts +0 -14
  86. package/dist/src/components/Checkbox/CheckboxGroup.d.ts +0 -29
  87. package/dist/src/components/Checkbox/index.d.ts +0 -5
  88. package/dist/src/components/Chip/Chip.d.ts +0 -21
  89. package/dist/src/components/Chip/index.d.ts +0 -1
  90. package/dist/src/components/Container/Container.d.ts +0 -5
  91. package/dist/src/components/Container/index.d.ts +0 -1
  92. package/dist/src/components/DatePicker/DatePicker.d.ts +0 -20
  93. package/dist/src/components/DatePicker/index.d.ts +0 -1
  94. package/dist/src/components/Dropdown/BareDropdown.d.ts +0 -41
  95. package/dist/src/components/Dropdown/Dropdown.d.ts +0 -24
  96. package/dist/src/components/Dropdown/index.d.ts +0 -1
  97. package/dist/src/components/FileInput/FileInput.d.ts +0 -3
  98. package/dist/src/components/FileInput/index.d.ts +0 -1
  99. package/dist/src/components/FileUpload/FileUpload.d.ts +0 -3
  100. package/dist/src/components/FileUpload/index.d.ts +0 -1
  101. package/dist/src/components/Flex/Flex.d.ts +0 -5
  102. package/dist/src/components/Flex/index.d.ts +0 -1
  103. package/dist/src/components/FormControlLayout/FormControlLayout.d.ts +0 -6
  104. package/dist/src/components/FormControlLayout/index.d.ts +0 -1
  105. package/dist/src/components/FormControlWrapper/FormControlWrapper.d.ts +0 -11
  106. package/dist/src/components/FormControlWrapper/index.d.ts +0 -1
  107. package/dist/src/components/Grid/Grid.d.ts +0 -12
  108. package/dist/src/components/Grid/index.d.ts +0 -1
  109. package/dist/src/components/Group/Group.d.ts +0 -5
  110. package/dist/src/components/Group/index.d.ts +0 -1
  111. package/dist/src/components/HoverCard/HoverCard.d.ts +0 -29
  112. package/dist/src/components/HoverCard/index.d.ts +0 -1
  113. package/dist/src/components/Label/Label.d.ts +0 -7
  114. package/dist/src/components/Label/index.d.ts +0 -1
  115. package/dist/src/components/Link/Link.d.ts +0 -20
  116. package/dist/src/components/Link/index.d.ts +0 -1
  117. package/dist/src/components/Loader/Loader.d.ts +0 -5
  118. package/dist/src/components/Loader/index.d.ts +0 -1
  119. package/dist/src/components/Menu/Menu.d.ts +0 -54
  120. package/dist/src/components/Menu/index.d.ts +0 -1
  121. package/dist/src/components/Modal/Modal.d.ts +0 -78
  122. package/dist/src/components/Modal/index.d.ts +0 -1
  123. package/dist/src/components/NumberInput/NumberInput.d.ts +0 -24
  124. package/dist/src/components/NumberInput/index.d.ts +0 -1
  125. package/dist/src/components/Pagination/Pagination.d.ts +0 -23
  126. package/dist/src/components/Pagination/Pagination.icons.d.ts +0 -8
  127. package/dist/src/components/Pagination/index.d.ts +0 -1
  128. package/dist/src/components/Panel/Panel.d.ts +0 -52
  129. package/dist/src/components/Panel/index.d.ts +0 -1
  130. package/dist/src/components/Radio/Radio.d.ts +0 -13
  131. package/dist/src/components/Radio/RadioGroup.d.ts +0 -9
  132. package/dist/src/components/Radio/index.d.ts +0 -2
  133. package/dist/src/components/ReadOnlyField/ReadOnlyBooleanField.d.ts +0 -5
  134. package/dist/src/components/ReadOnlyField/ReadOnlyField.d.ts +0 -5
  135. package/dist/src/components/ReadOnlyField/ReadOnlySwitchField.d.ts +0 -5
  136. package/dist/src/components/ReadOnlyField/ReadOnlyTextField.d.ts +0 -8
  137. package/dist/src/components/ReadOnlyField/WithReadOnlyWrapper.d.ts +0 -28
  138. package/dist/src/components/ReadOnlyField/index.d.ts +0 -2
  139. package/dist/src/components/SegmentedControl/SegmentedControl.d.ts +0 -32
  140. package/dist/src/components/SegmentedControl/index.d.ts +0 -1
  141. package/dist/src/components/Slider/Slider.d.ts +0 -30
  142. package/dist/src/components/Slider/index.d.ts +0 -1
  143. package/dist/src/components/Stack/Stack.d.ts +0 -5
  144. package/dist/src/components/Stack/index.d.ts +0 -1
  145. package/dist/src/components/Stepper/Stepper.d.ts +0 -97
  146. package/dist/src/components/Stepper/index.d.ts +0 -1
  147. package/dist/src/components/Switch/Switch.d.ts +0 -10
  148. package/dist/src/components/Switch/SwitchGroup.d.ts +0 -24
  149. package/dist/src/components/Switch/index.d.ts +0 -2
  150. package/dist/src/components/Table/Table.d.ts +0 -154
  151. package/dist/src/components/Table/index.d.ts +0 -1
  152. package/dist/src/components/Tabs/Tabs.d.ts +0 -57
  153. package/dist/src/components/Tabs/index.d.ts +0 -1
  154. package/dist/src/components/Text/Text.d.ts +0 -20
  155. package/dist/src/components/Text/index.d.ts +0 -1
  156. package/dist/src/components/TextArea/TextArea.d.ts +0 -24
  157. package/dist/src/components/TextArea/index.d.ts +0 -1
  158. package/dist/src/components/TextField/TextField.d.ts +0 -24
  159. package/dist/src/components/TextField/index.d.ts +0 -1
  160. package/dist/src/components/TimePicker/TimePicker.d.ts +0 -17
  161. package/dist/src/components/TimePicker/index.d.ts +0 -1
  162. package/dist/src/components/Timeline/Timeline.d.ts +0 -16
  163. package/dist/src/components/Timeline/TimelineItem.d.ts +0 -29
  164. package/dist/src/components/Timeline/index.d.ts +0 -2
  165. package/dist/src/components/Title/Title.d.ts +0 -21
  166. package/dist/src/components/Title/index.d.ts +0 -1
  167. package/dist/src/components/Toast/Toast.d.ts +0 -32
  168. package/dist/src/components/Toast/index.d.ts +0 -1
  169. package/dist/src/components/Tooltip/Tooltip.d.ts +0 -28
  170. package/dist/src/components/Tooltip/index.d.ts +0 -1
  171. package/dist/src/components/TransferList/TransferList.d.ts +0 -4
  172. package/dist/src/components/TransferList/index.d.ts +0 -1
  173. package/dist/src/components/Tree/Tree.d.ts +0 -18
  174. package/dist/src/components/Tree/index.d.ts +0 -1
  175. package/dist/src/components/Typography/Typography.d.ts +0 -38
  176. package/dist/src/components/Typography/index.d.ts +0 -1
  177. package/dist/src/components/index.d.ts +0 -50
  178. package/dist/src/index.d.ts +0 -91
  179. package/dist/src/types/index.d.ts +0 -6
  180. package/dist/src/utils/ColorSchemeWrapper.d.ts +0 -3
  181. package/dist/src/utils/RequireAccessibleLabel.d.ts +0 -1
  182. package/dist/src/utils/copyToClipboard.d.ts +0 -8
  183. package/dist/src/utils/filterStylingProps.d.ts +0 -29
  184. package/dist/src/utils/index.d.ts +0 -1
  185. package/dist/vite.config.d.ts +0 -2
  186. package/dist/vitest.workspace.d.ts +0 -2
  187. package/src/types/mantine.d.ts +0 -7
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.21.0",
16
+ "version": "0.22.0",
17
17
  "publishConfig": {
18
18
  "access": "public"
19
19
  },
@@ -0,0 +1,63 @@
1
+ # Accordion – implementation notes
2
+
3
+ Decisions and design tweaks specific to the UI Kit's Accordion wrapped against `@mui/material`.
4
+
5
+ ---
6
+
7
+ ## 1. Context-based container (no native MUI equivalent)
8
+
9
+ **Decision:** MUI has no group-level `Accordion` — its own `Accordion` component _is_ the
10
+ item, controlled individually via `expanded`/`onChange`. Recursica's container/group model
11
+ (single `value`, `multiple`, cascading `chevron`) is reimplemented with a React context
12
+ (`AccordionContext`) that each `AccordionItem`/`AccordionControl` reads from.
13
+ **Implementation:** `AccordionBase` owns the controlled/uncontrolled `value` state and hands
14
+ `{ value, onChange, chevron }` down via context; `AccordionItem` derives its own `expanded`
15
+ from that context instead of taking it as a prop.
16
+
17
+ ---
18
+
19
+ ## 2. Prop-contract Omits protect the context wiring
20
+
21
+ **Decision:** Because `AccordionItem`/`AccordionControl` are thin wrappers around real MUI
22
+ components (`MuiAccordion`/`MuiAccordionSummary`), their native `expanded`/`onChange`/
23
+ `expandIcon` props would otherwise collide with the values these wrappers compute from
24
+ context.
25
+ **Implementation:** `AccordionItemWrapperProps` omits `expanded`/`onChange`/`defaultExpanded`
26
+ from `MuiAccordionProps`; `AccordionControlWrapperProps` omits `expandIcon` from
27
+ `MuiAccordionSummaryProps`. Neither is exposed on `RecursicaAccordionItemProps`/
28
+ `RecursicaAccordionControlProps`, so passing them is a compile error, not a silent override.
29
+
30
+ ---
31
+
32
+ ## 3. `disabled` is a formally supported, shared prop
33
+
34
+ **Decision:** `RecursicaAccordionItemProps.disabled` is shared as-is with MUI's native
35
+ `Accordion.disabled` — same name, same shape, no reshaping needed. MUI already blocks
36
+ click/keyboard interaction on the control internally once `disabled` is set (its `AccordionSummary`
37
+ renders as a real disabled `<button>`); the visual dim is Recursica's own, not MUI's default.
38
+ **Implementation:** `AccordionItem` destructures `disabled` explicitly (rather than letting it
39
+ ride through in `...rest`) so it can also drive `data-disabled` on the item root for the dim.
40
+ MUI's own default disabled treatment — a separate hardcoded background/opacity on both the
41
+ item root and the control (`.Mui-disabled`) — is neutralized in `Accordion.module.css` so the
42
+ token-driven `.item[data-disabled]` dim is the single source of truth.
43
+
44
+ ---
45
+
46
+ ## 4. Chevron via `expandIcon` wrapper
47
+
48
+ **Decision:** MUI's expand indicator is set through `AccordionSummary.expandIcon`, not a
49
+ child. Recursica's container-level `chevron` (or the default arrow) is threaded through
50
+ context and rendered into that slot.
51
+ **Implementation:** `AccordionControl` resolves `ctx?.chevron ?? <ChevronIcon />` and passes
52
+ it as `expandIcon={<span className={styles.chevron}>{resolvedChevron}</span>}`.
53
+
54
+ ---
55
+
56
+ ## 5. Transparent background for `Layer` overrides
57
+
58
+ **Decision:** Same reasoning as the Mantine adapter's hover-fix note — MUI's `Paper`-derived
59
+ `Accordion` root ships its own background; Recursica needs it transparent so ambient `Layer`
60
+ tokens can apply.
61
+ **Implementation:** `AccordionItem` merges `backgroundColor: "transparent"` into `style`
62
+ ahead of any caller-supplied `style`, and forces `disableGutters`/`elevation={0}`/`square` to
63
+ strip MUI's own spacing/elevation/border-radius defaults.
@@ -168,11 +168,11 @@
168
168
  margin: 0 !important;
169
169
  }
170
170
 
171
- /* Mantine natively places hover states on the control node. We explicitly re-apply our own
171
+ /* MUI natively places hover states on the control node. We explicitly re-apply our own
172
172
  constant background (MUI's native :hover CSS would otherwise show through) and use an
173
173
  overlay ::after structure with the generic hover tokens for the actual hover tint; no per-item
174
174
  hover-color/hover-opacity tokens exist anymore. */
175
- .control:hover {
175
+ .control:hover:not(:disabled) {
176
176
  background-color: var(
177
177
  --recursica_ui-kit_components_accordion-header_variants_appearance_closed_properties_colors_background-color
178
178
  );
@@ -192,6 +192,25 @@
192
192
  opacity: var(--recursica_brand_states_hover_opacity);
193
193
  }
194
194
 
195
+ /* MUI applies its own default disabled treatment on both the item root and the control
196
+ (`.Mui-disabled`, each with its own hardcoded background/opacity) — neutralized here so the
197
+ token-driven dim below is the single source of truth. */
198
+ .item:global(.Mui-disabled) {
199
+ background-color: transparent;
200
+ }
201
+ .control:global(.Mui-disabled) {
202
+ opacity: 1;
203
+ }
204
+
205
+ /* Dims the whole item — control and any currently-visible panel content alike — using the
206
+ generic disabled opacity token, same convention as every other disabled component in the
207
+ design system (no accordion-item-specific disabled token exists). MUI's own `disabled` prop
208
+ already blocks click/keyboard interaction on the control internally; this only handles the
209
+ visual dim. */
210
+ .item[data-disabled] {
211
+ opacity: var(--recursica_brand_states_disabled);
212
+ }
213
+
195
214
  /* Label logic inside the Control wrapper */
196
215
  .label {
197
216
  flex: 1;
@@ -123,6 +123,44 @@ export const WithIcons: StoryObj<typeof Accordion> = {
123
123
  },
124
124
  };
125
125
 
126
+ export const Disabled: StoryObj<typeof Accordion> = {
127
+ render: () => {
128
+ return (
129
+ <Layer layer={0} style={{ padding: "24px" }}>
130
+ <Accordion defaultValue="expanded-disabled" chevron={<ChevronIcon />}>
131
+ <Accordion.Item value="expanded-disabled" disabled>
132
+ <Accordion.Control leftIcon={<SVGIcon />}>
133
+ Expanded and Disabled
134
+ </Accordion.Control>
135
+ <Accordion.Panel>
136
+ This item starts expanded so the panel content's dimming can be
137
+ verified alongside the control's, not just the collapsed header.
138
+ </Accordion.Panel>
139
+ </Accordion.Item>
140
+
141
+ <Accordion.Item value="collapsed-disabled" disabled>
142
+ <Accordion.Control leftIcon={<SVGIcon />}>
143
+ Collapsed and Disabled
144
+ </Accordion.Control>
145
+ <Accordion.Panel>
146
+ Clicking or tabbing to this control should have no effect.
147
+ </Accordion.Panel>
148
+ </Accordion.Item>
149
+
150
+ <Accordion.Item value="enabled">
151
+ <Accordion.Control leftIcon={<SVGIcon />}>
152
+ Enabled, for Comparison
153
+ </Accordion.Control>
154
+ <Accordion.Panel>
155
+ A normal, interactive item alongside the disabled ones above.
156
+ </Accordion.Panel>
157
+ </Accordion.Item>
158
+ </Accordion>
159
+ </Layer>
160
+ );
161
+ },
162
+ };
163
+
126
164
  export const LayerOne: StoryObj<typeof Accordion> = {
127
165
  render: () => {
128
166
  return (
@@ -17,6 +17,7 @@ import {
17
17
  type RecursicaAccordionProps,
18
18
  type RecursicaAccordionItemProps,
19
19
  type RecursicaAccordionControlProps,
20
+ type RecursicaAccordionPanelProps,
20
21
  } from "@recursica/adapter-common";
21
22
 
22
23
  const ChevronIcon = () => (
@@ -48,7 +49,7 @@ export type AccordionProps = RecursicaOverStyled<
48
49
  const AccordionBase = forwardRef<HTMLDivElement, AccordionProps>(
49
50
  function Accordion(
50
51
  {
51
- variant = "unstyled",
52
+ variant = "default",
52
53
  overStyled = false,
53
54
  value,
54
55
  defaultValue,
@@ -96,9 +97,9 @@ const AccordionBase = forwardRef<HTMLDivElement, AccordionProps>(
96
97
  >
97
98
  <div
98
99
  ref={ref}
100
+ {...(sanitizedProps as React.HTMLAttributes<HTMLDivElement>)}
99
101
  className={finalClass}
100
102
  data-variant={variant}
101
- {...(sanitizedProps as React.HTMLAttributes<HTMLDivElement>)}
102
103
  >
103
104
  {children}
104
105
  </div>
@@ -109,7 +110,15 @@ const AccordionBase = forwardRef<HTMLDivElement, AccordionProps>(
109
110
  AccordionBase.displayName = "Accordion";
110
111
 
111
112
  export type AccordionItemWrapperProps = RecursicaOverStyled<
112
- Omit<MuiAccordionProps, "children" | "value"> & {
113
+ // `expanded`/`onChange` are computed internally from the group context — omitted so a
114
+ // caller can't silently detach this item from the controlled state. `defaultExpanded` has
115
+ // no meaning here since expansion is always driven by the group's `value`. `disabled` is
116
+ // shared as-is with `RecursicaAccordionItemProps.disabled` — same name, same shape, no
117
+ // reshaping needed; see ACCORDION_IMPLEMENTATION_NOTES.md.
118
+ Omit<
119
+ MuiAccordionProps,
120
+ "children" | "value" | "expanded" | "onChange" | "defaultExpanded"
121
+ > & {
113
122
  children?: React.ReactNode;
114
123
  value: string;
115
124
  } & RecursicaAccordionItemProps
@@ -125,6 +134,7 @@ export const AccordionItem = forwardRef<
125
134
  divider = true,
126
135
  children,
127
136
  value,
137
+ disabled = false,
128
138
  overStyled = false,
129
139
  style,
130
140
  ...rest
@@ -157,17 +167,19 @@ export const AccordionItem = forwardRef<
157
167
  return (
158
168
  <MuiAccordion
159
169
  ref={ref}
170
+ {...(sanitizedProps as unknown as Omit<
171
+ MuiAccordionProps,
172
+ "expanded" | "onChange" | "defaultExpanded" | "disabled"
173
+ >)}
160
174
  className={finalClass}
161
175
  disableGutters
162
176
  elevation={0}
163
177
  square
164
178
  expanded={isExpanded}
165
179
  onChange={handleChange}
180
+ disabled={disabled}
181
+ data-disabled={disabled || undefined}
166
182
  style={mergedStyle}
167
- {...(sanitizedProps as unknown as Omit<
168
- MuiAccordionProps,
169
- "expanded" | "onChange"
170
- >)}
171
183
  >
172
184
  {
173
185
  (title ? (
@@ -187,7 +199,9 @@ AccordionItem.displayName = "AccordionItem";
187
199
 
188
200
  // ==== ACCORDION CONTROL ====
189
201
  export type AccordionControlWrapperProps = RecursicaOverStyled<
190
- MuiAccordionSummaryProps & RecursicaAccordionControlProps
202
+ // `expandIcon` is resolved internally from the container's `chevron` (via context) —
203
+ // omitted so a caller can't silently override it with MUI's native chevron slot.
204
+ Omit<MuiAccordionSummaryProps, "expandIcon"> & RecursicaAccordionControlProps
191
205
  >;
192
206
 
193
207
  export const AccordionControl = forwardRef<
@@ -210,9 +224,9 @@ export const AccordionControl = forwardRef<
210
224
  return (
211
225
  <MuiAccordionSummary
212
226
  ref={ref}
227
+ {...(sanitizedProps as unknown as MuiAccordionSummaryProps)}
213
228
  className={finalClass}
214
229
  expandIcon={<span className={styles.chevron}>{resolvedChevron}</span>}
215
- {...(sanitizedProps as unknown as MuiAccordionSummaryProps)}
216
230
  >
217
231
  {leftIcon && (
218
232
  <span className={styles.iconLeftWrapper} aria-hidden>
@@ -226,8 +240,9 @@ export const AccordionControl = forwardRef<
226
240
  AccordionControl.displayName = "AccordionControl";
227
241
 
228
242
  // ==== ACCORDION PANEL ====
229
- export type AccordionPanelWrapperProps =
230
- RecursicaOverStyled<MuiAccordionDetailsProps>;
243
+ export type AccordionPanelWrapperProps = RecursicaOverStyled<
244
+ MuiAccordionDetailsProps & RecursicaAccordionPanelProps
245
+ >;
231
246
 
232
247
  export const AccordionPanel = forwardRef<
233
248
  HTMLDivElement,
@@ -243,8 +258,8 @@ export const AccordionPanel = forwardRef<
243
258
  return (
244
259
  <MuiAccordionDetails
245
260
  ref={ref}
246
- className={finalClass}
247
261
  {...(sanitizedProps as unknown as MuiAccordionDetailsProps)}
262
+ className={finalClass}
248
263
  >
249
264
  <div className={styles.content}>{children}</div>
250
265
  </MuiAccordionDetails>
@@ -0,0 +1,59 @@
1
+ # AssistiveElement – implementation notes
2
+
3
+ Decisions and design tweaks specific to the UI Kit's AssistiveElement wrapped against
4
+ `@mui/material`.
5
+
6
+ ---
7
+
8
+ ## 1. Forced `component="div"` trades away `FormHelperText`'s default semantics
9
+
10
+ **Decision:** `FormHelperText` renders a `<p>` by default — a reasonable semantic choice for
11
+ standalone helper/error text. This adapter forces `component="div"` instead, purely so the
12
+ icon + text can sit side by side in a flex row; `<p>` can't contain the flex layout the same
13
+ way without other tradeoffs.
14
+ **Implementation:** `component="div"` and `error={isError}` (derived from `assistiveVariant`)
15
+ are both computed internally and omitted from `AssistiveElementProps` (`Omit<FormHelperTextProps,
16
+ "error" | "component">`) so a caller can't silently override either via spread.
17
+ `aria-describedby`/`aria-errormessage` association with a field is a different story: this
18
+ component has no reference to any sibling field, so it can't wire that itself. It's already
19
+ handled one layer up, in `FormControlWrapper` — which generates the ids, passes them down via
20
+ this component's own `id` prop, and clones `aria-describedby`/`aria-errormessage` onto the
21
+ field. Only a fully standalone `<AssistiveElement>` used outside `FormControlWrapper` still
22
+ needs that wiring done by hand. `role="alert"` for the error variant is handled here instead
23
+ (see §4) — that piece genuinely belongs at this component's own level.
24
+
25
+ ---
26
+
27
+ ## 2. CSS specificity tie against MUI's own `.Mui-error` color
28
+
29
+ **Decision:** MUI's `FormHelperText` sets its own error color via `.MuiFormHelperText-root.Mui-error`
30
+ (two classes) whenever `error` is set. Our own `.root[data-variant="error"]` (one class + one
31
+ attribute) lands at the _same_ specificity — without help, whichever rule loads later in the
32
+ merged stylesheet wins, which is import-order luck rather than a deterministic override.
33
+ **Implementation:** `!important` on that one rule's `color` guarantees ours wins regardless of
34
+ load order (same reasoning as the focus-ring override in the Accordion adapter). Not needed for
35
+ the `help` variant — MUI's unconditional base `color` on `.root` alone already has lower
36
+ specificity than our attribute-qualified rule there.
37
+
38
+ ---
39
+
40
+ ## 3. Native passthrough capabilities, left unblocked and undocumented
41
+
42
+ **Decision:** `FormHelperText`'s own `disabled`/`filled`/`focused`/`margin`/`required`/`variant`/
43
+ `classes`/`sx` all pass straight through today (none of them are omitted, and none are
44
+ computed internally so there's nothing for them to silently override). They're also not added
45
+ to `RecursicaAssistiveElementProps` — same "accidental passthrough" class as Accordion's
46
+ `disabled` was before it got formalized. Known asymmetry: MUI callers can reach for these
47
+ today, Mantine callers can't (Mantine's `AssistiveElement` wraps no native component at all).
48
+ Revisit if a real use case shows up for any of them.
49
+
50
+ ---
51
+
52
+ ## 4. `role="alert"` defaults on for the error variant
53
+
54
+ **Decision:** Error text needs to be announced by assistive tech as it appears or changes;
55
+ static help text doesn't. Rather than requiring every integrator to remember `role="alert"`,
56
+ `assistiveVariant="error"` defaults it automatically.
57
+ **Implementation:** `role` is destructured out and resolved as `role ?? (isError ? "alert" :
58
+ undefined)` before rendering — an explicit caller-supplied `role` always wins over the default,
59
+ and `"help"` gets no default at all.
@@ -101,10 +101,17 @@
101
101
  VARIANTS: Error
102
102
  ========================================= */
103
103
 
104
+ /* MUI's own `FormHelperText` sets its error color via `.MuiFormHelperText-root.Mui-error`
105
+ (two classes) whenever `error` is set — the exact same specificity as our own
106
+ `.root[data-variant="error"]` (one class + one attribute), so whichever rule loads later
107
+ in the merged stylesheet would otherwise win. `!important` makes ours win deterministically
108
+ regardless of CSS import order (same reasoning as the focus-ring override elsewhere in this
109
+ codebase). Not needed for the `help` variant above — MUI's unconditional base `color` on
110
+ `.root` alone has lower specificity than ours there already. */
104
111
  .root[data-variant="error"] {
105
112
  color: var(
106
113
  --recursica_ui-kit_components_assistive-element_variants_types_error_properties_colors_text-color
107
- );
114
+ ) !important;
108
115
  }
109
116
 
110
117
  .root[data-variant="error"] .icon {
@@ -1,15 +1,19 @@
1
1
  import React from "react";
2
2
  import { FormHelperText, type FormHelperTextProps } from "@mui/material";
3
- import { filterStylingProps } from "../../utils/filterStylingProps";
3
+ import {
4
+ filterStylingProps,
5
+ type RecursicaOverStyled,
6
+ } from "../../utils/filterStylingProps";
4
7
  import styles from "./AssistiveElement.module.css";
5
8
 
6
9
  import { type RecursicaAssistiveElementProps } from "@recursica/adapter-common";
7
10
 
8
- export interface AssistiveElementProps
9
- extends FormHelperTextProps,
10
- RecursicaAssistiveElementProps {
11
- overStyled?: boolean;
12
- }
11
+ // `error`/`component` are computed internally (from `assistiveVariant`, and forced to "div"
12
+ // for layout) — omitted so a caller can't silently override either via spread.
13
+ export type AssistiveElementProps = RecursicaOverStyled<
14
+ Omit<FormHelperTextProps, "error" | "component"> &
15
+ RecursicaAssistiveElementProps
16
+ >;
13
17
 
14
18
  export const AssistiveElement = React.forwardRef<
15
19
  HTMLParagraphElement,
@@ -21,6 +25,7 @@ export const AssistiveElement = React.forwardRef<
21
25
  overStyled = false,
22
26
  className,
23
27
  children,
28
+ role,
24
29
  ...rest
25
30
  } = props;
26
31
 
@@ -61,15 +66,20 @@ export const AssistiveElement = React.forwardRef<
61
66
  // Map to MUI's native error prop if it's explicitly an error state
62
67
  const isError = assistiveVariant === "error";
63
68
 
69
+ // Announce error text as it appears/changes; a caller-supplied `role` always wins. No
70
+ // default for `help` — static descriptive text doesn't need a live region.
71
+ const resolvedRole = role ?? (isError ? "alert" : undefined);
72
+
64
73
  // Force component = "div" so flex layouts execute correctly inside HelperText
65
74
  return (
66
75
  <FormHelperText
67
76
  ref={ref}
77
+ {...(sanitizedProps as Omit<FormHelperTextProps, "error" | "component">)}
68
78
  component="div"
69
79
  error={isError}
80
+ role={resolvedRole}
70
81
  data-variant={assistiveVariant}
71
82
  className={className ? `${styles.root} ${className}` : styles.root}
72
- {...(sanitizedProps as FormHelperTextProps)}
73
83
  >
74
84
  {assistiveWithIcon && (
75
85
  <div className={styles.icon}>
@@ -134,7 +134,7 @@ export const Autocomplete = forwardRef<HTMLInputElement, AutocompleteProps>(
134
134
  label={label}
135
135
  assistiveText={assistiveText}
136
136
  assistiveWithIcon={assistiveWithIcon}
137
- error={!!error}
137
+ error={error}
138
138
  required={required}
139
139
  id={id}
140
140
  disabled={disabled}
@@ -165,13 +165,29 @@ export const Autocomplete = forwardRef<HTMLInputElement, AutocompleteProps>(
165
165
  }}
166
166
  options={data || []}
167
167
  renderInput={(params) => {
168
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
169
168
  const { InputProps, ...restParams } = params;
170
169
  return (
171
170
  <MuiTextField
172
171
  {...restParams}
173
172
  placeholder={placeholder}
174
173
  variant="standard"
174
+ InputProps={{
175
+ ...InputProps,
176
+ startAdornment: leftSection ? (
177
+ <span className={styles.section} data-position="left">
178
+ {leftSection}
179
+ </span>
180
+ ) : (
181
+ InputProps.startAdornment
182
+ ),
183
+ endAdornment: rightSection ? (
184
+ <span className={styles.section} data-position="right">
185
+ {rightSection}
186
+ </span>
187
+ ) : (
188
+ InputProps.endAdornment
189
+ ),
190
+ }}
175
191
  />
176
192
  );
177
193
  }}
@@ -0,0 +1,36 @@
1
+ # Avatar Component Implementation Notes
2
+
3
+ ## Architecture Decisions
4
+
5
+ The `Avatar` component is an adapter over MUI's `Avatar`.
6
+
7
+ - We do not wrap `MuiAvatar` in any custom standard `div` elements, preserving DOM structure.
8
+ - All styles strictly pull from explicit `--recursica_ui-kit_components_avatar_*` CSS tokens.
9
+
10
+ ## `data-style`/`data-variant`, not MUI's own classes
11
+
12
+ Same reasoning as the Mantine adapter: `data-style="image|icon|text"` is set based on which of
13
+ `src`/`icon`/`children` is present, and `data-variant` carries Recursica's own `variant` value
14
+ (`"solid"|"outline"|"ghost"`) — `Avatar.module.css` keys every visual treatment (background,
15
+ border, border-radius) off these two attributes, not off anything MUI's own classes do.
16
+
17
+ ## `variant` — a name collision with a different meaning, not a shared concept
18
+
19
+ **Found while auditing this component (not in Forge's original report):** MUI's native
20
+ `Avatar.variant` means **shape** (`'circular'|'rounded'|'square'`) — unlike almost every other
21
+ MUI component (including Mantine's `Avatar.variant`, which really is the same color-treatment
22
+ concept Recursica's `variant` is), MUI repurposes this name for something else entirely.
23
+
24
+ The previous implementation mapped Recursica's `variant` (`solid|outline|ghost`) to strings
25
+ (`"filled"|"outline"|"transparent"`) and fed them into MUI's native `variant` prop via an
26
+ `as unknown as MuiAvatarProps["variant"]` cast — none of those strings are valid MUI shape
27
+ values, so it was silently a no-op on MUI's own shape class (visually masked only because
28
+ `Avatar.module.css` already forces border-radius per `data-style` unconditionally, regardless
29
+ of what shape class MUI resolves to).
30
+
31
+ **Fix:** stopped feeding Recursica's `variant` into MUI's native `variant` prop at all.
32
+ `<MuiAvatar>` is now given an explicit, real, valid `variant="circular"` — Recursica has no
33
+ shape concept of its own to expose here, and avatars are always circular by design, so this is
34
+ just being explicit about that instead of relying on MUI's own default. `Omit<MuiAvatarProps,
35
+ "variant">` in the type prevents a caller from reaching MUI's real shape prop under Recursica's
36
+ `variant` name and getting confused about what it does.
@@ -11,9 +11,12 @@ import styles from "./Avatar.module.css";
11
11
 
12
12
  import { type RecursicaAvatarProps } from "@recursica/adapter-common";
13
13
 
14
+ // MUI's native `variant` is Avatar's *shape* ('circular'|'rounded'|'square'), not a color
15
+ // treatment like every other MUI component's `variant` — unlike Mantine's, where `variant`
16
+ // really is the same color-treatment concept ours is. Omitted so Recursica's own `variant`
17
+ // (a completely different concept) can't be confused with it; see AVATAR_IMPLEMENTATION_NOTES.md.
14
18
  export type AvatarProps = RecursicaOverStyled<
15
- Omit<MuiAvatarProps, "variant" | "size" | "color" | "radius"> &
16
- RecursicaAvatarProps
19
+ Omit<MuiAvatarProps, "variant"> & RecursicaAvatarProps
17
20
  >;
18
21
 
19
22
  const _Avatar = forwardRef<HTMLDivElement, AvatarProps>(function Avatar(
@@ -28,12 +31,6 @@ const _Avatar = forwardRef<HTMLDivElement, AvatarProps>(function Avatar(
28
31
  },
29
32
  ref,
30
33
  ) {
31
- const mapVariant = {
32
- solid: "filled",
33
- outline: "outline",
34
- ghost: "transparent",
35
- } as const;
36
-
37
34
  const sanitizedProps = filterStylingProps(rest, overStyled);
38
35
  const restRecord = sanitizedProps as Record<string, unknown>;
39
36
 
@@ -74,7 +71,7 @@ const _Avatar = forwardRef<HTMLDivElement, AvatarProps>(function Avatar(
74
71
  ref={ref}
75
72
  className={classNameProp}
76
73
  classes={mergedClassNames}
77
- variant={mapVariant[variant] as unknown as MuiAvatarProps["variant"]}
74
+ variant="circular"
78
75
  src={src}
79
76
  data-variant={variant}
80
77
  data-size={size}
@@ -113,15 +113,3 @@
113
113
  --recursica_ui-kit_components_badge_variants_styles_warning_properties_colors_text-color
114
114
  );
115
115
  }
116
-
117
- /* Target Mantine internal children if needed for typography constraints */
118
- .root :global(.mantine-Badge-label),
119
- .root :global(.mantine-Badge-section) {
120
- /* HARDCODE: Prevent Mantine from overriding our typography setup inside inner wrappers */
121
- font-size: inherit;
122
- font-weight: inherit;
123
- line-height: inherit;
124
- font-family: inherit;
125
- letter-spacing: inherit;
126
- text-transform: inherit;
127
- }
@@ -26,8 +26,6 @@ const _Badge = forwardRef<HTMLDivElement, BadgeProps>(function Badge(
26
26
 
27
27
  const mergedClassNames: Partial<Record<string, string>> = {
28
28
  root: styles.root,
29
- section: styles.section,
30
- label: styles.label,
31
29
  };
32
30
 
33
31
  const classNamesProp = (sanitizedProps as Record<string, unknown>).classNames;
@@ -38,8 +36,6 @@ const _Badge = forwardRef<HTMLDivElement, BadgeProps>(function Badge(
38
36
  ) {
39
37
  const o = classNamesProp as Partial<Record<string, string>>;
40
38
  mergedClassNames.root = o.root ? `${styles.root} ${o.root}` : styles.root;
41
- mergedClassNames.section = o.section ?? styles.section;
42
- mergedClassNames.label = o.label ?? styles.label;
43
39
  }
44
40
 
45
41
  const classNameProp = (sanitizedProps as Record<string, unknown>)
@@ -77,9 +77,9 @@ const CardBase = forwardRef<HTMLDivElement, CardProps>(function Card(
77
77
  return (
78
78
  <MuiCard
79
79
  ref={ref}
80
+ {...(sanitizedProps as unknown as MuiCardProps)}
80
81
  className={classNameProp}
81
82
  classes={mergedClassNames}
82
- {...(sanitizedProps as unknown as MuiCardProps)}
83
83
  />
84
84
  );
85
85
  });
@@ -100,10 +100,10 @@ export const CardSection = forwardRef<HTMLDivElement, CardSectionProps>(
100
100
  return (
101
101
  <div
102
102
  ref={ref}
103
+ {...(sanitizedProps as unknown as React.HTMLAttributes<HTMLDivElement>)}
103
104
  className={
104
105
  classNameProp ? `${styles.section} ${classNameProp}` : styles.section
105
106
  }
106
- {...(sanitizedProps as unknown as React.HTMLAttributes<HTMLDivElement>)}
107
107
  />
108
108
  );
109
109
  },
@@ -125,10 +125,10 @@ export const CardHeader = forwardRef<HTMLDivElement, CardHeaderProps>(
125
125
  return (
126
126
  <div
127
127
  ref={ref}
128
+ {...(sanitizedProps as unknown as React.HTMLAttributes<HTMLDivElement>)}
128
129
  className={
129
130
  classNameProp ? `${styles.header} ${classNameProp}` : styles.header
130
131
  }
131
- {...(sanitizedProps as unknown as React.HTMLAttributes<HTMLDivElement>)}
132
132
  />
133
133
  );
134
134
  },
@@ -150,10 +150,10 @@ export const CardFooter = forwardRef<HTMLDivElement, CardFooterProps>(
150
150
  return (
151
151
  <div
152
152
  ref={ref}
153
+ {...(sanitizedProps as unknown as React.HTMLAttributes<HTMLDivElement>)}
153
154
  className={
154
155
  classNameProp ? `${styles.footer} ${classNameProp}` : styles.footer
155
156
  }
156
- {...(sanitizedProps as unknown as React.HTMLAttributes<HTMLDivElement>)}
157
157
  />
158
158
  );
159
159
  },
@@ -177,10 +177,10 @@ export const CardContent = forwardRef<HTMLDivElement, CardContentProps>(
177
177
  return (
178
178
  <div
179
179
  ref={ref}
180
+ {...sanitizedProps}
180
181
  className={
181
182
  classNameProp ? `${styles.content} ${classNameProp}` : styles.content
182
183
  }
183
- {...sanitizedProps}
184
184
  />
185
185
  );
186
186
  },