@recursica/mantine-adapter 0.19.0 → 0.21.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.
@@ -1,186 +1,231 @@
1
- import {
2
- Container,
3
- Paper,
4
- Title,
5
- Text,
6
- List,
7
- Divider,
8
- Group,
9
- Code,
10
- } from "@mantine/core";
1
+ import { Container, Card, Title, Text, Group, Stack } from "./components";
11
2
  import { Button } from "./components/Button/Button";
12
3
 
13
4
  export const OverStylingInfo = () => {
14
5
  return (
15
- <Container size="md" py="xl">
16
- <Paper withBorder p="xl" radius="md">
17
- <Title order={1} mb="md">
18
- Over Styling (<Code>overStyled</Code>)
19
- </Title>
20
- <Text mb="md">
21
- By default, all Recursica components are strictly sandboxed. This
22
- means they are protected against arbitrary styling configurations
23
- (like passing generic React <Code>style</Code> objects, custom{" "}
24
- <Code>classNames</Code> injections, or using deep Mantine layout hooks
25
- like <Code>bg</Code> and <Code>c</Code>). This strict compile-time and
26
- run-time enforcement guarantees that your design system tokens remain
27
- true across your application.
28
- </Text>
29
- <Text mb="md">
30
- However, there may be edge cases where a developer absolutely must
31
- modify a component beyond what the design tokens natively allow. For
32
- this, we provide the <strong>escape hatch</strong> property:{" "}
33
- <Code>overStyled={`{true}`}</Code>.
34
- </Text>
6
+ <Container size="md" style={{ padding: "32px 0" }}>
7
+ <Card>
8
+ <Card.Content>
9
+ <Title order={1} mb="rec-md">
10
+ Over Styling (<code>overStyled</code>)
11
+ </Title>
12
+ <Text mb="rec-md">
13
+ By default, all Recursica components are strictly sandboxed. This
14
+ means they are protected against arbitrary styling configurations
15
+ (like passing generic React <code>style</code> objects, custom{" "}
16
+ <code>classNames</code> injections, or using deep Mantine layout
17
+ hooks like <code>bg</code> and <code>c</code>). This strict
18
+ compile-time and run-time enforcement guarantees that your design
19
+ system tokens remain true across your application.
20
+ </Text>
21
+ <Text mb="rec-md">
22
+ However, there may be edge cases where a developer absolutely must
23
+ modify a component beyond what the design tokens natively allow. For
24
+ this, we provide the <strong>escape hatch</strong> property:{" "}
25
+ <code>overStyled={`{true}`}</code>.
26
+ </Text>
35
27
 
36
- <Title order={3} mb="sm">
37
- The Core Philosophy
38
- </Title>
39
- <Text mb="sm">
40
- **You should not over-style components.** Using{" "}
41
- <Code>overStyled</Code> explicitly signifies that you are breaking
42
- design system rules.
43
- </Text>
44
- <List mb="xl" type="ordered">
45
- <List.Item>
46
- <strong>Technical Debt:</strong> If over-styling is required, it
47
- should be treated as a short-term workaround. Ideally, the component
48
- will be refactored once the required layouts or variants are
49
- officially integrated into the core Recursica component library.
50
- </List.Item>
51
- <List.Item>
52
- <strong>Auditing & Searching:</strong> Because this pattern creates
53
- technical debt, we enforce the explicit <Code>overStyled</Code>{" "}
54
- boolean. This provides a highly auditable, easily searchable string.
55
- Product managers and engineers can quickly grep the codebase for{" "}
56
- <Code>overStyled</Code> (or the <Code>RecursicaOverStyled</Code>{" "}
57
- typings) to hunt down components that don't match standard patterns.
58
- </List.Item>
59
- <List.Item>
60
- <strong>Highly Custom Components:</strong> If your application
61
- genuinely requires massive custom layouts that the UI kit cannot
62
- support, <strong>do not hack the Recursica component</strong>.
63
- Instead, it is highly encouraged that you import the underlying
64
- primitive component directly from <Code>@mantine/core</Code> and
65
- construct your independent feature there. While you can utilize raw
66
- Recursica CSS variables on these custom components, note that they
67
- are not guaranteed to be accurately maintained as Recursica evolves.
68
- Keep strict components strict!
69
- </List.Item>
70
- </List>
28
+ <Title order={3} mb="rec-sm">
29
+ The Core Philosophy
30
+ </Title>
31
+ <Text mb="rec-sm">
32
+ **You should not over-style components.** Using{" "}
33
+ <code>overStyled</code> explicitly signifies that you are breaking
34
+ design system rules.
35
+ </Text>
36
+ <Stack
37
+ component="ol"
38
+ style={{ paddingLeft: "24px" }}
39
+ mb="rec-xl"
40
+ gap="rec-sm"
41
+ >
42
+ <li>
43
+ <Text>
44
+ <strong>Technical Debt:</strong> If over-styling is required, it
45
+ should be treated as a short-term workaround. Ideally, the
46
+ component will be refactored once the required layouts or
47
+ variants are officially integrated into the core Recursica
48
+ component library.
49
+ </Text>
50
+ </li>
51
+ <li>
52
+ <Text>
53
+ <strong>Auditing & Searching:</strong> Because this pattern
54
+ creates technical debt, we enforce the explicit{" "}
55
+ <code>overStyled</code> boolean. This provides a highly
56
+ auditable, easily searchable string. Product managers and
57
+ engineers can quickly grep the codebase for{" "}
58
+ <code>overStyled</code> (or the <code>RecursicaOverStyled</code>{" "}
59
+ typings) to hunt down components that don't match standard
60
+ patterns.
61
+ </Text>
62
+ </li>
63
+ <li>
64
+ <Text>
65
+ <strong>Highly Custom Components:</strong> If your application
66
+ genuinely requires massive custom layouts that the UI kit cannot
67
+ support, <strong>do not hack the Recursica component</strong>.
68
+ Instead, it is highly encouraged that you import the underlying
69
+ primitive component directly from <code>@mantine/core</code> and
70
+ construct your independent feature there. While you can utilize
71
+ raw Recursica CSS variables on these custom components, note
72
+ that they are not guaranteed to be accurately maintained as
73
+ Recursica evolves. Keep strict components strict!
74
+ </Text>
75
+ </li>
76
+ </Stack>
71
77
 
72
- <Divider mb="xl" />
78
+ <Stack
79
+ style={{ height: 1, backgroundColor: "#eaeaea" }}
80
+ mb="rec-xl"
81
+ />
73
82
 
74
- <Title order={3} mb="sm">
75
- Permitted Layout Properties
76
- </Title>
77
- <Text mb="sm">
78
- Unlike deep styling bounds (colors, typography, padding, dimensions),
79
- external <strong>layout spacing properties</strong> like Margins (
80
- <Code>m</Code>, <Code>mt</Code>, <Code>mb</Code>, <Code>mx</Code>) are
81
- safely <strong>permitted by default</strong>. This allows integrators
82
- to structurally compose components alongside siblings without
83
- breaching internal token boundaries.
84
- </Text>
85
- <Text mb="xl">
86
- When using layout properties, you have the flexibility to use either
87
- ecosystem seamlessly:
88
- </Text>
89
- <List mb="md" type="ordered">
90
- <List.Item>
91
- <strong>Mantine Core Values:</strong> Passing standard Mantine sizes
92
- (like <Code>mt="md"</Code>) passes straight through to Mantine
93
- natively, allowing you to interface completely normally with a
94
- parent application's existing Mantine Theme setup that might fall
95
- outside Recursica's scope.
96
- </List.Item>
97
- <List.Item>
98
- <strong>Recursica Strict Tokens:</strong> Passing our custom
99
- prefixed tokens (like <Code>mt="rec-md"</Code>) signals our internal
100
- layout interceptor to securely translate the value directly to our
101
- native <Code>recursica_brand_dimensions</Code> CSS variables. This
102
- ensures strict design token measurements while sharing the exact
103
- same prop interface!
104
- </List.Item>
105
- </List>
106
- <Text mb="sm" fw={500}>
107
- Available Recursica Layout Tokens:
108
- </Text>
109
- <List mb="xl" type="unordered">
110
- <List.Item>
111
- <Code>rec-none</Code> (0px limit)
112
- </List.Item>
113
- <List.Item>
114
- <Code>rec-sm</Code> (0.5x scaling)
115
- </List.Item>
116
- <List.Item>
117
- <Code>rec-default</Code> (1.0x scaling)
118
- </List.Item>
119
- <List.Item>
120
- <Code>rec-md</Code> (1.5x scaling)
121
- </List.Item>
122
- <List.Item>
123
- <Code>rec-lg</Code> (2.0x scaling)
124
- </List.Item>
125
- <List.Item>
126
- <Code>rec-xl</Code> (3.0x scaling)
127
- </List.Item>
128
- <List.Item>
129
- <Code>rec-2xl</Code> (4.0x scaling)
130
- </List.Item>
131
- </List>
83
+ <Title order={3} mb="rec-sm">
84
+ Permitted Layout Properties
85
+ </Title>
86
+ <Text mb="rec-sm">
87
+ Unlike deep styling bounds (colors, typography, padding,
88
+ dimensions), external <strong>layout spacing properties</strong>{" "}
89
+ like Margins (<code>m</code>, <code>mt</code>, <code>mb</code>,{" "}
90
+ <code>mx</code>) are safely <strong>permitted by default</strong>.
91
+ This allows integrators to structurally compose components alongside
92
+ siblings without breaching internal token boundaries.
93
+ </Text>
94
+ <Text mb="rec-xl">
95
+ When using layout properties, you have the flexibility to use either
96
+ ecosystem seamlessly:
97
+ </Text>
98
+ <Stack
99
+ component="ol"
100
+ style={{ paddingLeft: "24px" }}
101
+ mb="rec-md"
102
+ gap="rec-sm"
103
+ >
104
+ <li>
105
+ <Text>
106
+ <strong>Mantine Core Values:</strong> Passing standard Mantine
107
+ sizes (like <code>mt="md"</code>) passes straight through to
108
+ Mantine natively, allowing you to interface completely normally
109
+ with a parent application's existing Mantine Theme setup that
110
+ might fall outside Recursica's scope.
111
+ </Text>
112
+ </li>
113
+ <li>
114
+ <Text>
115
+ <strong>Recursica Strict Tokens:</strong> Passing our custom
116
+ prefixed tokens (like <code>mt="rec-md"</code>) signals our
117
+ internal layout interceptor to securely translate the value
118
+ directly to our native <code>recursica_brand_dimensions</code>{" "}
119
+ CSS variables. This ensures strict design token measurements
120
+ while sharing the exact same prop interface!
121
+ </Text>
122
+ </li>
123
+ </Stack>
124
+ <Text mb="rec-sm" overStyled fw={500}>
125
+ Available Recursica Layout Tokens:
126
+ </Text>
127
+ <Stack
128
+ component="ul"
129
+ style={{ paddingLeft: "24px" }}
130
+ mb="rec-xl"
131
+ gap="rec-none"
132
+ >
133
+ <li>
134
+ <Text>
135
+ <code>rec-none</code> (0px limit)
136
+ </Text>
137
+ </li>
138
+ <li>
139
+ <Text>
140
+ <code>rec-sm</code> (0.5x scaling)
141
+ </Text>
142
+ </li>
143
+ <li>
144
+ <Text>
145
+ <code>rec-default</code> (1.0x scaling)
146
+ </Text>
147
+ </li>
148
+ <li>
149
+ <Text>
150
+ <code>rec-md</code> (1.5x scaling)
151
+ </Text>
152
+ </li>
153
+ <li>
154
+ <Text>
155
+ <code>rec-lg</code> (2.0x scaling)
156
+ </Text>
157
+ </li>
158
+ <li>
159
+ <Text>
160
+ <code>rec-xl</code> (3.0x scaling)
161
+ </Text>
162
+ </li>
163
+ <li>
164
+ <Text>
165
+ <code>rec-2xl</code> (4.0x scaling)
166
+ </Text>
167
+ </li>
168
+ </Stack>
132
169
 
133
- <Divider mb="xl" />
170
+ <Stack
171
+ style={{ height: 1, backgroundColor: "#eaeaea" }}
172
+ mb="rec-xl"
173
+ />
134
174
 
135
- <Title order={3} mb="sm">
136
- Primitive Layout Components Exemption
137
- </Title>
138
- <Text mb="sm">
139
- Unlike complex UI components (Buttons, Tabs, Inputs) which are
140
- strictly protected, <strong>Primitive Layout Components</strong> (
141
- <Code>Flex</Code>,<Code>Stack</Code>, <Code>Group</Code>,{" "}
142
- <Code>Container</Code>) are entirely exempt from the{" "}
143
- <Code>RecursicaOverStyled</Code> gatekeeper.
144
- </Text>
145
- <Text mb="xl">
146
- Because the entire functional purpose of these components is
147
- structural layout composition, developers are free to pass any
148
- standard Mantine width, height, padding, margin, gap, and alignment
149
- property directly to them without needing to flag{" "}
150
- <Code>overStyled={`{true}`}</Code>. The internal custom token mapper
151
- (such as converting <Code>gap="rec-md"</Code>) is still active
152
- natively on these wrappers.
153
- </Text>
175
+ <Title order={3} mb="rec-sm">
176
+ Primitive Layout Components Exemption
177
+ </Title>
178
+ <Text mb="rec-sm">
179
+ Unlike complex UI components (Buttons, Tabs, Inputs) which are
180
+ strictly protected, <strong>Primitive Layout Components</strong> (
181
+ <code>Flex</code>, <code>Stack</code>, <code>Group</code>,{" "}
182
+ <code>Container</code>) are entirely exempt from the{" "}
183
+ <code>RecursicaOverStyled</code> gatekeeper.
184
+ </Text>
185
+ <Text mb="rec-xl">
186
+ Because the entire functional purpose of these components is
187
+ structural layout composition, developers are free to pass any
188
+ standard Mantine width, height, padding, margin, gap, and alignment
189
+ property directly to them without needing to flag{" "}
190
+ <code>overStyled={`{true}`}</code>. The internal custom token mapper
191
+ (such as converting <code>gap="rec-md"</code>) is still active
192
+ natively on these wrappers.
193
+ </Text>
154
194
 
155
- <Divider mb="xl" />
195
+ <Stack
196
+ style={{ height: 1, backgroundColor: "#eaeaea" }}
197
+ mb="rec-xl"
198
+ />
156
199
 
157
- <Title order={3} mb="md">
158
- Live Example
159
- </Title>
160
- <Text mb="xl">
161
- Below is a side-by-side comparison. The first is a standard Recursica
162
- Button protected by the design tokens mapping. The second flagrantly
163
- forces <Code>overStyled={`{true}`}</Code>, allowing Mantine's native
164
- styling generics to punch right through the sandbox layout.
165
- </Text>
200
+ <Title order={3} mb="rec-md">
201
+ Live Example
202
+ </Title>
203
+ <Text mb="rec-xl">
204
+ Below is a side-by-side comparison. The first is a standard
205
+ Recursica Button protected by the design tokens mapping. The second
206
+ flagrantly forces <code>overStyled={`{true}`}</code>, allowing
207
+ Mantine's native styling generics to punch right through the sandbox
208
+ layout.
209
+ </Text>
166
210
 
167
- <Group gap="xl">
168
- <div>
169
- <Text size="sm" c="dimmed" mb="xs">
170
- Strict Baseline (Default)
171
- </Text>
172
- <Button variant="solid">Standard UI Kit Button</Button>
173
- </div>
174
- <div>
175
- <Text size="sm" c="dimmed" mb="xs">
176
- overStyled={`{true}`}
177
- </Text>
178
- <Button overStyled={true} bg="pink" c="black" radius="xl">
179
- Unsafe Pink Marketing Button
180
- </Button>
181
- </div>
182
- </Group>
183
- </Paper>
211
+ <Group gap="rec-xl">
212
+ <Stack gap="rec-sm">
213
+ <Text overStyled size="sm" c="dimmed">
214
+ Strict Baseline (Default)
215
+ </Text>
216
+ <Button variant="solid">Standard UI Kit Button</Button>
217
+ </Stack>
218
+ <Stack gap="rec-sm">
219
+ <Text overStyled size="sm" c="dimmed">
220
+ overStyled={`{true}`}
221
+ </Text>
222
+ <Button overStyled={true} bg="pink" c="black" radius="xl">
223
+ Unsafe Pink Marketing Button
224
+ </Button>
225
+ </Stack>
226
+ </Group>
227
+ </Card.Content>
228
+ </Card>
184
229
  </Container>
185
230
  );
186
231
  };
