@recursica/mui-adapter 0.24.0 → 0.26.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 (72) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/index.d.ts +189 -28
  3. package/dist/mui-adapter.cjs +85 -85
  4. package/dist/mui-adapter.cjs.map +1 -1
  5. package/dist/mui-adapter.css +1 -1
  6. package/dist/mui-adapter.js +29694 -24393
  7. package/dist/mui-adapter.js.map +1 -1
  8. package/llms.txt +1 -0
  9. package/package.json +1 -1
  10. package/src/GlobalExemptions.modules.css +0 -6
  11. package/src/components/Accordion/Accordion.module.css +0 -8
  12. package/src/components/AssistiveElement/AssistiveElement.tsx +1 -1
  13. package/src/components/Autocomplete/Autocomplete.module.css +0 -8
  14. package/src/components/Avatar/Avatar.module.css +0 -8
  15. package/src/components/Button/BUTTON_IMPLEMENTATION_NOTES.md +12 -0
  16. package/src/components/Button/Button.module.css +6 -18
  17. package/src/components/Checkbox/Checkbox.tsx +4 -1
  18. package/src/components/Chip/Chip.module.css +0 -27
  19. package/src/components/DatePicker/DATEPICKER_IMPLEMENTATION_NOTES.md +29 -0
  20. package/src/components/DatePicker/DatePicker.icons.tsx +35 -0
  21. package/src/components/DatePicker/DatePicker.module.css +451 -42
  22. package/src/components/DatePicker/DatePicker.stories.tsx +23 -25
  23. package/src/components/DatePicker/DatePicker.tsx +229 -47
  24. package/src/components/DatePicker/USAGE.md +14 -1
  25. package/src/components/Dropdown/Dropdown.module.css +0 -7
  26. package/src/components/FileInput/FileInput.module.css +0 -21
  27. package/src/components/FileInput/FileInput.tsx +6 -0
  28. package/src/components/FileUpload/FileUpload.module.css +0 -11
  29. package/src/components/HoverCard/HoverCard.module.css +1 -7
  30. package/src/components/Label/Label.module.css +0 -6
  31. package/src/components/Link/Link.module.css +0 -13
  32. package/src/components/Menu/Menu.module.css +0 -5
  33. package/src/components/Modal/Modal.module.css +0 -11
  34. package/src/components/NumberInput/NumberInput.module.css +0 -7
  35. package/src/components/Pagination/Pagination.module.css +0 -81
  36. package/src/components/Popover/IMPLEMENTATION_NOTES.md +22 -0
  37. package/src/components/Popover/Popover.module.css +118 -0
  38. package/src/components/Popover/Popover.stories.tsx +133 -0
  39. package/src/components/Popover/Popover.tsx +275 -0
  40. package/src/components/Popover/USAGE.md +69 -0
  41. package/src/components/Popover/index.ts +1 -0
  42. package/src/components/SegmentedControl/IMPLEMENTATION_NOTES.md +6 -0
  43. package/src/components/SegmentedControl/SegmentedControl.module.css +32 -11
  44. package/src/components/SegmentedControl/SegmentedControl.tsx +18 -4
  45. package/src/components/Slider/IMPLEMENTATION_NOTES.md +29 -0
  46. package/src/components/Slider/Slider.module.css +80 -17
  47. package/src/components/Slider/Slider.stories.tsx +1 -1
  48. package/src/components/Slider/Slider.tsx +36 -1
  49. package/src/components/Stepper/IMPLEMENTATION_NOTES.md +54 -0
  50. package/src/components/Stepper/Stepper.module.css +139 -106
  51. package/src/components/Stepper/Stepper.tsx +76 -10
  52. package/src/components/Stepper/USAGE.md +4 -0
  53. package/src/components/Tabs/IMPLEMENTATION_NOTES.md +12 -0
  54. package/src/components/Tabs/Tabs.module.css +107 -24
  55. package/src/components/Tabs/Tabs.tsx +1 -0
  56. package/src/components/TextArea/TextArea.module.css +20 -11
  57. package/src/components/TextArea/TextArea.tsx +12 -23
  58. package/src/components/TextField/TextField.module.css +0 -8
  59. package/src/components/TimePicker/TimePicker.module.css +0 -16
  60. package/src/components/Timeline/IMPLEMENTATION_NOTES.md +24 -3
  61. package/src/components/Timeline/Timeline.module.css +55 -73
  62. package/src/components/Timeline/Timeline.tsx +23 -27
  63. package/src/components/Timeline/TimelineItem.tsx +38 -35
  64. package/src/components/Toast/Toast.module.css +0 -7
  65. package/src/components/Tooltip/Tooltip.module.css +0 -9
  66. package/src/components/TransferList/TRANSFERLIST_IMPLEMENTATION_NOTES.md +140 -0
  67. package/src/components/TransferList/TransferList.module.css +174 -38
  68. package/src/components/TransferList/TransferList.stories.tsx +110 -6
  69. package/src/components/TransferList/TransferList.tsx +417 -8
  70. package/src/components/TransferList/USAGE.md +37 -6
  71. package/src/components/index.ts +1 -0
  72. package/src/index.ts +3 -0
