@recursica/mantine-adapter 0.41.0 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "url": "git+https://github.com/borderux/recursica.git",
14
14
  "directory": "packages/mantine-adapter"
15
15
  },
16
- "version": "0.41.0",
16
+ "version": "0.42.0",
17
17
  "type": "module",
18
18
  "main": "./dist/mantine-adapter.cjs",
19
19
  "module": "./dist/mantine-adapter.js",
@@ -68,6 +68,16 @@ export const CheckboxGroup = forwardRef<HTMLDivElement, CheckboxGroupProps>(
68
68
  // Delete prohibited sizing hooks from bypassing the variables
69
69
  delete restRecord["size"];
70
70
 
71
+ // Mantine's own `Checkbox.Group` always establishes a context whose `value` defaults to
72
+ // `[]` (see @mantine/core's `useUncontrolled({ finalValue: [] })`), and its `Checkbox` spreads
73
+ // that context-derived `checked` *after* the individually-passed `checked` prop — so any
74
+ // `Checkbox` nested in a real `Checkbox.Group`, even one given neither `value` nor
75
+ // `defaultValue`, has its own `checked` prop silently forced to `false`. Callers who only want
76
+ // this component for its layout (item-gap spacing), not array-tracked selection — e.g.
77
+ // `TransferList`'s ungrouped rows, which pass each `Checkbox` its own controlled `checked` —
78
+ // must skip Mantine's real group primitive entirely rather than hand it an empty value.
79
+ const isArrayControlled = value !== undefined || defaultValue !== undefined;
80
+
71
81
  return (
72
82
  <WithReadOnlyWrapper
73
83
  className={className}
@@ -97,18 +107,30 @@ export const CheckboxGroup = forwardRef<HTMLDivElement, CheckboxGroupProps>(
97
107
  readOnlyValue={value !== undefined ? value : defaultValue}
98
108
  readOnlyNativeProps={props}
99
109
  activeComponent={
100
- <MantineCheckbox.Group
101
- ref={ref}
102
- /* Natively bind local disabled lock dynamically */
103
- {...(sanitizedProps as unknown as MantineCheckboxGroupProps)}
104
- disabled={readOnly || (restRecord as any).disabled}
105
- value={value as any}
106
- defaultValue={defaultValue as any}
107
- >
108
- <div className={styles.groupRoot} data-layout={formLayout}>
110
+ isArrayControlled ? (
111
+ <MantineCheckbox.Group
112
+ ref={ref}
113
+ /* Natively bind local disabled lock dynamically */
114
+ {...(sanitizedProps as unknown as MantineCheckboxGroupProps)}
115
+ disabled={readOnly || (restRecord as any).disabled}
116
+ value={value as any}
117
+ defaultValue={defaultValue as any}
118
+ >
119
+ <div className={styles.groupRoot} data-layout={formLayout}>
120
+ {children}
121
+ </div>
122
+ </MantineCheckbox.Group>
123
+ ) : (
124
+ <div
125
+ ref={ref}
126
+ role="group"
127
+ {...(sanitizedProps as Record<string, unknown>)}
128
+ className={styles.groupRoot}
129
+ data-layout={formLayout}
130
+ >
109
131
  {children}
110
132
  </div>
111
- </MantineCheckbox.Group>
133
+ )
112
134
  }
113
135
  />
114
136
  );
@@ -389,6 +389,12 @@ export const FileInput = forwardRef<HTMLDivElement, FileInputProps>(
389
389
  e.stopPropagation();
390
390
  handleClearAll();
391
391
  }}
392
+ onKeyDown={(e) => {
393
+ if (e.key !== "Enter" && e.key !== " ") return;
394
+ e.preventDefault();
395
+ e.stopPropagation();
396
+ handleClearAll();
397
+ }}
392
398
  />
393
399
  )}
394
400
 
