@recursica/mantine-adapter 0.55.0 → 0.55.2
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 +26 -0
- package/dist/index.d.ts +34 -9
- package/dist/mantine-adapter.cjs +2 -2
- package/dist/mantine-adapter.cjs.map +1 -1
- package/dist/mantine-adapter.css +1 -1
- package/dist/mantine-adapter.js +1767 -1728
- package/dist/mantine-adapter.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Accordion/Accordion.stories.tsx +0 -24
- package/src/components/AssistiveElement/AssistiveElement.stories.tsx +3 -9
- package/src/components/Avatar/Avatar.stories.tsx +0 -15
- package/src/components/Breadcrumb/BREADCRUMB_IMPLEMENTATION_NOTES.md +33 -0
- package/src/components/Breadcrumb/Breadcrumb.module.css +20 -0
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +55 -5
- package/src/components/Breadcrumb/Breadcrumb.tsx +17 -3
- package/src/components/Breadcrumb/markCurrentPageItem.test.ts +49 -0
- package/src/components/Button/Button.module.css +16 -0
- package/src/components/Button/Button.tsx +14 -1
- package/src/components/Button/IMPLEMENTATION_NOTES.md +31 -5
- package/src/components/DatePicker/DATEPICKER_IMPLEMENTATION_NOTES.md +2 -1
- package/src/components/DatePicker/DatePicker.tsx +22 -8
- package/src/components/Dropdown/Dropdown.stories.tsx +2 -8
- package/src/components/Dropdown/Dropdown.tsx +2 -1
- package/src/components/FormControlLayout/FormControlLayout.stories.tsx +1 -1
- package/src/components/Grid/GRID_IMPLEMENTATION_NOTES.md +9 -0
- package/src/components/Grid/Grid.stories.tsx +24 -0
- package/src/components/Label/Label.stories.tsx +1 -1
- package/src/components/Link/IMPLEMENTATION_NOTES.md +1 -1
- package/src/components/Link/Link.module.css +20 -0
- package/src/components/NumberInput/NUMBER_INPUT_IMPLEMENTATION_NOTES.md +6 -0
- package/src/components/NumberInput/NumberInput.module.css +11 -0
- package/src/components/Panel/Panel.tsx +1 -1
- package/src/components/Stepper/IMPLEMENTATION_NOTES.md +28 -0
- package/src/components/Stepper/Stepper.module.css +6 -6
- package/src/components/Tabs/IMPLEMENTATION_NOTES.md +27 -0
- package/src/components/Tabs/Tabs.module.css +30 -9
- package/src/components/TimePicker/TIMEPICKER_IMPLEMENTATION_NOTES.md +31 -0
- package/src/components/TimePicker/TimePicker.module.css +30 -3
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.55.
|
|
16
|
+
"version": "0.55.2",
|
|
17
17
|
"type": "module",
|
|
18
18
|
"main": "./dist/mantine-adapter.cjs",
|
|
19
19
|
"module": "./dist/mantine-adapter.js",
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
"vitest": "^3.2.4"
|
|
104
104
|
},
|
|
105
105
|
"dependencies": {
|
|
106
|
-
"@recursica/adapter-common": "^0.28.
|
|
106
|
+
"@recursica/adapter-common": "^0.28.2"
|
|
107
107
|
},
|
|
108
108
|
"peerDependencies": {
|
|
109
109
|
"@mantine/core": "^8.0.0",
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
2
|
import { Accordion } from "./Accordion";
|
|
3
|
-
import { Layer } from "@recursica/adapter-common";
|
|
4
3
|
|
|
5
4
|
const SVGIcon = () => (
|
|
6
5
|
<svg
|
|
@@ -188,26 +187,3 @@ export const Disabled: StoryObj<typeof Accordion> = {
|
|
|
188
187
|
);
|
|
189
188
|
},
|
|
190
189
|
};
|
|
191
|
-
|
|
192
|
-
/** Demonstrates the component nested inside a non-default layer — the one case where an
|
|
193
|
-
* explicit `<Layer>` wrap belongs in a story (see COMPONENT_STORYBOOK_GUIDE.md §9). */
|
|
194
|
-
export const LayerOne: StoryObj<typeof Accordion> = {
|
|
195
|
-
render: () => {
|
|
196
|
-
return (
|
|
197
|
-
<Layer layer={1} style={{ padding: "24px" }}>
|
|
198
|
-
<Accordion defaultValue="demo" chevron={<ChevronIcon />}>
|
|
199
|
-
<Accordion.Item value="demo">
|
|
200
|
-
<Accordion.Control leftIcon={<SVGIcon />}>
|
|
201
|
-
Layer 1 Render Engine
|
|
202
|
-
</Accordion.Control>
|
|
203
|
-
<Accordion.Panel>
|
|
204
|
-
This Accordion dynamically updates its colors, borders, and
|
|
205
|
-
typography variables because it is wrapped securely by the
|
|
206
|
-
simulated `data-recursica-layer="1"`.
|
|
207
|
-
</Accordion.Panel>
|
|
208
|
-
</Accordion.Item>
|
|
209
|
-
</Accordion>
|
|
210
|
-
</Layer>
|
|
211
|
-
);
|
|
212
|
-
},
|
|
213
|
-
};
|
|
@@ -38,9 +38,7 @@ export const DefaultHelp: Story = {
|
|
|
38
38
|
},
|
|
39
39
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
|
|
40
40
|
render: ({ withLayer, layer, ...args }: any) => (
|
|
41
|
-
<
|
|
42
|
-
<AssistiveElement {...args} />
|
|
43
|
-
</Layer>
|
|
41
|
+
<AssistiveElement {...args} />
|
|
44
42
|
),
|
|
45
43
|
};
|
|
46
44
|
|
|
@@ -53,9 +51,7 @@ export const ErrorState: Story = {
|
|
|
53
51
|
},
|
|
54
52
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
|
|
55
53
|
render: ({ withLayer, layer, ...args }: any) => (
|
|
56
|
-
<
|
|
57
|
-
<AssistiveElement {...args} />
|
|
58
|
-
</Layer>
|
|
54
|
+
<AssistiveElement {...args} />
|
|
59
55
|
),
|
|
60
56
|
};
|
|
61
57
|
|
|
@@ -68,8 +64,6 @@ export const NoIconHelp: Story = {
|
|
|
68
64
|
},
|
|
69
65
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
|
|
70
66
|
render: ({ withLayer, layer, ...args }: any) => (
|
|
71
|
-
<
|
|
72
|
-
<AssistiveElement {...args} />
|
|
73
|
-
</Layer>
|
|
67
|
+
<AssistiveElement {...args} />
|
|
74
68
|
),
|
|
75
69
|
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
3
|
import { Avatar } from "./Avatar";
|
|
4
|
-
import { Layer } from "@recursica/adapter-common";
|
|
5
4
|
|
|
6
5
|
type AvatarStoryProps = React.ComponentProps<typeof Avatar>;
|
|
7
6
|
|
|
@@ -91,17 +90,3 @@ export const IconSmallGhost: Story = {
|
|
|
91
90
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
|
|
92
91
|
render: ({ withLayer, layer, ...args }: any) => <Avatar {...args} />,
|
|
93
92
|
};
|
|
94
|
-
|
|
95
|
-
export const LayerOneOutline: Story = {
|
|
96
|
-
args: {
|
|
97
|
-
children: "L1",
|
|
98
|
-
variant: "outline",
|
|
99
|
-
size: "default",
|
|
100
|
-
},
|
|
101
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
|
|
102
|
-
render: ({ withLayer, layer, ...args }: any) => (
|
|
103
|
-
<Layer layer={1} style={{ padding: "24px", display: "inline-block" }}>
|
|
104
|
-
<Avatar {...args} />
|
|
105
|
-
</Layer>
|
|
106
|
-
),
|
|
107
|
-
};
|
|
@@ -12,3 +12,36 @@ We attach `gap` to `.root` directly within `.module.css`. Mantine's inner `separ
|
|
|
12
12
|
|
|
13
13
|
We preserve standard hierarchical composition based on Mantine's defaults. The component natively accepts typical anchor tags as children without wrapping them in structural spans, allowing developers to utilize `Link` wrappers as needed contextually.
|
|
14
14
|
The separator character is inherited natively from Mantine's default configuration (`/`) unless overridden by passing `separator={<Icon />}` to the component props.
|
|
15
|
+
|
|
16
|
+
## Current-page Crumb: Best-effort Enforcement, Not a Guarantee
|
|
17
|
+
|
|
18
|
+
**The last crumb should always be plain, non-interactive text — a `<span>`, not a `Link`.** It
|
|
19
|
+
represents the current page; per standard breadcrumb UX and the ARIA breadcrumb pattern, it
|
|
20
|
+
shouldn't be clickable. Passing a `Link` (or an anchor) for the last item is a misuse of the
|
|
21
|
+
component, even though `Breadcrumb.tsx` tries to correct for it — see below.
|
|
22
|
+
|
|
23
|
+
Breadcrumb intentionally keeps the flexible Mantine `children`-as-anchors interface (unlike BEAM's
|
|
24
|
+
`Breadcrumb.Item`, which renders its own `Link` internally and can't accept a caller-supplied
|
|
25
|
+
router-aware Link or `onClick`). That flexibility means Breadcrumb.tsx can't fully own what the
|
|
26
|
+
last item renders as, so it does what it can via `markCurrentPageItem` (`@recursica/adapter-common`):
|
|
27
|
+
|
|
28
|
+
- Adds `aria-current="page"` to the last child, always.
|
|
29
|
+
- If that child looks interactive (has `href` or `onClick`), strips both and sets `tabIndex={-1}`.
|
|
30
|
+
- In dev, warns to the console when it had to strip anything — a signal the caller should switch
|
|
31
|
+
that item to plain text instead of relying on this fallback.
|
|
32
|
+
|
|
33
|
+
Backed up by a CSS rule targeting `[aria-current="page"]` (Breadcrumb.module.css) that resets
|
|
34
|
+
color/text-decoration and sets `pointer-events: none`, and sets `font-family`/`font-size` to Link's own tokens
|
|
35
|
+
(per product decision: the current item should typographically match Link, just without Link's
|
|
36
|
+
color/decoration/interactivity).
|
|
37
|
+
|
|
38
|
+
**Why this isn't a real guarantee:** `cloneElement` can only override props an element actually
|
|
39
|
+
reads. A custom Link component (e.g. React Router's `<Link to="...">`) that navigates from its own
|
|
40
|
+
internal handler, independent of a passed-through `onClick`, will still navigate — stripping
|
|
41
|
+
`onClick`/`href` here doesn't touch that internal logic, and `pointer-events: none` only blocks
|
|
42
|
+
mouse activation, not keyboard/Enter on a focusable custom element. Treat all of the above as a
|
|
43
|
+
safety net for the common case (plain anchors, our own `Link`), not a substitute for callers doing
|
|
44
|
+
the right thing and passing plain text for the last crumb.
|
|
45
|
+
|
|
46
|
+
The story's `items` convenience prop demonstrates the correct pattern: every crumb except the last
|
|
47
|
+
is wrapped in `Link`, and the last is a plain `<span>`.
|
|
@@ -26,3 +26,23 @@
|
|
|
26
26
|
/* Neutralize Mantine's built-in margin-inline (--mantine-spacing-xs default) so .root's gap alone controls spacing */
|
|
27
27
|
margin-inline: 0;
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
/* Current-page item (last crumb): Breadcrumb.tsx (via markCurrentPageItem) marks it
|
|
31
|
+
aria-current="page". This is a style-layer backstop in case that item still renders as a Link
|
|
32
|
+
despite having its href/onClick stripped — see BREADCRUMB_IMPLEMENTATION_NOTES.md. Two classes'
|
|
33
|
+
worth of specificity beats Link's single .root color/text-decoration rule regardless of module
|
|
34
|
+
load order. pointer-events only blocks mouse activation, not a router Link's own internal
|
|
35
|
+
handler — the last crumb should really just be plain text. */
|
|
36
|
+
.root [aria-current="page"] {
|
|
37
|
+
color: inherit;
|
|
38
|
+
text-decoration: none;
|
|
39
|
+
pointer-events: none;
|
|
40
|
+
|
|
41
|
+
/* Per product decision: the current-page item should match Link's font-family/size (it's
|
|
42
|
+
typically rendered as plain text, with no font styling of its own — otherwise it falls back
|
|
43
|
+
to Mantine's default font-size) but not any of Link's other styling. */
|
|
44
|
+
font-family: var(
|
|
45
|
+
--recursica_ui-kit_components_link_properties_text_font-family
|
|
46
|
+
);
|
|
47
|
+
font-size: var(--recursica_ui-kit_components_link_properties_text_font-size);
|
|
48
|
+
}
|
|
@@ -11,6 +11,18 @@ const meta: Meta<BreadcrumbStoryProps> = {
|
|
|
11
11
|
title: "UI-Kit/Breadcrumb",
|
|
12
12
|
component: Breadcrumb,
|
|
13
13
|
tags: ["autodocs"],
|
|
14
|
+
parameters: {
|
|
15
|
+
docs: {
|
|
16
|
+
description: {
|
|
17
|
+
component:
|
|
18
|
+
"**Proper usage** (see `Default`): every crumb except the last is a `Link`; the last " +
|
|
19
|
+
"crumb — the current page — is plain text (e.g. a `<span>`), not a link. `Breadcrumb` " +
|
|
20
|
+
"does its best to neutralize a `Link` passed as the last item anyway (see " +
|
|
21
|
+
"`LastItemAsLink`), but that's a safety net, not something to rely on — see " +
|
|
22
|
+
"BREADCRUMB_IMPLEMENTATION_NOTES.md.",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
14
26
|
argTypes: {
|
|
15
27
|
children: {
|
|
16
28
|
table: {
|
|
@@ -35,11 +47,17 @@ const meta: Meta<BreadcrumbStoryProps> = {
|
|
|
35
47
|
},
|
|
36
48
|
render: ({ items, children, ...args }) => {
|
|
37
49
|
const mappedChildren = items
|
|
38
|
-
? items.map((label, index) =>
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
? items.map((label, index) =>
|
|
51
|
+
// The last crumb represents the current page, so it's plain text,
|
|
52
|
+
// not a link — matches standard breadcrumb UX.
|
|
53
|
+
index === items.length - 1 ? (
|
|
54
|
+
<span key={index}>{label}</span>
|
|
55
|
+
) : (
|
|
56
|
+
<Link href="#" key={index}>
|
|
57
|
+
{label}
|
|
58
|
+
</Link>
|
|
59
|
+
),
|
|
60
|
+
)
|
|
43
61
|
: children;
|
|
44
62
|
|
|
45
63
|
return <Breadcrumb children={mappedChildren} {...args} />;
|
|
@@ -62,3 +80,35 @@ export const CustomSeparator: Story = {
|
|
|
62
80
|
separator: "→",
|
|
63
81
|
},
|
|
64
82
|
};
|
|
83
|
+
|
|
84
|
+
export const LastItemAsLink: Story = {
|
|
85
|
+
name: "Last Item As Link (Incorrect Usage)",
|
|
86
|
+
args: {
|
|
87
|
+
// Bypass the `items` convenience prop (which always renders the last crumb as a span) to
|
|
88
|
+
// demonstrate what happens if a caller wraps every crumb, including the last, in a `Link`.
|
|
89
|
+
items: undefined,
|
|
90
|
+
children: [
|
|
91
|
+
<Link href="#" key="dashboard">
|
|
92
|
+
Dashboard
|
|
93
|
+
</Link>,
|
|
94
|
+
<Link href="#" key="settings">
|
|
95
|
+
Settings
|
|
96
|
+
</Link>,
|
|
97
|
+
<Link href="#" key="security">
|
|
98
|
+
Security
|
|
99
|
+
</Link>,
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
parameters: {
|
|
103
|
+
docs: {
|
|
104
|
+
description: {
|
|
105
|
+
story:
|
|
106
|
+
"Incorrect usage, kept as a story to prove Breadcrumb still handles it: the last " +
|
|
107
|
+
'crumb is a `Link`, but Breadcrumb adds `aria-current="page"`, strips its `href`/' +
|
|
108
|
+
"`onClick`, drops it from the tab order, and the CSS reset removes Link's color/" +
|
|
109
|
+
"underline — it renders identically to `Default`'s plain-text current item. Don't " +
|
|
110
|
+
"rely on this; pass plain text for the last crumb instead.",
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
};
|
|
@@ -12,16 +12,30 @@ import styles from "./Breadcrumb.module.css";
|
|
|
12
12
|
|
|
13
13
|
// Currently there are no Recursica prop additions specific to Breadcrumbs (like size or variant)
|
|
14
14
|
// according to recursica_ui-kit.json
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
markCurrentPageItem,
|
|
17
|
+
type RecursicaBreadcrumbProps,
|
|
18
|
+
} from "@recursica/adapter-common";
|
|
16
19
|
|
|
17
20
|
export type BreadcrumbProps = RecursicaOverStyled<
|
|
18
21
|
Omit<MantineBreadcrumbsProps, "variant" | "size"> & RecursicaBreadcrumbProps
|
|
19
22
|
>;
|
|
20
23
|
|
|
24
|
+
/**
|
|
25
|
+
* The last child should always be plain, non-interactive text (e.g. a `<span>`) — it represents
|
|
26
|
+
* the current page and shouldn't be a link. Breadcrumb tries to correct for a Link/anchor passed
|
|
27
|
+
* there anyway via `markCurrentPageItem` (aria-current, stripped href/onClick, CSS reset in
|
|
28
|
+
* Breadcrumb.module.css), but that's a best-effort safety net, not a guarantee — it can't stop a
|
|
29
|
+
* custom Link component (e.g. a router Link) that navigates from its own internal handler. See
|
|
30
|
+
* BREADCRUMB_IMPLEMENTATION_NOTES.md.
|
|
31
|
+
*/
|
|
21
32
|
export const Breadcrumb = forwardRef<HTMLDivElement, BreadcrumbProps>(
|
|
22
|
-
function Breadcrumb(
|
|
33
|
+
function Breadcrumb(
|
|
34
|
+
{ overStyled = false, separator = ">", children, ...rest },
|
|
35
|
+
ref,
|
|
36
|
+
) {
|
|
23
37
|
const sanitizedProps = filterStylingProps(
|
|
24
|
-
{ separator, ...rest },
|
|
38
|
+
{ separator, children: markCurrentPageItem(children), ...rest },
|
|
25
39
|
overStyled,
|
|
26
40
|
);
|
|
27
41
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createElement, type ReactElement } from "react";
|
|
2
|
+
import { describe, it, expect } from "vitest";
|
|
3
|
+
import { markCurrentPageItem } from "@recursica/adapter-common";
|
|
4
|
+
|
|
5
|
+
type Props = Record<string, unknown>;
|
|
6
|
+
|
|
7
|
+
describe("markCurrentPageItem", () => {
|
|
8
|
+
it("marks the last item aria-current=page and leaves earlier items untouched", () => {
|
|
9
|
+
const items = markCurrentPageItem([
|
|
10
|
+
createElement("a", { href: "/one" }, "One"),
|
|
11
|
+
createElement("a", { href: "/two" }, "Two"),
|
|
12
|
+
createElement("span", {}, "Three"),
|
|
13
|
+
]) as ReactElement<Props>[];
|
|
14
|
+
|
|
15
|
+
expect(items[0].props.href).toBe("/one");
|
|
16
|
+
expect(items[0].props["aria-current"]).toBeUndefined();
|
|
17
|
+
expect(items[1].props.href).toBe("/two");
|
|
18
|
+
expect(items[1].props["aria-current"]).toBeUndefined();
|
|
19
|
+
expect(items[2].props["aria-current"]).toBe("page");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("strips href/onClick and drops tab order on an interactive last item", () => {
|
|
23
|
+
const onClick = () => {};
|
|
24
|
+
const [last] = markCurrentPageItem([
|
|
25
|
+
createElement("a", { href: "/current", onClick }, "Current"),
|
|
26
|
+
]) as ReactElement<Props>[];
|
|
27
|
+
|
|
28
|
+
expect(last.props.href).toBeUndefined();
|
|
29
|
+
expect(last.props.onClick).toBeUndefined();
|
|
30
|
+
expect(last.props.tabIndex).toBe(-1);
|
|
31
|
+
expect(last.props["aria-current"]).toBe("page");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("leaves a non-interactive last item's other props untouched", () => {
|
|
35
|
+
const [last] = markCurrentPageItem([
|
|
36
|
+
createElement("span", { className: "current" }, "Current"),
|
|
37
|
+
]) as ReactElement<Props>[];
|
|
38
|
+
|
|
39
|
+
expect(last.props.className).toBe("current");
|
|
40
|
+
expect(last.props.tabIndex).toBeUndefined();
|
|
41
|
+
expect(last.props["aria-current"]).toBe("page");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("passes through non-element children (e.g. plain text) unchanged", () => {
|
|
45
|
+
const items = markCurrentPageItem(["First", "Last"]);
|
|
46
|
+
|
|
47
|
+
expect(items).toEqual(["First", "Last"]);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -334,6 +334,14 @@
|
|
|
334
334
|
color: var(
|
|
335
335
|
--recursica_ui-kit_components_button_variants_styles_solid_properties_colors_text-color
|
|
336
336
|
);
|
|
337
|
+
/* Exposed so the loading-state Loader (color="var(--button-color)" in Button.tsx) matches
|
|
338
|
+
this exact text-color token instead of Mantine's own unrelated --button-color default.
|
|
339
|
+
!important: Mantine's own Button sets --button-color via an inline `style` attribute on
|
|
340
|
+
this same root element, which normal-weight stylesheet rules can never outrank regardless
|
|
341
|
+
of selector specificity (see IMPLEMENTATION_NOTES.md). */
|
|
342
|
+
--button-color: var(
|
|
343
|
+
--recursica_ui-kit_components_button_variants_styles_solid_properties_colors_text-color
|
|
344
|
+
) !important;
|
|
337
345
|
|
|
338
346
|
.iconWrapper {
|
|
339
347
|
color: var(
|
|
@@ -367,6 +375,10 @@
|
|
|
367
375
|
color: var(
|
|
368
376
|
--recursica_ui-kit_components_button_variants_styles_outline_properties_colors_text-color
|
|
369
377
|
);
|
|
378
|
+
/* !important: see solid variant's identical comment above. */
|
|
379
|
+
--button-color: var(
|
|
380
|
+
--recursica_ui-kit_components_button_variants_styles_outline_properties_colors_text-color
|
|
381
|
+
) !important;
|
|
370
382
|
|
|
371
383
|
.iconWrapper {
|
|
372
384
|
color: var(
|
|
@@ -400,6 +412,10 @@
|
|
|
400
412
|
color: var(
|
|
401
413
|
--recursica_ui-kit_components_button_variants_styles_text_properties_colors_text-color
|
|
402
414
|
);
|
|
415
|
+
/* !important: see solid variant's identical comment above. */
|
|
416
|
+
--button-color: var(
|
|
417
|
+
--recursica_ui-kit_components_button_variants_styles_text_properties_colors_text-color
|
|
418
|
+
) !important;
|
|
403
419
|
|
|
404
420
|
.iconWrapper {
|
|
405
421
|
color: var(
|
|
@@ -117,8 +117,21 @@ const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
|
|
117
117
|
|
|
118
118
|
let mergedLoaderProps = userLoaderProps;
|
|
119
119
|
if (useRecursicaLoader) {
|
|
120
|
+
// color="var(--button-color)" makes the loader ring match this button's own text color
|
|
121
|
+
// (Mantine's Button sets --button-color per variant/state) instead of Loader's own
|
|
122
|
+
// standalone indicator-color token, which Loader.module.css's `.root` would otherwise
|
|
123
|
+
// apply unconditionally, ignoring this nesting context. overStyled is required to pass
|
|
124
|
+
// `color` at all (blocked by default) — fully internal composition, no prop threading to
|
|
125
|
+
// callers, so it doesn't widen Loader's public API. See Button's IMPLEMENTATION_NOTES.md.
|
|
120
126
|
mergedLoaderProps = {
|
|
121
|
-
children:
|
|
127
|
+
children: (
|
|
128
|
+
<Loader
|
|
129
|
+
variant={loaderVariant}
|
|
130
|
+
size={resolvedLoaderSize}
|
|
131
|
+
overStyled
|
|
132
|
+
color="var(--button-color)"
|
|
133
|
+
/>
|
|
134
|
+
),
|
|
122
135
|
...userLoaderProps,
|
|
123
136
|
};
|
|
124
137
|
}
|
|
@@ -48,11 +48,37 @@ Mantine's `.mantine-Button-label` flex centering breaks primitive truncation log
|
|
|
48
48
|
|
|
49
49
|
---
|
|
50
50
|
|
|
51
|
-
## Loader color contrast
|
|
52
|
-
|
|
53
|
-
**
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
## Loader color contrast — fixed (source-of-truth audit, 2026-08-30)
|
|
52
|
+
|
|
53
|
+
**Superseded:** the entry below originally documented an explicit decision _not_ to fix this.
|
|
54
|
+
Re-verified during the mantine-vs-mui source-of-truth audit by measuring actual computed styles
|
|
55
|
+
in the `Loading` story: the loader ring rendered `#c21b43` (Loader's own standalone
|
|
56
|
+
`indicator-color` token) against a label text color of `rgb(249, 249, 249)` — a real, confirmed
|
|
57
|
+
mismatch on the `solid` variant, not a false report.
|
|
58
|
+
|
|
59
|
+
**Root cause:** `Loader.module.css`'s `.root` unconditionally sets `--loader-color:
|
|
60
|
+
var(--recursica_..._loader_properties_indicator-color)` directly on the Loader's own root
|
|
61
|
+
`<span>`. Mantine's own Button internally wraps `loaderProps.children` in a generic `<Loader
|
|
62
|
+
size color>` that sets `--loader-color: var(--button-color)` as an _inline_ style on an outer
|
|
63
|
+
wrapper div — but our own nested `<Loader>` (rendered as `loaderProps.children`) has an identical
|
|
64
|
+
class (`mantine-Loader-root`) on its own root, so its own stylesheet rule locally re-declares
|
|
65
|
+
`--loader-color`, shadowing the inherited value from that outer wrapper for anything computed on
|
|
66
|
+
or under our own element (CSS custom properties resolve against the nearest self-or-ancestor
|
|
67
|
+
declaration — a local re-declaration always wins over what an ancestor tried to pass down,
|
|
68
|
+
regardless of whether the ancestor's value came from an inline style or not).
|
|
69
|
+
|
|
70
|
+
**Fix:** pass `color="var(--button-color)"` to the Button-owned `<Loader>` — Mantine's own
|
|
71
|
+
`Loader` factory writes any `color` prop straight through `getThemeColor()`/`parseThemeColor()`
|
|
72
|
+
(safely falls through unresolved CSS values like a `var()` reference verbatim, per
|
|
73
|
+
`@mantine/core`'s own `parse-theme-color.cjs`) into an _inline_ `--loader-color` style on that
|
|
74
|
+
same root span — which, being inline, now wins over our own `.root` stylesheet declaration.
|
|
75
|
+
Also added `--button-color` (`!important`) to each variant's own block in `Button.module.css`,
|
|
76
|
+
set to that variant's own `colors_text-color` token — needed because Mantine's Button sets its
|
|
77
|
+
own generic `--button-color: var(--mantine-color-white)` via an inline style on the button root
|
|
78
|
+
itself, which our external stylesheet couldn't otherwise outrank at equal/lower priority
|
|
79
|
+
regardless of selector specificity. Verified live: loader ring color now matches the label text
|
|
80
|
+
color exactly (`rgb(249, 249, 249)`) across solid/outline/text variants, no visual regression on
|
|
81
|
+
non-loading stories.
|
|
56
82
|
|
|
57
83
|
---
|
|
58
84
|
|
|
@@ -7,7 +7,7 @@ The `DatePicker` component is a wrapper around the `@mantine/dates` `DatePickerI
|
|
|
7
7
|
## Structural Constraints
|
|
8
8
|
|
|
9
9
|
1. **Naked Input Usage**: We intentionally pass `label={undefined}`, `description={undefined}`, and `error={undefined}` to the Mantine `DatePickerInput` component. This suppresses Mantine's internal macro form wrapping and ensures that only our `WithReadOnlyWrapper` > `FormControlWrapper` orchestrates labels, description text, and ARIA state error boundaries.
|
|
10
|
-
2. **Read-Only Implementation**:
|
|
10
|
+
2. **Read-Only Implementation**: `readOnlyValue` is formatted via `@mantine/dates`' own `getFormattedDate` helper using the same `valueFormat` the editable `DatePickerInput` renders with — so a read-only date reads identically to that same value while editable (`05/20/26`, not a raw JS `Date.toString()`). Developers can still pass a custom `readOnlyComponent` to fully replace this rendering.
|
|
11
11
|
3. **Calendar Portal/Dropdown (Figma Token Issue)**: The Recursica UI Kit's `date-picker` component in Figma fails to export any explicit structural or color properties for the calendar popover itself (e.g. elevation, surface background, selected day colors, hover, today, in-range). To solve this organically within the framework without breaking strict token adherence, we manually map every calendar state to the closest existing Recursica token instead of leaving any of it on Mantine's own un-tokenized defaults:
|
|
12
12
|
- **Surface** (`.dropdown`) & **selected day** (`.day[data-selected]`): `--recursica_ui-kit_components_hover-card-popover` tokens for elevation/padding/surface, `--recursica_ui-kit_components_button_variants_styles_solid` tokens for the selected day fill (unchanged from before).
|
|
13
13
|
- **Header content, weekday row** (`.calendarHeader`, `.weekday`): the hover-card-popover content-color token — previously unmapped, so these leaked Mantine's own text colors.
|
|
@@ -28,3 +28,4 @@ The `DatePicker` component is a wrapper around the `@mantine/dates` `DatePickerI
|
|
|
28
28
|
- **Default placeholder**: `placeholder="MM / DD / YY"`, set the same way (before `sanitizedProps`, so consumers can override).
|
|
29
29
|
- **Single-line enforcement (bug fix)**: `DatePickerInput` renders its value/placeholder as a `<button>`'s text content, not a native `<input>` — it wraps like any other block text by default. A long placeholder (e.g. the original story's `"Select a deadline..."`) wrapped to two lines, inflating the field's height well past its `min-height` token and making the field visually look narrower than its actual `width` token value even though the box itself was always the right size. Fixed with `white-space: nowrap`/`text-overflow: ellipsis`/`overflow-x: clip`/`overflow-y: visible` on `.input`, the same convention `FileInput.module.css`'s `.value` uses.
|
|
30
30
|
- **Day focus ring (bug fix)**: Mantine's `Day` ships no built-in `:focus-visible` style of its own (unlike `.calendarHeaderControl`/`.calendarHeaderLevel`, which are styled as `Button` and inherited a focus ring from that treatment), so a focused day — e.g. the auto-focused selected/today day when the calendar opens — fell through to the browser's native blue outline instead of a Recursica token. Fixed by adding `.day:focus-visible` with the same generic `--recursica_brand_states_focus_*` box-shadow ring already used by `.input` and `.calendarHeaderControl`/`.calendarHeaderLevel`. mui-adapter's `.MuiPickerDay-root.Mui-focusVisible` already had this — only the mantine-adapter side was missing it.
|
|
31
|
+
- **Read-only value format (bug fix, 2026-08-30)**: `readOnlyValue` used to be a bare `String(value)`/`String(defaultValue)` cast — `Date`'s own `toString()`, e.g. `"Wed May 20 2026 17:00:00 GMT-0700 (Pacific Daylight Time)"` — completely ignoring `valueFormat`, so `ui-kit-datepicker--static-read-only` rendered a totally different (and far less usable) date string than the same value shown editable, or than mui-adapter's equivalent (which already formats its read-only text via dayjs). Fixed by running the value through `@mantine/dates`' exported `getFormattedDate({ type: "default", date, locale: "en", format: valueFormat, labelSeparator: "" })` — the same formatter `DatePickerInput` uses internally — so read-only and editable rendering of the same value always agree.
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import React, { forwardRef } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
DatePickerInput,
|
|
4
|
+
type DatePickerInputProps,
|
|
5
|
+
getFormattedDate,
|
|
6
|
+
} from "@mantine/dates";
|
|
3
7
|
import { type ReadOnlyControlProps } from "@recursica/adapter-common";
|
|
4
8
|
import {
|
|
5
9
|
filterStylingProps,
|
|
@@ -161,13 +165,23 @@ export const DatePicker = forwardRef<HTMLButtonElement, DatePickerProps>(
|
|
|
161
165
|
readOnlyComponent={readOnlyComponent}
|
|
162
166
|
emptyValueComponent={emptyValueComponent}
|
|
163
167
|
readOnlyType="text"
|
|
164
|
-
readOnlyValue={
|
|
165
|
-
value !== undefined
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
168
|
+
readOnlyValue={(() => {
|
|
169
|
+
const readOnlyDate = value !== undefined ? value : defaultValue;
|
|
170
|
+
// `DatePickerInput` formats its own displayed value via `valueFormat` internally
|
|
171
|
+
// (using this same `getFormattedDate` helper) — the read-only text swap-in bypassed
|
|
172
|
+
// that entirely and rendered `String(date)` instead (a raw JS `Date.toString()`, e.g.
|
|
173
|
+
// "Wed May 20 2026 17:00:00 GMT-0700 (Pacific Daylight Time)"), so a read-only
|
|
174
|
+
// DatePicker never matched what the same value looks like while editable.
|
|
175
|
+
return readOnlyDate
|
|
176
|
+
? getFormattedDate({
|
|
177
|
+
type: "default",
|
|
178
|
+
date: readOnlyDate,
|
|
179
|
+
locale: "en",
|
|
180
|
+
format: valueFormat,
|
|
181
|
+
labelSeparator: "",
|
|
182
|
+
})
|
|
183
|
+
: undefined;
|
|
184
|
+
})()}
|
|
171
185
|
readOnlyNativeProps={props}
|
|
172
186
|
activeComponent={
|
|
173
187
|
/* Naked Input execution safely decoupled from Mantine's macro Input.Wrapper DOM hooks */
|
|
@@ -27,7 +27,6 @@ const meta: Meta<DropdownStoryProps> = {
|
|
|
27
27
|
disabled: false,
|
|
28
28
|
required: false,
|
|
29
29
|
readOnly: false,
|
|
30
|
-
searchable: false,
|
|
31
30
|
clearable: false,
|
|
32
31
|
},
|
|
33
32
|
argTypes: {
|
|
@@ -38,9 +37,6 @@ const meta: Meta<DropdownStoryProps> = {
|
|
|
38
37
|
readOnly: {
|
|
39
38
|
control: "boolean",
|
|
40
39
|
},
|
|
41
|
-
searchable: {
|
|
42
|
-
control: "boolean",
|
|
43
|
-
},
|
|
44
40
|
clearable: {
|
|
45
41
|
control: "boolean",
|
|
46
42
|
},
|
|
@@ -63,12 +59,10 @@ export const Default: Story = {
|
|
|
63
59
|
args: {},
|
|
64
60
|
};
|
|
65
61
|
|
|
66
|
-
export const
|
|
62
|
+
export const Clearable: Story = {
|
|
67
63
|
args: {
|
|
68
|
-
label: "
|
|
69
|
-
searchable: true,
|
|
64
|
+
label: "Clearable Options",
|
|
70
65
|
clearable: true,
|
|
71
|
-
placeholder: "Start typing...",
|
|
72
66
|
},
|
|
73
67
|
};
|
|
74
68
|
|
|
@@ -23,7 +23,7 @@ import { type RecursicaDropdownProps as BaseRecursicaDropdownProps } from "@recu
|
|
|
23
23
|
export interface RecursicaDropdownProps
|
|
24
24
|
extends Omit<
|
|
25
25
|
MantineSelectProps,
|
|
26
|
-
"size" | "variant" | "radius" | "wrapperProps" | "data"
|
|
26
|
+
"size" | "variant" | "radius" | "wrapperProps" | "data" | "searchable"
|
|
27
27
|
>,
|
|
28
28
|
Omit<
|
|
29
29
|
RecursicaFormControlWrapperProps,
|
|
@@ -40,6 +40,7 @@ const UNSUPPORTED_PROPS = [
|
|
|
40
40
|
"size", // Recursica controls sizing via the `size` variant + design tokens, not raw dimensions.
|
|
41
41
|
"variant", // Colors/variants are token-driven; the library's native palette isn't exposed.
|
|
42
42
|
"radius", // Recursica does not expose the library's native corner-radius system.
|
|
43
|
+
"searchable", // Not a supported Dropdown feature — that's what makes it an AutoComplete.
|
|
43
44
|
] as const satisfies readonly (keyof MantineSelectProps)[];
|
|
44
45
|
|
|
45
46
|
export const Dropdown = forwardRef<HTMLInputElement, DropdownProps>(
|
|
@@ -7,3 +7,12 @@ Per the layout-components rule (no formal Recursica common props definition —
|
|
|
7
7
|
`Grid.Col`'s `span`, `offset`, `order`, `visibleFrom`, and `hiddenFrom` props are passed straight through to Mantine's `Grid.Col` unchanged, using Mantine's own breakpoint scale (`xs`/`sm`/`md`/`lg`/`xl` — there is no `base` breakpoint in Mantine; `xs` is the smallest).
|
|
8
8
|
|
|
9
9
|
Like the other primitive layout components, `Grid` and `Grid.Col` use `WithRecursicaSpacing<T>` rather than `RecursicaOverStyled<T>` and are listed in the "Primitive Layout Components Exemption" section of `OVERSTYLING.md`.
|
|
10
|
+
|
|
11
|
+
## Added `ResponsiveSizes` story (2026-08-30, source-of-truth audit)
|
|
12
|
+
|
|
13
|
+
mui-adapter has a `ui-kit-grid--responsive-sizes` story with no counterpart here — its `story
|
|
14
|
+
parity with source of truth` check would otherwise fail for mui-adapter. Added an equivalent
|
|
15
|
+
`ResponsiveSizes` story using Mantine's own `span` prop (Mantine's `Grid.Col` has no `size` prop
|
|
16
|
+
— see the naming-divergence note above and mui-adapter's own `IMPLEMENTATION_NOTES.md`). This is
|
|
17
|
+
functionally identical to the existing `ResponsiveSpans` story above; it exists purely so
|
|
18
|
+
mui-adapter's differently-named story id has a source-of-truth golden to diff against.
|
|
@@ -113,6 +113,30 @@ export const ResponsiveSpans: Story = {
|
|
|
113
113
|
),
|
|
114
114
|
};
|
|
115
115
|
|
|
116
|
+
// Mirrors mui-adapter's `ResponsiveSizes` story (same breakpoints/content) under mantine's own
|
|
117
|
+
// prop name — Mantine's Grid.Col has no `size` prop, only `span`, which is what `ResponsiveSpans`
|
|
118
|
+
// above already exercises. This one exists purely so mui-adapter's `responsive-sizes` story id
|
|
119
|
+
// has a source-of-truth counterpart to diff against (see Grid's IMPLEMENTATION_NOTES.md).
|
|
120
|
+
export const ResponsiveSizes: Story = {
|
|
121
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
|
|
122
|
+
render: ({ withLayer, layer, ...args }: any) => (
|
|
123
|
+
<Grid {...args}>
|
|
124
|
+
<Grid.Col span={{ xs: 12, sm: 6, md: 3 }}>
|
|
125
|
+
<Swatch>xs 12 / sm 6 / md 3</Swatch>
|
|
126
|
+
</Grid.Col>
|
|
127
|
+
<Grid.Col span={{ xs: 12, sm: 6, md: 3 }}>
|
|
128
|
+
<Swatch>xs 12 / sm 6 / md 3</Swatch>
|
|
129
|
+
</Grid.Col>
|
|
130
|
+
<Grid.Col span={{ xs: 12, sm: 6, md: 3 }}>
|
|
131
|
+
<Swatch>xs 12 / sm 6 / md 3</Swatch>
|
|
132
|
+
</Grid.Col>
|
|
133
|
+
<Grid.Col span={{ xs: 12, sm: 6, md: 3 }}>
|
|
134
|
+
<Swatch>xs 12 / sm 6 / md 3</Swatch>
|
|
135
|
+
</Grid.Col>
|
|
136
|
+
</Grid>
|
|
137
|
+
),
|
|
138
|
+
};
|
|
139
|
+
|
|
116
140
|
export const Offset: Story = {
|
|
117
141
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
|
|
118
142
|
render: ({ withLayer, layer, ...args }: any) => (
|
|
@@ -20,7 +20,7 @@ const meta: Meta<LabelStoryProps> = {
|
|
|
20
20
|
argTypes: {
|
|
21
21
|
labelSize: {
|
|
22
22
|
control: "inline-radio",
|
|
23
|
-
options: ["default", "small"
|
|
23
|
+
options: ["default", "small"],
|
|
24
24
|
description:
|
|
25
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
26
|
},
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## States in Design Tokens
|
|
4
4
|
|
|
5
|
-
The design tokens (`recursica_variables_scoped.css`) provide the base link styling directly on `--recursica_ui-kit_components_link_properties_*` (no distinct `default` state), plus a `visited` variant that overrides only `colors_text-color`/`colors_icon-color`. Hover is not a per-component `link_variants_states_hover_*` token; it's driven by the brand-level `--recursica_brand_states_link_decoration` token (resolves to `underline`), applied on `.root:hover`. Earlier this token was exported as an invalid quoted string (`"underline"`), which silently failed and was mistaken for the token not existing at all — now fixed upstream to a bare keyword. There
|
|
5
|
+
The design tokens (`recursica_variables_scoped.css`) provide the base link styling directly on `--recursica_ui-kit_components_link_properties_*` (no distinct `default` state), plus a `visited` variant that overrides only `colors_text-color`/`colors_icon-color`. Hover is not a per-component `link_variants_states_hover_*` token; it's driven by the brand-level `--recursica_brand_states_link_decoration` token (resolves to `underline`), applied on `.root:hover`. Earlier this token was exported as an invalid quoted string (`"underline"`), which silently failed and was mistaken for the token not existing at all — now fixed upstream to a bare keyword. There is no token for `active`. There isn't a per-component `focus` token either, but focus isn't left to the browser default: like every other interactive component (see Button), `.root:focus-visible` replaces the native outline with the global `--recursica_brand_states_focus_*` ring. This was previously missing here — Link (and anything composing it, e.g. Breadcrumb) fell back to the browser's native focus outline instead.
|
|
6
6
|
|
|
7
7
|
## Overriding Mantine's underline Prop
|
|
8
8
|
|