@@ -2,10 +2,24 @@
2
2
  Recursica Tabs CSS module natively binding to Figma variables.
3
3
  */
4
4
 
5
- .root {
5
+ /* Unlike Mantine's .root (which internally wraps List+Panel and always spans its parent),
6
+ MUI's .root is only the tablist — it sits beside the panel in the consumer's own flex row for
7
+ vertical orientation. Stretching it to 100% width there fights the panel for space and drags
8
+ the border-right rail/indicator away from the tab content, so only horizontal gets full width;
9
+ vertical sizes to its content instead, same as Mantine's vertical .list does. */
10
+ .root[data-orientation="horizontal"] {
6
11
  width: 100%;
7
12
  }
8
13
 
14
+ /* Mantine's vertical .list stretches to the root's full height via flex align-items, so a
15
+ border-right divider on it spans the whole rail. MUI's scroller (the .list's parent) is a plain
16
+ block for vertical, not a flex container, so .list sizes to its own content (just the stacked
17
+ tabs) and a divider on it stops short at the last tab instead of running the full height —
18
+ spell the stretch out explicitly instead. */
19
+ .root[data-orientation="vertical"] .list {
20
+ height: 100%;
21
+ }
22
+
9
23
  /*
10
24
  Mantine Tabs Anatomy:
11
25
  .root
@@ -125,6 +139,15 @@
125
139
  --tabs-color: var(
126
140
  --recursica_ui-kit_components_tabs-item_variants_styles_outline_variants_selection-states_active_properties_colors_border-color
127
141
  ) !important;
142
+ --tabs-active-bg: var(
143
+ --recursica_ui-kit_components_tabs-item_variants_styles_outline_variants_selection-states_active_properties_colors_background-color
144
+ );
145
+ /* The active tab's own background-color token above is transparent by design (it's meant to
146
+ let whichever ambient Layer surface sits behind the Tabs show through). That means it can't
147
+ double as an opaque "eraser" for the baseline-patch trick below, which needs a real paint to
148
+ replace, not reveal-through. Layer 0 is the default/outermost surface a Tabs instance sits
149
+ on absent a wrapping <Layer>; see OUTLINE SELECTION BORDER below for why this is needed. */
150
+ --tabs-baseline-patch-bg: var(--recursica_brand_layer_0_properties_surface);
128
151
  }
129
152
 