@@ -56,3 +56,4 @@ This document contains specific design decisions, architectural constraints, and
56
56
  - Thumb, icon, and numeric input no longer have per-component focus colors. Instead they get a focus _ring_ built from the generic `--recursica_brand_states_focus_{border-size,color,margin,blur}` tokens, applied as a `box-shadow` (crisp inner ring at `border-size` plus a soft outer glow using `blur`/`margin`) rather than a color swap.
57
57
  - The thumb's focus box-shadow layers the ring on top of its existing `thumb-elevation` shadow rather than replacing it.
58
58
  - `.sliderMark[data-filled]` has no default-state "-active" step-indicator token anymore (only `disabled`/`error` define one); it reuses `colors_track-active` so filled marks stay visually tied to the filled portion of the track. Worth a design review if a distinct filled-mark color is expected.
59
+ - `markLabel` was already mapped in the `classNames` prop but had no matching `.sliderMarkLabel` rule in this file, so Mantine silently fell back to its own default theme grey instead of any recursica token. Added a `.sliderMarkLabel` rule reusing the min-max-label typography tokens with `color: inherit`, matching the mui-adapter's equivalent.
@@ -135,6 +135,40 @@
135
135
  );
136
136
  }
137
137
 
138
+ /*
139
+ * Mantine's default markLabel has no dedicated recursica token; it renders with Mantine's own
140
+ * theme grey instead of inheriting the container text color. Reusing the min-max-label
141
+ * typography tokens and inheriting color keeps this visually consistent with the min/max guides.
142
+ */
143
+ .sliderMarkLabel {
144
+ color: inherit;
145
+
146
+ font-family: var(
147
+ --recursica_ui-kit_components_slider_properties_min-max-label_font-family
148
+ );
149
+ font-size: var(
150
+ --recursica_ui-kit_components_slider_properties_min-max-label_font-size
151
+ );
152
+ font-style: var(
153
+ --recursica_ui-kit_components_slider_properties_min-max-label_font-style
154
+ );
155
+ font-weight: var(
156
+ --recursica_ui-kit_components_slider_properties_min-max-label_font-weight
157
+ );
158
+ letter-spacing: var(
159
+ --recursica_ui-kit_components_slider_properties_min-max-label_letter-spacing
160
+ );
161
+ line-height: var(
162
+ --recursica_ui-kit_components_slider_properties_min-max-label_line-height
163
+ );
164
+ text-decoration: var(
165
+ --recursica_ui-kit_components_slider_properties_min-max-label_text-decoration
166
+ );
167
+ text-transform: var(
168
+ --recursica_ui-kit_components_slider_properties_min-max-label_text-transform
169
+ );
170
+ }
171
+
138
172
  /* ==========================================
139
173
  3. Min / Max Custom Typography Labels & Floating Active Value
140
174
  ========================================== */
@@ -92,7 +92,7 @@ export const Disabled: Story = {
92
92
  label: "Decommissioned Server Node",
93
93
  assistiveText: "Modifications to this environment are frozen.",
94
94
  defaultValue: 35,
95
- disabled: false,
95
+ disabled: true,
96
96
  },
97
97
  };
98
98
 
@@ -121,20 +121,16 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
121
121
  /* Naked Input execution safely decoupled from Mantine's macro Input.Wrapper DOM hooks */
122
122
  <MantineTextarea
123
123
  ref={ref}
124
+ {...(sanitizedProps as unknown as MantineTextareaProps)}
124
125
  classNames={mergedClassNames}
125
126
  disabled={disabled}
126
127
  value={value}
127
128
  defaultValue={defaultValue}
128
129
  label={undefined}
129
130
  description={undefined}
130
- error={undefined}
131
+ error={!!error}
131
132
  required={undefined}
132
133
  withAsterisk={undefined}
133
- wrapperProps={{
134
- "data-disabled": disabled ? "true" : undefined,
135
- "data-error": error ? "true" : undefined,
136
- }}
137
- {...(sanitizedProps as unknown as MantineTextareaProps)}
138
134
  />
139
135
  }
140
136
  />
