@recursica/mui-adapter 0.37.0 → 0.39.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/index.d.ts +17 -21
  3. package/dist/mui-adapter.cjs +50 -50
  4. package/dist/mui-adapter.cjs.map +1 -1
  5. package/dist/mui-adapter.css +1 -1
  6. package/dist/mui-adapter.js +3188 -3167
  7. package/dist/mui-adapter.js.map +1 -1
  8. package/package.json +5 -3
  9. package/src/Introduction.stories.tsx +2 -1
  10. package/src/OverStyling.tsx +1 -2
  11. package/src/Version.tsx +2 -2
  12. package/src/components/Accordion/Accordion.stories.tsx +36 -0
  13. package/src/components/{Autocomplete/Autocomplete.stories.tsx → AutoComplete/AutoComplete.stories.tsx} +2 -2
  14. package/src/components/{Autocomplete/Autocomplete.tsx → AutoComplete/AutoComplete.tsx} +5 -5
  15. package/src/components/AutoComplete/index.ts +1 -0
  16. package/src/components/FormControlLayout/FormControlLayout.stories.tsx +36 -0
  17. package/src/components/Label/Label.stories.tsx +63 -54
  18. package/src/components/Loader/Loader.animate.dom.test.tsx +72 -0
  19. package/src/components/Loader/Loader.module.css +10 -0
  20. package/src/components/Loader/Loader.stories.tsx +20 -0
  21. package/src/components/Loader/Loader.tsx +8 -1
  22. package/src/components/Table/TABLE_IMPLEMENTATION_NOTES.md +11 -0
  23. package/src/components/Table/Table.module.css +14 -0
  24. package/src/components/Table/Table.stories.tsx +1 -1
  25. package/src/components/Table/USAGE.md +2 -2
  26. package/src/components/TimePicker/TIMEPICKER_IMPLEMENTATION_NOTES.md +8 -1
  27. package/src/components/TimePicker/TimePicker.module.css +43 -0
  28. package/src/components/TimePicker/TimePicker.stories.tsx +22 -0
  29. package/src/components/TimePicker/TimePicker.tsx +24 -6
  30. package/src/components/index.ts +1 -2
  31. package/src/index.ts +3 -4
  32. package/src/components/Autocomplete/index.ts +0 -1
  33. package/src/components/Box/Box.stories.tsx +0 -68
  34. package/src/components/Box/Box.test.tsx +0 -1
  35. package/src/components/Box/Box.tsx +0 -24
  36. package/src/components/Box/IMPLEMENTATION_NOTES.md +0 -5
  37. package/src/components/Box/USAGE.md +0 -48
  38. package/src/components/Box/index.ts +0 -1
  39. /package/src/components/{Autocomplete/Autocomplete.module.css → AutoComplete/AutoComplete.module.css} +0 -0
  40. /package/src/components/{Autocomplete/Autocomplete.test.tsx → AutoComplete/AutoComplete.test.tsx} +0 -0
  41. /package/src/components/{Autocomplete/AUTOCOMPLETE_IMPLEMENTATION_NOTES.md → AutoComplete/IMPLEMENTATION_NOTES.md} +0 -0
  42. /package/src/components/{Autocomplete → AutoComplete}/USAGE.md +0 -0
package/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "url": "git+https://github.com/borderux/recursica.git",
14
14
  "directory": "packages/mui-adapter"
15
15
  },
16
- "version": "0.37.0",
16
+ "version": "0.39.0",
17
17
  "publishConfig": {
18
18
  "access": "public"
19
19
  },
@@ -61,11 +61,13 @@
61
61
  "build": "vite build --mode library",
62
62
  "build-storybook": "storybook build -o storybook-static",
63
63
  "lint": "eslint .",
64
- "storybook": "storybook dev -p 6012",
64
+ "storybook": "storybook dev",
65
65
  "analyze-tokens": "analyze-tokens --css ./recursica_variables_scoped.css --dir src/components --output token-analysis.json",
66
66
  "prebuild": "npm run analyze-tokens",
67
67
  "adapter-tester": "adapter-tester --serve",
68
68
  "adapter-tester:automated": "adapter-tester",
69
+ "adapter-tester:update-golden": "adapter-tester --update-golden",
70
+ "adapter-tester:source-of-truth": "adapter-tester --divergence-only",
69
71
  "test": "vitest run --project unit",
70
72
  "test:dom": "vitest run --project dom"