@@ -6,6 +6,7 @@
6
6
  overflow: hidden; /* Truncation bounding box */
7
7
  position: relative;
8
8
  transition: all 0.2s ease;
9
+ width: fit-content; /* Prevent stretching in flex columns */
9
10
 
10
11
  /* Shared Defaults */
11
12
  font-family: var(
@@ -35,13 +36,16 @@
35
36
  }
36
37
 
37
38
  /* Internal Mantine reset & layout logic for Truncation */
38
- .root > * {
39
+ .root > *:not(.loader) {
39
40
  min-width: 0;
40
- width: 100%;
41
41
  position: relative;
42
42
  z-index: 1;
43
43
  }
44
44
 
45
+ .loader {
46
+ /* Mantine absolutely positions this; we just need a hook to exclude it from the child reset */
47
+ }
48
+
45
49
  .label {
46
50
  display: flex;
47
51
  align-items: center;
@@ -52,7 +56,6 @@
52
56
  .labelText {
53
57
  display: block;
54
58
  min-width: 0;
55
- width: 100%;
56
59
  overflow: hidden;
57
60
  text-overflow: ellipsis;
58
61
  white-space: nowrap;
@@ -76,6 +79,11 @@
76
79
  --recursica_ui-kit_components_button_variants_sizes_default_properties_horizontal-padding
77
80
  );
78
81
  }
82
+ .root[data-size="default"] .labelText {
83
+ max-width: var(
84
+ --recursica_ui-kit_components_button_variants_sizes_default_properties_max-label-width
85
+ );
86
+ }
79
87
  .root[data-size="small"] {
80
88
  border-radius: var(
81
89
  --recursica_ui-kit_components_button_variants_sizes_small_properties_border-radius
@@ -111,6 +119,11 @@
111
119
  )
112
120
  );
113
121
  }