@@ -0,0 +1,111 @@
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
+ ## Token interpretation: unlabeled tokens with more than one plausible layout target
67
+
68
+ The 31-variable schema (`properties_*` + two `layouts.*` + `states.{disabled,error}`) has a few
69
+ names that don't pin down a single location by themselves:
70
+
71
+ - **`properties_gap`** — applied to the row holding `[source pane, transfer column, target pane]`.
72
+ It's the only _unnested_ gap the schema defines at the component's own level (not under
73
+ `header-style`, a state, or a layout), so it reads as the component's top-level layout gap.
74
+ - **`properties_title-filter-gap`** — between each pane's header row (label + count `Badge`) and
75
+ its search field.
76
+ - **`properties_filter-items-gap`** — between each pane's search field and its item list.
77
+ - **`properties_header-style_*` / `properties_colors_header-color`** — typography/color for each
78
+ pane's own header text, not the overall `FormControlWrapper` label (which has its own type
79
+ styling already).
80
+ - **`properties_width` (300px) / `properties_height` (200px)** — taken as each pane's own fixed
81
+ box size (`flex: 1 1 <width>`, so panes still stretch evenly in a wider container; `height` is
82
+ literal since a dual-listbox needs a bounded, independently-scrollable list).
83
+
84
+ No exemptions needed beyond `border-size` (see below) — all 31 variables are referenced; verified
85
+ zero broken/unused via `recursica-token-analyzer`.
86
+
87
+ ## Border-size intentionally unused
88
+
89
+ Same house policy `TextField`/`TextArea`/`FileInput` already follow: `border-size` (base,
90
+ `disabled`, `error`) is `recursica-ignore`d and a flat 1px border is applied uniformly instead, so
91
+ switching states doesn't shift layout.
92
+
93
+ ## No forge-defined focus state
94
+
95
+ `transfer-list` has no `states.focus` axis in the export — same as `FileInput`/`TextField`. Focus
96
+ is handled per-control (each `Checkbox`/`TextField`/`Button` already has its own focus ring); the
97
+ pane box itself has no focus state to render.
98
+
99
+ ## Grouping and search are local, not per-item persisted state
100
+
101
+ Search filters each pane independently (`sourceSearch`/`targetSearch`); grouped items render under
102
+ a `CheckboxGroup` keyed by `item.group`, ungrouped items render as plain `Checkbox` rows above the
103
+ groups (alphabetized by group name) — same two-bucket split Forge's reference uses. Selection
104
+ (`sourceSelected`/`targetSelected`) is cleared on every transfer, matching Forge's behavior.
105
+
106
+ ## Chevron icons are inline, not a shared icon set entry
107
+
108
+ Same precedent as `Tree`'s `ExpandGlyph`/`FileInput`'s `ClearIcon`: a single-chevron and a
109
+ double-chevron SVG are defined locally and flipped via `transform: scaleX(-1)` for the "left"
110
+ direction, rather than maintaining four separate paths or adding new shared icon-set entries for a
111
+ single component.
@@ -1,41 +1,182 @@
1
- /*
2
- * EXEMPTIONS:
3
- * - TransferList is a "Coming Soon" stub component.
4
- * - It renders a placeholder and has no active implementation or CSS stylesheet yet.
5
- * - The following tokens generated by Figma are ignored until the component is fully built.
6
- */
7
-
8
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_border-radius */
9
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_filter-items-gap */
10
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_gap */
11
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_header-style_fontFamily */
12
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_header-style_fontSize */
13
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_header-style_fontStyle */
14
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_header-style_fontWeight */
15
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_header-style_letterSpacing */
16
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_header-style_lineHeight */
17
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_header-style_textCase */
18
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_header-style_textDecoration */
19
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_height */
20
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_horizontal-padding */
21
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_title-filter-gap */
22
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_vertical-padding */
23
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_width */
24
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_layouts_side-by-side_properties_top-bottom-margin */
25
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_layouts_stacked_properties_top-bottom-margin */
26
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_default_properties_border-size */
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 disabled/error state transitions (same exemption
4
+ TextArea takes). */
5
+ /* recursica-ignore: --recursica_ui-kit_components_transfer-list_properties_border-size */
27
6
  /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_disabled_properties_border-size */