71
73
  },
@@ -111,7 +113,7 @@
111
113
  "vitest": "^3.2.4"
112
114
  },
113
115
  "dependencies": {
114
- "@recursica/adapter-common": "^0.26.0",
116
+ "@recursica/adapter-common": "^0.28.0",
115
117
  "dayjs": "^1.11.21"
116
118
  },
117
119
  "peerDependencies": {
@@ -6,7 +6,8 @@ import { OverStylingInfo } from "./OverStyling";
6
6
  const DOCS_URL = "https://recursica.com";
7
7
  const FORGE_URL = "https://forge.recursica.com";
8
8
 
9
- import { Container, Box, Group, Heading, Text, Link } from "./components";
9
+ import { Box } from "@mui/material";
10
+ import { Container, Group, Heading, Text, Link } from "./components";
10
11
 
11
12
  function IntroductionContent() {
12
13
  return (
@@ -1,9 +1,8 @@
1
1
  import ReactMarkdown from "react-markdown";
2
- import { Paper, Divider } from "@mui/material";
2
+ import { Box, Paper, Divider } from "@mui/material";
3
3
  import {
4
4
  Container,
5
5
  Stack,
6
- Box,
7
6
  Text as RawText,
8
7
  Heading,
9
8
  Switch as RawSwitch,
package/src/Version.tsx CHANGED
@@ -1,6 +1,6 @@
1
1
  import ReactMarkdown from "react-markdown";
2
- import { Paper, Divider } from "@mui/material";
3
- import { Container, Heading, Group, Link, Box } from "./components";
2
+ import { Paper, Divider, Box } from "@mui/material";
3
+ import { Container, Heading, Group, Link } from "./components";
4
4
  import pkg from "../package.json";
5
5
  import changelog from "../CHANGELOG.md?raw";
6
6
 
@@ -123,6 +123,42 @@ export const WithIcons: StoryObj<typeof Accordion> = {
123
123
  },
124
124
  };
125
125
 
126
+ export const LongTitleTruncation: StoryObj<typeof Accordion> = {
127
+ render: () => {
128
+ return (
129
+ // `style` is stripped from Accordion itself (only passes through with overStyled);
130
+ // constrain width on a plain wrapper div instead to force truncation.
131
+ <Layer layer={0} style={{ padding: "24px" }}>
132
+ <div style={{ maxWidth: 320 }}>
133
+ <Accordion defaultValue="long-title" chevron={<ChevronIcon />}>
134
+ <Accordion.Item value="long-title">
135
+ <Accordion.Control leftIcon={<SVGIcon />}>
136
+ This is a deliberately very long accordion item title used to
137
+ verify that overflowing text truncates with a CSS ellipsis
138
+ instead of wrapping or overflowing the header
139
+ </Accordion.Control>
140
+ <Accordion.Panel>
141
+ The header label above should truncate to a single line with a
142
+ trailing ellipsis (…) rather than wrapping onto multiple lines
143
+ or pushing the chevron out of view.
144
+ </Accordion.Panel>
145
+ </Accordion.Item>
146
+
147
+ <Accordion.Item value="short-title">
148
+ <Accordion.Control leftIcon={<SVGIcon />}>
149
+ Short Title
150
+ </Accordion.Control>
151
+ <Accordion.Panel>
152
+ A short title in the same accordion for visual comparison.
153
+ </Accordion.Panel>
154
+ </Accordion.Item>
155
+ </Accordion>
156
+ </div>
157
+ </Layer>
158
+ );
159
+ },
160
+ };
161
+
126
162
  export const Disabled: StoryObj<typeof Accordion> = {
127
163
  render: () => {
128
164
  return (
@@ -1,8 +1,8 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react";
2
- import { Autocomplete as AutoComplete } from "./Autocomplete";
2
+ import { AutoComplete } from "./AutoComplete";
3
3
  import { formControlArgTypes } from "../../../.storybook/commonArgTypes";
4
4
  import { renderRichOptionContent } from "../../utils/renderRichOption";
5
- import styles from "./Autocomplete.module.css";
5
+ import styles from "./AutoComplete.module.css";
6
6
 
7
7
  const meta: Meta<typeof AutoComplete> = {
8
8
  title: "UI-Kit/AutoComplete",
@@ -19,7 +19,7 @@ import {
19
19
  import { type RecursicaFormControlWrapperProps } from "../FormControlWrapper/FormControlWrapper";
20
20
  import { WithReadOnlyWrapper } from "../ReadOnlyField/WithReadOnlyWrapper";
21
21
  import { renderRichOptionContent } from "../../utils/renderRichOption";
22
- import styles from "./Autocomplete.module.css";
22
+ import styles from "./AutoComplete.module.css";
23
23
 
24
24
  import { type RecursicaAutocompleteProps as BaseRecursicaAutocompleteProps } from "@recursica/adapter-common";
25
25
 
@@ -46,10 +46,10 @@ export interface RecursicaAutocompleteProps
46
46
  ReadOnlyControlProps,
47
47
  BaseRecursicaAutocompleteProps {}
48
48
 
49
- export type AutocompleteProps = RecursicaOverStyled<RecursicaAutocompleteProps>;
49
+ export type AutoCompleteProps = RecursicaOverStyled<RecursicaAutocompleteProps>;
50
50
 
51
- export const Autocomplete = forwardRef<HTMLInputElement, AutocompleteProps>(
52
- function Autocomplete(props, ref) {
51
+ export const AutoComplete = forwardRef<HTMLInputElement, AutoCompleteProps>(
52
+ function AutoComplete(props, ref) {
53
53
  const {
54
54
  overStyled = false,
55
55
  formLayout = "stacked",
@@ -255,4 +255,4 @@ export const Autocomplete = forwardRef<HTMLInputElement, AutocompleteProps>(
255
255
  },
256
256
  );
257
257
 
258
- Autocomplete.displayName = "Autocomplete";
258
+ AutoComplete.displayName = "AutoComplete";
@@ -0,0 +1 @@
1
+ export * from "./AutoComplete";
@@ -1,6 +1,7 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react";
2
2
  import { FormControlLayout } from "./FormControlLayout";
3
3
  import { Switch } from "../Switch/Switch";
4
+ import { Label } from "../Label/Label";
4
5
 
5
6
  const meta: Meta<typeof FormControlLayout> = {
6
7
  title: "UI-Kit/FormControlLayout",
@@ -80,3 +81,38 @@ export const SideBySideLayout: Story = {
80
81
  children: <Switch label="Offset switch aligning with grid" />,
81
82
  },
82
83
  };
84
+
85
+ /**
86
+ * A real Label (not a placeholder) in a stacked layout, at `labelSize="default"`. Even
87
+ * though the label sits above the field rather than beside it, its own column still caps
88
+ * to the design system's stacked-layout width (224px) rather than stretching to the full
89
+ * container — long label text wraps instead of spanning edge-to-edge.
90
+ */
91
+ export const StackedLayoutWithLabelDefault: Story = {
92
+ args: {
93
+ formLayout: "stacked",
94
+ labelSize: "default",
95
+ leftSection: (
96
+ <Label labelSize="default">
97
+ A fairly long label to show the stacked-layout width cap in action
98
+ </Label>
99
+ ),
100
+ children: <Switch label="Input area content" />,
101
+ },
102
+ };
103
+
104
+ /**
105
+ * Same as above, at `labelSize="small"` — the stacked-layout width cap is narrower (80px).
106
+ */
107
+ export const StackedLayoutWithLabelSmall: Story = {
108
+ args: {
109
+ formLayout: "stacked",
110
+ labelSize: "small",
111
+ leftSection: (
112
+ <Label labelSize="small">
113
+ A fairly long label to show the stacked-layout width cap in action
114
+ </Label>
115
+ ),
116
+ children: <Switch label="Input area content" />,
117
+ },
118
+ };
@@ -1,9 +1,7 @@
1
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
1
  import React from "react";
3
2
  import type { Meta, StoryObj } from "@storybook/react";
4
3
  import { Label } from "./Label";
5
- import { TextField } from "../TextField/TextField";
6
- import { formControlArgTypes } from "../../../.storybook/commonArgTypes";
4
+ import { Button } from "../Button/Button";
7
5
 
8
6
  type LabelStoryProps = React.ComponentProps<typeof Label>;
9
7
 
@@ -15,12 +13,43 @@ const meta: Meta<LabelStoryProps> = {
15
13
  docs: {
16
14
  description: {
17
15
  component:
18
- "The `Label` component is a strict Recursica-styled wrapper around Mantine's native `Input.Label`. It serves as the primary compositional primitive for all form fields, preserving Mantine's accessibility associations and context while strictly enforcing the Recursica atomic design system.\n\n### Usage with Form Inputs\nWhen working with form structures, render this `Label` component directly above your inputs or supply it to a component's overriding properties. The component automatically maps structural layout dimensions, dynamic alignment (`left` vs `right`), custom indicator gaps, and integrates a customized `optionalText` and `withEditIcon` flow that safely bypasses Mantine's native required asterisk mechanisms.",
16
+ "The `Label` component is a strict Recursica-styled wrapper around Mantine's native `Input.Label`. It serves as the primary compositional primitive for all form fields, preserving Mantine's accessibility associations and context while strictly enforcing the Recursica atomic design system.\n\n### Usage with Form Inputs\nThis component only renders the label itself — layout concerns like `stacked` vs `side-by-side` positioning relative to an input live on `FormControlLayout`/`FormControlWrapper`, not here. Render this `Label` in isolation to verify its own states, or see `UI-Kit/FormControlLayout` for how it composes into a full form field.",
19
17
  },
20
18
  },
21
19
  },
22
20
  argTypes: {
23
- ...formControlArgTypes,
21
+ labelSize: {
22
+ control: "inline-radio",
23
+ options: ["default", "small", "md"],
24
+ description:
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
+ },
27
+ labelAlignment: {
28
+ control: "inline-radio",
29
+ options: ["left", "right"],
30
+ description: "Text alignment of the label content.",
31
+ },
32
+ required: {
33
+ control: "boolean",
34
+ description:
35
+ "Renders the required asterisk (suppressed automatically when `labelWithEditIcon` is set, and mutually exclusive with `labelOptionalText`).",
36
+ },
37
+ labelOptionalText: {
38
+ control: "text",
39
+ description:
40
+ "Secondary text rendered beneath the label. Pass `true` for the default '(Optional)' string, or a custom node/string. Suppressed when `required` is true.",
41
+ },
42
+ labelWithEditIcon: {
43
+ control: "boolean",
44
+ description:
45
+ "Replaces the default edit icon slot with an interactive edit affordance; replaces the required asterisk visually when both are set.",
46
+ },
47
+ labelActionArea: {
48
+ table: { disable: true },
49
+ },
50
+ onLabelEditClick: {
51
+ table: { disable: true },
52
+ },
24
53
  },
25
54
  };
26
55
 
@@ -28,95 +57,75 @@ export default meta;
28
57
 
29
58
  type Story = StoryObj<LabelStoryProps>;
30
59
 
31
- // Utility mapping to pipe raw Label args structurally into TextField accurately
32
- const renderWithTextField = ({ children, ...args }: LabelStoryProps) => (
33
- <TextField
34
- label={children as React.ReactNode}
35
- placeholder="Form Control primitive mapped..."
36
- {...(args as any)}
37
- />
38
- );
39
-
40
60
  export const Default: Story = {
41
61
  args: {
42
- children: "Dynamic Label (Controls)",
43
-
62
+ children: "Label",
44
63
  labelSize: "default",
45
64
  labelAlignment: "left",
46
65
  required: false,
47
66
  labelOptionalText: "",
48
67
  labelWithEditIcon: false,
49
68
  },
50
- render: renderWithTextField,
51
69
  };
52
70
 
53
- export const StackedDefault: Story = {
71
+ export const Required: Story = {
54
72
  args: {
55
- children: "Email Address",
73
+ children: "Required Field",
74
+ required: true,
56
75
  },
57
- render: renderWithTextField,
58
76
  };
59
77
 
60
- export const StackedRequired: Story = {
78
+ export const RequiredSuppressesOptionalText: Story = {
61
79
  args: {
62
- children: "Primary Network Node",
63
-
80
+ children: "Full Name",
64
81
  required: true,
82
+ labelOptionalText: "This should not render",
65
83
  },
66
- render: renderWithTextField,
67
84
  };
68
85
 
69
- export const StackedWithEditIcon: Story = {
86
+ export const WithOptionalText: Story = {
70
87
  args: {
71
- children: "Environment Variables",
72
-
73
- labelWithEditIcon: true,
88
+ children: "Bio",
89
+ labelOptionalText: "Max 100 characters",
74
90
  },
75
- render: renderWithTextField,
76
91
  };
77
92
 
78
- export const SideBySideDefault: Story = {
93
+ export const BooleanOptionalText: Story = {
79
94
  args: {
80
- children: "Status",
81
-
82
- labelSize: "default",
95
+ children: "Middle Initial",
96
+ labelOptionalText: true,
83
97
  },
84
- render: renderWithTextField,
85
98
  };
86
99
 
87
- export const RequiredSuppressesOptionalText: Story = {
100
+ export const WithEditIcon: Story = {
88
101
  args: {
89
- children: "Full Name",
90
-
91
- required: true,
92
- labelOptionalText: "This should not render",
102
+ children: "Shipping Address",
103
+ labelWithEditIcon: true,
93
104
  },
94
- render: renderWithTextField,
95
105
  };
96
106
 
97
- export const BooleanOptionalText: Story = {
107
+ export const RequiredWithEditIcon: Story = {
98
108
  args: {
99
- children: "Middle Initial",
100
-
101
- labelOptionalText: true,
109
+ children: "Primary Network Node",
110
+ required: true,
111
+ labelWithEditIcon: true,
102
112
  },
103
- render: renderWithTextField,
104
113
  };
105
114
 
106
- export const WithEditIcon: Story = {
115
+ export const RightAligned: Story = {
107
116
  args: {
108
- children: "Shipping Address",
109
-
110
- labelWithEditIcon: true,
117
+ children: "Status",
118
+ labelAlignment: "right",
111
119
  },
112
- render: renderWithTextField,
113
120
  };
114
121
 
115
- export const LayerOneSideBySide: Story = {
122
+ export const WithActionArea: Story = {
116
123
  args: {
117
124
  children: "Configuration",
118
-
119
- labelWithEditIcon: true,
125
+ labelActionArea: (
126
+ <Button variant="text" size="small">
127
+ Edit
128
+ </Button>
129
+ ),
120
130
  },
121
- render: renderWithTextField,
122
131
  };
@@ -0,0 +1,72 @@
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 { Loader } from "./Loader";
6
+
7
+ function mount(node: React.ReactElement): {
8
+ container: HTMLElement;
9
+ root: Root;
10
+ } {
11
+ const container = document.createElement("div");
12
+ document.body.appendChild(container);
13
+ const root = createRoot(container);
14
+ flushSync(() => root.render(node));
15
+ return { container, root };
16
+ }
17
+
18
+ function unmount({ container, root }: { container: HTMLElement; root: Root }) {
19
+ root.unmount();
20
+ container.remove();
21
+ }
22
+
23
+ /**
24
+ * `animate={false}` must deterministically freeze every variant — including
25
+ * the bars'/dots' individually-animated child spans, which the freeze rule
26
+ * can only reach via a descendant selector, not the `data-variant`-scoped
27
+ * rules the size/thickness CSS uses.
28
+ */
29
+ describe("Loader animate prop", () => {
30
+ it.each(["oval", "bars", "dots"] as const)(
31
+ "freezes every animated element for variant=%s when animate is false, and animates by default",
32
+ (variant) => {
33
+ const animated = mount(<Loader variant={variant} />);
34
+ const frozen = mount(<Loader variant={variant} animate={false} />);
35
+
36
+ try {
37
+ const animatedRoot =
38
+ animated.container.querySelector("[data-variant]")!;
39
+ const frozenRoot = frozen.container.querySelector("[data-variant]")!;
40
+
41
+ const animatedTargets = [
42
+ animatedRoot,
43
+ ...Array.from(animatedRoot.querySelectorAll("*")),
44
+ ];
45
+ const frozenTargets = [
46
+ frozenRoot,
47
+ ...Array.from(frozenRoot.querySelectorAll("*")),
48
+ ];
49
+
50
+ // At least one element (or the root's own ::after, for oval) actually
51
+ // animates by default — otherwise this test would trivially pass.
52
+ const hasAnimation = (el: Element, pseudo?: string) =>
53
+ getComputedStyle(el, pseudo).animationName !== "none";
54
+ const animatedHasMotion =
55
+ animatedTargets.some((el) => hasAnimation(el)) ||
56
+ hasAnimation(animatedRoot, "::after");
57
+ expect(animatedHasMotion).toBe(true);
58
+
59
+ // Frozen: nothing animates, root included, pseudo-element included.
60
+ for (const el of frozenTargets) {
61
+ expect(getComputedStyle(el).animationName).toBe("none");
62
+ }
63
+ expect(getComputedStyle(frozenRoot, "::after").animationName).toBe(
64
+ "none",
65
+ );
66
+ } finally {
67
+ unmount(animated);
68
+ unmount(frozen);
69
+ }
70
+ },
71
+ );
72
+ });
@@ -9,6 +9,16 @@
9
9
  box-sizing: border-box;
10
10
  }
11
11
 
12
+ /* `animate={false}` — freezes every variant's animation (the oval's own
13
+ * spinning ::after, and the bars'/dots' individually-animated child spans)
14
+ * so the loader renders deterministically, e.g. for a visual-regression
15
+ * snapshot that would otherwise diff differently every run. */
16
+ .root[data-animate="false"],
17
+ .root[data-animate="false"] *,
18
+ .root[data-animate="false"]::after {
19
+ animation: none !important;
20
+ }
21
+
12
22
  /* data-size mapping */
13
23
 
14
24
  /* SMALL */
@@ -41,6 +41,11 @@ const meta: Meta<LoaderStoryArgs> = {
41
41
  description:
42
42
  "Applies a wrapping context to observe rendering logic externally",
43
43
  },
44
+ animate: {
45
+ control: "boolean",
46
+ description:
47
+ "Freezes the CSS animation when false — deterministic, for visual regression",
48
+ },
44
49
  },
45
50
  };
46
51
 
@@ -48,6 +53,11 @@ export default meta;
48
53
 
49
54
  type Story = StoryObj<LoaderStoryArgs>;
50
55
 
56
+ /**
57
+ * Animated — excluded from visual regression (`adapter-tester.config.json`),
58
+ * since a moving animation diffs differently every run. See the `Static*`
59
+ * stories below for the deterministic, visual-regression-covered equivalents.
60
+ */
51
61
  export const Default: Story = {
52
62
  args: {
53
63
  variant: "oval",
@@ -62,33 +72,43 @@ export const Default: Story = {
62
72
  ),
63
73
  };
64
74
 
75
+ /** `animate: false` freezes the spin — deterministic for visual regression. */
65
76
  export const StaticOvalDefault: Story = {
66
77
  args: {
67
78
  variant: "oval",
68
79
  size: "default",
80
+ animate: false,
69
81
  },
70
82
  // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
71
83
  render: ({ withLayer, layer, ...args }: any) => <Loader {...args} />,
72
84
  };
73
85
 
86
+ /** `animate: false` freezes the bars — deterministic for visual regression. */
74
87
  export const StaticBarsLarge: Story = {
75
88
  args: {
76
89
  variant: "bars",
77
90
  size: "large",
91
+ animate: false,
78
92
  },
79
93
  // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
80
94
  render: ({ withLayer, layer, ...args }: any) => <Loader {...args} />,
81
95
  };
82
96
 
97
+ /** `animate: false` freezes the dots — deterministic for visual regression. */
83
98
  export const StaticDotsSmall: Story = {
84
99
  args: {
85
100
  variant: "dots",
86
101
  size: "sm",
102
+ animate: false,
87
103
  },
88
104
  // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
89
105
  render: ({ withLayer, layer, ...args }: any) => <Loader {...args} />,
90
106
  };
91
107
 
108
+ /**
109
+ * Animated — excluded from visual regression, same as `Default`; this one
110
+ * additionally demonstrates rendering inside a `layer={2}` context.
111
+ */
92
112
  export const LayerTwoOval: Story = {
93
113
  args: {
94
114
  variant: "oval",
@@ -12,7 +12,13 @@ export type LoaderProps = RecursicaOverStyled<
12
12
  >;
13
13
 
14
14
  export const Loader = forwardRef<HTMLSpanElement, LoaderProps>(function Loader(
15
- { variant = "oval", size = "default", overStyled = false, ...rest },
15
+ {
16
+ variant = "oval",
17
+ size = "default",
18
+ animate = true,
19
+ overStyled = false,
20
+ ...rest
21
+ },
16
22
  ref,
17
23
  ) {
18
24
  const mapSize = {
@@ -54,6 +60,7 @@ export const Loader = forwardRef<HTMLSpanElement, LoaderProps>(function Loader(
54
60
  ref={ref}
55
61
  data-variant={variant}
56
62
  data-size={resolvedSize}
63
+ data-animate={animate}
57
64
  {...sanitizedProps}
58
65
  className={finalClass}
59
66
  >
@@ -55,6 +55,17 @@ than Mantine's. `Table.module.css` resets `.row:global(.Mui-selected)` to
55
55
  `background-color: transparent` so only the Recursica token color shows — same
56
56
  override-the-library's-own-styling rule the canonical guide documents for hover.
57
57
 
58
+ ## Currency alignment on header/footer reuses the table-cell token
59
+
60
+ `--recursica_ui-kit_components_table-cell_properties_currency-style_text-align` (`right`) is the
61
+ only currency-style text-align token the UI Kit exports — there's no
62
+ `table-header_properties_currency-style_*` block at all, and the
63
+ `table-footer_properties_currency-style_*` block skips `text-align` specifically.
64
+ `Table.module.css` reuses the table-cell token for both `thead .cell[data-currency="true"]` and
65
+ `tfoot .cell[data-currency="true"]` so header/footer currency cells stay right-aligned in step
66
+ with the body. `Table.Cell` already threads `variant`/`data-currency` through regardless of
67
+ context, so no `Table.tsx` change was needed here (unlike `mantine-adapter`'s `Table.Th`).
68
+
58
69
  ## Selectable rows (checkboxes)
59
70
 
60
71
  See `mantine-adapter`'s `TABLE_IMPLEMENTATION_NOTES.md` — confirmed 2026-08-22 that neither
@@ -225,6 +225,15 @@
225
225
  );
226
226
  }
227
227
 
228
+ /* Currency column header alignment override. No dedicated table-header currency-style token
229
+ exists yet, so this reuses the table-cell currency-style text-align token to match the
230
+ currency value cells below it. */
231
+ .root thead .cell[data-currency="true"] {
232
+ text-align: var(
233
+ --recursica_ui-kit_components_table-cell_properties_currency-style_text-align
234
+ );
235
+ }
236
+
228
237
  /* Sort label — MUI's TableSortLabel renders its own arrow icon; re-color it to inherit the
229
238
  cell's own text color (instead of MUI's default sort-label color) and size/space it with the
230
239
  same tokens Mantine's own inline chevron uses. */
@@ -425,6 +434,11 @@
425
434
  line-height: var(
426
435
  --recursica_ui-kit_components_table-footer_properties_currency-style_line-height
427
436
  );
437
+ /* No table-footer currency-style text-align token exists yet; reuse the table-cell one so
438
+ footer currency cells match the body's right alignment. */
439
+ text-align: var(
440
+ --recursica_ui-kit_components_table-cell_properties_currency-style_text-align
441
+ );
428
442
  text-decoration: var(
429
443
  --recursica_ui-kit_components_table-footer_properties_currency-style_text-decoration
430
444
  );
@@ -128,7 +128,7 @@ export const CurrencyColumnWithFooter: Story = {
128
128
  <Table.Head>
129
129
  <Table.Row>
130
130
  <Table.Cell>Item</Table.Cell>
131
- <Table.Cell>Price</Table.Cell>
131
+ <Table.Cell variant="currency">Price</Table.Cell>
132
132
  </Table.Row>
133
133
  </Table.Head>
134
134
  <Table.Body>
@@ -45,14 +45,14 @@ export default function Demo() {
45
45
  ## 3. Row and Cell States
46
46
 
47
47
  - **`Table.Row`**: `selected` applies the selected-row background (and MUI's own `selected`/`aria-selected`); `disabled` dims the row and applies the disabled cell colors to every cell in it.
48
- - **`Table.Cell`**: `sorted="asc" | "desc"` applies the sorted header style (pair with `Table.SortLabel` for the actual sort icon — see below); `variant="currency"` applies the currency text style; `disabled` dims the cell.
48
+ - **`Table.Cell`**: `sorted="asc" | "desc"` applies the sorted header style (pair with `Table.SortLabel` for the actual sort icon — see below); `variant="currency"` applies the currency text style, right-aligning it in any context (header, body, or footer); `disabled` dims the cell.
49
49
  - **`Table.SortLabel`** (wraps MUI's `TableSortLabel`): renders the actual sort arrow. Compose it inside a `Table.Cell` the same way MUI's own docs do.
50
50
 
51
51
  ```tsx
52
52
  <Table.Head>
53
53
  <Table.Row>
54
54
  <Table.Cell>Name</Table.Cell>
55
- <Table.Cell sorted="asc">
55
+ <Table.Cell sorted="asc" variant="currency">
56
56
  <Table.SortLabel active direction="asc">
57
57
  Balance
58
58
  </Table.SortLabel>