@recursica/adapter-mantine-v8 1.3.5 → 1.4.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 +12 -0
- package/dist/adapter-mantine-v8.cjs +2 -2
- package/dist/adapter-mantine-v8.cjs.map +1 -1
- package/dist/adapter-mantine-v8.css +1 -1
- package/dist/adapter-mantine-v8.js +1028 -1012
- package/dist/adapter-mantine-v8.js.map +1 -1
- package/dist/index.d.ts +10 -6
- package/package.json +2 -2
- package/src/components/Button/Button.stories.tsx +12 -0
- package/src/components/Checkbox/CHECKBOX_IMPLEMENTATION_NOTES.md +4 -0
- package/src/components/Modal/MODAL_IMPLEMENTATION_NOTES.md +4 -0
- package/src/components/Modal/Modal.tsx +23 -8
- package/src/components/Modal/USAGE.md +6 -0
- package/src/components/NativeInputA11y.dom.test.tsx +60 -0
- package/src/components/Panel/PANEL_IMPLEMENTATION_NOTES.md +9 -1
- package/src/components/Panel/Panel.module.css +105 -0
- package/src/components/Panel/Panel.stories.tsx +0 -8
- package/src/components/Panel/Panel.tsx +31 -8
- package/src/components/Panel/USAGE.md +9 -2
- package/src/components/Radio/RADIO_IMPLEMENTATION_NOTES.md +5 -0
- package/src/components/Switch/SWITCH_IMPLEMENTATION_NOTES.md +4 -0
package/dist/index.d.ts
CHANGED
|
@@ -911,7 +911,7 @@ export declare type ModalOverlayProps = RecursicaOverStyled<ModalOverlayProps_2>
|
|
|
911
911
|
* Native Mantine abstract properties like `size`, `radius`, and `shadow` have been stripped out
|
|
912
912
|
* because they directly conflict with the non-negotiable pixel boundaries defined in the Recursica UI Kit.
|
|
913
913
|
*/
|
|
914
|
-
export declare type ModalProps = RecursicaOverStyled<Omit<ModalProps_2, "size" | "radius" | "shadow"> & RecursicaModalProps>;
|
|
914
|
+
export declare type ModalProps = RecursicaOverStyled<Omit<ModalProps_2, "size" | "radius" | "shadow" | "title" | "onClose" | "withCloseButton" | "closeButtonProps"> & RecursicaModalProps>;
|
|
915
915
|
|
|
916
916
|
declare const ModalRoot: default_2.ForwardRefExoticComponent<ModalRootProps & default_2.RefAttributes<HTMLDivElement>>;
|
|
917
917
|
|
|
@@ -1029,12 +1029,16 @@ export declare const Panel: PanelComponent;
|
|
|
1029
1029
|
*
|
|
1030
1030
|
* ```tsx
|
|
1031
1031
|
* <Panel opened={opened} onClose={close} title="Panel Title" placement="right">
|
|
1032
|
-
*
|
|
1033
|
-
*
|
|
1034
|
-
* </Panel.Body>
|
|
1032
|
+
* Content goes here
|
|
1033
|
+
* <Panel.Footer>...</Panel.Footer>
|
|
1035
1034
|
* </Panel>
|
|
1036
1035
|
* ```
|
|
1037
1036
|
*
|
|
1037
|
+
* The header is required and always rendered: `title` is required, and an icon-only Recursica
|
|
1038
|
+
* Close button (`aria-label` "Close", overridable via `closeButtonAriaLabel`) calls the required
|
|
1039
|
+
* `onClose`. The footer is optional. Passing `header` replaces the whole default header; avoid it,
|
|
1040
|
+
* because then nothing guarantees the title or the Close button.
|
|
1041
|
+
*
|
|
1038
1042
|
* Always non-modal, with no props to change it: the page behind stays usable
|
|
1039
1043
|
* (no overlay, focus trap or scroll lock), focus returns to the opener on close,
|
|
1040
1044
|
* Escape always closes, and outside clicks never do.
|
|
@@ -1050,7 +1054,7 @@ export declare const Panel: PanelComponent;
|
|
|
1050
1054
|
* - `Panel.Stack` — Stacked drawer context
|
|
1051
1055
|
*/
|
|
1052
1056
|
declare const PanelBase: {
|
|
1053
|
-
({ overStyled, placement, keepMounted, wrapHeaderText, ...rest }: PanelProps): JSX.Element;
|
|
1057
|
+
({ overStyled, placement, keepMounted, wrapHeaderText, title, onClose, closeButtonAriaLabel, header, children, ...rest }: PanelProps): JSX.Element;
|
|
1054
1058
|
displayName: string;
|
|
1055
1059
|
};
|
|
1056
1060
|
|
|
@@ -1244,7 +1248,7 @@ export declare interface RecursicaNumberInputProps extends Omit<NumberInputProps
|
|
|
1244
1248
|
/**
|
|
1245
1249
|
* Recursica Panel root props. Extends Mantine Drawer.
|
|
1246
1250
|
*/
|
|
1247
|
-
export declare interface RecursicaPanelProps extends Omit<DrawerProps, "size" | "styles" | "classNames" | "style" | "position" | "opened" | "withOverlay" | "trapFocus" | "lockScroll" | "returnFocus" | "closeOnEscape" | "closeOnClickOutside">, RecursicaPanelProps_2 {
|
|
1251
|
+
export declare interface RecursicaPanelProps extends Omit<DrawerProps, "size" | "styles" | "classNames" | "style" | "position" | "opened" | "title" | "withCloseButton" | "closeButtonProps" | "withOverlay" | "trapFocus" | "lockScroll" | "returnFocus" | "closeOnEscape" | "closeOnClickOutside">, RecursicaPanelProps_2 {
|
|
1248
1252
|
}
|
|
1249
1253
|
|
|
1250
1254
|
export declare interface RecursicaRadioGroupProps extends Omit<RadioGroupProps_2, "size" | "labelProps" | "defaultValue" | "value">, Omit<RecursicaFormControlWrapperProps, "controlMaxWidth" | "controlMinWidth">, ReadOnlyControlProps, RecursicaRadioGroupProps_2 {
|
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"type": "git",
|
|
13
13
|
"url": "git+https://github.com/borderux/recursica-adapter-mantine-v8.git"
|
|
14
14
|
},
|
|
15
|
-
"version": "1.
|
|
15
|
+
"version": "1.4.0",
|
|
16
16
|
"publishConfig": {
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"release": "changeset publish"
|
|
75
75
|
},
|
|
76
76
|
"dependencies": {
|
|
77
|
-
"@recursica/adapter-common": "^1.
|
|
77
|
+
"@recursica/adapter-common": "^1.3.0"
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
80
|
"@changesets/cli": "^2.29.7",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
3
|
import { Button } from "./Button";
|
|
4
|
+
import { Loader } from "../Loader/Loader";
|
|
4
5
|
import { Layer } from "@recursica/adapter-common";
|
|
5
6
|
|
|
6
7
|
type ButtonStoryProps = React.ComponentProps<typeof Button>;
|
|
@@ -180,6 +181,17 @@ export const Loading: Story = {
|
|
|
180
181
|
variant: "solid",
|
|
181
182
|
size: "default",
|
|
182
183
|
loading: true,
|
|
184
|
+
// Frozen spinner so the visual-regression snapshot is deterministic.
|
|
185
|
+
loaderProps: {
|
|
186
|
+
children: (
|
|
187
|
+
<Loader
|
|
188
|
+
size="default"
|
|
189
|
+
overStyled
|
|
190
|
+
color="var(--button-color)"
|
|
191
|
+
animate={false}
|
|
192
|
+
/>
|
|
193
|
+
),
|
|
194
|
+
},
|
|
183
195
|
},
|
|
184
196
|
parameters: {
|
|
185
197
|
docs: {
|
|
@@ -23,3 +23,7 @@ This calculation ensures that the optical center of the `.inner` checkmark vecto
|
|
|
23
23
|
### Checkbox.Group Overrides
|
|
24
24
|
|
|
25
25
|
To decouple `<CheckboxGroup>` away from `<Input.Wrapper>`, we explicitly extract the raw array execution mapped correctly against our identical `RecursicaFormControlWrapperProps` schema structurally!
|
|
26
|
+
|
|
27
|
+
### Accessible name lands on the native `<input>` (verified 2026-10-15)
|
|
28
|
+
|
|
29
|
+
`RequireAccessibleLabel` enforces a name at compile time (`label`, `aria-label` or `aria-labelledby`). Mantine forwards `aria-*` to the native `<input>` and auto-generates the `id` that links `<label for>`, so no adapter code is needed. This is the behavior the other adapters must match. Covered by `src/components/NativeInputA11y.dom.test.tsx`, which asserts it for Checkbox, Radio and Switch.
|
|
@@ -30,3 +30,7 @@ Despite §1 above, `.content` never actually scaled fluidly: Mantine's own CSS s
|
|
|
30
30
|
### 5. Close button restyled to match Button
|
|
31
31
|
|
|
32
32
|
`.close` reuses Button's text-variant/icon-only/small tokens (radius, padding, background/icon color, hover overlay, focus ring) instead of Mantine's native CloseButton look. Two overrides need `!important`/an explicit reset to win: Mantine's `CloseIcon` sizes itself via an inline `style` (`--cb-icon-size`, default `70%`), which beats any plain class rule — same category as Tree's `--level-offset` override; and Mantine's own subtle-variant `:hover` background (from `CloseButton.css`) is pinned back to the Button background token so only our `::after` overlay renders hover feedback.
|
|
33
|
+
|
|
34
|
+
### 3. Required header with an always-present Close button (2026-10-16)
|
|
35
|
+
|
|
36
|
+
`title` and `onClose` are required (contract in `RecursicaModalProps`, `@recursica/adapter-common`), and `withCloseButton` and `closeButtonProps` are removed. `Modal.tsx` always renders `Modal.Header` > `Modal.Title` + `Modal.CloseButton` with `aria-label="Close"` unless `closeButtonAriaLabel` is passed. A `header` prop replaces the whole default header and is discouraged in its JSDoc.
|
|
@@ -26,7 +26,17 @@ import { type RecursicaModalProps } from "@recursica/adapter-common";
|
|
|
26
26
|
* because they directly conflict with the non-negotiable pixel boundaries defined in the Recursica UI Kit.
|
|
27
27
|
*/
|
|
28
28
|
export type ModalProps = RecursicaOverStyled<
|
|
29
|
-
Omit<
|
|
29
|
+
Omit<
|
|
30
|
+
MantineModalProps,
|
|
31
|
+
| "size"
|
|
32
|
+
| "radius"
|
|
33
|
+
| "shadow"
|
|
34
|
+
| "title"
|
|
35
|
+
| "onClose"
|
|
36
|
+
| "withCloseButton"
|
|
37
|
+
| "closeButtonProps"
|
|
38
|
+
> &
|
|
39
|
+
RecursicaModalProps
|
|
30
40
|
>;
|
|
31
41
|
|
|
32
42
|
/**
|
|
@@ -37,6 +47,11 @@ export type ModalProps = RecursicaOverStyled<
|
|
|
37
47
|
* It automatically enforces Figma UI Kit geometries (`max-width: 960px`, `min-width: 304px`)
|
|
38
48
|
* and handles internal body scrolling explicitly via CSS module overrides.
|
|
39
49
|
*
|
|
50
|
+
* The header is required and always rendered: `title` is required, and an icon-only Recursica
|
|
51
|
+
* Close button (`aria-label` "Close", overridable via `closeButtonAriaLabel`) calls the required
|
|
52
|
+
* `onClose`. The footer is optional. Passing `header` replaces the whole default header; avoid it,
|
|
53
|
+
* because then nothing guarantees the title or the Close button.
|
|
54
|
+
*
|
|
40
55
|
* @example
|
|
41
56
|
* ```tsx
|
|
42
57
|
* <Modal opened={opened} onClose={close} title="Hello World">
|
|
@@ -49,10 +64,11 @@ const ModalInner = React.forwardRef<HTMLDivElement, ModalProps>(function Modal(
|
|
|
49
64
|
overStyled = false,
|
|
50
65
|
children,
|
|
51
66
|
title,
|
|
52
|
-
|
|
67
|
+
onClose,
|
|
68
|
+
closeButtonAriaLabel = "Close",
|
|
69
|
+
header,
|
|
53
70
|
overlayProps,
|
|
54
71
|
withOverlay = true,
|
|
55
|
-
closeButtonProps,
|
|
56
72
|
...rest
|
|
57
73
|
},
|
|
58
74
|
ref,
|
|
@@ -90,16 +106,15 @@ const ModalInner = React.forwardRef<HTMLDivElement, ModalProps>(function Modal(
|
|
|
90
106
|
MantineModalProps,
|
|
91
107
|
"size" | "radius" | "shadow"
|
|
92
108
|
>)}
|
|
109
|
+
onClose={onClose}
|
|
93
110
|
classNames={mergedClassNames}
|
|
94
111
|
>
|
|
95
112
|
{withOverlay && <MantineModal.Overlay {...overlayProps} />}
|
|
96
113
|
<MantineModal.Content>
|
|
97
|
-
{
|
|
114
|
+
{header ?? (
|
|
98
115
|
<MantineModal.Header>
|
|
99
|
-
|
|
100
|
-
{
|
|
101
|
-
<MantineModal.CloseButton {...closeButtonProps} />
|
|
102
|
-
)}
|
|
116
|
+
<MantineModal.Title>{title}</MantineModal.Title>
|
|
117
|
+
<MantineModal.CloseButton aria-label={closeButtonAriaLabel} />
|
|
103
118
|
</MantineModal.Header>
|
|
104
119
|
)}
|
|
105
120
|
<ModalBody>{children}</ModalBody>
|
|
@@ -43,6 +43,12 @@ All Recursica components in the `@recursica/adapter-mantine-v8` package adhere s
|
|
|
43
43
|
|
|
44
44
|
## 4. Key Integration Features & Constraints
|
|
45
45
|
|
|
46
|
+
### Header: required title and Close button
|
|
47
|
+
|
|
48
|
+
`title` and `onClose` are required. The header always renders the title and an icon-only Recursica Close button with `aria-label="Close"`; pass `closeButtonAriaLabel` to translate it. `withCloseButton` has been removed: there is no way to hide the Close button.
|
|
49
|
+
|
|
50
|
+
The `header` prop replaces the whole default header. Avoid it: a custom header must supply its own title and Close button, and the adapter can no longer guarantee either.
|
|
51
|
+
|
|
46
52
|
## Architecture
|
|
47
53
|
|
|
48
54
|
`Modal` wraps Mantine's `<Modal>` component. Native Mantine styling props (`size`, `radius`, `shadow`) are not used — appearance and sizing are fully controlled by the design system tokens instead.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { describe, it, expect } from "vitest";
|
|
3
|
+
import { createRoot } from "react-dom/client";
|
|
4
|
+
import { flushSync } from "react-dom";
|
|
5
|
+
import { Checkbox } from "./Checkbox/Checkbox";
|
|
6
|
+
import { Radio } from "./Radio/Radio";
|
|
7
|
+
import { Switch } from "./Switch/Switch";
|
|
8
|
+
import { MantineProvider } from "@mantine/core";
|
|
9
|
+
const wrap = (n: React.ReactElement) => <MantineProvider>{n}</MantineProvider>;
|
|
10
|
+
|
|
11
|
+
// Where the naming attributes land matters, not just that they render: a screen reader reads
|
|
12
|
+
// the native <input>, so aria-* on a wrapper leaves the control unnamed.
|
|
13
|
+
function render(node: React.ReactElement): HTMLElement {
|
|
14
|
+
const container = document.createElement("div");
|
|
15
|
+
document.body.appendChild(container);
|
|
16
|
+
flushSync(() => createRoot(container).render(wrap(node)));
|
|
17
|
+
return container;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const controls = [
|
|
21
|
+
["Checkbox", Checkbox],
|
|
22
|
+
["Radio", Radio],
|
|
23
|
+
["Switch", Switch],
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
describe.each(controls)("%s accessible name", (_name, Control) => {
|
|
27
|
+
// The union type under test is already enforced at compile time; cast to exercise each branch.
|
|
28
|
+
const C = Control as unknown as React.ComponentType<Record<string, unknown>>;
|
|
29
|
+
|
|
30
|
+
it("puts aria-label on the native input only", () => {
|
|
31
|
+
const c = render(<C aria-label="Select all" />);
|
|
32
|
+
expect(c.querySelector("input")!.getAttribute("aria-label")).toBe(
|
|
33
|
+
"Select all",
|
|
34
|
+
);
|
|
35
|
+
expect(c.querySelectorAll("[aria-label]")).toHaveLength(1);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("puts aria-labelledby and aria-describedby on the native input only", () => {
|
|
39
|
+
const c = render(<C aria-labelledby="a" aria-describedby="b" />);
|
|
40
|
+
const input = c.querySelector("input")!;
|
|
41
|
+
expect(input.getAttribute("aria-labelledby")).toBe("a");
|
|
42
|
+
expect(input.getAttribute("aria-describedby")).toBe("b");
|
|
43
|
+
expect(c.querySelectorAll("[aria-labelledby]")).toHaveLength(1);
|
|
44
|
+
expect(c.querySelectorAll("[aria-describedby]")).toHaveLength(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("links a visible label to the input without a caller-supplied id", () => {
|
|
48
|
+
const c = render(<C label="Hello" />);
|
|
49
|
+
const input = c.querySelector("input")!;
|
|
50
|
+
const label = c.querySelector("label")!;
|
|
51
|
+
expect(input.id).not.toBe("");
|
|
52
|
+
expect(label.getAttribute("for")).toBe(input.id);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("keeps a caller-supplied id", () => {
|
|
56
|
+
const c = render(<C label="Hello" id="mine" />);
|
|
57
|
+
expect(c.querySelector("input")!.id).toBe("mine");
|
|
58
|
+
expect(c.querySelector("label")!.getAttribute("for")).toBe("mine");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -129,4 +129,12 @@ Project-wide fix; see Chip's `CHIP_IMPLEMENTATION_NOTES.md` for the original dis
|
|
|
129
129
|
|
|
130
130
|
**Decision:** Panel is never modal, and there are no props to change that.
|
|
131
131
|
|
|
132
|
-
**Implementation:** The house rule is that the page behind a panel stays usable. `Panel.tsx` always passes `
|
|
132
|
+
**Implementation:** The house rule is that the page behind a panel stays usable. `Panel.tsx` composes `Drawer.Root` without a `Drawer.Overlay` and always passes `trapFocus={false}`, `lockScroll={false}`, `returnFocus`, `closeOnEscape` and `closeOnClickOutside={false}` to Mantine's Drawer, after the caller's props, and omits those keys from `RecursicaPanelProps`. Escape is independent of outside clicks and always closes the panel; clicking the page behind never does. Every adapter must match: no overlay, no focus trap, no scroll lock, no `aria-modal`, no `aria-hidden` on the rest of the page.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 11. Required header with an always-present Close button (Matt Massey, 2026-10-16)
|
|
137
|
+
|
|
138
|
+
**Decision:** Every Panel has a header with a required `title` and a Close button wired to a required `onClose`. The footer is optional.
|
|
139
|
+
|
|
140
|
+
**Implementation:** The contract lives in `RecursicaPanelProps` in `@recursica/adapter-common`. `Panel.tsx` composes `Drawer.Root` > `Drawer.Content` > (`Drawer.Header` > `Drawer.Title` + `Drawer.CloseButton`) + `Drawer.Body`, so the header cannot be omitted. The Close button gets `aria-label="Close"` unless `closeButtonAriaLabel` is passed. `title`, `withCloseButton` and `closeButtonProps` are omitted from the Mantine props type. The `.close` class matches Button's text-variant, icon-only, small-size treatment (same as Modal), so the Close button looks like a Recursica icon-only Button. A `header` prop replaces the whole default header; it is discouraged in its JSDoc because nothing then guarantees a title or Close button.
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/* Brand-layer exemptions (recursica-allow-brand) — see recursica-token-analyzer README.md.
|
|
2
|
+
* Close button is styled to match the Button component's text/icon-only variant, so it reuses
|
|
3
|
+
* the same global hover/focus state tokens Button.module.css exempts.
|
|
4
|
+
* recursica-allow-brand: --recursica_brand_states_focus_blur
|
|
5
|
+
* recursica-allow-brand: --recursica_brand_states_focus_border-size
|
|
6
|
+
* recursica-allow-brand: --recursica_brand_states_focus_color
|
|
7
|
+
* recursica-allow-brand: --recursica_brand_states_focus_margin
|
|
8
|
+
* recursica-allow-brand: --recursica_brand_states_hover_color
|
|
9
|
+
* recursica-allow-brand: --recursica_brand_states_hover_opacity
|
|
10
|
+
*/
|
|
11
|
+
|
|
1
12
|
/* HARDCODED VALUES:
|
|
2
13
|
- border-style: solid: Structural rendering rule for the panel border.
|
|
3
14
|
- .contentTop {
|
|
@@ -232,3 +243,97 @@
|
|
|
232
243
|
/* Render over scrolling content */
|
|
233
244
|
z-index: 10;
|
|
234
245
|
}
|
|
246
|
+
|
|
247
|
+
/* ======================================
|
|
248
|
+
PANEL CLOSE BUTTON
|
|
249
|
+
====================================== */
|
|
250
|
+
|
|
251
|
+
.close {
|
|
252
|
+
/* Matches the Button component's text-variant, icon-only, small-size visual treatment
|
|
253
|
+
(see Button.module.css) so the modal close control looks like a Recursica Button
|
|
254
|
+
rather than Mantine's native CloseButton. */
|
|
255
|
+
box-sizing: border-box;
|
|
256
|
+
display: flex;
|
|
257
|
+
align-items: center;
|
|
258
|
+
justify-content: center;
|
|
259
|
+
position: relative;
|
|
260
|
+
overflow: hidden;
|
|
261
|
+
transition: all 0.2s ease;
|
|
262
|
+
|
|
263
|
+
height: var(
|
|
264
|
+
--recursica_ui-kit_components_button_variants_sizes_small_properties_height
|
|
265
|
+
);
|
|
266
|
+
min-width: var(
|
|
267
|
+
--recursica_ui-kit_components_button_variants_content_icon-only_variants_sizes_small_properties_min-width
|
|
268
|
+
);
|
|
269
|
+
padding: 0
|
|
270
|
+
var(
|
|
271
|
+
--recursica_ui-kit_components_button_variants_content_icon-only_variants_sizes_small_properties_horizontal-padding
|
|
272
|
+
);
|
|
273
|
+
border-radius: var(
|
|
274
|
+
--recursica_ui-kit_components_button_variants_content_icon-only_variants_sizes_small_properties_border-radius
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
border-style: solid;
|
|
278
|
+
border-width: var(
|
|
279
|
+
--recursica_ui-kit_components_button_variants_styles_text_properties_border-size
|
|
280
|
+
);
|
|
281
|
+
border-color: var(
|
|
282
|
+
--recursica_ui-kit_components_button_variants_styles_text_properties_colors_border-color
|
|
283
|
+
);
|
|
284
|
+
background-color: var(
|
|
285
|
+
--recursica_ui-kit_components_button_variants_styles_text_properties_colors_background-color
|
|
286
|
+
);
|
|
287
|
+
color: var(
|
|
288
|
+
--recursica_ui-kit_components_button_variants_styles_text_properties_colors_icon-color
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/* Mantine's CloseIcon sets width/height via an inline `style` attribute
|
|
293
|
+
(`--cb-icon-size`, default 70%), which beats any plain class rule regardless of specificity —
|
|
294
|
+
same category of override as Tree's `--level-offset` (see Tree/IMPLEMENTATION_NOTES.md).
|
|
295
|
+
`!important` is required here to land the Button's small icon-size token instead. */
|
|
296
|
+
.close svg {
|
|
297
|
+
position: relative;
|
|
298
|
+
z-index: 1;
|
|
299
|
+
width: var(
|
|
300
|
+
--recursica_ui-kit_components_button_variants_sizes_small_properties_icon
|
|
301
|
+
) !important;
|
|
302
|
+
height: var(
|
|
303
|
+
--recursica_ui-kit_components_button_variants_sizes_small_properties_icon
|
|
304
|
+
) !important;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.close::after {
|
|
308
|
+
content: "";
|
|
309
|
+
position: absolute;
|
|
310
|
+
inset: 0;
|
|
311
|
+
border-radius: inherit;
|
|
312
|
+
z-index: 0;
|
|
313
|
+
pointer-events: none;
|
|
314
|
+
transition: opacity 150ms ease;
|
|
315
|
+
opacity: 0;
|
|
316
|
+
background-color: var(--recursica_brand_states_hover_color);
|
|
317
|
+
}
|
|
318
|
+
.close:hover:not(:disabled)::after {
|
|
319
|
+
opacity: var(--recursica_brand_states_hover_opacity);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/* Mantine's own subtle-variant hover background (CloseButton.css) would otherwise show through
|
|
323
|
+
underneath the overlay above — pin it back to the Button's own background token so only the
|
|
324
|
+
`::after` overlay renders the hover feedback, matching Button's single-overlay treatment. */
|
|
325
|
+
.close:hover {
|
|
326
|
+
background-color: var(
|
|
327
|
+
--recursica_ui-kit_components_button_variants_styles_text_properties_colors_background-color
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.close:focus-visible {
|
|
332
|
+
outline: none;
|
|
333
|
+
box-shadow:
|
|
334
|
+
0 0 0 var(--recursica_brand_states_focus_border-size)
|
|
335
|
+
var(--recursica_brand_states_focus_color),
|
|
336
|
+
0 0 var(--recursica_brand_states_focus_blur)
|
|
337
|
+
var(--recursica_brand_states_focus_margin)
|
|
338
|
+
var(--recursica_brand_states_focus_color);
|
|
339
|
+
}
|
|
@@ -21,10 +21,6 @@ const meta: Meta = {
|
|
|
21
21
|
control: "text",
|
|
22
22
|
description: "Panel title displayed in the header.",
|
|
23
23
|
},
|
|
24
|
-
withCloseButton: {
|
|
25
|
-
control: "boolean",
|
|
26
|
-
description: "Whether to display the close button in the header.",
|
|
27
|
-
},
|
|
28
24
|
wrapHeaderText: {
|
|
29
25
|
control: "boolean",
|
|
30
26
|
description:
|
|
@@ -76,7 +72,6 @@ export const Default: Story = {
|
|
|
76
72
|
args: {
|
|
77
73
|
placement: "right",
|
|
78
74
|
title: "Panel Title",
|
|
79
|
-
withCloseButton: true,
|
|
80
75
|
wrapHeaderText: false,
|
|
81
76
|
},
|
|
82
77
|
render: ({ wrapHeaderText, ...args }: PanelStoryArgs) => {
|
|
@@ -117,7 +112,6 @@ export const LeftPlacement: Story = {
|
|
|
117
112
|
args: {
|
|
118
113
|
placement: "left",
|
|
119
114
|
title: "Navigation",
|
|
120
|
-
withCloseButton: true,
|
|
121
115
|
wrapHeaderText: false,
|
|
122
116
|
},
|
|
123
117
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
@@ -149,7 +143,6 @@ export const ScrollableContent: Story = {
|
|
|
149
143
|
args: {
|
|
150
144
|
placement: "right",
|
|
151
145
|
title: "Scrollable Panel",
|
|
152
|
-
withCloseButton: true,
|
|
153
146
|
wrapHeaderText: false,
|
|
154
147
|
},
|
|
155
148
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
@@ -191,7 +184,6 @@ export const LongTitle: Story = {
|
|
|
191
184
|
placement: "right",
|
|
192
185
|
title:
|
|
193
186
|
"This is a ridiculously long panel title designed to test how the header CSS handles text overflow and whether it truncates correctly or breaks the layout",
|
|
194
|
-
withCloseButton: true,
|
|
195
187
|
wrapHeaderText: true,
|
|
196
188
|
},
|
|
197
189
|
render: ({ ...args }: PanelStoryArgs) => {
|
|
@@ -35,6 +35,9 @@ export interface RecursicaPanelProps
|
|
|
35
35
|
| "style"
|
|
36
36
|
| "position"
|
|
37
37
|
| "opened"
|
|
38
|
+
| "title"
|
|
39
|
+
| "withCloseButton"
|
|
40
|
+
| "closeButtonProps"
|
|
38
41
|
| "withOverlay"
|
|
39
42
|
| "trapFocus"
|
|
40
43
|
| "lockScroll"
|
|
@@ -56,12 +59,16 @@ export type PanelProps = RecursicaOverStyled<RecursicaPanelProps>;
|
|
|
56
59
|
*
|
|
57
60
|
* ```tsx
|
|
58
61
|
* <Panel opened={opened} onClose={close} title="Panel Title" placement="right">
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* </Panel.Body>
|
|
62
|
+
* Content goes here
|
|
63
|
+
* <Panel.Footer>...</Panel.Footer>
|
|
62
64
|
* </Panel>
|
|
63
65
|
* ```
|
|
64
66
|
*
|
|
67
|
+
* The header is required and always rendered: `title` is required, and an icon-only Recursica
|
|
68
|
+
* Close button (`aria-label` "Close", overridable via `closeButtonAriaLabel`) calls the required
|
|
69
|
+
* `onClose`. The footer is optional. Passing `header` replaces the whole default header; avoid it,
|
|
70
|
+
* because then nothing guarantees the title or the Close button.
|
|
71
|
+
*
|
|
65
72
|
* Always non-modal, with no props to change it: the page behind stays usable
|
|
66
73
|
* (no overlay, focus trap or scroll lock), focus returns to the opener on close,
|
|
67
74
|
* Escape always closes, and outside clicks never do.
|
|
@@ -81,6 +88,11 @@ const PanelBase = function Panel({
|
|
|
81
88
|
placement = "right",
|
|
82
89
|
keepMounted = true,
|
|
83
90
|
wrapHeaderText = true,
|
|
91
|
+
title,
|
|
92
|
+
onClose,
|
|
93
|
+
closeButtonAriaLabel = "Close",
|
|
94
|
+
header,
|
|
95
|
+
children,
|
|
84
96
|
...rest
|
|
85
97
|
}: PanelProps) {
|
|
86
98
|
const sanitizedProps = filterStylingProps(rest, overStyled);
|
|
@@ -100,6 +112,7 @@ const PanelBase = function Panel({
|
|
|
100
112
|
}`,
|
|
101
113
|
header: styles.header,
|
|
102
114
|
title: wrapHeaderText ? styles.titleTruncate : styles.title,
|
|
115
|
+
close: styles.close,
|
|
103
116
|
body: styles.body,
|
|
104
117
|
inner: styles.inner,
|
|
105
118
|
},
|
|
@@ -107,20 +120,30 @@ const PanelBase = function Panel({
|
|
|
107
120
|
);
|
|
108
121
|
|
|
109
122
|
return (
|
|
110
|
-
<MantineDrawer
|
|
111
|
-
{...(sanitizedProps as unknown as
|
|
123
|
+
<MantineDrawer.Root
|
|
124
|
+
{...(sanitizedProps as unknown as MantineDrawerRootProps)}
|
|
125
|
+
onClose={onClose}
|
|
112
126
|
size="var(--recursica_ui-kit_components_panel_properties_max-width)"
|
|
113
127
|
position={placement} /* Recursica default: right; Mantine default: left */
|
|
114
128
|
keepMounted={keepMounted}
|
|
115
|
-
/* Always non-modal: the page behind stays usable, Escape always closes */
|
|
116
|
-
withOverlay={false}
|
|
129
|
+
/* Always non-modal and overlay-less: the page behind stays usable, Escape always closes */
|
|
117
130
|
trapFocus={false}
|
|
118
131
|
lockScroll={false}
|
|
119
132
|
returnFocus
|
|
120
133
|
closeOnEscape
|
|
121
134
|
closeOnClickOutside={false}
|
|
122
135
|
classNames={mergedClassNames}
|
|
123
|
-
|
|
136
|
+
>
|
|
137
|
+
<MantineDrawer.Content>
|
|
138
|
+
{header ?? (
|
|
139
|
+
<MantineDrawer.Header>
|
|
140
|
+
<MantineDrawer.Title>{title}</MantineDrawer.Title>
|
|
141
|
+
<MantineDrawer.CloseButton aria-label={closeButtonAriaLabel} />
|
|
142
|
+
</MantineDrawer.Header>
|
|
143
|
+
)}
|
|
144
|
+
<MantineDrawer.Body>{children}</MantineDrawer.Body>
|
|
145
|
+
</MantineDrawer.Content>
|
|
146
|
+
</MantineDrawer.Root>
|
|
124
147
|
);
|
|
125
148
|
};
|
|
126
149
|
PanelBase.displayName = "Panel";
|
|
@@ -15,12 +15,13 @@ import { Panel } from "@recursica/adapter-mantine-v8";
|
|
|
15
15
|
## 2. Basic Example
|
|
16
16
|
|
|
17
17
|
```tsx
|
|
18
|
-
import React from "react";
|
|
18
|
+
import React, { useState } from "react";
|
|
19
19
|
import { Panel } from "@recursica/adapter-mantine-v8";
|
|
20
20
|
|
|
21
21
|
export default function Demo() {
|
|
22
|
+
const [opened, setOpened] = useState(false);
|
|
22
23
|
return (
|
|
23
|
-
<Panel>
|
|
24
|
+
<Panel opened={opened} onClose={() => setOpened(false)} title="Settings">
|
|
24
25
|
<Text>This is a clean, structured visual container.</Text>
|
|
25
26
|
</Panel>
|
|
26
27
|
);
|
|
@@ -47,6 +48,12 @@ All Recursica components in the `@recursica/adapter-mantine-v8` package adhere s
|
|
|
47
48
|
|
|
48
49
|
Use the `placement` prop (not `position`) to choose which edge the panel slides in from: `"left"`, `"right"`, `"top"`, or `"bottom"`. It defaults to `"right"`.
|
|
49
50
|
|
|
51
|
+
### Header: required title and Close button
|
|
52
|
+
|
|
53
|
+
`title` and `onClose` are required. The header always renders the title and an icon-only Recursica Close button with `aria-label="Close"`; pass `closeButtonAriaLabel` to translate it. There is no prop to hide the Close button.
|
|
54
|
+
|
|
55
|
+
The `header` prop replaces the whole default header. Avoid it: a custom header must supply its own title and Close button, and the adapter can no longer guarantee either.
|
|
56
|
+
|
|
50
57
|
### Panel.Footer
|
|
51
58
|
|
|
52
59
|
A `Panel.Footer` sub-component is available for footer content and action buttons.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Radio Implementation Notes
|
|
2
|
+
|
|
3
|
+
### Accessible name lands on the native `<input>` (verified 2026-10-15)
|
|
4
|
+
|
|
5
|
+
`RequireAccessibleLabel` enforces a name at compile time (`label`, `aria-label` or `aria-labelledby`). Mantine forwards `aria-*` to the native `<input>` and auto-generates the `id` that links `<label for>`, so no adapter code is needed. This is the behavior the other adapters must match. Covered by `src/components/NativeInputA11y.dom.test.tsx`, which asserts it for Checkbox, Radio and Switch.
|
|
@@ -62,3 +62,7 @@ mui-only bug: same code pattern, same result, here. Fixed by not capping the gro
|
|
|
62
62
|
width at all — each switch's own label already wraps at 200px via `.labelWrapper`, so no
|
|
63
63
|
group-level cap is needed. Verified live in both adapters: side-by-side now shows the label
|
|
64
64
|
column at left with the switches beside it; stacked layout unaffected.
|
|
65
|
+
|
|
66
|
+
### Accessible name lands on the native `<input>` (verified 2026-10-15)
|
|
67
|
+
|
|
68
|
+
`RequireAccessibleLabel` enforces a name at compile time (`label`, `aria-label` or `aria-labelledby`). Mantine forwards `aria-*` to the native `<input>` and auto-generates the `id` that links `<label for>`, so no adapter code is needed. This is the behavior the other adapters must match. Covered by `src/components/NativeInputA11y.dom.test.tsx`, which asserts it for Checkbox, Radio and Switch.
|