@recursica/adapter-mantine-v8 1.3.2 → 1.3.3

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/dist/index.d.ts CHANGED
@@ -1035,6 +1035,10 @@ export declare const Panel: PanelComponent;
1035
1035
  * </Panel>
1036
1036
  * ```
1037
1037
  *
1038
+ * Always non-modal, with no props to change it: the page behind stays usable
1039
+ * (no overlay, focus trap or scroll lock), focus returns to the opener on close,
1040
+ * Escape always closes, and outside clicks never do.
1041
+ *
1038
1042
  * Mantine Drawer sub-components available via dot-notation:
1039
1043
  * - `Panel.Header` — Top section with title and close button
1040
1044
  * - `Panel.Title` — Title text within the header
@@ -1240,7 +1244,7 @@ export declare interface RecursicaNumberInputProps extends Omit<NumberInputProps
1240
1244
  /**
1241
1245
  * Recursica Panel root props. Extends Mantine Drawer.
1242
1246
  */
1243
- export declare interface RecursicaPanelProps extends Omit<DrawerProps, "size" | "styles" | "classNames" | "style" | "position" | "opened">, RecursicaPanelProps_2 {
1247
+ export declare interface RecursicaPanelProps extends Omit<DrawerProps, "size" | "styles" | "classNames" | "style" | "position" | "opened" | "withOverlay" | "trapFocus" | "lockScroll" | "returnFocus" | "closeOnEscape" | "closeOnClickOutside">, RecursicaPanelProps_2 {
1244
1248
  }
1245
1249
 
1246
1250
  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.3.2",
15
+ "version": "1.3.3",
16
16
  "publishConfig": {
17
17
  "access": "public"
18
18
  },
@@ -16,3 +16,12 @@ line — headings are the primary intended use case for this property. Not a des
16
16
  layout algorithm choice, so it's hardcoded rather than pulled from `recursica_variables_scoped.css`.
17
17
  Chromium/Firefox only balance up to ~6 lines, which comfortably covers heading text. No fallback
18
18
  needed — browsers that don't support the value just ignore the declaration.
19
+
20
+ ## Heading owns semantic `h1`-`h6` (Matt Massey, 2026-10-08)
21
+
22
+ **Decision:** `Heading` is the only component that renders semantic `<h1>`-`<h6>` elements. Each
23
+ `order` maps to a heading level that Recursica and Forge define and style
24
+ (`recursica_brand_typography_h{order}`).
25
+
26
+ **Implementation:** Heading's styles are fixed by the Recursica JSON/CSS. Do not add variants to
27
+ Heading. Any other kind of text belongs in `Text`.
@@ -122,3 +122,11 @@ Project-wide fix; see Chip's `CHIP_IMPLEMENTATION_NOTES.md` for the original dis
122
122
  **Decision:** The `border-radius` token is applied only to the corners on the edge that faces the page, not all four.
123
123
 
124
124
  **Implementation:** A panel with `placement="right"` (the default) rounds its upper-left and lower-left corners only; its right corners are square against the screen edge. A panel with `placement="left"` rounds its upper-right and lower-right corners only. `Panel.tsx` adds `.contentRight` / `.contentLeft` to the `content` class, and those zero the two outer corners. `top` and `bottom` placements are unchanged (all four corners rounded).
125
+
126
+ ---
127
+
128
+ ## 10. Always non-modal (Matt Massey, 2026-10-08)
129
+
130
+ **Decision:** Panel is never modal, and there are no props to change that.
131
+
132
+ **Implementation:** The house rule is that the page behind a panel stays usable. `Panel.tsx` always passes `withOverlay={false}`, `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.
@@ -21,10 +21,6 @@ const meta: Meta = {
21
21
  control: "text",
22
22
  description: "Panel title displayed in the header.",
23
23
  },