130
153
  .root[data-variant="outline"][data-orientation="horizontal"] {
@@ -223,18 +246,17 @@
223
246
 
224
247
  /* ---------------------------------
225
248
  SELECTION INDICATOR & BASELINE
226
- Mantine draws the active-tab underline/side-border and the full-length baseline natively off
227
- the shared --tabs-color/--tabs-border-color vars above. MUI has no equivalent built-in, so we
228
- drive its own indicator slot (classes.indicator, wired in Tabs.tsx) and the list's baseline
229
- border explicitly from the same vars. Pills has no baseline/indicator — selection is carried
230
- entirely by the pill's own background/border (see COLORS & TYPOGRAPHY below).
249
+ Mantine draws the active-tab underline natively off the shared --tabs-color var above. MUI has
250
+ no equivalent built-in, so we drive its own indicator slot (classes.indicator, wired in
251
+ Tabs.tsx) and the list's baseline border explicitly from the same vars. Pills has no underline
252
+ indicator — selection shows via the pill's own background/border (see COLORS & TYPOGRAPHY
253
+ below). Outline repurposes the indicator as a baseline patch rather than an underline (see
254
+ below).
231
255
  --------------------------------- */
232
- .root[data-variant="default"] .indicator,
233
- .root[data-variant="outline"] .indicator {
256
+ .root[data-variant="default"] .indicator {
234
257
  background-color: var(--tabs-color) !important;
235
258
  }
236
- .root[data-variant="default"][data-orientation="horizontal"] .indicator,
237
- .root[data-variant="outline"][data-orientation="horizontal"] .indicator {
259
+ .root[data-variant="default"][data-orientation="horizontal"] .indicator {
238
260
  height: var(--tabs-active-border-size) !important;
239
261
  }
240
262
  /* MUI's Tabs root has no border/padding of its own, so the list's margin-bottom (the
@@ -250,27 +272,52 @@
250
272
  --recursica_ui-kit_components_tabs_variants_styles_default_variants_orientation_horizontal_properties_tabs-content-gap
251
273
  ) !important;
252
274
  }
275
+ /* width: same reasoning as the horizontal height above, rotated 90°.
276
+ right: same reasoning as the horizontal bottom offset above, rotated 90° — the list's
277
+ margin-right (this orientation's tabs-content-gap, spacing the panel to its right) inflates the
278
+ root's auto width past the list's own border-right edge. The indicator is absolutely positioned
279
+ against that wider box, so without this offset it renders content-gap pixels to the right of the
280
+ divider instead of flush against it. */
281
+ .root[data-variant="default"][data-orientation="vertical"] .indicator {
282
+ width: var(--tabs-active-border-size) !important;
283
+ right: var(
284
+ --recursica_ui-kit_components_tabs_variants_styles_default_variants_orientation_vertical_properties_tabs-content-gap
285
+ ) !important;
286
+ }
287
+ .root[data-variant="default"][data-orientation="horizontal"][data-inverted]
288
+ .indicator {
289
+ bottom: auto !important;
290
+ top: 0 !important;
291
+ }
292
+ .root[data-variant="pills"] .indicator {
293
+ display: none !important;
294
+ }
295
+
296
+ /* Outline has no sliding underline of its own, but reuses the indicator element (already
297
+ positioned/sized by MUI to exactly track the selected tab, no matter which one) as a baseline
298
+ patch instead of hiding it — see OUTLINE SELECTION BORDER below for why a patch is needed at
299
+ all, and why it has to live here (in the scroller) rather than on the tab itself. */
300
+ .root[data-variant="outline"] .indicator {
301
+ background-color: var(--tabs-baseline-patch-bg) !important;
302
+ }
253
303
  .root[data-variant="outline"][data-orientation="horizontal"]:not(
254
304
  [data-inverted]
255
305
  )
256
306
  .indicator {
307
+ height: var(--tabs-inactive-border-size) !important;
257
308
  bottom: var(
258
309
  --recursica_ui-kit_components_tabs_variants_styles_outline_variants_orientation_horizontal_properties_tabs-content-gap
259
310
  ) !important;
260
311
  }
261
- .root[data-variant="default"][data-orientation="vertical"] .indicator,
262
- .root[data-variant="outline"][data-orientation="vertical"] .indicator {
263
- width: var(--tabs-active-border-size) !important;
264
- }
265
- .root[data-variant="default"][data-orientation="horizontal"][data-inverted]
266
- .indicator,
267
312
  .root[data-variant="outline"][data-orientation="horizontal"][data-inverted]
268
313
  .indicator {
314
+ height: var(--tabs-inactive-border-size) !important;
269
315
  bottom: auto !important;
270
- top: 0 !important;
316
+ top: calc(-1 * var(--tabs-inactive-border-size)) !important;
271
317
  }
272
- .root[data-variant="pills"] .indicator {
273
- display: none !important;
318
+ .root[data-variant="outline"][data-orientation="vertical"] .indicator {
319
+ width: var(--tabs-inactive-border-size) !important;
320
+ right: calc(-1 * var(--tabs-inactive-border-size)) !important;
274
321
  }
275
322
 
276
323
  .root[data-variant="default"][data-orientation="horizontal"] .list,
@@ -356,6 +403,41 @@
356
403
  border-radius: var(--tabs-border-radius) 0 0 var(--tabs-border-radius);
357
404
  }
358
405
 
406
+ /* ---------------------------------
407
+ OUTLINE SELECTION BORDER
408
+ Mantine's outline tab marks selection with a bordered box left open on the edge that touches
409
+ the list's own baseline, so the box visually connects to it instead of floating above it.
410
+ Every tab keeps the border geometry (transparent by default) so selecting a tab doesn't shift
411
+ layout; only the color changes on selection. The open edge here has 0 border-width, which
412
+ leaves the list's own baseline border showing straight through underneath — see the indicator
413
+ patch in SELECTION INDICATOR & BASELINE above, which covers just that segment. (A pseudo-element
414
+ on the tab itself can't do this: MUI clips tab content to its own bounds for ripple containment,
415
+ so anything extending past the tab's box to reach the list's border gets clipped away.)
416
+ --------------------------------- */
417
+ .root[data-variant="outline"] .tab {
418
+ border-style: solid;
419
+ border-color: transparent;
420
+ }
421
+ .root[data-variant="outline"][data-orientation="horizontal"]:not(
422
+ [data-inverted]
423
+ )
424
+ .tab {
425
+ border-width: var(--tabs-inactive-border-size)
426
+ var(--tabs-inactive-border-size) 0 var(--tabs-inactive-border-size);
427
+ }
428
+ .root[data-variant="outline"][data-orientation="horizontal"][data-inverted]
429
+ .tab {
430
+ border-width: 0 var(--tabs-inactive-border-size)
431
+ var(--tabs-inactive-border-size) var(--tabs-inactive-border-size);
432
+ }
433
+ .root[data-variant="outline"][data-orientation="vertical"] .tab {
434
+ border-width: var(--tabs-inactive-border-size) 0
435
+ var(--tabs-inactive-border-size) var(--tabs-inactive-border-size);
436
+ }
437
+ .root[data-variant="outline"] .tab:global(.Mui-selected) {
438
+ border-color: var(--tabs-color) !important;
439
+ }
440
+
359
441
  /* ---------------------------------
360
442
  COLORS & TYPOGRAPHY
361
443
  Because colors are defined per-layer and theme via generic variable structures,
@@ -485,9 +567,7 @@
485
567
  }
486
568
 
487
569
  .root[data-variant="outline"] .tab:global(.Mui-selected) {
488
- background-color: var(
489
- --recursica_ui-kit_components_tabs-item_variants_styles_outline_variants_selection-states_active_properties_colors_background-color
490
- ) !important;
570
+ background-color: var(--tabs-active-bg) !important;
491
571
  color: var(
492
572
  --recursica_ui-kit_components_tabs-item_variants_styles_outline_variants_selection-states_active_properties_colors_text-color
493
573
  ) !important;
@@ -635,8 +715,11 @@
635
715
  z-index: 0;
636
716
  }
637
717
 
638
- /* Ensure tab content sits above the hover background */
639
- .root .tab > * {
718
+ /* Ensure tab content sits above the hover background. Excludes MuiTouchRipple-root: it relies on
719
+ its native position:absolute to stay out of flow, and forcing it to position:relative here
720
+ turns it into a real flex item — inserting an extra element-gap into the tab's flex row (and
721
+ growing the tab's width) the first time it's clicked, permanently shifting later siblings. */
722
+ .root .tab > *:not(:global(.MuiTouchRipple-root)) {
640
723
  z-index: 1;
641
724
  position: relative;
642
725
  }
@@ -62,6 +62,7 @@ export const Tab = forwardRef<HTMLDivElement, TabProps>(
62
62
  return (
63
63
  <MuiTab
64
64
  ref={ref}
65
+ disableRipple
65
66
  className={`${styles.tab} ${className || ""}`}
66
67
  {...(sanitizedProps as MuiTabProps)}
67
68
  />
@@ -1,10 +1,3 @@
1
- /* EXEMPTIONS:
2
- - border-size variables are ignored because a uniform 1px border is applied globally to prevent
3
- unexpected layout shift or flickering during focused, disabled, or error state transitions. */
4
- /* recursica-ignore: --recursica_ui-kit_components_textarea_properties_border-size */
5
- /* recursica-ignore: --recursica_ui-kit_components_textarea_variants_states_disabled_properties_border-size */
6
- /* recursica-ignore: --recursica_ui-kit_components_textarea_variants_states_error_properties_border-size */
7
-
8
1
  /* LAYOUT SPACING OVERRIDES:
9
2
  - Sets the --form-control-margin-bottom spacing hook to map component-specific layout tokens.
10
3
  - Also sets the --textarea-control-{max,min}-width hooks consumed inline in TextArea.tsx, since
@@ -127,6 +120,19 @@
127
120
  color: inherit;
128
121
  }
129
122
 
123
+ /* MUI always renders multiline inputs through TextareaAutosize, which sets an inline `height`
124
+ that otherwise wins over the min-height above. Pin the height to the same rows-based formula
125
+ when autosize isn't requested, so it matches the plain (non-autosizing) textarea Mantine renders. */
126
+ .root:not([data-autosize]) .input {
127
+ height: calc(
128
+ var(--recursica_ui-kit_components_textarea_properties_rows) *
129
+ var(--recursica_ui-kit_components_textarea_properties_text_line-height) +
130
+ 2 *
131
+ var(--recursica_ui-kit_components_textarea_properties_vertical-padding) +
132
+ 2px
133
+ ) !important;
134
+ }
135
+
130
136
  /* -------------------------------------
131
137
  STATE CASCADE ARCHITECTURE
132
138
  -------------------------------------- */
@@ -144,8 +150,10 @@
144
150
  var(--recursica_brand_states_focus_color);
145
151
  }
146
152
 
147
- /* Error State Mapping (Propagated strictly down from the wrapper DOM context) */
148
- .root[data-error] .input {
153
+ /* Error State Mapping
154
+ MUI only stamps the global `Mui-error` state class onto the InputBase root slot (not the
155
+ input element itself), so the error trigger is scoped from `.root` down to `.input`. */
156
+ .root:global(.Mui-error) .input {
149
157
  border-color: var(
150
158
  --recursica_ui-kit_components_textarea_variants_states_error_properties_colors_border-color
151
159
  ) !important;
@@ -157,8 +165,9 @@
157
165
  ) !important;
158
166
  }
159
167
 
160
- /* Disabled State Mapping (Propagated strictly down from the wrapper DOM context) */
161
- .root[data-disabled] .input {
168
+ /* Disabled State Mapping
169
+ MUI stamps `Mui-disabled` onto both the root slot and the input element directly. */
170
+ .input:global(.Mui-disabled) {
162
171
  border-color: var(
163
172
  --recursica_ui-kit_components_textarea_variants_states_disabled_properties_colors_border-color
164
173
  ) !important;
@@ -66,6 +66,7 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
66
66
  emptyValueComponent,
67
67
  value,
68
68
  defaultValue,
69
+ autosize,
69
70
  ...rest
70
71
  } = props;
71
72
  const sanitizedProps = filterStylingProps(rest, overStyled);
@@ -76,27 +77,6 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
76
77
  delete restRecord["variant"];
77
78
  delete restRecord["radius"];
78
79
 
79
- // Securely map core native blocks down ensuring nested CSS modules map precisely
80
- const mergedClassNames: Partial<Record<string, string>> = {
81
- wrapper: styles.root, // The nested Input internal relative wrapper bounding box
82
- input: styles.input,
83
- };
84
-
85
- const classNamesProp = restRecord.classNames;
86
- if (
87
- classNamesProp &&
88
- typeof classNamesProp === "object" &&
89
- !Array.isArray(classNamesProp)
90
- ) {
91
- const o = classNamesProp as Partial<Record<string, string>>;
92
- mergedClassNames.wrapper = o.wrapper
93
- ? `${styles.root} ${o.wrapper}`
94
- : styles.root;
95
- mergedClassNames.input = o.input
96
- ? `${styles.input} ${o.input}`
97
- : styles.input;
98
- }
99
-
100
80
  const wrapperClass = className
101
81
  ? `${styles.layoutOverride} ${className}`
102
82
  : styles.layoutOverride;
@@ -130,15 +110,24 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
130
110
  /* Naked Input execution safely decoupled from Mui's macro Input.Wrapper DOM hooks */
131
111
  <MuiTextarea
132
112
  multiline
113
+ variant="standard"
133
114
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
134
115
  inputRef={ref as any}
135
- classes={mergedClassNames}
136
116
  disabled={disabled}
137
117
  value={value}
138
118
  defaultValue={defaultValue}
139
119
  label={undefined}
140
- error={undefined}
120
+ error={!!error}
141
121
  required={undefined}
122
+ slotProps={{
123
+ input: {
124
+ disableUnderline: true,
125
+ classes: { root: styles.root, input: styles.input },
126
+ ...({
127
+ "data-autosize": autosize ? "true" : undefined,
128
+ } as Record<string, unknown>),
129
+ },
130
+ }}
142
131
  {...(sanitizedProps as unknown as MuiTextareaProps)}
143
132
  />
144
133
  }
@@ -1,11 +1,3 @@
1
- /* EXEMPTIONS:
2
- - state-specific border-size variables are ignored because a uniform 1px border is applied globally to prevent
3
- unexpected layout shift or flickering during focus, disabled, or error state transitions. */
4
- /* recursica-ignore: --recursica_ui-kit_components_text-field_variants_states_default_properties_border-size */
5
- /* recursica-ignore: --recursica_ui-kit_components_text-field_variants_states_disabled_properties_border-size */
6
- /* recursica-ignore: --recursica_ui-kit_components_text-field_variants_states_error_properties_border-size */
7
- /* recursica-ignore: --recursica_ui-kit_components_text-field_variants_states_focus_properties_border-size */
8
-
9
1
  /* LAYOUT SPACING OVERRIDES:
10
2
  - Sets the --form-control-margin-bottom spacing hook to map component-specific layout tokens.
11
3
  - Also sets the --text-field-control-{max,min}-width hooks consumed inline in TextField.tsx,
@@ -1,19 +1,3 @@
1
- /* EXEMPTIONS:
2
- - state-specific border-size variables are ignored because a uniform border-size is applied globally
3
- to prevent unexpected layout shift or flickering during focus, disabled, or error state transitions.
4
- - icon-size/icon-color/icon-text-gap have no equivalent here: the time field has no icon slot,
5
- unlike TextField/DatePicker.
6
- - placeholder-opacity has no equivalent here: MUI X's field renders empty sections via its own
7
- internal placeholder styling, not a native ::placeholder pseudo-element we can target. */
8
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_error_properties_border-size */
9
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_disabled_properties_border-size */
10
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_icon-size */
11
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_icon-text-gap */
12
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_colors_icon-color */
13
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_properties_placeholder-opacity */
14
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_disabled_properties_colors_icon-color */
15
- /* recursica-ignore: --recursica_ui-kit_components_time-picker_variants_states_error_properties_colors_icon-color */
16
-
17
1
  /* LAYOUT SPACING OVERRIDES:
18
2
  - Sets the --form-control-margin-bottom spacing hook to map component-specific layout tokens. */
19
3
  .layoutOverride {
@@ -1,5 +1,26 @@
1
1
  # Timeline Implementation Notes
2
2
 
3
- - **`TimelineItem.tsx`'s `classes` prop likely does nothing (needs follow-up):** `TimelineItem.tsx` passes `classes={{ item: styles.item, itemBody: styles.itemBody, itemContent: styles.itemContent, itemBullet: styles.itemBullet, itemTitle: styles.itemTitle }}` to `MuiTimelineItem`. But `@mui/lab`'s `TimelineItem` only defines these class slots (see `node_modules/@mui/lab/TimelineItem/timelineItemClasses.d.ts`): `root`, `positionLeft`, `positionRight`, `positionAlternate`, `positionAlternateReverse`, `missingOppositeContent`. There is no `item`/`itemBody`/`itemContent`/`itemBullet`/`itemTitle` slot — `useUtilityClasses` in `TimelineItem.js` builds its class list from a fixed `slots` object keyed only by those real slot names and passes it through `composeClasses`, which ignores any other keys on the `classes` object we pass in. So `.item`, `.itemBody`, `.itemContent`, `.itemBullet`, and `.itemTitle` in `Timeline.module.css` (lines with those selectors) are not actually attached to any rendered element today — they're dead CSS.
4
- - Discovered 2026-08-06 while wiring up the `--recursica_ui-kit_components_timeline_properties_item-gap` token (see changeset `fix-mui-adapter-broken-tokens`). That specific fix didn't depend on these classes — it uses `gap` on `.root` (a real, correctly-applied slot), which spaces direct `<li>` children regardless of their own class — so it's unaffected by this bug and needed no workaround.
5
- - **Needs a real fix later:** likely means auditing which of `.item`/`.itemBody`/`.itemContent`/`.itemBullet`/`.itemTitle`'s rules are actually load-bearing (vs. now-inert overrides of Mantine's naming that never carried over structurally to MUI), and either (a) finding the correct way to target those parts under MUI's actual DOM (e.g. `:global()` selectors against MUI's own internal class names, similar to the pattern used elsewhere in this adapter for `TableCell`/`TableRow`), or (b) restructuring `TimelineItem.tsx` to apply these as plain `className`s on wrapper elements it already renders internally, since `classes` won't work for slots MUI doesn't define.
3
+ ## Architecture
4
+
5
+ Unlike Mantine, `@mui/lab`'s `TimelineItem` has no built-in bullet/connector — it only
6
+ composes whatever you nest inside it. `TimelineItem.tsx` renders `TimelineSeparator` >
7
+ `TimelineDot` (`.itemBullet`) + `TimelineConnector` (`.itemConnector`), and `TimelineContent`
8
+ (`.itemBody`) containing the `title`/description/`timestamp` nodes, to reproduce Mantine's
9
+ single-component bullet + connecting-line structure.
10
+
11
+ - `Timeline.tsx` computes `active`/`isLast` per item (Mui has no `active` concept at all) and
12
+ injects them into each `Timeline.Item` as internal `__active`/`__isLast` props, mirroring
13
+ Mantine's own `cloneElement` pattern for propagating active state down to children.
14
+ - `data-active` / `data-variant` are set directly on `TimelineItem`'s real `li` root and
15
+ consumed via plain descendant selectors (`.item[data-active] .itemBullet`, etc.) in
16
+ `Timeline.module.css` — Mui's `TimelineItem` only defines a `root` classes slot, so (unlike
17
+ Mantine) passing a `classes` object with `item`/`itemBullet`/`itemBody`/... keys is a no-op;
18
+ those class names must be applied as plain `className`s on the elements we render ourselves.
19
+ - The last item omits `TimelineConnector` entirely so no trailing line renders past it.
20
+ - `Timeline.module.css` neutralizes Mui's `TimelineItem::before` opposite-content spacer
21
+ (`flex: 0; padding: 0`), which otherwise pushes the separator/content right since this
22
+ adapter never renders `TimelineOppositeContent`.
23
+
24
+ ## Limitations & Missing Tokens
25
+
26
+ - **Avatar Bullet Size**: There is no specific pixel variable provided for the Avatar bullet size in the UI kit tokens (`avatar-size` evaluates to `"default"`). To maintain exact mathematical centering with the connector line, the CSS falls back to inheriting the `default` bullet size (`20px`) for avatar nodes natively. If users supply a custom sized `img` tag, it must adhere to inline structural constraints or flex mappings.
@@ -1,81 +1,58 @@
1
- /* EXEMPTIONS:
2
- - timeline-bullet_variants_types_avatar_properties_avatar-size is ignored because under the avatar timeline variant,
3
- the bullet container size uses fallback bullet-size configurations to maintain vertical alignment with the
4
- inactive/active connector line, while the nested Avatar subcomponent handles its own sizing natively. */
5
- /* recursica-ignore: --recursica_ui-kit_components_timeline-bullet_variants_types_avatar_properties_avatar-size */
6
-
7
1
  .root {
8
2
  display: flex;
9
3
  flex-direction: column;
10
4
  gap: var(--recursica_ui-kit_components_timeline_properties_item-gap);
5
+ margin: 0 !important;
6
+ padding: 0 !important;
11
7
  }
12
8
 
13
9
  .item {
14
- /* Mantine uses these vars to calculate the exact left: position of the connector line */
15
- --tl-line-width: var(
16
- --recursica_ui-kit_components_timeline_variants_selection-states_inactive_properties_connector-size
17
- ) !important;
18
-
19
- &[data-active] {
20
- --tl-line-width: var(
21
- --recursica_ui-kit_components_timeline_variants_selection-states_active_properties_connector-size
22
- ) !important;
23
- }
24
-
25
- &[data-variant="default"] {
26
- --tl-bullet-size: var(
27
- --recursica_ui-kit_components_timeline-bullet_variants_types_default_properties_bullet-size
28
- ) !important;
29
- }
30
- &[data-variant="icon"] {
31
- --tl-bullet-size: var(
32
- --recursica_ui-kit_components_timeline-bullet_variants_types_icon_properties_bullet-size
33
- ) !important;
34
- }
35
- &[data-variant="icon-alternative"] {
36
- --tl-bullet-size: var(
37
- --recursica_ui-kit_components_timeline-bullet_variants_types_icon-alternative_properties_bullet-size
38
- ) !important;
39
- }
40
- &[data-variant="avatar"] {
41
- /* Avatar sizing fallback to default to maintain connector alignment */
42
- --tl-bullet-size: var(
43
- --recursica_ui-kit_components_timeline-bullet_variants_types_default_properties_bullet-size
44
- ) !important;
45
- }
46
-
47
- /* Control connector lines via the item ::before pseudo-element */
48
- /* Inactive connector */
49
- &::before {
50
- border-left-width: var(
51
- --recursica_ui-kit_components_timeline_variants_selection-states_inactive_properties_connector-size
52
- ) !important;
53
- border-left-color: var(
54
- --recursica_ui-kit_components_timeline_variants_selection-states_inactive_properties_colors_connector-color
55
- ) !important;
56
- }
10
+ /* Mui gives every item a 70px min-height by default; content should drive height instead */
11
+ min-height: 0 !important;
12
+ }
57
13
 
58
- /* Active connector */
59
- &[data-active]::before {
60
- border-left-width: var(
61
- --recursica_ui-kit_components_timeline_variants_selection-states_active_properties_connector-size
62
- ) !important;
63
- border-left-color: var(
64
- --recursica_ui-kit_components_timeline_variants_selection-states_active_properties_colors_connector-color
65
- ) !important;
66
- }
14
+ /* Mui reserves a flex:1 spacer (via ::before) for the opposite-content column even when none is
15
+ rendered. Recursica's Timeline never uses TimelineOppositeContent, so neutralize it to keep the
16
+ bullet/connector column flush against the left edge. */
17
+ :global(.MuiTimelineItem-root)::before {
18
+ content: none !important;
19
+ flex: 0 !important;
20
+ padding: 0 !important;
67
21
  }
68
22
 
69
23
  .itemBody {
70
- /* Space between the bullet and the content */
71
- padding-left: var(
72
- --recursica_ui-kit_components_timeline_properties_bullet-content-gap
24
+ padding: 0 0 0
25
+ var(--recursica_ui-kit_components_timeline_properties_bullet-content-gap) !important;
26
+ }
27
+
28
+ .itemConnector {
29
+ width: var(
30
+ --recursica_ui-kit_components_timeline_variants_selection-states_inactive_properties_connector-size
31
+ ) !important;
32
+ background-color: var(
33
+ --recursica_ui-kit_components_timeline_variants_selection-states_inactive_properties_colors_connector-color
34
+ ) !important;
35
+ }
36
+ .item[data-active] .itemConnector {
37
+ width: var(
38
+ --recursica_ui-kit_components_timeline_variants_selection-states_active_properties_connector-size
39
+ ) !important;
40
+ background-color: var(
41
+ --recursica_ui-kit_components_timeline_variants_selection-states_active_properties_colors_connector-color
73
42
  ) !important;
74
43
  }
75
44
 
76
45
  .itemBullet {
46
+ display: flex !important;
47
+ align-items: center;
48
+ justify-content: center;
49
+ align-self: center !important;
50
+ margin: 0 !important;
51
+ padding: 0 !important;
52
+ box-shadow: none !important;
53
+
77
54
  /* Default Variant */
78
- :global(.mantine-Timeline-item)[data-variant="default"] & {
55
+ .item[data-variant="default"] & {
79
56
  width: var(
80
57
  --recursica_ui-kit_components_timeline-bullet_variants_types_default_properties_bullet-size
81
58
  ) !important;
@@ -96,7 +73,7 @@
96
73
  --recursica_ui-kit_components_timeline-bullet_variants_types_default_variants_selection-states_inactive_properties_colors_border-color
97
74
  ) !important;
98
75
  }
99
- :global(.mantine-Timeline-item)[data-variant="default"][data-active] & {
76
+ .item[data-variant="default"][data-active] & {
100
77
  background-color: var(
101
78
  --recursica_ui-kit_components_timeline-bullet_variants_types_default_variants_selection-states_active_properties_colors_background-color
102
79
  ) !important;
@@ -106,7 +83,7 @@
106
83
  }
107
84
 
108
85
  /* Icon Variant */
109
- :global(.mantine-Timeline-item)[data-variant="icon"] & {
86
+ .item[data-variant="icon"] & {
110
87
  width: var(
111
88
  --recursica_ui-kit_components_timeline-bullet_variants_types_icon_properties_bullet-size
112
89
  ) !important;
@@ -143,7 +120,7 @@
143
120
  );
144
121
  }
145
122
  }
146
- :global(.mantine-Timeline-item)[data-variant="icon"][data-active] & {
123
+ .item[data-variant="icon"][data-active] & {
147
124
  background-color: var(
148
125
  --recursica_ui-kit_components_timeline-bullet_variants_types_icon_variants_selection-states_active_properties_colors_background-color
149
126
  ) !important;
@@ -156,7 +133,7 @@
156
133
  }
157
134
 
158
135
  /* Icon Alternative Variant */
159
- :global(.mantine-Timeline-item)[data-variant="icon-alternative"] & {
136
+ .item[data-variant="icon-alternative"] & {
160
137
  width: var(
161
138
  --recursica_ui-kit_components_timeline-bullet_variants_types_icon-alternative_properties_bullet-size
162
139
  ) !important;
@@ -192,8 +169,7 @@
192
169
  );
193
170
  }
194
171
  }
195
- :global(.mantine-Timeline-item)[data-variant="icon-alternative"][data-active]
196
- & {
172
+ .item[data-variant="icon-alternative"][data-active] & {
197
173
  background-color: var(
198
174
  --recursica_ui-kit_components_timeline-bullet_variants_types_icon-alternative_variants_selection-states_active_properties_colors_background-color
199
175
  ) !important;
@@ -206,8 +182,14 @@
206
182
  }
207
183
 
208
184
  /* Avatar Variant */
209
- :global(.mantine-Timeline-item)[data-variant="avatar"] & {
185
+ .item[data-variant="avatar"] & {
210
186
  /* Avatar sizing is often governed by the avatar itself, but we'll apply the radius and opacity */
187
+ width: var(
188
+ --recursica_ui-kit_components_timeline-bullet_variants_types_default_properties_bullet-size
189
+ ) !important;
190
+ height: var(
191
+ --recursica_ui-kit_components_timeline-bullet_variants_types_default_properties_bullet-size
192
+ ) !important;
211
193
  border-radius: var(
212
194
  --recursica_ui-kit_components_timeline-bullet_variants_types_avatar_properties_border-radius
213
195
  ) !important;
@@ -225,7 +207,7 @@
225
207
  --recursica_ui-kit_components_timeline-bullet_variants_types_avatar_variants_selection-states_inactive_properties_colors_border-color
226
208
  ) !important;
227
209
  }
228
- :global(.mantine-Timeline-item)[data-variant="avatar"][data-active] & {
210
+ .item[data-variant="avatar"][data-active] & {
229
211
  opacity: var(
230
212
  --recursica_ui-kit_components_timeline-bullet_variants_types_avatar_variants_selection-states_active_properties_avatar-opacity
231
213
  ) !important;
@@ -275,7 +257,7 @@
275
257
  ) !important;
276
258
 
277
259
  /* Active Title Color */
278
- :global(.mantine-Timeline-item)[data-active] & {
260
+ .item[data-active] & {
279
261
  color: var(
280
262
  --recursica_ui-kit_components_timeline_variants_selection-states_active_properties_colors_title-color
281
263
  ) !important;
@@ -323,7 +305,7 @@
323
305
  --recursica_ui-kit_components_timeline_variants_selection-states_inactive_properties_colors_description-color
324
306
  );
325
307
 
326
- :global(.mantine-Timeline-item)[data-active] & {
308
+ .item[data-active] & {
327
309
  color: var(
328
310
  --recursica_ui-kit_components_timeline_variants_selection-states_active_properties_colors_description-color
329
311
  );
@@ -360,7 +342,7 @@
360
342
  --recursica_ui-kit_components_timeline_variants_selection-states_inactive_properties_colors_timestamp-color
361
343
  );
362
344
 
363
- :global(.mantine-Timeline-item)[data-active] & {
345
+ .item[data-active] & {
364
346
  color: var(
365
347
  --recursica_ui-kit_components_timeline_variants_selection-states_active_properties_colors_timestamp-color
366
348
  );