122
+ .root[data-size="small"] .labelText {
123
+ max-width: var(
124
+ --recursica_ui-kit_components_button_variants_sizes_small_properties_max-label-width
125
+ );
126
+ }
114
127
 
115
128
  /* Icon Resets */
116
129
  .iconWrapper {
@@ -167,8 +180,16 @@
167
180
  }
168
181
 
169
182
  /* Disabled */
170
- .root:disabled {
171
- opacity: var(--recursica_brand_states_disabled);
183
+ .root[data-size="default"]:disabled {
184
+ opacity: var(
185
+ --recursica_ui-kit_components_button_variants_sizes_default_properties_disabled-opacity
186
+ );
187
+ cursor: not-allowed;
188
+ }
189
+ .root[data-size="small"]:disabled {
190
+ opacity: var(
191
+ --recursica_ui-kit_components_button_variants_sizes_small_properties_disabled-opacity
192
+ );
172
193
  cursor: not-allowed;
173
194
  }
174
195
 
@@ -20,6 +20,30 @@ const meta: Meta<ButtonStoryProps> = {
20
20
  options: ["default", "small"],
21
21
  description: "The size of the button",
22
22
  },
23
+ loading: {
24
+ control: "boolean",
25
+ description: "Sets the button to a loading state",
26
+ },
27
+ useRecursicaLoader: {
28
+ control: "boolean",
29
+ description:
30
+ "Use the Recursica Loader component instead of the default Mantine loader",
31
+ },
32
+ loaderVariant: {
33
+ control: "select",
34
+ options: ["oval", "bars", "dots"],
35
+ description: "The visual variant of the Recursica Loader",
36
+ },
37
+ loaderSize: {
38
+ control: "select",
39
+ options: [undefined, "sm", "md", "lg", "small", "default", "large"],
40
+ description: "The size variant for the loader",
41
+ },
42
+ },
43
+ args: {
44
+ useRecursicaLoader: true,
45
+ loaderVariant: "oval",
46
+ loaderSize: undefined,
23
47
  },
