@recursica/mantine-adapter 0.53.0 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/dist/index.d.ts +10 -0
- package/dist/mantine-adapter.cjs +1 -1
- package/dist/mantine-adapter.cjs.map +1 -1
- package/dist/mantine-adapter.css +1 -1
- package/dist/mantine-adapter.js +634 -617
- package/dist/mantine-adapter.js.map +1 -1
- package/package.json +5 -4
- package/src/components/Accordion/Accordion.module.css +17 -8
- package/src/components/AutoComplete/AutoComplete.module.css +9 -2
- package/src/components/Chip/Chip.module.css +18 -0
- package/src/components/DatePicker/DatePicker.module.css +9 -2
- package/src/components/Dropdown/Dropdown.module.css +9 -2
- package/src/components/FileInput/FileInput.module.css +12 -0
- package/src/components/FileUpload/FileUpload.module.css +6 -1
- package/src/components/FormControlLayout/FormControlLayout.module.css +12 -0
- package/src/components/FormControlLayout/FormControlLayout.stories.tsx +36 -0
- package/src/components/HoverCard/HoverCard.module.css +6 -1
- package/src/components/Label/Label.stories.tsx +63 -54
- package/src/components/Link/Link.module.css +5 -0
- package/src/components/Loader/Loader.animate.dom.test.tsx +82 -0
- package/src/components/Loader/Loader.module.css +10 -0
- package/src/components/Loader/Loader.stories.tsx +20 -0
- package/src/components/Loader/Loader.tsx +8 -1
- package/src/components/Modal/Modal.module.css +6 -2
- package/src/components/NumberInput/NumberInput.module.css +9 -2
- package/src/components/SegmentedControl/SegmentedControl.module.css +6 -0
- package/src/components/Table/TABLE_IMPLEMENTATION_NOTES.md +11 -0
- package/src/components/Table/Table.module.css +14 -0
- package/src/components/Table/Table.stories.tsx +1 -1
- package/src/components/Table/Table.tsx +9 -1
- package/src/components/Table/USAGE.md +4 -2
- package/src/components/TextArea/TextArea.module.css +9 -2
- package/src/components/TextField/TextField.module.css +9 -2
- package/src/components/TimePicker/TIMEPICKER_IMPLEMENTATION_NOTES.md +7 -1
- package/src/components/TimePicker/TimePicker.module.css +44 -0
- package/src/components/TimePicker/TimePicker.stories.tsx +22 -0
- package/src/components/TimePicker/TimePicker.tsx +1 -0
- package/src/components/Timeline/Timeline.module.css +5 -0
- package/src/components/Toast/Toast.module.css +8 -0
- package/src/components/Tooltip/Tooltip.module.css +9 -1
- package/src/components/TransferList/TransferList.module.css +9 -1
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.
|
|
16
|
+
"version": "0.55.0",
|
|
17
17
|
"type": "module",
|
|
18
18
|
"main": "./dist/mantine-adapter.cjs",
|
|
19
19
|
"module": "./dist/mantine-adapter.js",
|
|
@@ -57,12 +57,13 @@
|
|
|
57
57
|
"build": "vite build --mode library",
|
|
58
58
|
"build-storybook": "storybook build -o storybook-static",
|
|
59
59
|
"lint": "eslint .",
|
|
60
|
-
"storybook": "storybook dev
|
|
60
|
+
"storybook": "storybook dev",
|
|
61
61
|
"analyze-tokens": "analyze-tokens --css ./recursica_variables_scoped.css --dir src/components --output token-analysis.json",
|
|
62
62
|
"prebuild": "npm run analyze-tokens",
|
|
63
63
|
"test": "vitest run --project unit",
|
|
64
64
|
"test:dom": "vitest run --project dom",
|
|
65
|
-
"adapter-tester:automated": "adapter-tester"
|
|
65
|
+
"adapter-tester:automated": "adapter-tester",
|
|
66
|
+
"adapter-tester:update-golden": "adapter-tester --update-golden"
|
|
66
67
|
},
|
|
67
68
|
"devDependencies": {
|
|
68
69
|
"@eslint/js": "^9.25.0",
|
|
@@ -102,7 +103,7 @@
|
|
|
102
103
|
"vitest": "^3.2.4"
|
|
103
104
|
},
|
|
104
105
|
"dependencies": {
|
|
105
|
-
"@recursica/adapter-common": "^0.
|
|
106
|
+
"@recursica/adapter-common": "^0.28.0"
|
|
106
107
|
},
|
|
107
108
|
"peerDependencies": {
|
|
108
109
|
"@mantine/core": "^8.0.0",
|
|
@@ -96,14 +96,11 @@
|
|
|
96
96
|
--recursica_ui-kit_components_accordion-header_properties_horizontal-padding
|
|
97
97
|
);
|
|
98
98
|
|
|
99
|
-
/*
|
|
100
|
-
background swap on expand, even though the token schema defines an "open" appearance color) */
|
|
99
|
+
/* Closed (resting) background/border — swapped to the open appearance below when expanded */
|
|
101
100
|
background-color: var(
|
|
102
101
|
--recursica_ui-kit_components_accordion-header_variants_appearance_closed_properties_colors_background-color
|
|
103
102
|
);
|
|
104
103
|
|
|
105
|
-
/* Header's own border box, constant across collapsed/expanded (only its closed-state
|
|
106
|
-
border-color is used, matching the background decision above) */
|
|
107
104
|
border-style: solid;
|
|
108
105
|
border-width: var(
|
|
109
106
|
--recursica_ui-kit_components_accordion-header_properties_border-size
|
|
@@ -153,20 +150,32 @@
|
|
|
153
150
|
}
|
|
154
151
|
|
|
155
152
|
.item[data-active] .control {
|
|
153
|
+
background-color: var(
|
|
154
|
+
--recursica_ui-kit_components_accordion-header_variants_appearance_open_properties_colors_background-color
|
|
155
|
+
);
|
|
156
|
+
border-color: var(
|
|
157
|
+
--recursica_ui-kit_components_accordion-header_variants_appearance_open_properties_colors_border-color
|
|
158
|
+
);
|
|
156
159
|
color: var(
|
|
157
160
|
--recursica_ui-kit_components_accordion-header_variants_appearance_open_properties_colors_text-color
|
|
158
161
|
);
|
|
159
162
|
}
|
|
160
163
|
|
|
161
|
-
/* Mantine natively places hover states on the control node. We explicitly re-apply our own
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
hover-color/hover-opacity
|
|
164
|
+
/* Mantine natively places hover states on the control node. We explicitly re-apply our own real
|
|
165
|
+
background (Mantine's native :hover CSS would otherwise show through) — closed or open,
|
|
166
|
+
matching whichever appearance is currently active — and use an overlay ::after structure with
|
|
167
|
+
the generic hover tokens for the actual hover tint; no per-item hover-color/hover-opacity
|
|
168
|
+
tokens exist anymore. */
|
|
165
169
|
.control:hover:not(:disabled) {
|
|
166
170
|
background-color: var(
|
|
167
171
|
--recursica_ui-kit_components_accordion-header_variants_appearance_closed_properties_colors_background-color
|
|
168
172
|
);
|
|
169
173
|
}
|
|
174
|
+
.item[data-active] .control:hover:not(:disabled) {
|
|
175
|
+
background-color: var(
|
|
176
|
+
--recursica_ui-kit_components_accordion-header_variants_appearance_open_properties_colors_background-color
|
|
177
|
+
);
|
|
178
|
+
}
|
|
170
179
|
.control::after {
|
|
171
180
|
content: "";
|
|
172
181
|
position: absolute;
|
|
@@ -37,12 +37,17 @@
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/* HARDCODED VALUES:
|
|
40
|
-
- border-width: 1px. Native geometric boundary for the input box.
|
|
41
40
|
- border-style: solid. Native structural rendering rule.
|
|
42
41
|
- outline: none. Bypassing browser focus rings to rely strictly on Recursica focus states natively.
|
|
43
42
|
- flex/layout: display: flex on the root wrapper safely encapsulating internal section rendering.
|
|
44
43
|
*/
|
|
45
44
|
|
|
45
|
+
/* EXEMPTIONS:
|
|
46
|
+
- border-size is constant across disabled/error states — only border-color changes; the
|
|
47
|
+
disabled/error border-size tokens are never applied by design. */
|
|
48
|
+
/* recursica-ignore: --recursica_ui-kit_components_autocomplete_variants_states_disabled_properties_border-size */
|
|
49
|
+
/* recursica-ignore: --recursica_ui-kit_components_autocomplete_variants_states_error_properties_border-size */
|
|
50
|
+
|
|
46
51
|
.root {
|
|
47
52
|
display: flex;
|
|
48
53
|
position: relative;
|
|
@@ -89,7 +94,9 @@
|
|
|
89
94
|
border-radius: var(
|
|
90
95
|
--recursica_ui-kit_components_autocomplete_properties_border-radius
|
|
91
96
|
);
|
|
92
|
-
border-width:
|
|
97
|
+
border-width: var(
|
|
98
|
+
--recursica_ui-kit_components_autocomplete_properties_border-size
|
|
99
|
+
);
|
|
93
100
|
border-style: solid;
|
|
94
101
|
|
|
95
102
|
/* Strict Typography Unification */
|
|
@@ -17,6 +17,24 @@
|
|
|
17
17
|
background: transparent; (Override mantine default)
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
+
/* EXEMPTIONS:
|
|
21
|
+
- The leading icon and the selected/check icon are mutually exclusive (Chip.tsx only renders
|
|
22
|
+
the leading icon when `!checked`; Mantine's own check icon takes over when checked) — so for
|
|
23
|
+
any given state, exactly one of `leading-icon-color`/`selected-icon-color` ever applies. The
|
|
24
|
+
other one, and the non-state-specific base `properties_leading-icon-color`/
|
|
25
|
+
`properties_close-icon-color` (superseded by the per-selection-state values actually used),
|
|
26
|
+
are never referenced by design, not missing. Same for the generic (non-leading/selected)
|
|
27
|
+
`colors_icon-color` on the error variants — the more specific leading/selected token already
|
|
28
|
+
covers it. */
|
|
29
|
+
/* recursica-ignore: --recursica_ui-kit_components_chip_properties_close-icon-color */
|
|
30
|
+
/* recursica-ignore: --recursica_ui-kit_components_chip_properties_leading-icon-color */
|
|
31
|
+
/* recursica-ignore: --recursica_ui-kit_components_chip_variants_selection-states_selected_properties_colors_leading-icon-color */
|
|
32
|
+
/* recursica-ignore: --recursica_ui-kit_components_chip_variants_selection-states_selected_variants_states_error_properties_colors_icon-color */
|
|
33
|
+
/* recursica-ignore: --recursica_ui-kit_components_chip_variants_selection-states_selected_variants_states_error_properties_colors_leading-icon-color */
|
|
34
|
+
/* recursica-ignore: --recursica_ui-kit_components_chip_variants_selection-states_unselected_properties_colors_selected-icon-color */
|
|
35
|
+
/* recursica-ignore: --recursica_ui-kit_components_chip_variants_selection-states_unselected_variants_states_error_properties_colors_icon-color */
|
|
36
|
+
/* recursica-ignore: --recursica_ui-kit_components_chip_variants_selection-states_unselected_variants_states_error_properties_colors_selected-icon-color */
|
|
37
|
+
|
|
20
38
|
.root.root {
|
|
21
39
|
/* Set preset layout variables to override mantine defaults safely */
|
|
22
40
|
box-sizing: border-box;
|
|
@@ -29,12 +29,17 @@
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/* HARDCODED VALUES:
|
|
32
|
-
- border-width: 1px. Native geometric boundary for the input box.
|
|
33
32
|
- border-style: solid. Native structural rendering rule.
|
|
34
33
|
- outline: none. Bypassing browser focus rings to rely strictly on Recursica focus states natively.
|
|
35
34
|
- flex/layout: display: flex on the root wrapper safely encapsulating internal section rendering.
|
|
36
35
|
*/
|
|
37
36
|
|
|
37
|
+
/* EXEMPTIONS:
|
|
38
|
+
- border-size is constant across disabled/error states — only border-color changes; the
|
|
39
|
+
disabled/error border-size tokens are never applied by design. */
|
|
40
|
+
/* recursica-ignore: --recursica_ui-kit_components_date-picker_variants_states_disabled_properties_border-size */
|
|
41
|
+
/* recursica-ignore: --recursica_ui-kit_components_date-picker_variants_states_error_properties_border-size */
|
|
42
|
+
|
|
38
43
|
.root {
|
|
39
44
|
display: flex;
|
|
40
45
|
position: relative;
|
|
@@ -85,7 +90,9 @@
|
|
|
85
90
|
border-radius: var(
|
|
86
91
|
--recursica_ui-kit_components_date-picker_properties_border-radius
|
|
87
92
|
);
|
|
88
|
-
border-width:
|
|
93
|
+
border-width: var(
|
|
94
|
+
--recursica_ui-kit_components_date-picker_properties_border-size
|
|
95
|
+
);
|
|
89
96
|
border-style: solid;
|
|
90
97
|
|
|
91
98
|
/* Strict Typography Unification */
|
|
@@ -37,13 +37,18 @@
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/* HARDCODED VALUES:
|
|
40
|
-
- border-width: 1px. Native geometric boundary for the input box.
|
|
41
40
|
- border-style: solid. Native structural rendering rule.
|
|
42
41
|
- outline: none. Bypassing browser focus rings to rely strictly on Recursica focus states natively.
|
|
43
42
|
- flex/layout: display: flex on the root wrapper safely encapsulating internal section rendering.
|
|
44
43
|
- placeholder opacity: 0.5. The dropdown parameters currently lack a dedicated placeholder-opacity variable.
|
|
45
44
|
*/
|
|
46
45
|
|
|
46
|
+
/* EXEMPTIONS:
|
|
47
|
+
- border-size is constant across disabled/error states — only border-color changes; the
|
|
48
|
+
disabled/error border-size tokens are never applied by design. */
|
|
49
|
+
/* recursica-ignore: --recursica_ui-kit_components_dropdown_variants_states_disabled_properties_border-size */
|
|
50
|
+
/* recursica-ignore: --recursica_ui-kit_components_dropdown_variants_states_error_properties_border-size */
|
|
51
|
+
|
|
47
52
|
.root {
|
|
48
53
|
display: flex;
|
|
49
54
|
position: relative;
|
|
@@ -86,7 +91,9 @@
|
|
|
86
91
|
border-radius: var(
|
|
87
92
|
--recursica_ui-kit_components_dropdown_properties_border-radius
|
|
88
93
|
);
|
|
89
|
-
border-width:
|
|
94
|
+
border-width: var(
|
|
95
|
+
--recursica_ui-kit_components_dropdown_properties_border-size
|
|
96
|
+
);
|
|
90
97
|
border-style: solid;
|
|
91
98
|
|
|
92
99
|
/* Strict Typography Unification */
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
* recursica-allow-brand: --recursica_brand_states_hover_opacity
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
/* EXEMPTIONS:
|
|
12
|
+
- border-size is constant across disabled/error states — only border-color changes; the
|
|
13
|
+
disabled/error border-size tokens are never applied by design. */
|
|
14
|
+
/* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_disabled_properties_border-size */
|
|
15
|
+
/* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_error_properties_border-size */
|
|
16
|
+
|
|
11
17
|
/* LAYOUT SPACING OVERRIDES:
|
|
12
18
|
- Sets the --form-control-margin-bottom spacing hook to map component-specific layout tokens.
|
|
13
19
|
- Also sets the --file-input-control-{max,min}-width hooks consumed inline in FileInput.tsx,
|
|
@@ -205,6 +211,12 @@
|
|
|
205
211
|
|
|
206
212
|
/* Structural placement only — the button's own "text" variant/size="small" tokens (including its
|
|
207
213
|
own disabled state) own its visual appearance, same as Tree's .expandButton. */
|
|
214
|
+
/* EXEMPTIONS:
|
|
215
|
+
- trailing-icon color (base/disabled/error) is ignored — the trailing icon is a nested Button
|
|
216
|
+
that owns its own color tokens per state; FileInput never recolors it directly. */
|
|
217
|
+
/* recursica-ignore: --recursica_ui-kit_components_file-input_properties_colors_trailing-icon */
|
|
218
|
+
/* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_disabled_properties_colors_trailing-icon */
|
|
219
|
+
/* recursica-ignore: --recursica_ui-kit_components_file-input_variants_states_error_properties_colors_trailing-icon */
|
|
208
220
|
.trailingIcon {
|
|
209
221
|
position: relative;
|
|
210
222
|
z-index: 1;
|
|
@@ -7,11 +7,16 @@
|
|
|
7
7
|
/* HARDCODED VALUES:
|
|
8
8
|
- .root: display: flex; flex-direction: column; width: 100% — structural layout, not a design
|
|
9
9
|
token concern (matches how other block-level components like Layer are laid out).
|
|
10
|
-
- .dropzone: border-style: dashed — see EXEMPTIONS
|
|
10
|
+
- .dropzone: border-style: dashed — see EXEMPTIONS below.
|
|
11
11
|
- .uploadIcon: width/height: 2rem — no icon-size token exists for file-upload (unlike
|
|
12
12
|
TextField/DatePicker); sized to visually match the Figma reference. Revisit if a dedicated
|
|
13
13
|
token is ever added. */
|
|
14
14
|
|
|
15
|
+
/* EXEMPTIONS:
|
|
16
|
+
- border-style is ignored: the dropzone is deliberately dashed (conventional drop-target
|
|
17
|
+
affordance), not the UI Kit's solid-border default. */
|
|
18
|
+
/* recursica-ignore: --recursica_ui-kit_components_file-upload_properties_border-style */
|
|
19
|
+
|
|
15
20
|
/* LAYOUT SPACING OVERRIDES:
|
|
16
21
|
- Sets the --form-control-margin-bottom spacing hook to map component-specific layout tokens. */
|
|
17
22
|
.layoutOverride {
|
|
@@ -41,6 +41,18 @@
|
|
|
41
41
|
);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
.root[data-form-layout="stacked"] .leftSection[data-size="default"] {
|
|
45
|
+
width: var(
|
|
46
|
+
--recursica_ui-kit_components_label_variants_layouts_stacked_variants_sizes_default_properties_width
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.root[data-form-layout="stacked"] .leftSection[data-size="small"] {
|
|
51
|
+
width: var(
|
|
52
|
+
--recursica_ui-kit_components_label_variants_layouts_stacked_variants_sizes_small_properties_width
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
44
56
|
/* SideBySide Layout constraints */
|
|
45
57
|
.root[data-form-layout="side-by-side"] .leftSection {
|
|
46
58
|
padding-top: var(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
2
|
import { FormControlLayout } from "./FormControlLayout";
|
|
3
3
|
import { Switch } from "../Switch/Switch";
|
|
4
|
+
import { Label } from "../Label/Label";
|
|
4
5
|
|
|
5
6
|
const meta: Meta<typeof FormControlLayout> = {
|
|
6
7
|
title: "UI-Kit/FormControlLayout",
|
|
@@ -80,3 +81,38 @@ export const SideBySideLayout: Story = {
|
|
|
80
81
|
children: <Switch label="Offset switch aligning with grid" />,
|
|
81
82
|
},
|
|
82
83
|
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A real Label (not a placeholder) in a stacked layout, at `labelSize="default"`. Even
|
|
87
|
+
* though the label sits above the field rather than beside it, its own column still caps
|
|
88
|
+
* to the design system's stacked-layout width (224px) rather than stretching to the full
|
|
89
|
+
* container — long label text wraps instead of spanning edge-to-edge.
|
|
90
|
+
*/
|
|
91
|
+
export const StackedLayoutWithLabelDefault: Story = {
|
|
92
|
+
args: {
|
|
93
|
+
formLayout: "stacked",
|
|
94
|
+
labelSize: "default",
|
|
95
|
+
leftSection: (
|
|
96
|
+
<Label labelSize="default">
|
|
97
|
+
A fairly long label to show the stacked-layout width cap in action
|
|
98
|
+
</Label>
|
|
99
|
+
),
|
|
100
|
+
children: <Switch label="Input area content" />,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Same as above, at `labelSize="small"` — the stacked-layout width cap is narrower (80px).
|
|
106
|
+
*/
|
|
107
|
+
export const StackedLayoutWithLabelSmall: Story = {
|
|
108
|
+
args: {
|
|
109
|
+
formLayout: "stacked",
|
|
110
|
+
labelSize: "small",
|
|
111
|
+
leftSection: (
|
|
112
|
+
<Label labelSize="small">
|
|
113
|
+
A fairly long label to show the stacked-layout width cap in action
|
|
114
|
+
</Label>
|
|
115
|
+
),
|
|
116
|
+
children: <Switch label="Input area content" />,
|
|
117
|
+
},
|
|
118
|
+
};
|
|
@@ -3,10 +3,15 @@
|
|
|
3
3
|
which may not set border-style natively). Same pattern as Menu.
|
|
4
4
|
- arrowSize defaults to 16 in HoverCard.tsx. Mantine uses arrowSize for inline width/height
|
|
5
5
|
and positioning (-arrowSize/2) calculations that cannot be CSS-driven. The default matches
|
|
6
|
-
the Recursica beak-size token (16px).
|
|
6
|
+
the Recursica beak-size token (16px).
|
|
7
7
|
- All structural layout (display, position, overflow) is deferred to Mantine's native
|
|
8
8
|
behavior. We only override visual design tokens (colors, typography, spacing, borders). */
|
|
9
9
|
|
|
10
|
+
/* EXEMPTIONS:
|
|
11
|
+
- beak-size is ignored: the arrow's size is Mantine's own `arrowSize` JS prop, not CSS-driven —
|
|
12
|
+
see HARDCODED VALUES above. */
|
|
13
|
+
/* recursica-ignore: --recursica_ui-kit_components_hover-card-popover_properties_beak-size */
|
|
14
|
+
|
|
10
15
|
/* ======================================
|
|
11
16
|
DROPDOWN CONTAINER
|
|
12
17
|
====================================== */
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
1
|
import React from "react";
|
|
3
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
4
3
|
import { Label } from "./Label";
|
|
5
|
-
import {
|
|
6
|
-
import { formControlArgTypes } from "../../../.storybook/commonArgTypes";
|
|
4
|
+
import { Button } from "../Button/Button";
|
|
7
5
|
|
|
8
6
|
type LabelStoryProps = React.ComponentProps<typeof Label>;
|
|
9
7
|
|
|
@@ -15,12 +13,43 @@ const meta: Meta<LabelStoryProps> = {
|
|
|
15
13
|
docs: {
|
|
16
14
|
description: {
|
|
17
15
|
component:
|
|
18
|
-
"The `Label` component is a strict Recursica-styled wrapper around Mantine's native `Input.Label`. It serves as the primary compositional primitive for all form fields, preserving Mantine's accessibility associations and context while strictly enforcing the Recursica atomic design system.\n\n### Usage with Form Inputs\
|
|
16
|
+
"The `Label` component is a strict Recursica-styled wrapper around Mantine's native `Input.Label`. It serves as the primary compositional primitive for all form fields, preserving Mantine's accessibility associations and context while strictly enforcing the Recursica atomic design system.\n\n### Usage with Form Inputs\nThis component only renders the label itself — layout concerns like `stacked` vs `side-by-side` positioning relative to an input live on `FormControlLayout`/`FormControlWrapper`, not here. Render this `Label` in isolation to verify its own states, or see `UI-Kit/FormControlLayout` for how it composes into a full form field.",
|
|
19
17
|
},
|
|
20
18
|
},
|
|
21
19
|
},
|
|
22
20
|
argTypes: {
|
|
23
|
-
|
|
21
|
+
labelSize: {
|
|
22
|
+
control: "inline-radio",
|
|
23
|
+
options: ["default", "small", "md"],
|
|
24
|
+
description:
|
|
25
|
+
"Sizing metrics for the Label. Only visually distinguishable once composed inside a `side-by-side` FormControlLayout, which is where the resulting width constraint applies.",
|
|
26
|
+
},
|
|
27
|
+
labelAlignment: {
|
|
28
|
+
control: "inline-radio",
|
|
29
|
+
options: ["left", "right"],
|
|
30
|
+
description: "Text alignment of the label content.",
|
|
31
|
+
},
|
|
32
|
+
required: {
|
|
33
|
+
control: "boolean",
|
|
34
|
+
description:
|
|
35
|
+
"Renders the required asterisk (suppressed automatically when `labelWithEditIcon` is set, and mutually exclusive with `labelOptionalText`).",
|
|
36
|
+
},
|
|
37
|
+
labelOptionalText: {
|
|
38
|
+
control: "text",
|
|
39
|
+
description:
|
|
40
|
+
"Secondary text rendered beneath the label. Pass `true` for the default '(Optional)' string, or a custom node/string. Suppressed when `required` is true.",
|
|
41
|
+
},
|
|
42
|
+
labelWithEditIcon: {
|
|
43
|
+
control: "boolean",
|
|
44
|
+
description:
|
|
45
|
+
"Replaces the default edit icon slot with an interactive edit affordance; replaces the required asterisk visually when both are set.",
|
|
46
|
+
},
|
|
47
|
+
labelActionArea: {
|
|
48
|
+
table: { disable: true },
|
|
49
|
+
},
|
|
50
|
+
onLabelEditClick: {
|
|
51
|
+
table: { disable: true },
|
|
52
|
+
},
|
|
24
53
|
},
|
|
25
54
|
};
|
|
26
55
|
|
|
@@ -28,95 +57,75 @@ export default meta;
|
|
|
28
57
|
|
|
29
58
|
type Story = StoryObj<LabelStoryProps>;
|
|
30
59
|
|
|
31
|
-
// Utility mapping to pipe raw Label args structurally into TextField accurately
|
|
32
|
-
const renderWithTextField = ({ children, ...args }: LabelStoryProps) => (
|
|
33
|
-
<TextField
|
|
34
|
-
label={children as React.ReactNode}
|
|
35
|
-
placeholder="Form Control primitive mapped..."
|
|
36
|
-
{...(args as any)}
|
|
37
|
-
/>
|
|
38
|
-
);
|
|
39
|
-
|
|
40
60
|
export const Default: Story = {
|
|
41
61
|
args: {
|
|
42
|
-
children: "
|
|
43
|
-
|
|
62
|
+
children: "Label",
|
|
44
63
|
labelSize: "default",
|
|
45
64
|
labelAlignment: "left",
|
|
46
65
|
required: false,
|
|
47
66
|
labelOptionalText: "",
|
|
48
67
|
labelWithEditIcon: false,
|
|
49
68
|
},
|
|
50
|
-
render: renderWithTextField,
|
|
51
69
|
};
|
|
52
70
|
|
|
53
|
-
export const
|
|
71
|
+
export const Required: Story = {
|
|
54
72
|
args: {
|
|
55
|
-
children: "
|
|
73
|
+
children: "Required Field",
|
|
74
|
+
required: true,
|
|
56
75
|
},
|
|
57
|
-
render: renderWithTextField,
|
|
58
76
|
};
|
|
59
77
|
|
|
60
|
-
export const
|
|
78
|
+
export const RequiredSuppressesOptionalText: Story = {
|
|
61
79
|
args: {
|
|
62
|
-
children: "
|
|
63
|
-
|
|
80
|
+
children: "Full Name",
|
|
64
81
|
required: true,
|
|
82
|
+
labelOptionalText: "This should not render",
|
|
65
83
|
},
|
|
66
|
-
render: renderWithTextField,
|
|
67
84
|
};
|
|
68
85
|
|
|
69
|
-
export const
|
|
86
|
+
export const WithOptionalText: Story = {
|
|
70
87
|
args: {
|
|
71
|
-
children: "
|
|
72
|
-
|
|
73
|
-
labelWithEditIcon: true,
|
|
88
|
+
children: "Bio",
|
|
89
|
+
labelOptionalText: "Max 100 characters",
|
|
74
90
|
},
|
|
75
|
-
render: renderWithTextField,
|
|
76
91
|
};
|
|
77
92
|
|
|
78
|
-
export const
|
|
93
|
+
export const BooleanOptionalText: Story = {
|
|
79
94
|
args: {
|
|
80
|
-
children: "
|
|
81
|
-
|
|
82
|
-
labelSize: "default",
|
|
95
|
+
children: "Middle Initial",
|
|
96
|
+
labelOptionalText: true,
|
|
83
97
|
},
|
|
84
|
-
render: renderWithTextField,
|
|
85
98
|
};
|
|
86
99
|
|
|
87
|
-
export const
|
|
100
|
+
export const WithEditIcon: Story = {
|
|
88
101
|
args: {
|
|
89
|
-
children: "
|
|
90
|
-
|
|
91
|
-
required: true,
|
|
92
|
-
labelOptionalText: "This should not render",
|
|
102
|
+
children: "Shipping Address",
|
|
103
|
+
labelWithEditIcon: true,
|
|
93
104
|
},
|
|
94
|
-
render: renderWithTextField,
|
|
95
105
|
};
|
|
96
106
|
|
|
97
|
-
export const
|
|
107
|
+
export const RequiredWithEditIcon: Story = {
|
|
98
108
|
args: {
|
|
99
|
-
children: "
|
|
100
|
-
|
|
101
|
-
|
|
109
|
+
children: "Primary Network Node",
|
|
110
|
+
required: true,
|
|
111
|
+
labelWithEditIcon: true,
|
|
102
112
|
},
|
|
103
|
-
render: renderWithTextField,
|
|
104
113
|
};
|
|
105
114
|
|
|
106
|
-
export const
|
|
115
|
+
export const RightAligned: Story = {
|
|
107
116
|
args: {
|
|
108
|
-
children: "
|
|
109
|
-
|
|
110
|
-
labelWithEditIcon: true,
|
|
117
|
+
children: "Status",
|
|
118
|
+
labelAlignment: "right",
|
|
111
119
|
},
|
|
112
|
-
render: renderWithTextField,
|
|
113
120
|
};
|
|
114
121
|
|
|
115
|
-
export const
|
|
122
|
+
export const WithActionArea: Story = {
|
|
116
123
|
args: {
|
|
117
124
|
children: "Configuration",
|
|
118
|
-
|
|
119
|
-
|
|
125
|
+
labelActionArea: (
|
|
126
|
+
<Button variant="text" size="small">
|
|
127
|
+
Edit
|
|
128
|
+
</Button>
|
|
129
|
+
),
|
|
120
130
|
},
|
|
121
|
-
render: renderWithTextField,
|
|
122
131
|
};
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
* recursica-allow-brand: --recursica_brand_states_link_decoration
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
/* EXEMPTIONS:
|
|
7
|
+
- text_text-transform is ignored because links naturally inherit casing directly from text
|
|
8
|
+
children rather than requiring custom CSS transforms. */
|
|
9
|
+
/* recursica-ignore: --recursica_ui-kit_components_link_properties_text_text-transform */
|
|
10
|
+
|
|
6
11
|
/*
|
|
7
12
|
HARDCODED VALUES:
|
|
8
13
|
- display: inline-flex; (Base layout to align icon and text correctly)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { describe, it, expect } from "vitest";
|
|
3
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
4
|
+
import { flushSync } from "react-dom";
|
|
5
|
+
import { MantineProvider } from "@mantine/core";
|
|
6
|
+
import "@mantine/core/styles.css";
|
|
7
|
+
import { Loader } from "./Loader";
|
|
8
|
+
|
|
9
|
+
function mount(node: React.ReactElement): {
|
|
10
|
+
container: HTMLElement;
|
|
11
|
+
root: Root;
|
|
12
|
+
} {
|
|
13
|
+
const container = document.createElement("div");
|
|
14
|
+
document.body.appendChild(container);
|
|
15
|
+
const root = createRoot(container);
|
|
16
|
+
flushSync(() => root.render(node));
|
|
17
|
+
return { container, root };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function unmount({ container, root }: { container: HTMLElement; root: Root }) {
|
|
21
|
+
root.unmount();
|
|
22
|
+
container.remove();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* `animate={false}` must deterministically freeze every variant — including
|
|
27
|
+
* the bars'/dots' individually-animated child spans that Mantine renders
|
|
28
|
+
* without our own class names, so they can only be reached by a descendant
|
|
29
|
+
* selector, not the `data-variant`-scoped rules the size/thickness CSS uses.
|
|
30
|
+
*/
|
|
31
|
+
describe("Loader animate prop", () => {
|
|
32
|
+
it.each(["oval", "bars", "dots"] as const)(
|
|
33
|
+
"freezes every animated element for variant=%s when animate is false, and animates by default",
|
|
34
|
+
(variant) => {
|
|
35
|
+
const animated = mount(
|
|
36
|
+
<MantineProvider defaultColorScheme="light">
|
|
37
|
+
<Loader variant={variant} />
|
|
38
|
+
</MantineProvider>,
|
|
39
|
+
);
|
|
40
|
+
const frozen = mount(
|
|
41
|
+
<MantineProvider defaultColorScheme="light">
|
|
42
|
+
<Loader variant={variant} animate={false} />
|
|
43
|
+
</MantineProvider>,
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const animatedRoot =
|
|
48
|
+
animated.container.querySelector("[data-variant]")!;
|
|
49
|
+
const frozenRoot = frozen.container.querySelector("[data-variant]")!;
|
|
50
|
+
|
|
51
|
+
const animatedTargets = [
|
|
52
|
+
animatedRoot,
|
|
53
|
+
...Array.from(animatedRoot.querySelectorAll("*")),
|
|
54
|
+
];
|
|
55
|
+
const frozenTargets = [
|
|
56
|
+
frozenRoot,
|
|
57
|
+
...Array.from(frozenRoot.querySelectorAll("*")),
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
// At least one element (or the root's own ::after, for oval) actually
|
|
61
|
+
// animates by default — otherwise this test would trivially pass.
|
|
62
|
+
const hasAnimation = (el: Element, pseudo?: string) =>
|
|
63
|
+
getComputedStyle(el, pseudo).animationName !== "none";
|
|
64
|
+
const animatedHasMotion =
|
|
65
|
+
animatedTargets.some((el) => hasAnimation(el)) ||
|
|
66
|
+
hasAnimation(animatedRoot, "::after");
|
|
67
|
+
expect(animatedHasMotion).toBe(true);
|
|
68
|
+
|
|
69
|
+
// Frozen: nothing animates, root included, pseudo-element included.
|
|
70
|
+
for (const el of frozenTargets) {
|
|
71
|
+
expect(getComputedStyle(el).animationName).toBe("none");
|
|
72
|
+
}
|
|
73
|
+
expect(getComputedStyle(frozenRoot, "::after").animationName).toBe(
|
|
74
|
+
"none",
|
|
75
|
+
);
|
|
76
|
+
} finally {
|
|
77
|
+
unmount(animated);
|
|
78
|
+
unmount(frozen);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
});
|
|
@@ -5,6 +5,16 @@
|
|
|
5
5
|
);
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
/* `animate={false}` — freezes every variant's animation (the oval's own
|
|
9
|
+
* spinning ::after, and the bars'/dots' individually-animated child spans)
|
|
10
|
+
* so the loader renders deterministically, e.g. for a visual-regression
|
|
11
|
+
* snapshot that would otherwise diff differently every run. */
|
|
12
|
+
.root[data-animate="false"],
|
|
13
|
+
.root[data-animate="false"] *,
|
|
14
|
+
.root[data-animate="false"]::after {
|
|
15
|
+
animation: none !important;
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
/* data-size mapping */
|
|
9
19
|
|
|
10
20
|
/* SMALL */
|