@recursica/mantine-adapter 0.50.1 → 0.50.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/index.d.ts +107 -139
  3. package/dist/mantine-adapter.cjs +1 -1
  4. package/dist/mantine-adapter.cjs.map +1 -1
  5. package/dist/mantine-adapter.css +1 -1
  6. package/dist/mantine-adapter.js +4 -3
  7. package/dist/mantine-adapter.js.map +1 -1
  8. package/package.json +5 -4
  9. package/src/components/Accordion/Accordion.test.tsx +1 -0
  10. package/src/components/AssistiveElement/AssistiveElement.test.tsx +1 -0
  11. package/src/components/AutoComplete/AutoComplete.test.tsx +1 -0
  12. package/src/components/Avatar/Avatar.test.tsx +1 -0
  13. package/src/components/Badge/Badge.test.tsx +1 -0
  14. package/src/components/Breadcrumb/Breadcrumb.test.tsx +1 -0
  15. package/src/components/Button/Button.module.css +1 -0
  16. package/src/components/Button/Button.styleIsolation.dom.test.tsx +98 -0
  17. package/src/components/Button/IMPLEMENTATION_NOTES.md +2 -0
  18. package/src/components/Card/Card.test.tsx +1 -0
  19. package/src/components/Checkbox/Checkbox.test.tsx +1 -0
  20. package/src/components/Chip/Chip.test.tsx +1 -0
  21. package/src/components/Container/Container.test.tsx +1 -0
  22. package/src/components/DatePicker/DatePicker.test.tsx +1 -0
  23. package/src/components/Dropdown/Dropdown.test.tsx +1 -0
  24. package/src/components/FileInput/FileInput.test.tsx +1 -0
  25. package/src/components/FileUpload/FileUpload.test.tsx +1 -0
  26. package/src/components/Flex/Flex.test.tsx +1 -0
  27. package/src/components/Flex/Flex.tsx +5 -5
  28. package/src/components/FormControlLayout/FormControlLayout.test.tsx +1 -0
  29. package/src/components/FormControlWrapper/FormControlWrapper.test.tsx +1 -0
  30. package/src/components/Grid/GRID_IMPLEMENTATION_NOTES.md +2 -2
  31. package/src/components/Grid/Grid.stories.tsx +11 -11
  32. package/src/components/Grid/Grid.test.tsx +1 -0
  33. package/src/components/Grid/Grid.tsx +10 -15
  34. package/src/components/Grid/USAGE.md +3 -3
  35. package/src/components/Group/Group.test.tsx +1 -0
  36. package/src/components/Group/Group.tsx +5 -5
  37. package/src/components/HoverCard/HoverCard.test.tsx +1 -0
  38. package/src/components/Label/Label.test.tsx +1 -0
  39. package/src/components/Link/Link.test.tsx +1 -0
  40. package/src/components/Loader/Loader.test.tsx +1 -0
  41. package/src/components/Menu/Menu.test.tsx +1 -0
  42. package/src/components/Modal/MODAL_IMPLEMENTATION_NOTES.md +12 -0
  43. package/src/components/Modal/Modal.module.css +119 -1
  44. package/src/components/Modal/Modal.stories.tsx +28 -2
  45. package/src/components/Modal/Modal.test.tsx +1 -0
  46. package/src/components/NumberInput/NumberInput.test.tsx +1 -0
  47. package/src/components/Pagination/Pagination.test.tsx +1 -0
  48. package/src/components/Panel/Panel.test.tsx +1 -0
  49. package/src/components/Popover/Popover.test.tsx +1 -0
  50. package/src/components/Radio/Radio.test.tsx +1 -0
  51. package/src/components/ReadOnlyField/ReadOnlyField.test.tsx +1 -0
  52. package/src/components/SegmentedControl/SegmentedControl.test.tsx +1 -0
  53. package/src/components/Slider/Slider.test.tsx +1 -0
  54. package/src/components/Stack/Stack.test.tsx +1 -0
  55. package/src/components/Stack/Stack.tsx +5 -5
  56. package/src/components/Stepper/Stepper.test.tsx +1 -0
  57. package/src/components/Switch/Switch.test.tsx +1 -0
  58. package/src/components/Table/Table.test.tsx +1 -0
  59. package/src/components/Tabs/Tabs.test.tsx +1 -0
  60. package/src/components/Text/Text.stories.tsx +3 -16
  61. package/src/components/Text/Text.test.tsx +1 -0
  62. package/src/components/TextArea/TextArea.test.tsx +1 -0
  63. package/src/components/TextField/TextField.test.tsx +1 -0
  64. package/src/components/TimePicker/TimePicker.test.tsx +1 -0
  65. package/src/components/Timeline/Timeline.test.tsx +1 -0
  66. package/src/components/Title/Title.stories.tsx +3 -16
  67. package/src/components/Title/Title.test.tsx +1 -0
  68. package/src/components/Toast/Toast.test.tsx +1 -0
  69. package/src/components/Tooltip/Tooltip.test.tsx +1 -0
  70. package/src/components/TransferList/TransferList.test.tsx +1 -0
  71. package/src/components/Tree/Tree.test.tsx +1 -0
  72. package/src/utils/filterStylingProps.test.ts +85 -0
  73. package/src/utils/filterStylingProps.ts +1 -0
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.50.1",
16
+ "version": "0.50.4",
17
17
  "type": "module",