24
48
  };
25
49
 
@@ -112,3 +136,34 @@ export const PolymorphicAsLink: Story = {
112
136
  target: "_blank",
113
137
  },
114
138
  };
139
+
140
+ export const TruncatedLabel: Story = {
141
+ args: {
142
+ children:
143
+ "This is an exceptionally long button label designed to demonstrate how the component handles text overflow by applying an ellipsis rather than breaking the layout or wrapping to multiple lines.",
144
+ variant: "solid",
145
+ size: "default",
146
+ },
147
+ render: (args: ButtonStoryProps) => (
148
+ <div style={{ maxWidth: "250px" }}>
149
+ <Button {...args} />
150
+ </div>
151
+ ),
152
+ };
153
+
154
+ export const Loading: Story = {
155
+ args: {
156
+ children: "Saving Changes",
157
+ variant: "solid",
158
+ size: "default",
159
+ loading: true,
160
+ },
161
+ parameters: {
162
+ docs: {
163
+ description: {
164
+ story:
165
+ "When `loading={true}` is applied, the Button injects the Recursica `<Loader />` component. Per Recursica design rules, placing a Button in a loading state automatically forces the `disabled={true}` state on the underlying element. This ensures the button immediately receives the brand theme disabled opacities without relying solely on semantic logic.",
166
+ },
167
+ },
168
+ },
169
+ };
@@ -1,4 +1,6 @@
1
1
  import React, { forwardRef } from "react";
