@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.
Files changed (42) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/index.d.ts +10 -0
  3. package/dist/mantine-adapter.cjs +1 -1
  4. package/dist/mantine-adapter.cjs.map +1 -1
  5. package/dist/mantine-adapter.css +1 -1
  6. package/dist/mantine-adapter.js +634 -617
  7. package/dist/mantine-adapter.js.map +1 -1
  8. package/package.json +5 -4
  9. package/src/components/Accordion/Accordion.module.css +17 -8
  10. package/src/components/AutoComplete/AutoComplete.module.css +9 -2
  11. package/src/components/Chip/Chip.module.css +18 -0
  12. package/src/components/DatePicker/DatePicker.module.css +9 -2
  13. package/src/components/Dropdown/Dropdown.module.css +9 -2
  14. package/src/components/FileInput/FileInput.module.css +12 -0
  15. package/src/components/FileUpload/FileUpload.module.css +6 -1
  16. package/src/components/FormControlLayout/FormControlLayout.module.css +12 -0
  17. package/src/components/FormControlLayout/FormControlLayout.stories.tsx +36 -0
  18. package/src/components/HoverCard/HoverCard.module.css +6 -1
  19. package/src/components/Label/Label.stories.tsx +63 -54
  20. package/src/components/Link/Link.module.css +5 -0
  21. package/src/components/Loader/Loader.animate.dom.test.tsx +82 -0
  22. package/src/components/Loader/Loader.module.css +10 -0
  23. package/src/components/Loader/Loader.stories.tsx +20 -0
  24. package/src/components/Loader/Loader.tsx +8 -1
  25. package/src/components/Modal/Modal.module.css +6 -2
  26. package/src/components/NumberInput/NumberInput.module.css +9 -2
  27. package/src/components/SegmentedControl/SegmentedControl.module.css +6 -0
  28. package/src/components/Table/TABLE_IMPLEMENTATION_NOTES.md +11 -0
  29. package/src/components/Table/Table.module.css +14 -0
  30. package/src/components/Table/Table.stories.tsx +1 -1
  31. package/src/components/Table/Table.tsx +9 -1
  32. package/src/components/Table/USAGE.md +4 -2
  33. package/src/components/TextArea/TextArea.module.css +9 -2
  34. package/src/components/TextField/TextField.module.css +9 -2
  35. package/src/components/TimePicker/TIMEPICKER_IMPLEMENTATION_NOTES.md +7 -1
  36. package/src/components/TimePicker/TimePicker.module.css +44 -0
  37. package/src/components/TimePicker/TimePicker.stories.tsx +22 -0
  38. package/src/components/TimePicker/TimePicker.tsx +1 -0
  39. package/src/components/Timeline/Timeline.module.css +5 -0
  40. package/src/components/Toast/Toast.module.css +8 -0
  41. package/src/components/Tooltip/Tooltip.module.css +9 -1
  42. 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.53.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 -p 6011",
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.26.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
- /* Background is intentionally constant across collapsed/expanded (Forge doesn't apply a
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
- constant background (Mantine's native :hover CSS would otherwise show through) and use an
163
- overlay ::after structure with the generic hover tokens for the actual hover tint; no per-item
164
- hover-color/hover-opacity tokens exist anymore. */
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: 1px;
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: 1px;
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: 1px;
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 above.
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). See COMPONENT_ISSUES.md for details.
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 { TextField } from "../TextField/TextField";
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\nWhen working with form structures, render this `Label` component directly above your inputs or supply it to a component's overriding properties. The component automatically maps structural layout dimensions, dynamic alignment (`left` vs `right`), custom indicator gaps, and integrates a customized `optionalText` and `withEditIcon` flow that safely bypasses Mantine's native required asterisk mechanisms.",
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
- ...formControlArgTypes,
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: "Dynamic Label (Controls)",
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 StackedDefault: Story = {
71
+ export const Required: Story = {
54
72
  args: {
55
- children: "Email Address",
73
+ children: "Required Field",
74
+ required: true,
56
75
  },
57
- render: renderWithTextField,
58
76
  };
59
77
 
60
- export const StackedRequired: Story = {
78
+ export const RequiredSuppressesOptionalText: Story = {
61
79
  args: {
62
- children: "Primary Network Node",
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 StackedWithEditIcon: Story = {
86
+ export const WithOptionalText: Story = {
70
87
  args: {
71
- children: "Environment Variables",
72
-
73
- labelWithEditIcon: true,
88
+ children: "Bio",
89
+ labelOptionalText: "Max 100 characters",
74
90
  },
75
- render: renderWithTextField,
76
91
  };
77
92
 
78
- export const SideBySideDefault: Story = {
93
+ export const BooleanOptionalText: Story = {
79
94
  args: {
80
- children: "Status",
81
-
82
- labelSize: "default",
95
+ children: "Middle Initial",
96
+ labelOptionalText: true,
83
97
  },
84
- render: renderWithTextField,
85
98
  };
86
99
 
87
- export const RequiredSuppressesOptionalText: Story = {
100
+ export const WithEditIcon: Story = {
88
101
  args: {
89
- children: "Full Name",
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 BooleanOptionalText: Story = {
107
+ export const RequiredWithEditIcon: Story = {
98
108
  args: {
99
- children: "Middle Initial",
100
-
101
- labelOptionalText: true,
109
+ children: "Primary Network Node",
110
+ required: true,
111
+ labelWithEditIcon: true,
102
112
  },
103
- render: renderWithTextField,
104
113
  };
105
114
 
106
- export const WithEditIcon: Story = {
115
+ export const RightAligned: Story = {
107
116
  args: {
108
- children: "Shipping Address",
109
-
110
- labelWithEditIcon: true,
117
+ children: "Status",
118
+ labelAlignment: "right",
111
119
  },
112
- render: renderWithTextField,
113
120
  };
114
121
 
115
- export const LayerOneSideBySide: Story = {
122
+ export const WithActionArea: Story = {
116
123
  args: {
117
124
  children: "Configuration",
118
-
119
- labelWithEditIcon: true,
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 */