18
18
  "main": "./dist/mantine-adapter.cjs",
19
19
  "module": "./dist/mantine-adapter.js",
@@ -55,10 +55,11 @@
55
55
  "lint": "eslint .",
56
56
  "storybook": "storybook dev -p 6011",
57
57
  "analyze-tokens": "analyze-tokens --css @recursica/official-release/recursica_variables_scoped.css --dir src/components --output token-analysis.json",
58
- "prebuild": "npm run analyze-tokens"
58
+ "prebuild": "npm run analyze-tokens",
59
+ "test": "vitest run --project unit",
60
+ "test:dom": "vitest run --project dom"
59
61
  },
60
62
  "devDependencies": {
61
- "@chromatic-com/storybook": "^5.1.1",
62
63
  "@eslint/js": "^9.25.0",
63
64
  "@recursica/recursica-postcss-vars": "*",
64
65
  "@recursica/storybook-template": "*",
@@ -94,7 +95,7 @@
94
95
  "vitest": "^3.2.4"
95
96
  },
96
97
  "dependencies": {
97
- "@recursica/adapter-common": "^0.25.0",
98
+ "@recursica/adapter-common": "^0.25.1",
98
99
  "@recursica/official-release": "^2.8.0"
99
100
  },
100
101
  "peerDependencies": {
@@ -0,0 +1 @@
1
+ // Placeholder for future Accordion component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future AssistiveElement component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future AutoComplete component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future Avatar component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future Badge component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future Breadcrumb component tests. Add tests here if/when needed.
@@ -24,6 +24,7 @@
24
24
  position: relative;
25
25
  transition: all 0.2s ease;
26
26
  width: fit-content; /* Prevent stretching in flex columns */
27
+ max-width: 100%; /* Let a constraining parent (e.g. max-width container) actually shrink the button; fit-content alone ignores it */
27
28
 
28
29
  /* Shared Defaults */
29
30
  font-family: var(
@@ -0,0 +1,98 @@
1
+ import React from "react";
2
+ import { describe, it, expect } from "vitest";
3
+ import { createRoot, type Root } from "react-dom/client";
4
+ import { flushSync } from "react-dom";
5
+ import { MantineProvider, Button as MantineButton } from "@mantine/core";
6
+ import { Button as RecursicaButton } from "./Button";
7
+
8
+ function mount(node: React.ReactElement): {
9
+ container: HTMLElement;
10
+ root: Root;
11
+ } {
12
+ const container = document.createElement("div");
13
+ document.body.appendChild(container);
14
+ const root = createRoot(container);
15
+ flushSync(() => root.render(node));
16
+ return { container, root };
17
+ }
18
+
19
+ function unmount({ container, root }: { container: HTMLElement; root: Root }) {
20
+ root.unmount();
21
+ container.remove();
22
+ }
23
+
24
+ function withProvider(node: React.ReactElement) {
25
+ return <MantineProvider defaultColorScheme="light">{node}</MantineProvider>;
26
+ }
27
+
28
+ /**
29
+ * Guards against Recursica's Button CSS bleeding onto (or being bled onto by) a plain,
30
+ * unwrapped Mantine Button rendered in the same document. The two must be visibly different
31
+ * (Recursica's own classes/data-attributes), but the plain kit button must render identically
32
+ * whether or not a Recursica Button exists alongside it.
33
+ */
34
+ describe("Button style isolation from the underlying Mantine kit", () => {
35
+ it("adds Recursica-only classes/attributes without leaking them onto a plain Mantine Button", () => {
36
+ const recursica = mount(
37
+ withProvider(<RecursicaButton>Recursica</RecursicaButton>),
38
+ );
39
+ const kit = mount(withProvider(<MantineButton>Plain</MantineButton>));
40
+
41
+ try {
42
+ const recursicaButton = recursica.container.querySelector("button")!;
43
+ const kitButton = kit.container.querySelector("button")!;
44
+
45
+ const recursicaClasses = Array.from(recursicaButton.classList);
46
+ const kitClasses = new Set(kitButton.classList);
47
+ const recursicaOnlyClasses = recursicaClasses.filter(
48
+ (cls) => !kitClasses.has(cls),
49
+ );
50
+
51
+ // Recursica's Button.module.css classes must exist...
52
+ expect(recursicaOnlyClasses.length).toBeGreaterThan(0);
53
+ // ...and none of them are present on the plain kit button.
54
+ for (const cls of recursicaOnlyClasses) {
55
+ expect(kitButton.classList.contains(cls)).toBe(false);
56
+ }
57
+
58
+ // Recursica-only markers confirm the two are meaningfully different components.
59
+ expect(recursicaButton.getAttribute("data-variant")).toBe("solid");
60
+ expect(kitButton.getAttribute("data-variant")).toBeNull();
61
+ } finally {
62
+ unmount(recursica);
63
+ unmount(kit);
64
+ }
65
+ });
66
+
67
+ it("renders a plain Mantine Button identically whether or not Recursica's Button is mounted alongside it", () => {
68
+ const recursica = mount(
69
+ withProvider(<RecursicaButton>Recursica</RecursicaButton>),
70
+ );
71
+ const kitAlongsideRecursica = mount(
72
+ withProvider(<MantineButton>Plain</MantineButton>),
73
+ );
74
+ const kitIsolated = mount(
75
+ withProvider(<MantineButton>Plain</MantineButton>),
76
+ );
77
+
78
+ try {
79
+ const kitButton =
80
+ kitAlongsideRecursica.container.querySelector("button")!;
81
+ const isolatedButton = kitIsolated.container.querySelector("button")!;
82
+
83
+ expect(Array.from(kitButton.classList).sort()).toEqual(
84
+ Array.from(isolatedButton.classList).sort(),
85
+ );
86
+
87
+ const kitStyle = getComputedStyle(kitButton);
88
+ const isolatedStyle = getComputedStyle(isolatedButton);
89
+ expect(kitStyle.borderRadius).toBe(isolatedStyle.borderRadius);
90
+ expect(kitStyle.backgroundColor).toBe(isolatedStyle.backgroundColor);
91
+ expect(kitStyle.fontFamily).toBe(isolatedStyle.fontFamily);
92
+ } finally {
93
+ unmount(recursica);
94
+ unmount(kitAlongsideRecursica);
95
+ unmount(kitIsolated);
96
+ }
97
+ });
98
+ });
@@ -38,6 +38,8 @@ Mantine's `.mantine-Button-label` flex centering breaks primitive truncation log
38
38
  - The structural children wrap into `<span className={styles.labelText}>`.
39
39
  - **`.labelText`** binds `overflow: hidden; text-overflow: ellipsis; white-space: nowrap;` creating flawless string cutoffs strictly at exact UI constraints.
40
40
 
41
+ **Bug found (2026-08-27):** none of the above clips the button itself against a _constraining ancestor_ (e.g. `<div style={{ maxWidth: 250 }}>` — see the `TruncatedLabel` story). `.root` uses `width: fit-content` so it hugs its own content when unconstrained, but `fit-content` on this box doesn't reliably resize down against a narrower parent's resolved width — verified via Playwright: the button rendered at its full 534px `max-content` width and visually overflowed the 250px wrapper, even though the wrapper itself measured 250px. `max-width: 100%` on `.root` (percentage of the actual containing block) fixes it — it doesn't fight `width: fit-content` for the normal unconstrained case, it only kicks in when the parent is narrower than the content's intrinsic width.
42
+
41
43
  ---
42
44
 
43
45
  ## Disabled state: brand theme opacity (implicit)
@@ -0,0 +1 @@
1
+ // Placeholder for future Card component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future Checkbox component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future Chip component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future Container component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future DatePicker component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future Dropdown component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future FileInput component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future FileUpload component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future Flex component tests. Add tests here if/when needed.
@@ -10,18 +10,18 @@ import {
10
10
  } from "../../utils/filterStylingProps";
11
11
  import styles from "./Flex.module.css";
12
12
 
13
- import { type RecursicaFlexProps } from "@recursica/adapter-common";
14
-
15
13
  /**
16
14
  * Flex layout wrapper.
17
15
  *
18
16
  * Note: Unlike complex UI components, primitive layout components (Flex, Stack, Group, Container)
19
17
  * DO NOT use the `RecursicaOverStyled` gatekeeper. Developers must be able to freely pass
20
18
  * width, height, padding, margins, and flexbox alignment props to construct structural layouts.
19
+ *
20
+ * No formal Recursica prop contract here: this simply passes through Mantine's own
21
+ * `FlexProps` (gap/rowGap/columnGap/direction/align/justify/wrap are all native to Mantine),
22
+ * layered only with rec- spacing token support via `WithRecursicaSpacing`.
21
23
  */
22
- export type FlexProps = WithRecursicaSpacing<
23
- MantineFlexProps & RecursicaFlexProps
24
- >;
24
+ export type FlexProps = WithRecursicaSpacing<MantineFlexProps>;
25
25
 
26
26
  const _Flex = forwardRef<HTMLDivElement, FlexProps>(function Flex(
27
27
  { children, gap = "rec-default", rowGap, columnGap, ...rest },
@@ -0,0 +1 @@
1
+ // Placeholder for future FormControlLayout component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future FormControlWrapper component tests. Add tests here if/when needed.
@@ -2,8 +2,8 @@
2
2
 
3
3
  The `Grid` component is a generic 12-column (by default) layout wrapper mapped directly to Mantine's `Grid`/`Grid.Col`. It requires no custom logical layouts or CSS workarounds — Mantine handles column geometry, breakpoints, and wrapping natively.
4
4
 
5
- The one deliberate divergence from Mantine's native API: the public `gap` prop replaces Mantine's `gutter`, for naming consistency with the other primitive layout components (`Flex`, `Stack`, `Group`). `gap` is translated to Mantine's `gutter` prop internally via `mapLayoutProps`, the same utility used to resolve `rec-*` spacing tokens elsewhere.
5
+ Per the layout-components rule (no formal Recursica common props definition — these simply pass through the underlying kit's own props), `Grid` and `Grid.Col` are typed directly off Mantine's own `GridProps`/`GridColProps` with no Recursica-specific prop renaming. The public prop for spacing between columns is Mantine's own `gutter` (previously renamed to `gap` for cross-component consistency — that rename has been reverted since it wasn't a real Mantine prop). `gutter` still accepts `rec-*` spacing tokens, resolved via `mapLayoutProps`, the same utility used elsewhere.
6
6
 
7
- `Grid.Col`'s `span`, `offset`, `order`, `visibleFrom`, and `hiddenFrom` props are passed straight through to Mantine's `Grid.Col` unchanged — Mantine already uses generic, portable naming for these.
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`.
@@ -19,12 +19,12 @@ const meta: Meta<GridStoryProps> = {
19
19
  },
20
20
  },
21
21
  args: {
22
- gap: "rec-default",
22
+ gutter: "rec-default",
23
23
  columns: 12,
24
24
  grow: false,
25
25
  },
26
26
  argTypes: {
27
- gap: {
27
+ gutter: {
28
28
  control: "select",
29
29
  options: [
30
30
  "rec-none",
@@ -35,7 +35,7 @@ const meta: Meta<GridStoryProps> = {
35
35
  "rec-xl",
36
36
  "rec-2xl",
37
37
  ],
38
- description: "Gap between columns",
38
+ description: "Gutter between columns",
39
39
  },
40
40
  columns: {
41
41
  control: "number",
@@ -97,17 +97,17 @@ export const ResponsiveSpans: Story = {
97
97
  // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
98
98
  render: ({ withLayer, layer, ...args }: any) => (
99
99
  <Grid {...args}>
100
- <Grid.Col span={{ base: 12, sm: 6, md: 3 }}>
101
- <Swatch>base 12 / sm 6 / md 3</Swatch>
100
+ <Grid.Col span={{ xs: 12, sm: 6, md: 3 }}>
101
+ <Swatch>xs 12 / sm 6 / md 3</Swatch>
102
102
  </Grid.Col>
103
- <Grid.Col span={{ base: 12, sm: 6, md: 3 }}>
104
- <Swatch>base 12 / sm 6 / md 3</Swatch>
103
+ <Grid.Col span={{ xs: 12, sm: 6, md: 3 }}>
104
+ <Swatch>xs 12 / sm 6 / md 3</Swatch>
105
105
  </Grid.Col>
106
- <Grid.Col span={{ base: 12, sm: 6, md: 3 }}>
107
- <Swatch>base 12 / sm 6 / md 3</Swatch>
106
+ <Grid.Col span={{ xs: 12, sm: 6, md: 3 }}>
107
+ <Swatch>xs 12 / sm 6 / md 3</Swatch>
108
108
  </Grid.Col>
109
- <Grid.Col span={{ base: 12, sm: 6, md: 3 }}>
110
- <Swatch>base 12 / sm 6 / md 3</Swatch>
109
+ <Grid.Col span={{ xs: 12, sm: 6, md: 3 }}>
110
+ <Swatch>xs 12 / sm 6 / md 3</Swatch>
111
111
  </Grid.Col>
112
112
  </Grid>
113
113
  ),
@@ -0,0 +1 @@
1
+ // Placeholder for future Grid component tests. Add tests here if/when needed.
@@ -11,24 +11,21 @@ import {
11
11
  } from "../../utils/filterStylingProps";
12
12
  import styles from "./Grid.module.css";
13
13
 
14
- import {
15
- type RecursicaGridProps,
16
- type RecursicaGridColProps,
17
- } from "@recursica/adapter-common";
18
-
19
14
  /**
20
15
  * Grid layout wrapper.
21
16
  *
22
17
  * Note: Unlike complex UI components, primitive layout components (Flex, Stack, Group, Container, Grid)
23
18
  * DO NOT use the `RecursicaOverStyled` gatekeeper. Developers must be able to freely pass
24
19
  * width, height, padding, margins, and flexbox alignment props to construct structural layouts.
20
+ *
21
+ * No formal Recursica prop contract here: this simply passes through Mantine's own
22
+ * `GridProps` (gutter/grow/columns/justify/align are all native to Mantine), layered only
23
+ * with rec- spacing token support via `WithRecursicaSpacing`.
25
24
  */
26
- export type GridProps = WithRecursicaSpacing<
27
- Omit<MantineGridProps, "gutter"> & RecursicaGridProps
28
- >;
25
+ export type GridProps = WithRecursicaSpacing<MantineGridProps>;
29
26
 
30
27
  const _Grid = forwardRef<HTMLDivElement, GridProps>(function Grid(
31
- { children, gap = "rec-default", ...rest },
28
+ { children, gutter = "rec-default", ...rest },
32
29
  ref,
33
30
  ) {
34
31
  const mergedClassNames: Partial<Record<string, string>> = {
@@ -50,15 +47,15 @@ const _Grid = forwardRef<HTMLDivElement, GridProps>(function Grid(
50
47
  ? `${styles.root} ${classNameProp}`
51
48
  : styles.root;
52
49
 
53
- const { gap: gutter, ...mappedRest } = mapLayoutProps({
54
- gap,
50
+ const { gutter: mappedGutter, ...mappedRest } = mapLayoutProps({
51
+ gutter,
55
52
  ...rest,
56
53
  } as Record<string, unknown>);
57
54
 
58
55
  return (
59
56
  <MantineGrid
60
57
  ref={ref}
61
- gutter={gutter as MantineGridProps["gutter"]}
58
+ gutter={mappedGutter as MantineGridProps["gutter"]}
62
59
  className={finalClass}
63
60
  classNames={mergedClassNames}
64
61
  {...(mappedRest as unknown as Omit<MantineGridProps, "gutter">)}
@@ -87,9 +84,7 @@ const GridBase = createPolymorphicComponent<"div", GridProps>(_Grid);
87
84
  // GRID.COL
88
85
  // ============================================================
89
86
 
90
- export type GridColProps = WithRecursicaSpacing<
91
- MantineGridColProps & RecursicaGridColProps
92
- >;
87
+ export type GridColProps = WithRecursicaSpacing<MantineGridColProps>;
93
88
 
94
89
  const _GridCol = forwardRef<HTMLDivElement, GridColProps>(function GridCol(
95
90
  { children, ...rest },
@@ -20,9 +20,9 @@ import { Grid } from "@recursica/mantine-adapter";
20
20
 
21
21
  export default function Demo() {
22
22
  return (
23
- <Grid gap="rec-default">
23
+ <Grid gutter="rec-default">
24
24
  <Grid.Col span={6}>Half width</Grid.Col>
25
- <Grid.Col span={{ base: 12, sm: 6, md: 3 }}>Responsive width</Grid.Col>
25
+ <Grid.Col span={{ xs: 12, sm: 6, md: 3 }}>Responsive width</Grid.Col>
26
26
  </Grid>
27
27
  );
28
28
  }
@@ -44,4 +44,4 @@ All Recursica components in the `@recursica/mantine-adapter` package adhere stri
44
44
 
45
45
  ## 4. Key Integration Features & Constraints
46
46
 
47
- The `Grid` component maps directly to Mantine's `Grid`/`Grid.Col`. The one deviation from Mantine's native API: the prop for spacing between columns is named `gap` (not Mantine's `gutter`), for consistency with `Flex`, `Stack`, and `Group`. Everything else — `columns`, `grow`, `justify`, `align` on `Grid`, and `span`, `offset`, `order`, `visibleFrom`, `hiddenFrom` on `Grid.Col` — matches Mantine's own naming and accepts the same shapes, including per-breakpoint objects (`{ base, xs, sm, md, lg, xl }`).
47
+ The `Grid` component maps directly to Mantine's `Grid`/`Grid.Col` with no Recursica-specific prop renaming — `gutter`, `columns`, `grow`, `justify`, `align` on `Grid`, and `span`, `offset`, `order`, `visibleFrom`, `hiddenFrom` on `Grid.Col` all match Mantine's own naming and accept the same shapes, including per-breakpoint objects (`{ xs, sm, md, lg, xl }`). `gutter` additionally accepts `rec-*` spacing tokens.
@@ -0,0 +1 @@
1
+ // Placeholder for future Group component tests. Add tests here if/when needed.
@@ -9,18 +9,18 @@ import {
9
9
  } from "../../utils/filterStylingProps";
10
10
  import styles from "./Group.module.css";
11
11
 
12
- import { type RecursicaGroupProps } from "@recursica/adapter-common";
13
-
14
12
  /**
15
13
  * Group flex layout wrapper.
16
14
  *
17
15
  * Note: Unlike complex UI components, primitive layout components (Flex, Stack, Group, Container)
18
16
  * DO NOT use the `RecursicaOverStyled` gatekeeper. Developers must be able to freely pass
19
17
  * width, height, padding, margins, and flexbox alignment props to construct structural layouts.
18
+ *
19
+ * No formal Recursica prop contract here: this simply passes through Mantine's own
20
+ * `GroupProps` (gap/rowGap/columnGap/justify/align/wrap are all native to Mantine),
21
+ * layered only with rec- spacing token support via `WithRecursicaSpacing`.
20
22
  */
21
- export type GroupProps = WithRecursicaSpacing<
22
- MantineGroupProps & RecursicaGroupProps
23
- >;
23
+ export type GroupProps = WithRecursicaSpacing<MantineGroupProps>;
24
24
 
25
25
  export const Group = forwardRef<HTMLDivElement, GroupProps>(function Group(
26
26
  { children, gap = "rec-default", ...rest },
@@ -0,0 +1 @@
1
+ // Placeholder for future HoverCard component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future Label component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future Link component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future Loader component tests. Add tests here if/when needed.
@@ -0,0 +1 @@
1
+ // Placeholder for future Menu component tests. Add tests here if/when needed.
@@ -13,3 +13,15 @@ Mantine natively exposes an abstract `size` prop (`"sm" | "md" | "lg" | "xl"`) t
13
13
  ### 2. Scroll Dividers behavior
14
14
 
15
15
  Mantine internally handles scroll state natively, dynamically showing/hiding a divider line when content overflows in `.body`. This logic is tightly coupled to React DOM measurements internally. Our component inherits this dynamic behavior rather than statically rendering a permanent divider, matching Mantine's robust overflow UX. However, we aggressively override the generated `border-bottom` via CSS modules to ensure that when it _does_ appear, it correctly utilizes the `--recursica_ui-kit_components_modal_colors_scroll-divider` variable and `--recursica_ui-kit_components_modal_properties_scroll-divider-thickness` token.
16
+
17
+ ### 3. Title truncation
18
+
19
+ `.title` truncates with an ellipsis (`overflow: hidden`, `white-space: nowrap`, `text-overflow: ellipsis`) rather than wrapping. It also needs `flex: 1 1 auto; min-width: 0;` since it's a flex child of `.header` alongside the close button — without `min-width: 0`, a flex item won't shrink below its content's intrinsic width, so ellipsis never engages. `.header`'s `display: flex` is likewise explicit rather than relied upon from Mantine's own header class, so the mui-adapter's plain-`<div>` header gets identical layout.
20
+
21
+ ### 4. Width was pinned to Mantine's `md` size, not content-driven
22
+
23
+ Despite §1 above, `.content` never actually scaled fluidly: Mantine's own CSS sets `flex: 0 0 var(--modal-size)` (defaulting to 440px) on the Content element, and our module only added `min-width`/`max-width` without touching `flex`. A fixed flex-basis with `flex-shrink: 0` pins the box at exactly 440px regardless of those bounds, so they were unreachable dead code — e.g. "Authentication Required" (the `Default` story's title) didn't fit at 440px and silently wrapped to two lines. Overriding to `flex: 0 1 auto` makes the width shrink-to-fit the content within `min-width`/`max-width`, which is what makes title truncation (§3) only kick in once a title would exceed `max-width` rather than truncating titles that would otherwise fit. `.content[data-full-screen]` restores Mantine's own `flex: 0 0 100%` since the shrink-to-fit override would otherwise stop `fullScreen` from filling the viewport.
24
+
25
+ ### 5. Close button restyled to match Button
26
+
27
+ `.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.
@@ -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
  .root {
2
13
  }
3
14
 
@@ -6,6 +17,12 @@
6
17
  }
7
18
 
8
19
  .content {
20
+ /* HARDCODE: Mantine pins content to a fixed flex-basis from its own size scale
21
+ (`flex: 0 0 var(--modal-size)`, defaulting to 440px). Overriding to `0 1 auto` makes width
22
+ content-driven (shrink-to-fit) instead of fixed, so min-width/max-width below actually bound
23
+ it rather than being unreachable dead code. */
24
+ flex: 0 1 auto;
25
+
9
26
  /* Geometric Bounds */
10
27
  min-width: var(--recursica_ui-kit_components_modal_properties_min-width);
11
28
  max-width: var(--recursica_ui-kit_components_modal_properties_max-width);
@@ -39,7 +56,16 @@
39
56
  flex-direction: column;
40
57
  }
41
58
 
59
+ .content[data-full-screen] {
60
+ /* HARDCODE: preserve Mantine's native full-screen sizing — the shrink-to-fit override above
61
+ would otherwise apply here too and stop `fullScreen` from filling the viewport. */
62
+ flex: 0 0 100%;
63
+ }
64
+
42
65
  .header {
66
+ display: flex; /* HARDCODE: puts the title and close button side-by-side so the title has a bounded width to truncate against */
67
+ align-items: center;
68
+ justify-content: space-between;
43
69
  padding: var(
44
70
  --recursica_ui-kit_components_modal_properties_header-footer-vertical-padding
45
71
  )
@@ -50,6 +76,12 @@
50
76
  }
51
77
 
52
78
  .title {
79
+ flex: 1 1 auto; /* HARDCODE: let the title claim the space between the header edge and the close button */
80
+ min-width: 0; /* HARDCODE: required for text-overflow ellipsis to take effect on a flex child */
81
+ overflow: hidden;
82
+ white-space: nowrap;
83
+ text-overflow: ellipsis;
84
+
53
85
  color: var(--recursica_ui-kit_components_modal_properties_colors_title);
54
86
 
55
87
  /* Direct Figma Typography Mapping */
@@ -173,5 +205,91 @@
173
205
  }
174
206
 
175
207
  .close {
176
- /* Inherits standard icon button tokens natively, but we can override if required */
208
+ /* Matches the Button component's text-variant, icon-only, small-size visual treatment
209
+ (see Button.module.css) so the modal close control looks like a Recursica Button
210
+ rather than Mantine's native CloseButton. */
211
+ box-sizing: border-box;
212
+ display: flex;
213
+ align-items: center;
214
+ justify-content: center;
215
+ position: relative;
216
+ overflow: hidden;
217
+ transition: all 0.2s ease;
218
+
219
+ height: var(
220
+ --recursica_ui-kit_components_button_variants_sizes_small_properties_height
221
+ );
222
+ min-width: var(
223
+ --recursica_ui-kit_components_button_variants_content_icon-only_variants_sizes_small_properties_min-width
224
+ );
225
+ padding: 0
226
+ var(
227
+ --recursica_ui-kit_components_button_variants_content_icon-only_variants_sizes_small_properties_horizontal-padding
228
+ );
229
+ border-radius: var(
230
+ --recursica_ui-kit_components_button_variants_content_icon-only_variants_sizes_small_properties_border-radius
231
+ );
232
+
233
+ border-style: solid;
234
+ border-width: var(
235
+ --recursica_ui-kit_components_button_variants_styles_text_properties_border-size
236
+ );
237
+ border-color: var(
238
+ --recursica_ui-kit_components_button_variants_styles_text_properties_colors_border-color
239
+ );
240
+ background-color: var(
241
+ --recursica_ui-kit_components_button_variants_styles_text_properties_colors_background-color
242
+ );
243
+ color: var(
244
+ --recursica_ui-kit_components_button_variants_styles_text_properties_colors_icon-color
245
+ );
246
+ }
247
+
248
+ /* Mantine's CloseIcon sets width/height via an inline `style` attribute
249
+ (`--cb-icon-size`, default 70%), which beats any plain class rule regardless of specificity —
250
+ same category of override as Tree's `--level-offset` (see Tree/IMPLEMENTATION_NOTES.md).
251
+ `!important` is required here to land the Button's small icon-size token instead. */
252
+ .close svg {
253
+ position: relative;
254
+ z-index: 1;
255
+ width: var(
256
+ --recursica_ui-kit_components_button_variants_sizes_small_properties_icon
257
+ ) !important;
258
+ height: var(
259
+ --recursica_ui-kit_components_button_variants_sizes_small_properties_icon
260
+ ) !important;
261
+ }
262
+
263
+ .close::after {
264
+ content: "";
265
+ position: absolute;
266
+ inset: 0;
267
+ border-radius: inherit;
268
+ z-index: 0;
269
+ pointer-events: none;
270
+ transition: opacity 150ms ease;
271
+ opacity: 0;
272
+ background-color: var(--recursica_brand_states_hover_color);
273
+ }
274
+ .close:hover:not(:disabled)::after {
275
+ opacity: var(--recursica_brand_states_hover_opacity);
276
+ }
277
+
278
+ /* Mantine's own subtle-variant hover background (CloseButton.css) would otherwise show through
279
+ underneath the overlay above — pin it back to the Button's own background token so only the
280
+ `::after` overlay renders the hover feedback, matching Button's single-overlay treatment. */
281
+ .close:hover {
282
+ background-color: var(
283
+ --recursica_ui-kit_components_button_variants_styles_text_properties_colors_background-color
284
+ );
285
+ }
286
+
287
+ .close:focus-visible {
288
+ outline: none;
289
+ box-shadow:
290
+ 0 0 0 var(--recursica_brand_states_focus_border-size)
291
+ var(--recursica_brand_states_focus_color),
292
+ 0 0 var(--recursica_brand_states_focus_blur)
293
+ var(--recursica_brand_states_focus_margin)
294
+ var(--recursica_brand_states_focus_color);
177
295
  }
@@ -19,7 +19,8 @@ export default meta;
19
19
  type Story = StoryObj<typeof Modal>;
20
20
 
21
21
  const DefaultWrapper = (args: ModalProps) => {
22
- const [opened, setOpened] = useState(false);
22
+ // Starts opened so the modal is visible without pressing a button first.
23
+ const [opened, setOpened] = useState(true);
23
24
  return (
24
25
  <>
25
26
  <Modal {...args} opened={opened} onClose={() => setOpened(false)}>
@@ -43,8 +44,33 @@ export const Default: Story = {
43
44
  render: (args) => <DefaultWrapper {...args} />,
44
45
  };
45
46
 
47
+ const LongTitleWrapper = (args: ModalProps) => {
48
+ const [opened, setOpened] = useState(true);
49
+ return (
50
+ <>
51
+ <Modal {...args} opened={opened} onClose={() => setOpened(false)}>
52
+ The title above is longer than the header can display, so it truncates
53
+ with an ellipsis instead of wrapping onto a second line.
54
+ <Modal.Footer>
55
+ <Button onClick={() => setOpened(false)}>Got it</Button>
56
+ </Modal.Footer>
57
+ </Modal>
58
+ <Button onClick={() => setOpened(true)}>Open Modal</Button>
59
+ </>
60
+ );
61
+ };
62
+
63
+ export const LongTitle: Story = {
64
+ args: {
65
+ title:
66
+ "This Modal Title Is Deliberately Long Enough To Exceed The Available Header Width",
67
+ },
68
+ render: (args) => <LongTitleWrapper {...args} />,
69
+ };
70
+
46
71
  const ScrollingWrapper = (args: ModalProps) => {
47
- const [opened, setOpened] = useState(false);
72
+ // Starts opened so the modal is visible without pressing a button first.
73
+ const [opened, setOpened] = useState(true);
48
74
  return (
49
75
  <>
50
76
  <Modal {...args} opened={opened} onClose={() => setOpened(false)}>
@@ -0,0 +1 @@
1
+ // Placeholder for future Modal component tests. Add tests here if/when needed.