2
+ import { Loader } from "../Loader/Loader";
3
+ import type { RecursicaLoaderProps } from "../Loader/Loader";
2
4
  import {
3
5
  Button as MantineButton,
4
6
  type ButtonProps as MantineButtonProps,
@@ -11,9 +13,18 @@ import {
11
13
  import styles from "./Button.module.css";
12
14
 
13
15
  export interface RecursicaButtonProps {
16
+ /** The visual style variant of the button */
14
17
  variant?: "solid" | "outline" | "text";
18
+ /** The size of the button */
15
19
  size?: "default" | "small";
20
+ /** An optional icon element to display to the left of the button text. Replaces Mantine's leftSection. */
16
21
  icon?: React.ReactNode;
22
+ /** Which Recursica Loader variant to use */
23
+ loaderVariant?: RecursicaLoaderProps["variant"];
24
+ /** The size variant for the loader */
25
+ loaderSize?: RecursicaLoaderProps["size"];
26
+ /** Whether to use the Recursica loader or fallback to the Mantine loader */
27
+ useRecursicaLoader?: boolean;
17
28
  }
18
29
 
19
30
  export type ButtonProps = RecursicaOverStyled<
@@ -34,6 +45,9 @@ const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
34
45
  icon,
35
46
  children,
36
47
  overStyled = false,
48
+ loaderVariant = "oval",
49
+ loaderSize,
50
+ useRecursicaLoader = true,
37
51
  ...rest
38
52
  },
39
53
  ref,
@@ -75,6 +89,7 @@ const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
75
89
  root: styles.root,
76
90
  section: styles.section,
77
91
  label: styles.label,
92
+ loader: styles.loader,
78
93
  };
