@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
@@ -7,7 +7,7 @@ import {
7
7
  type RecursicaOverStyled,
8
8
  } from "../../utils/filterStylingProps";
9
9
  import styles from "./Timeline.module.css";
10
- import { TimelineItem } from "./TimelineItem";
10
+ import { TimelineItem, type TimelineItemProps } from "./TimelineItem";
11
11
 
12
12
  import { type RecursicaTimelineProps } from "@recursica/adapter-common";
13
13
 
@@ -23,7 +23,7 @@ export type TimelineProps = RecursicaOverStyled<
23
23
 
24
24
  interface TimelineComponent
25
25
  extends React.ForwardRefExoticComponent<
26
- TimelineProps & React.RefAttributes<HTMLDivElement>
26
+ TimelineProps & React.RefAttributes<HTMLUListElement>
27
27
  > {
28
28
  Item: typeof TimelineItem;
29
29
  }
@@ -32,8 +32,10 @@ interface TimelineComponent
32
32
  * The `Timeline` component displays a list of events in chronological order.
33
33
  *
34
34
  * **Recursica Abstract:**
35
- * This component acts as a structural wrapper around Mui's `<Timeline>`.
36
- * It forces alignment and geometry strictly via the UI Kit's `.itemBullet` and connector definitions.
35
+ * This component acts as a structural wrapper around Mui's `<Timeline>`, composing
36
+ * `TimelineSeparator`/`TimelineDot`/`TimelineConnector`/`TimelineContent` per item
37
+ * (via `Timeline.Item`) so the bullet marker and connecting line render the same way
38
+ * they do in the Mantine adapter.
37
39
  *
38
40
  * @example
39
41
  * ```tsx
@@ -43,29 +45,21 @@ interface TimelineComponent
43
45
  * ```
44
46
  */
45
47
  const TimelineInner = React.forwardRef<HTMLUListElement, TimelineProps>(
46
- function Timeline({ overStyled = false, ...rest }, ref) {
48
+ function Timeline(
49
+ { overStyled = false, active = -1, children, ...rest },
50
+ ref,
51
+ ) {
47
52
  const sanitizedProps = filterStylingProps(rest, overStyled);
48
53
 
49
- const mergedClassNames: Partial<Record<string, string>> = {
50
- root: styles.root,
51
- };
52
-
53
- const classNamesProp = (sanitizedProps as Record<string, unknown>)
54
- .classNames;
55
- if (
56
- classNamesProp &&
57
- typeof classNamesProp === "object" &&
58
- !Array.isArray(classNamesProp)
59
- ) {
60
- const o = classNamesProp as Record<string, string>;
61
- Object.keys(o).forEach((key) => {
62
- if (mergedClassNames[key]) {
63
- mergedClassNames[key] = `${mergedClassNames[key]} ${o[key]}`;
64
- } else {
65
- mergedClassNames[key] = o[key];
66
- }
67
- });
68
- }
54
+ const items = React.Children.toArray(children);
55
+ const decoratedItems = items.map((item, index) =>
56
+ React.isValidElement(item)
57
+ ? React.cloneElement(item as React.ReactElement<TimelineItemProps>, {
58
+ __active: active >= index,
59
+ __isLast: index === items.length - 1,
60
+ })
61
+ : item,
62
+ );
69
63
 
70
64
  return (
71
65
  <MuiTimeline
@@ -74,8 +68,10 @@ const TimelineInner = React.forwardRef<HTMLUListElement, TimelineProps>(
74
68
  MuiTimelineProps,
75
69
  "color" | "radius" | "bulletSize" | "lineWidth"
76
70
  >)}
77
- classes={mergedClassNames}
78
- />
71
+ classes={{ root: styles.root }}
72
+ >
73
+ {decoratedItems}
74
+ </MuiTimeline>
79
75
  );
80
76
  },
81
77
  );
@@ -2,6 +2,10 @@ import React from "react";
2
2
  import MuiTimelineItem, {
3
3
  type TimelineItemProps as MuiTimelineItemProps,
4
4
  } from "@mui/lab/TimelineItem";