24
- withOverlay: {
25
- control: "boolean",
26
- description: "Whether to display a background overlay.",
27
- },
28
24
  withCloseButton: {
29
25
  control: "boolean",
30
26
  description: "Whether to display the close button in the header.",
@@ -80,7 +76,6 @@ export const Default: Story = {
80
76
  args: {
81
77
  placement: "right",
82
78
  title: "Panel Title",
83
- withOverlay: true,
84
79
  withCloseButton: true,
85
80
  wrapHeaderText: false,
86
81
  },
@@ -122,7 +117,6 @@ export const LeftPlacement: Story = {
122
117
  args: {
123
118
  placement: "left",
124
119
  title: "Navigation",
125
- withOverlay: true,
126
120
  withCloseButton: true,
127
121
  wrapHeaderText: false,
128
122
  },
@@ -155,7 +149,6 @@ export const ScrollableContent: Story = {
155
149
  args: {
156
150
  placement: "right",
157
151
  title: "Scrollable Panel",
158
- withOverlay: true,
159
152
  withCloseButton: true,
160
153
  wrapHeaderText: false,
161
154
  },
@@ -198,7 +191,6 @@ export const LongTitle: Story = {
198
191
  placement: "right",
199
192
  title:
200
193
  "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",
201
- withOverlay: true,
202
194
  withCloseButton: true,
203
195
  wrapHeaderText: true,
204
196
  },
@@ -13,7 +13,6 @@ import {
13
13
  import {
14
14
  filterStylingProps,
15
15
  mergeClassNames,
16
- withCallerOverride,
17
16
  type RecursicaOverStyled,
18
17
  } from "../../utils/filterStylingProps";
19
18
  import styles from "./Panel.module.css";
@@ -30,7 +29,18 @@ import { type RecursicaPanelProps as BaseRecursicaPanelProps } from "@recursica/
30
29
  export interface RecursicaPanelProps
31
30
  extends Omit<
32
31
  MantineDrawerProps,
33
- "size" | "styles" | "classNames" | "style" | "position" | "opened"
32
+ | "size"
33
+ | "styles"
34
+ | "classNames"
35
+ | "style"
36
+ | "position"
37
+ | "opened"
38
+ | "withOverlay"
39
+ | "trapFocus"
40
+ | "lockScroll"
41
+ | "returnFocus"
42
+ | "closeOnEscape"
43
+ | "closeOnClickOutside"
34
44
  >,
35
45
  BaseRecursicaPanelProps {}
36
46
 
@@ -52,6 +62,10 @@ export type PanelProps = RecursicaOverStyled<RecursicaPanelProps>;
52
62
  * </Panel>
53
63
  * ```
54
64
  *
65
+ * Always non-modal, with no props to change it: the page behind stays usable
66
+ * (no overlay, focus trap or scroll lock), focus returns to the opener on close,
67
+ * Escape always closes, and outside clicks never do.
68
+ *
55
69
  * Mantine Drawer sub-components available via dot-notation:
56
70
  * - `Panel.Header` — Top section with title and close button
57
71
  * - `Panel.Title` — Title text within the header
@@ -98,10 +112,13 @@ const PanelBase = function Panel({
98
112
  size="var(--recursica_ui-kit_components_panel_properties_max-width)"
99
113
  position={placement} /* Recursica default: right; Mantine default: left */
100
114
  keepMounted={keepMounted}
101
- closeOnClickOutside={withCallerOverride(
102
- Boolean(rest.opened),
103
- rest.closeOnClickOutside,
104
- )}
115
+ /* Always non-modal: the page behind stays usable, Escape always closes */
116
+ withOverlay={false}
117
+ trapFocus={false}
118
+ lockScroll={false}
119
+ returnFocus
120
+ closeOnEscape
121
+ closeOnClickOutside={false}
105
122
  classNames={mergedClassNames}
106
123
  />
107
124
  );
@@ -10,3 +10,13 @@ line. Not a design token — it's a layout algorithm choice, so it's hardcoded r
10
10
  from `recursica_variables_scoped.css`. Chromium/Firefox only balance up to ~6 lines; longer
11
11
  paragraphs silently fall back to normal wrapping past that point. No fallback needed — browsers
12
12
  that don't support the value just ignore the declaration.
13
+
14
+ ## Text must never render `h1`-`h6` (Matt Massey, 2026-10-08)
15
+
16
+ **Decision:** `Text` must never render an `<h1>`-`<h6>` element. It is not allowed. Semantic
17
+ headings use `Heading`.
18
+
19
+ **Implementation:** `Text` covers every other kind of text in the system (body, labels, captions,
20
+ etc.). Its variants can extend what the Recursica JSON and CSS define, so a variant need not
21
+ exist in the Recursica tokens. Heading levels are the one thing `Text` can't be: they have a
22
+ fixed definition and styling from Recursica and Forge, owned by `Heading`.
@@ -0,0 +1,51 @@
1
+ import React from "react";
2
+ import { describe, it, expect, vi } from "vitest";
3
+ import { createRoot } from "react-dom/client";
4
+ import { flushSync } from "react-dom";
5
+ import { MantineProvider } from "@mantine/core";
6
+ import { Text } from "./Text";
7
+
8
+ class Boundary extends React.Component<
9
+ { children: React.ReactNode },
10
+ { message: string | null }
11
+ > {
12
+ state = { message: null as string | null };
13
+ static getDerivedStateFromError(error: Error) {
14
+ return { message: error.message };
15
+ }
16
+ render() {
17
+ return this.state.message ?? this.props.children;
18
+ }
19
+ }
20
+
21
+ function render(component: string): HTMLElement {
22
+ const container = document.createElement("div");
23
+ document.body.appendChild(container);
24
+ const props = { component } as Record<string, unknown>;
25
+ flushSync(() =>
26
+ createRoot(container).render(
27
+ <MantineProvider>
28
+ <Boundary>
29
+ <Text {...(props as object)}>Content</Text>
30
+ </Boundary>
31
+ </MantineProvider>,
32
+ ),
33
+ );
34
+ return container;
35
+ }
36
+
37
+ describe("Text heading elements", () => {
38
+ it.each(["h1", "h2", "h3", "h4", "h5", "h6"])(
39
+ "throws when component is %s",
40
+ (component) => {
41
+ vi.spyOn(console, "error").mockImplementation(() => {});
42
+ expect(render(component).textContent).toContain(
43
+ `Text cannot render <${component}>. Use <Heading> for semantic h1-h6.`,
44
+ );
45
+ },
46
+ );
47
+
48
+ it.each(["p", "span", "label"])("renders component=%s", (component) => {
49
+ expect(render(component).querySelector(component)).not.toBeNull();
50
+ });
51
+ });
@@ -121,3 +121,28 @@ export const Emphasis: Story = {
121
121
  </div>
122
122
  ),
123
123
  };
124
+
125
+ export const AsElement: Story = {
126
+ args: {},
127
+ parameters: {
128
+ docs: {
129
+ description: {
130
+ story:
131
+ "`component` renders Text as an inline `span`, a `label` or a `div`. `h1` to `h6` throw; use `<Heading>`.",
132
+ },
133
+ },
134
+ },
135
+ render: () => (
136
+ <div style={{ display: "flex", flexDirection: "column", gap: "16px" }}>
137
+ <Text>Default is a block paragraph.</Text>
138
+ <div>
139
+ Inline text:{" "}
140
+ <Text component="span" emphasis="low">
141
+ a span inside a line
142
+ </Text>
143
+ .
144
+ </div>
145
+ <Text component="label">A label</Text>
146
+ </div>
147
+ ),
148
+ };
@@ -18,6 +18,8 @@ export type TextProps = RecursicaOverStyled<
18
18
  "color"
19
19
  >;
20
20
 
21
+ const HEADING_ELEMENTS = ["h1", "h2", "h3", "h4", "h5", "h6"];
22
+
21
23
  const _Text = forwardRef<HTMLDivElement, TextProps>(function Text(
22
24
  {
23
25
  overStyled = false,
@@ -28,6 +30,13 @@ const _Text = forwardRef<HTMLDivElement, TextProps>(function Text(
28
30
  },
29
31
  ref,
30
32
  ) {
33
+ const component = (rest as { component?: unknown }).component;
34
+ if (typeof component === "string" && HEADING_ELEMENTS.includes(component)) {
35
+ throw new Error(
36
+ `Text cannot render <${component}>. Use <Heading> for semantic h1-h6.`,
37
+ );
38
+ }
39
+
31
40
  const sanitizedProps = filterStylingProps(rest, overStyled);
32
41
  const classNameProp = (sanitizedProps as Record<string, unknown>)
33
42
  .className as string | undefined;