79
94
 
80
95
  const classNamesProp = restRecord.classNames;
@@ -94,6 +109,21 @@ const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
94
109
  ? `${styles.root} ${classNameProp}`
95
110
  : styles.root;
96
111
 
112
+ const userLoaderProps = restRecord.loaderProps as
113
+ | Record<string, any>
114
+ | undefined;
115
+
116
+ const resolvedLoaderSize =
117
+ loaderSize ?? (size === "small" ? "small" : "default");
118
+
119
+ let mergedLoaderProps = userLoaderProps;
120
+ if (useRecursicaLoader) {
121
+ mergedLoaderProps = {
122
+ children: <Loader variant={loaderVariant} size={resolvedLoaderSize} />,
123
+ ...userLoaderProps,
124
+ };
125
+ }
126
+
97
127
  return (
98
128
  <MantineButton
99
129
  ref={ref}
@@ -101,6 +131,7 @@ const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
101
131
  classNames={mergedClassNames}
102
132
  variant={mapVariant[variant]}
103
133
  size={mapSize[size]}
134
+ loaderProps={mergedLoaderProps}
104
135
  leftSection={
105
136
  icon != null ? (
106
137
  <span className={styles.iconWrapper} aria-hidden>
@@ -112,6 +143,7 @@ const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
112
143
  data-size={size}
113
144
  {...(isIconOnly ? { "data-icon-only": "" } : {})}
114
145
  {...sanitizedProps}
146
+ disabled={!!restRecord.disabled || !!restRecord.loading}
115
147
  >
116
148
  <span className={styles.labelText}>{children}</span>
117
149
  </MantineButton>
@@ -43,3 +43,19 @@ Mantine's `.mantine-Button-label` flex centering breaks primitive truncation log
43
43
  ## Disabled state: brand theme opacity (implicit)
44
44
 
45
45
  **Decision:** The UI kit enforces global brand theme disabled opacities. The `.root:disabled` logic implicitly overrides visibility locally via `var(--recursica_brand_states_disabled)`.
46
+
47
+ ---
48
+
49
+ ## Loader color contrast
50
+
51
+ **Decision:** When a Button is in a loading state, the `Recursica Loader` component is injected. The `Loader` component strictly defines its own colors and styles per variant, meaning it does not inherit the text color (`currentColor`) from the Button.
52
+
53
+ **Constraint:** This can lead to contrast issues (e.g., a blue dots loader inside a solid blue button). Design has explicitly decided not to address this at the moment. As such, developers using the `loading` prop must be aware that the loader's color is fixed by its internal tokens, not by the button's context.
54
+
55
+ ---
56
+
57
+ ## Loading state enforces disabled state
58
+
59
+ **Decision:** When `loading={true}` is passed to the Button, the component explicitly forces `disabled={true}` natively on the underlying element.
60
+
61
+ **Implementation:** This ensures that loading buttons automatically inherit the brand theme disabled opacities (via the `:disabled` CSS pseudo-class) rather than relying solely on Mantine's native `data-disabled` dataset logic, which may not trigger the strict visual fade required by the Recursica design system.