5
+ import TimelineSeparator from "@mui/lab/TimelineSeparator";
6
+ import TimelineDot from "@mui/lab/TimelineDot";
7
+ import TimelineConnector from "@mui/lab/TimelineConnector";
8
+ import TimelineContent from "@mui/lab/TimelineContent";
5
9
  import {
6
10
  filterStylingProps,
7
11
  type RecursicaOverStyled,
@@ -11,8 +15,15 @@ import styles from "./Timeline.module.css";
11
15
  import { type RecursicaTimelineItemProps } from "@recursica/adapter-common";
12
16
 
13
17
  export type TimelineItemProps = RecursicaOverStyled<
14
- Omit<MuiTimelineItemProps, "radius" | "color" | "lineVariant"> &
15
- RecursicaTimelineItemProps
18
+ Omit<MuiTimelineItemProps, "radius" | "color" | "lineVariant" | "title"> &
19
+ RecursicaTimelineItemProps & {
20
+ /** Heading rendered above the item's description/timestamp */
21
+ title?: React.ReactNode;
22
+ /** @internal injected by `Timeline` to mark items up to `active` */
23
+ __active?: boolean;
24
+ /** @internal injected by `Timeline`; suppresses the trailing connector on the last item */
25
+ __isLast?: boolean;
26
+ }
16
27
  >;
17
28
 
18
29
  /**
@@ -22,48 +33,30 @@ export type TimelineItemProps = RecursicaOverStyled<
22
33
  * The `Timeline.Item` has been extended to support a `timestamp` string natively,
23
34
  * rendering it below the body content. It also accepts a `bulletVariant` to morph
24
35
  * the structural dimensions of the node circle automatically.
36
+ *
37
+ * Internally it composes Mui's `TimelineSeparator`/`TimelineDot`/`TimelineConnector`/
38
+ * `TimelineContent` primitives (Mui has no single "item" abstraction like Mantine's
39
+ * `Timeline.Item` that renders a bullet + connector on its own).
25
40
  */
26
- export const TimelineItem = React.forwardRef<HTMLDivElement, TimelineItemProps>(
41
+ export const TimelineItem = React.forwardRef<HTMLLIElement, TimelineItemProps>(
27
42
  function TimelineItem(
28
43
  {
29
44
  overStyled = false,
30
45
  timestamp,
31
46
  bulletVariant = "default",
32
47
  bullet,
48
+ title,
33
49
  children,
50
+ __active = false,
51
+ __isLast = false,
34
52
  ...rest
35
53
  },
36
54
  ref,
37
55
  ) {
38
- void bullet;
39
56
  const sanitizedProps = filterStylingProps(rest, overStyled);
57
+ const { className: userClassName, ...restSanitizedProps } =
58
+ sanitizedProps as Record<string, unknown> & { className?: string };
40
59
 
41
- const mergedClassNames: Partial<Record<string, string>> = {
42
- item: styles.item,
43
- itemBody: styles.itemBody,
44
- itemContent: styles.itemContent,
45
- itemBullet: styles.itemBullet,
46
- itemTitle: styles.itemTitle,
47
- };
48
-
49
- const classNamesProp = (sanitizedProps as Record<string, unknown>)
50
- .classNames;
51
- if (
52
- classNamesProp &&
53
- typeof classNamesProp === "object" &&
54
- !Array.isArray(classNamesProp)
55
- ) {
56
- const o = classNamesProp as Record<string, string>;
57
- Object.keys(o).forEach((key) => {
58
- if (mergedClassNames[key]) {
59
- mergedClassNames[key] = `${mergedClassNames[key]} ${o[key]}`;
60
- } else {
61
- mergedClassNames[key] = o[key];
62
- }
63
- });
64
- }
65
-
66
- // Embed timestamp inside children if provided, wrapped in a specific class
67
60
  const content = timestamp ? (
68
61
  <>
69
62
  {children && <div className={styles.description}>{children}</div>}
@@ -76,14 +69,24 @@ export const TimelineItem = React.forwardRef<HTMLDivElement, TimelineItemProps>(
76
69
  return (
77
70
  <MuiTimelineItem
78
71
  ref={ref}
79
- {...(sanitizedProps as unknown as Omit<
72
+ data-variant={bulletVariant}
73
+ data-active={__active || undefined}
74
+ {...(restSanitizedProps as unknown as Omit<
80
75
  MuiTimelineItemProps,
81
- "radius" | "color" | "lineVariant"
76
+ "radius" | "color" | "lineVariant" | "title"
82
77
  >)}
83
- classes={mergedClassNames}
84
- data-variant={bulletVariant}
78
+ className={
79
+ userClassName ? `${styles.item} ${userClassName}` : styles.item
80
+ }
85
81
  >
86
- {content}
82
+ <TimelineSeparator>
83
+ <TimelineDot className={styles.itemBullet}>{bullet}</TimelineDot>
84
+ {!__isLast && <TimelineConnector className={styles.itemConnector} />}
85
+ </TimelineSeparator>
86
+ <TimelineContent className={styles.itemBody}>
87
+ {title && <div className={styles.itemTitle}>{title}</div>}
88
+ <div className={styles.itemContent}>{content}</div>
89
+ </TimelineContent>
87
90
  </MuiTimelineItem>
88
91
  );
89
92
  },
@@ -1,10 +1,3 @@
1
- /* EXEMPTIONS:
2
- - elevation levels layer-1, layer-2, and layer-3 are ignored because all toasts explicitly use the standard
3
- layer-0 elevation shadow for styling consistency. */
4
- /* recursica-ignore: --recursica_ui-kit_components_toast_properties_elevation_layer-1 */
5
- /* recursica-ignore: --recursica_ui-kit_components_toast_properties_elevation_layer-2 */
6
- /* recursica-ignore: --recursica_ui-kit_components_toast_properties_elevation_layer-3 */
7
-
8
1
  /* HARDCODED VALUES:
9
2
  - border-style: solid. Baseline so the per-variant border-color tokens actually render.
10
3
  */
@@ -1,12 +1,3 @@
1
- /* EXEMPTIONS:
2
- - beak-inset is ignored because Tooltip positioning calculations are handled dynamically by Mantine/Floating UI at the React runtime.
3
- - beak-size is ignored because Mantine's tooltip arrows compute their sizing and positional offsets
4
- dynamically within the React rendering engine (requires inline pixel number rather than standard CSS variables).
5
- The beak size is checked in JS against a fixed value of 16px. If this variable changes, the TSX components
6
- need to be reviewed/re-compiled. */
7
- /* recursica-ignore: --recursica_ui-kit_components_tooltip_properties_beak-inset = 8px */
8
- /* recursica-ignore: --recursica_ui-kit_components_tooltip_properties_beak-size = 16px */
9
-
10
1
  /* HARDCODED VALUES:
11
2
  - border-style: solid. Structural rendering rule for the tooltip border (Mantine uses Box
12
3
  which may not set border-style natively). Same pattern as Menu / HoverCard.
@@ -0,0 +1,140 @@
1
+ # TransferList Implementation Notes
2
+
3
+ ## Architecture overview
4
+
5
+ `TransferList` replaces the "coming soon" stub with a fully custom composite — neither
6
+ `@mantine/core` nor `@mui/material` ships a real dual-listbox component (MUI's docs "Transfer
7
+ List" is a List+Checkbox recipe, not a package export), so it's built from Recursica's own
8
+ primitives, the same situation `FileUpload`/`FileInput` were in.
9
+
10
+ Follows Matt's reference implementation (Forge's fallback `TransferList.tsx`) fairly closely:
11
+
12
+ - Two side-by-side panes (source/target), each: a header row (pane label + `Badge` count,
13
+ `"selected / total"` once something's checked), a search `TextField`, then a scrollable list of
14
+ `Checkbox` rows (`CheckboxGroup` for items carrying a `group` field).
15
+ - A column of four transfer `Button`s between the panes: single/double chevron, each direction —
16
+ single moves only the checked items in that pane, double moves everything in that pane.
17
+ - Data model: `data`/`defaultData` as a `[sourceItems, targetItems]` tuple + `onChange`, matching
18
+ Forge's controlled/uncontrolled pattern (same shape as `FileUpload`'s `files`/`onFilesAdded`).
19
+
20
+ ## Routed through `FormControlWrapper` directly, not Forge's hand-rolled label/assistive
21
+
22
+ Forge's reference renders its own `Label`/`AssistiveElement` directly instead of going through
23
+ `FormControlWrapper`. Per direction, this build uses `FormControlWrapper` instead — consistent
24
+ with every other form control in this codebase (and with `FileUpload`, which was reverted to
25
+ `FormControlWrapper` rather than hand-rolling that same thing). `formLayout`
26
+ (`stacked`/`side-by-side`), `label`, `required`, `assistiveText`/`description`/`helperText`, and
27
+ `error` all come from the standard wrapper; only the two panes + transfer buttons are the
28
+ component's own content.
29
+
30
+ ## `state` axis: `disabled`/`error` props, not Forge's `state` string
31
+
32
+ Forge's reference takes a single `state="default"|"disabled"|"error"` prop. This build instead
33
+ uses the same convention as every other Recursica control: a boolean `disabled` prop plus
34
+ `FormControlWrapper`'s own `error` (message) prop — `error`'s mere presence is what triggers the
35
+ error visuals, exactly like `TextArea`/`CheckboxGroup`. The token export only defines
36
+ `disabled`/`error` state variants (no `focus`), matching Matt's direction to "follow the tokens."
37
+
38
+ ## `readOnly` mode (added 2026-08-19)
39
+
40
+ Forge's reference has no read-only concept, but Matt asked for a `ReadOnly` story demonstrating
41
+ the selected items as a read-only list. Switched from calling `FormControlWrapper` directly to
42
+ routing through `WithReadOnlyWrapper` (the follow-up flagged in the original version of this note),
43
+ matching `TextArea`'s shape: `readOnly`/`readOnlyComponent`/`emptyValueComponent` added to the prop
44
+ surface via `ReadOnlyControlProps`, `readOnlyType="text"`, and `readOnlyValue` set to the target
45
+ pane's item labels (`effectiveData[1].map(item => item.label)`). Renders as a comma-joined text
46
+ list via the shared `ReadOnlyTextField`, same convention `CheckboxGroup`'s own read-only mode
47
+ already uses for an array of values — no new read-only renderer needed. The two panes + transfer
48
+ buttons (`activeComponent`) are skipped entirely when `readOnly` is set, same as every other
49
+ control.
50
+
51
+ ## Height/gap/padding token audit (2026-08-19)
52
+
53
+ Matt flagged the pane height as "too short" and asked that gap/padding be tied to recursica
54
+ variables. Re-verified against `recursica_variables_scoped.css`: `properties_height` (200px),
55
+ `properties_width`, `properties_vertical-padding`/`properties_horizontal-padding`, and
56
+ `properties_gap` were already wired to their tokens (see "Token interpretation" above) — the 200px
57
+ pane height is the design token's own value, not a hardcoded fallback. Two spacing values genuinely
58
+ have no covering token in the 31-variable schema and are intentionally left as hardcoded pixels
59
+ (see `TransferList.module.css` comments) rather than reusing another component's token: the gap
60
+ between stacked `CheckboxGroup` blocks in `.paneList` (ungrouped row + each named group), and the
61
+ gap between the four transfer buttons in `.transferColumn`. If a token is added to the schema for
62
+ either, wire it in directly; until then this matches the established precedent (e.g. `FileUpload`'s
63
+ untokened icon size) of leaving a truly uncovered value hardcoded with a documented reason instead
64
+ of borrowing a sibling component's namespace.
65
+
66
+ ## Fixed: checkboxes inside `CheckboxGroup` ignored their own `checked`/`onChange` (2026-08-19)
67
+
68
+ Matt's discrepancy report against the mantine source of truth: clicking an ungrouped checkbox in
69
+ the `Grouped` story showed no checkmark and never toggled selection, though the pane's `Badge`
70
+ count also never changed — meaning it silently did nothing, not "count going up" as literally
71
+ worded, confirmed live via Playwright against both Storybooks (`isChecked()` stayed `false`, badge
72
+ text stayed `5` across repeated clicks in mui; mantine correctly showed `1 / 5`, `2 / 5`).
73
+
74
+ Root cause was in `Checkbox.tsx`, not `TransferList.tsx`: `TransferList` wraps its ungrouped items
75
+ in a bare `<CheckboxGroup>` purely so they pick up the `item-gap` layout token (see the
76
+ `RECURSICA_COMPONENTS`/gap fix from 2026-08-18), without ever passing that group a `value`/
77
+ `onChange`. But `Checkbox.tsx` derived `isGrouped` from `groupContext !== null` alone — any
78
+ `CheckboxGroup` ancestor forced grouped-array semantics (`isChecked = groupContext.value.includes(...)`,
79
+ `handleChange` dispatching to `groupContext.onChange` instead of the checkbox's own `onChange`),
80
+ even though the group had no `value` array and no `onChange` to dispatch to. `TransferList`'s own
81
+ `checked={selected.has(item.value)}`/`onChange={() => onToggle(item.value)}` props were present but
82
+ silently overridden/ignored. Mantine's `Checkbox.tsx` never had this bug — it doesn't consult any
83
+ group context at all and always passes `checked`/`onChange` straight through to
84
+ `@mantine/core`'s `Checkbox`.
85
+
86
+ Fixed by only treating a `Checkbox` as group-controlled when it _doesn't_ already have its own
87
+ explicit `checked` prop: `isGrouped = groupContext !== null && restRecord.checked === undefined`.
88
+ A checkbox with its own `checked` (like `TransferList`'s rows) now always owns its source of truth;
89
+ only checkboxes relying on `Checkbox.Group`'s array-tracking (no individual `checked`, matched by
90
+ `value` — `CheckboxGroup.stories.tsx`'s usage) still defer to the group. Audited every
91
+ `CheckboxGroup`/`Checkbox` pairing in mui-adapter (`CheckboxGroup.stories.tsx` and `TransferList`) —
92
+ no existing usage combines both an individual `checked` and group-array `value`, so this is a
93
+ straight bug fix with no regression risk.
94
+
95
+ ## Token interpretation: unlabeled tokens with more than one plausible layout target
96
+
97
+ The 31-variable schema (`properties_*` + two `layouts.*` + `states.{disabled,error}`) has a few
98
+ names that don't pin down a single location by themselves:
99
+
100
+ - **`properties_gap`** — applied to the row holding `[source pane, transfer column, target pane]`.
101
+ It's the only _unnested_ gap the schema defines at the component's own level (not under
102
+ `header-style`, a state, or a layout), so it reads as the component's top-level layout gap.
103
+ - **`properties_title-filter-gap`** — between each pane's header row (label + count `Badge`) and
104
+ its search field.
105
+ - **`properties_filter-items-gap`** — between each pane's search field and its item list.
106
+ - **`properties_header-style_*` / `properties_colors_header-color`** — typography/color for each
107
+ pane's own header text, not the overall `FormControlWrapper` label (which has its own type
108
+ styling already).
109
+ - **`properties_width` (300px) / `properties_height` (200px)** — taken as each pane's own fixed
110
+ box size (`flex: 1 1 <width>`, so panes still stretch evenly in a wider container; `height` is
111
+ literal since a dual-listbox needs a bounded, independently-scrollable list).
112
+
113
+ No exemptions needed beyond `border-size` (see below) — all 31 variables are referenced; verified
114
+ zero broken/unused via `recursica-token-analyzer`.
115
+
116
+ ## Border-size intentionally unused
117
+
118
+ Same house policy `TextField`/`TextArea`/`FileInput` already follow: `border-size` (base,
119
+ `disabled`, `error`) is `recursica-ignore`d and a flat 1px border is applied uniformly instead, so
120
+ switching states doesn't shift layout.
121
+
122
+ ## No forge-defined focus state
123
+
124
+ `transfer-list` has no `states.focus` axis in the export — same as `FileInput`/`TextField`. Focus
125
+ is handled per-control (each `Checkbox`/`TextField`/`Button` already has its own focus ring); the
126
+ pane box itself has no focus state to render.
127
+
128
+ ## Grouping and search are local, not per-item persisted state
129
+
130
+ Search filters each pane independently (`sourceSearch`/`targetSearch`); grouped items render under
131
+ a `CheckboxGroup` keyed by `item.group`, ungrouped items render as plain `Checkbox` rows above the
132
+ groups (alphabetized by group name) — same two-bucket split Forge's reference uses. Selection
133
+ (`sourceSelected`/`targetSelected`) is cleared on every transfer, matching Forge's behavior.
134
+
135
+ ## Chevron icons are inline, not a shared icon set entry
136
+
137
+ Same precedent as `Tree`'s `ExpandGlyph`/`FileInput`'s `ClearIcon`: a single-chevron and a
138
+ double-chevron SVG are defined locally and flipped via `transform: scaleX(-1)` for the "left"
139
+ direction, rather than maintaining four separate paths or adding new shared icon-set entries for a
140
+ single component.
@@ -1,38 +1,174 @@
1
- /* EXEMPTIONS:
2
- This component is a placeholder stub. Once fully implemented, its specific
3
- tokens will be fully wired up to direct styles. */
4
-
5
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_border-radius */
6
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_filter-items-gap */
7
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_gap */
8
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_header-style_fontFamily */
9
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_header-style_fontSize */
10
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_header-style_fontStyle */
11
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_header-style_fontWeight */
12
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_header-style_letterSpacing */
13
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_header-style_lineHeight */
14
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_header-style_textCase */
15
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_header-style_textDecoration */
16
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_height */
17
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_horizontal-padding */
18
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_title-filter-gap */
19
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_vertical-padding */
20
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_width */
21
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_layouts_side-by-side_properties_top-bottom-margin */
22
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_layouts_stacked_properties_top-bottom-margin */
23
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_default_properties_border-size */
24
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_default_properties_colors_background */
25
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_default_properties_colors_border-color */
26
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_default_properties_colors_header-color */
27
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_disabled_properties_border-size */
28
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_disabled_properties_colors_background */
29
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_disabled_properties_colors_border-color */
30
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_disabled_properties_colors_header-color */
31
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_error_properties_border-size */
32
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_error_properties_colors_background */
33
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_error_properties_colors_border-color */
34
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_error_properties_colors_header-color */
35
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_focus_properties_border-size */
36
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_focus_properties_colors_background */
37
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_focus_properties_colors_border-color */
38
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_focus_properties_colors_header-color */
1
+ /* TOKEN INTERPRETATION NOTES (see TRANSFERLIST_IMPLEMENTATION_NOTES.md):
2
+ - `properties_gap` is applied to the row that holds [source pane, transfer buttons, target pane] —
3
+ it's the only unnested gap the schema defines at the component's own level.
4
+ - `properties_title-filter-gap` sits between each pane's header row and its search field.
5
+ - `properties_filter-items-gap` sits between each pane's search field and its item list. */
6
+
7
+ .layoutOverride {
8
+ --form-control-margin-bottom: var(
9
+ --recursica_ui-kit_components_transfer-list_variants_layouts_stacked_properties_top-bottom-margin
10
+ );
11
+ }
12
+
13
+ .layoutOverride[data-form-layout="side-by-side"] {
14
+ --form-control-margin-bottom: var(
15
+ --recursica_ui-kit_components_transfer-list_variants_layouts_side-by-side_properties_top-bottom-margin
16
+ );
17
+ }
18
+
19
+ .root {
20
+ display: flex;
21
+ width: 100%;
22
+ }
23
+
24
+ .panes {
25
+ display: flex;
26
+ align-items: stretch;
27
+ gap: var(--recursica_ui-kit_components_transfer-list_properties_gap);
28
+ width: 100%;
29
+ }
30
+
31
+ .pane {
32
+ display: flex;
33
+ flex-direction: column;
34
+ box-sizing: border-box;
35
+ min-width: 0;
36
+ flex: 1 1 var(--recursica_ui-kit_components_transfer-list_properties_width);
37
+ height: var(--recursica_ui-kit_components_transfer-list_properties_height);
38
+ padding: var(
39
+ --recursica_ui-kit_components_transfer-list_properties_vertical-padding
40
+ )
41
+ var(
42
+ --recursica_ui-kit_components_transfer-list_properties_horizontal-padding
43
+ );
44
+ border-width: 1px;
45
+ border-style: solid;
46
+ border-radius: var(
47
+ --recursica_ui-kit_components_transfer-list_properties_border-radius
48
+ );
49
+ border-color: var(
50
+ --recursica_ui-kit_components_transfer-list_properties_colors_border-color
51
+ );
52
+ background-color: var(
53
+ --recursica_ui-kit_components_transfer-list_properties_colors_background-color
54
+ );
55
+ }
56
+
57
+ .paneHeader {
58
+ display: flex;
59
+ align-items: center;
60
+ justify-content: space-between;
61
+ margin-bottom: var(
62
+ --recursica_ui-kit_components_transfer-list_properties_title-filter-gap
63
+ );
64
+ font-family: var(
65
+ --recursica_ui-kit_components_transfer-list_properties_header-style_fontFamily
66
+ );
67
+ font-size: var(
68
+ --recursica_ui-kit_components_transfer-list_properties_header-style_fontSize
69
+ );
70
+ font-style: var(
71
+ --recursica_ui-kit_components_transfer-list_properties_header-style_fontStyle
72
+ );
73
+ font-weight: var(
74
+ --recursica_ui-kit_components_transfer-list_properties_header-style_fontWeight
75
+ );
76
+ letter-spacing: var(
77
+ --recursica_ui-kit_components_transfer-list_properties_header-style_letterSpacing
78
+ );
79
+ line-height: var(
80
+ --recursica_ui-kit_components_transfer-list_properties_header-style_lineHeight
81
+ );
82
+ text-decoration: var(
83
+ --recursica_ui-kit_components_transfer-list_properties_header-style_textDecoration
84
+ );
85
+ text-transform: var(
86
+ --recursica_ui-kit_components_transfer-list_properties_header-style_textCase
87
+ );
88
+ color: var(
89
+ --recursica_ui-kit_components_transfer-list_properties_colors_header-color
90
+ );
91
+ }
92
+
93
+ .paneSearch {
94
+ margin-bottom: var(
95
+ --recursica_ui-kit_components_transfer-list_properties_filter-items-gap
96
+ );
97
+ }
98
+
99
+ .paneList {
100
+ flex: 1;
101
+ overflow-y: auto;
102
+ display: flex;
103
+ flex-direction: column;
104
+ /* No token exists for the gap between stacked CheckboxGroup blocks (ungrouped row +
105
+ each named group) — the 31-variable transfer-list schema only defines gaps at the
106
+ header/search/pane level (see TRANSFERLIST_IMPLEMENTATION_NOTES.md). Left as an
107
+ intentional hardcoded value rather than borrowing another component's token, same
108
+ as FileUpload's untokened icon size. */
109
+ gap: 2px;
110
+ min-height: 0;
111
+ }
112
+
113
+ .emptyState {
114
+ display: flex;
115
+ align-items: center;
116
+ justify-content: center;
117
+ flex: 1;
118
+ opacity: 0.6;
119
+ font-size: 14px;
120
+ padding: 16px;
121
+ text-align: center;
122
+ }
123
+
124
+ .transferColumn {
125
+ display: flex;
126
+ flex-direction: column;
127
+ align-items: center;
128
+ justify-content: center;
129
+ /* No token exists for spacing between the four transfer buttons — not part of the
130
+ 31-variable schema. Left as an intentional hardcoded value (see TRANSFERLIST_IMPLEMENTATION_NOTES.md). */
131
+ gap: 4px;
132
+ flex-shrink: 0;
133
+ }
134
+
135
+ .chevron[data-direction="left"] {
136
+ transform: scaleX(-1);
137
+ }
138
+
139
+ /* -------------------------------------
140
+ STATE CASCADE
141
+ -------------------------------------- */
142
+
143
+ .root[data-error] .pane {
144
+ border-color: var(
145
+ --recursica_ui-kit_components_transfer-list_variants_states_error_properties_colors_border-color
146
+ ) !important;
147
+ background-color: var(
148
+ --recursica_ui-kit_components_transfer-list_variants_states_error_properties_colors_background-color
149
+ ) !important;
150
+ }
151
+
152
+ .root[data-error] .paneHeader {
153
+ color: var(
154
+ --recursica_ui-kit_components_transfer-list_variants_states_error_properties_colors_header-color
155
+ ) !important;
156
+ }
157
+
158
+ .root[data-disabled] .pane {
159
+ border-color: var(
160
+ --recursica_ui-kit_components_transfer-list_variants_states_disabled_properties_colors_border-color
161
+ ) !important;
162
+ background-color: var(
163
+ --recursica_ui-kit_components_transfer-list_variants_states_disabled_properties_colors_background-color
164
+ ) !important;
165
+ opacity: var(
166
+ --recursica_ui-kit_components_transfer-list_variants_states_disabled_properties_opacity
167
+ );
168
+ }
169
+
170
+ .root[data-disabled] .paneHeader {
171
+ color: var(
172
+ --recursica_ui-kit_components_transfer-list_variants_states_disabled_properties_colors_header-color
173
+ ) !important;
174
+ }