28
7
  /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_error_properties_border-size */
29
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_focus_properties_border-size */
30
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_default_properties_colors_background */
31
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_default_properties_colors_border-color */
32
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_default_properties_colors_header-color */
33
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_disabled_properties_colors_background */
34
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_disabled_properties_colors_border-color */
35
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_disabled_properties_colors_header-color */
36
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_error_properties_colors_background */
37
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_error_properties_colors_border-color */
38
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_error_properties_colors_header-color */
39
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_focus_properties_colors_background */
40
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_focus_properties_colors_border-color */
41
- /* recursica-ignore: --recursica_ui-kit_components_transfer-list_variants_states_focus_properties_colors_header-color */
8
+
9
+ /* TOKEN INTERPRETATION NOTES (see TRANSFERLIST_IMPLEMENTATION_NOTES.md):
10
+ - `properties_gap` is applied to the row that holds [source pane, transfer buttons, target pane] —
11
+ it's the only unnested gap the schema defines at the component's own level.
12
+ - `properties_title-filter-gap` sits between each pane's header row and its search field.
13
+ - `properties_filter-items-gap` sits between each pane's search field and its item list. */
14
+
15
+ .layoutOverride {
16
+ --form-control-margin-bottom: var(
17
+ --recursica_ui-kit_components_transfer-list_variants_layouts_stacked_properties_top-bottom-margin
18
+ );
19
+ }
20
+
21
+ .layoutOverride[data-form-layout="side-by-side"] {
22
+ --form-control-margin-bottom: var(
23
+ --recursica_ui-kit_components_transfer-list_variants_layouts_side-by-side_properties_top-bottom-margin
24
+ );
25
+ }
26
+
27
+ .root {
28
+ display: flex;
29
+ width: 100%;
30
+ }
31
+
32
+ .panes {
33
+ display: flex;
34
+ align-items: stretch;
35
+ gap: var(--recursica_ui-kit_components_transfer-list_properties_gap);
36
+ width: 100%;
37
+ }
38
+
39
+ .pane {
40
+ display: flex;
41
+ flex-direction: column;
42
+ box-sizing: border-box;
43
+ min-width: 0;
44
+ flex: 1 1 var(--recursica_ui-kit_components_transfer-list_properties_width);
45
+ height: var(--recursica_ui-kit_components_transfer-list_properties_height);
46
+ padding: var(
47
+ --recursica_ui-kit_components_transfer-list_properties_vertical-padding
48
+ )
49
+ var(
50
+ --recursica_ui-kit_components_transfer-list_properties_horizontal-padding
51
+ );
52
+ border-width: 1px;
53
+ border-style: solid;
54
+ border-radius: var(
55
+ --recursica_ui-kit_components_transfer-list_properties_border-radius
56
+ );
57
+ border-color: var(
58
+ --recursica_ui-kit_components_transfer-list_properties_colors_border-color
59
+ );
60
+ background-color: var(
61
+ --recursica_ui-kit_components_transfer-list_properties_colors_background-color
62
+ );
63
+ }
64
+
65
+ .paneHeader {
66
+ display: flex;
67
+ align-items: center;
68
+ justify-content: space-between;
69
+ margin-bottom: var(
70
+ --recursica_ui-kit_components_transfer-list_properties_title-filter-gap
71
+ );
72
+ font-family: var(
73
+ --recursica_ui-kit_components_transfer-list_properties_header-style_fontFamily
74
+ );
75
+ font-size: var(
76
+ --recursica_ui-kit_components_transfer-list_properties_header-style_fontSize
77
+ );
78
+ font-style: var(
79
+ --recursica_ui-kit_components_transfer-list_properties_header-style_fontStyle
80
+ );
81
+ font-weight: var(
82
+ --recursica_ui-kit_components_transfer-list_properties_header-style_fontWeight
83
+ );
84
+ letter-spacing: var(
85
+ --recursica_ui-kit_components_transfer-list_properties_header-style_letterSpacing
86
+ );
87
+ line-height: var(
88
+ --recursica_ui-kit_components_transfer-list_properties_header-style_lineHeight
89
+ );
90
+ text-decoration: var(
91
+ --recursica_ui-kit_components_transfer-list_properties_header-style_textDecoration
92
+ );
93
+ text-transform: var(
94
+ --recursica_ui-kit_components_transfer-list_properties_header-style_textCase
95
+ );
96
+ color: var(
97
+ --recursica_ui-kit_components_transfer-list_properties_colors_header-color
98
+ );
99
+ }
100
+
101
+ .paneSearch {
102
+ margin-bottom: var(
103
+ --recursica_ui-kit_components_transfer-list_properties_filter-items-gap
104
+ );
105
+ }
106
+
107
+ .paneList {
108
+ flex: 1;
109
+ overflow-y: auto;
110
+ display: flex;
111
+ flex-direction: column;
112
+ /* No token exists for the gap between stacked CheckboxGroup blocks (ungrouped row +
113
+ each named group) — the 31-variable transfer-list schema only defines gaps at the
114
+ header/search/pane level (see TRANSFERLIST_IMPLEMENTATION_NOTES.md). Left as an
115
+ intentional hardcoded value rather than borrowing another component's token, same
116
+ as FileUpload's untokened icon size. */
117
+ gap: 2px;
118
+ min-height: 0;
119
+ }
120
+
121
+ .emptyState {
122
+ display: flex;
123
+ align-items: center;
124
+ justify-content: center;
125
+ flex: 1;
126
+ opacity: 0.6;
127
+ font-size: 14px;
128
+ padding: 16px;
129
+ text-align: center;
130
+ }
131
+
132
+ .transferColumn {
133
+ display: flex;
134
+ flex-direction: column;
135
+ align-items: center;
136
+ justify-content: center;
137
+ /* No token exists for spacing between the four transfer buttons — not part of the
138
+ 31-variable schema. Left as an intentional hardcoded value (see TRANSFERLIST_IMPLEMENTATION_NOTES.md). */
139
+ gap: 4px;
140
+ flex-shrink: 0;
141
+ }
142
+
143
+ .chevron[data-direction="left"] {
144
+ transform: scaleX(-1);
145
+ }
146
+
147
+ /* -------------------------------------
148
+ STATE CASCADE
149
+ -------------------------------------- */
150
+
151
+ .root[data-error] .pane {
152
+ border-color: var(
153
+ --recursica_ui-kit_components_transfer-list_variants_states_error_properties_colors_border-color
154
+ ) !important;
155
+ background-color: var(
156
+ --recursica_ui-kit_components_transfer-list_variants_states_error_properties_colors_background-color
157
+ ) !important;
158
+ }
159
+
160
+ .root[data-error] .paneHeader {
161
+ color: var(
162
+ --recursica_ui-kit_components_transfer-list_variants_states_error_properties_colors_header-color
163
+ ) !important;
164
+ }
165
+
166
+ .root[data-disabled] .pane {
167
+ border-color: var(
168
+ --recursica_ui-kit_components_transfer-list_variants_states_disabled_properties_colors_border-color
169
+ ) !important;
170
+ background-color: var(
171
+ --recursica_ui-kit_components_transfer-list_variants_states_disabled_properties_colors_background-color
172
+ ) !important;
173
+ opacity: var(
174
+ --recursica_ui-kit_components_transfer-list_variants_states_disabled_properties_opacity
175
+ );
176
+ }
177
+
178
+ .root[data-disabled] .paneHeader {
179
+ color: var(
180
+ --recursica_ui-kit_components_transfer-list_variants_states_disabled_properties_colors_header-color
181
+ ) !important;
182
+ }
@@ -1,17 +1,121 @@
1
+ import React from "react";
1
2
  import type { Meta, StoryObj } from "@storybook/react";
2
3
  import { TransferList } from "./TransferList";
3
- import { ComingSoon } from "@recursica/storybook-template";
4
+ import { formControlArgTypes } from "../../../.storybook/commonArgTypes";
4
5
 
5
- const meta: Meta<typeof TransferList> = {
6
- title: "UI-Kit/🚧 TransferList",
6
+ type TransferListStoryProps = React.ComponentProps<typeof TransferList>;
7
+
8
+ const SAMPLE_DATA: TransferListStoryProps["defaultData"] = [
9
+ [
10
+ { value: "alpha", label: "Alpha" },
11
+ { value: "bravo", label: "Bravo" },
12
+ { value: "charlie", label: "Charlie" },
13
+ { value: "delta", label: "Delta" },
14
+ { value: "echo", label: "Echo" },
15
+ ],
16
+ [{ value: "foxtrot", label: "Foxtrot" }],
17
+ ];
18
+
19
+ const GROUPED_DATA: TransferListStoryProps["defaultData"] = [
20
+ [
21
+ { value: "apple", label: "Apple", group: "Fruit" },
22
+ { value: "banana", label: "Banana", group: "Fruit" },
23
+ { value: "carrot", label: "Carrot", group: "Vegetable" },
24
+ { value: "daikon", label: "Daikon", group: "Vegetable" },
25
+ { value: "eagle", label: "Eagle" },
26
+ ],
27
+ [],
28
+ ];
29
+
30
+ const meta: Meta<TransferListStoryProps> = {
31
+ title: "UI-Kit/TransferList",
7
32
  component: TransferList,
8
33
  tags: ["autodocs"],
34
+ parameters: {
35
+ docs: {
36
+ description: {
37
+ component:
38
+ "TransferList (dual listbox) lets users move items between two lists. Composes FormControlWrapper, TextField, Checkbox, CheckboxGroup, Badge, and Button.",
39
+ },
40
+ },
41
+ },
42
+ args: {
43
+ label: "Assign users",
44
+ assistiveText: "Move users into the selected list.",
45
+ defaultData: SAMPLE_DATA,
46
+ disabled: false,
47
+ required: false,
48
+ },
49
+ argTypes: {
50
+ disabled: {
51
+ control: "boolean",
52
+ },
53
+ ...formControlArgTypes,
54
+ sourceLabel: {
55
+ control: "text",
56
+ },
57
+ targetLabel: {
58
+ control: "text",
59
+ },
60
+ searchable: {
61
+ control: "boolean",
62
+ },
63
+ searchPlaceholder: {
64
+ control: "text",
65
+ },
66
+ },
9
67
  };
10
68
 
11
69
  export default meta;
12
70
 
13
- type Story = StoryObj<typeof TransferList>;
71
+ type Story = StoryObj<TransferListStoryProps>;
72
+
73
+ export const Default: Story = {};
74
+
75
+ export const Grouped: Story = {
76
+ args: {
77
+ label: "Assign ingredients",
78
+ defaultData: GROUPED_DATA,
79
+ },
80
+ };
81
+
82
+ export const SideBySide: Story = {
83
+ args: {
84
+ formLayout: "side-by-side",
85
+ },
86
+ };
87
+
88
+ export const NoSearch: Story = {
89
+ args: {
90
+ label: "Assign users (no filtering)",
91
+ searchable: false,
92
+ },
93
+ };
94
+
95
+ export const StaticError: Story = {
96
+ args: {
97
+ error: "Select at least one user.",
98
+ defaultData: [[], SAMPLE_DATA![0]],
99
+ },
100
+ };
101
+
102
+ export const StaticDisabled: Story = {
103
+ args: {
104
+ disabled: true,
105
+ },
106
+ };
107
+
108
+ export const Empty: Story = {
109
+ args: {
110
+ label: "Assign users",
111
+ defaultData: [[], []],
112
+ },
113
+ };
14
114
 
15
- export const Default: Story = {
16
- render: () => <ComingSoon componentName="TransferList" />,
115
+ export const ReadOnly: Story = {
116
+ args: {
117
+ label: "Assigned users",
118
+ defaultData: [[], SAMPLE_DATA![0]],
119
+ readOnly: true,
120
+ },
17
121
  };