@recursica/mui-adapter 0.2.0 → 0.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.
Files changed (88) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/mui-adapter.cjs +1 -1
  3. package/dist/mui-adapter.cjs.map +1 -1
  4. package/dist/mui-adapter.css +1 -1
  5. package/dist/mui-adapter.js +286 -91
  6. package/dist/mui-adapter.js.map +1 -1
  7. package/dist/src/components/Box/Box.d.ts +2 -3
  8. package/dist/src/components/Button/Button.d.ts +40 -2
  9. package/dist/src/components/Container/Container.d.ts +8 -2
  10. package/dist/src/components/Flex/Flex.d.ts +25 -2
  11. package/dist/src/components/Group/Group.d.ts +10 -2
  12. package/dist/src/components/Link/Link.d.ts +13 -2
  13. package/dist/src/components/Loader/Loader.d.ts +9 -2
  14. package/dist/src/components/Stack/Stack.d.ts +8 -2
  15. package/dist/src/components/Text/Text.d.ts +18 -2
  16. package/dist/src/components/Title/Title.d.ts +17 -2
  17. package/dist/src/components/Typography/Typography.d.ts +27 -0
  18. package/dist/src/utils/filterStylingProps.d.ts +3 -0
  19. package/package.json +2 -1
  20. package/src/Introduction.stories.tsx +113 -110
  21. package/src/OverStyling.tsx +18 -23
  22. package/src/Version.tsx +14 -21
  23. package/src/components/Accordion/Accordion.stories.tsx +19 -0
  24. package/src/components/AssistiveElement/AssistiveElement.stories.tsx +19 -0
  25. package/src/components/Avatar/Avatar.stories.tsx +19 -0
  26. package/src/components/Badge/Badge.stories.tsx +19 -0
  27. package/src/components/Box/Box.stories.tsx +19 -10
  28. package/src/components/Box/Box.tsx +11 -14
  29. package/src/components/Box/IMPLEMENTATION_NOTES.md +5 -0
  30. package/src/components/Breadcrumb/Breadcrumb.stories.tsx +19 -0
  31. package/src/components/Button/BUTTON_IMPLEMENTATION_NOTES.md +33 -0
  32. package/src/components/Button/Button.module.css +343 -0
  33. package/src/components/Button/Button.stories.tsx +181 -5
  34. package/src/components/Button/Button.tsx +141 -5
  35. package/src/components/Card/Card.stories.tsx +19 -0
  36. package/src/components/Checkbox/Checkbox.stories.tsx +19 -0
  37. package/src/components/Chip/Chip.stories.tsx +19 -0
  38. package/src/components/Container/Container.stories.tsx +19 -0
  39. package/src/components/Container/Container.tsx +56 -5
  40. package/src/components/Container/IMPLEMENTATION_NOTES.md +5 -0
  41. package/src/components/DatePicker/DatePicker.stories.tsx +19 -0
  42. package/src/components/Dropdown/Dropdown.stories.tsx +19 -0
  43. package/src/components/FileInput/FileInput.stories.tsx +19 -0
  44. package/src/components/FileUpload/FileUpload.stories.tsx +19 -0
  45. package/src/components/Flex/Flex.stories.tsx +19 -0
  46. package/src/components/Flex/Flex.tsx +77 -5
  47. package/src/components/FormControlLayout/FormControlLayout.stories.tsx +19 -0
  48. package/src/components/FormControlWrapper/FormControlWrapper.stories.tsx +19 -0
  49. package/src/components/Group/Group.stories.tsx +133 -5
  50. package/src/components/Group/Group.tsx +57 -5
  51. package/src/components/HoverCard/HoverCard.stories.tsx +19 -0
  52. package/src/components/Label/Label.stories.tsx +19 -0
  53. package/src/components/Link/Link.module.css +133 -0
  54. package/src/components/Link/Link.stories.tsx +52 -4
  55. package/src/components/Link/Link.tsx +47 -5
  56. package/src/components/Loader/LOADER_IMPLEMENTATION_NOTES.md +25 -0
  57. package/src/components/Loader/Loader.module.css +166 -0
  58. package/src/components/Loader/Loader.stories.tsx +93 -6
  59. package/src/components/Loader/Loader.tsx +69 -5
  60. package/src/components/Menu/Menu.stories.tsx +19 -0
  61. package/src/components/Modal/Modal.stories.tsx +19 -0
  62. package/src/components/NumberInput/NumberInput.stories.tsx +19 -0
  63. package/src/components/Pagination/Pagination.stories.tsx +19 -0
  64. package/src/components/Panel/Panel.stories.tsx +19 -0
  65. package/src/components/Radio/Radio.stories.tsx +19 -0
  66. package/src/components/ReadOnlyField/ReadOnlyField.stories.tsx +19 -0
  67. package/src/components/SegmentedControl/SegmentedControl.stories.tsx +19 -0
  68. package/src/components/Slider/Slider.stories.tsx +19 -0
  69. package/src/components/Stack/Stack.stories.tsx +118 -5
  70. package/src/components/Stack/Stack.tsx +37 -5
  71. package/src/components/Stepper/Stepper.stories.tsx +19 -0
  72. package/src/components/Switch/Switch.stories.tsx +19 -0
  73. package/src/components/Table/Table.stories.tsx +19 -0
  74. package/src/components/Tabs/Tabs.stories.tsx +19 -0
  75. package/src/components/Text/Text.stories.tsx +87 -3
  76. package/src/components/Text/Text.tsx +27 -4
  77. package/src/components/TextArea/TextArea.stories.tsx +19 -0
  78. package/src/components/TextField/TextField.stories.tsx +19 -0
  79. package/src/components/TimePicker/TimePicker.stories.tsx +19 -0
  80. package/src/components/Timeline/Timeline.stories.tsx +19 -0
  81. package/src/components/Title/Title.stories.tsx +68 -3
  82. package/src/components/Title/Title.tsx +29 -5
  83. package/src/components/Toast/Toast.stories.tsx +19 -0
  84. package/src/components/Tooltip/Tooltip.stories.tsx +19 -0
  85. package/src/components/TransferList/TransferList.stories.tsx +19 -0
  86. package/src/components/Typography/Typography.tsx +42 -0
  87. package/src/utils/ColorSchemeWrapper.tsx +1 -5
  88. package/src/utils/filterStylingProps.ts +13 -1
@@ -1,7 +1,79 @@
1
- import React from "react";
1
+ /**
2
+ * Flex layout wrapper.
3
+ *
4
+ * NOTE: MUI does not have a native `<Flex>` component (it relies on `<Box display="flex">`
5
+ * or `<Stack>`). To maintain API parity with the mantine-adapter and provide a consistent
6
+ * developer experience, this component wraps MUI's Box and forces `display="flex"`.
7
+ *
8
+ * Like Stack and Group, this is a primitive layout component. It strictly filters out the
9
+ * `sx` prop using `OmitSx` and `filterSxProp` to enforce the use of standard layout scales.
10
+ */
11
+ import React, { forwardRef } from "react";
12
+ import { Box as MUIBox, type BoxProps as MUIBoxProps } from "@mui/material";
13
+ import {
14
+ type RecursicaSpacing,
15
+ SPACING_MAP,
16
+ type OmitSx,
17
+ filterSxProp,
18
+ } from "../../utils/filterStylingProps";
2
19
 
3
- export type FlexProps = React.HTMLAttributes<HTMLDivElement>;
20
+ export interface RecursicaFlexProps {
21
+ /**
22
+ * Content to render inside the flex container.
23
+ */
24
+ children?: React.ReactNode;
4
25
 
5
- export const Flex: React.FC<FlexProps> = (props) => {
6
- return <div {...props}>Flex</div>;
7
- };
26
+ /**
27
+ * The gap between flex items. Accepts standard MUI spacing or Recursica gap tokens
28
+ * (e.g. "rec-md").
29
+ */
30
+ gap?: string | number | RecursicaSpacing;
31
+
32
+ /**
33
+ * The vertical gap between flex items when wrapping. Accepts standard MUI spacing
34
+ * or Recursica gap tokens.
35
+ */
36
+ rowGap?: string | number | RecursicaSpacing;
37
+
38
+ /**
39
+ * The horizontal gap between flex items when wrapping. Accepts standard MUI spacing
40
+ * or Recursica gap tokens.
41
+ */
42
+ columnGap?: string | number | RecursicaSpacing;
43
+ }
44
+
45
+ export type FlexProps = OmitSx<MUIBoxProps & RecursicaFlexProps>;
46
+
47
+ export const Flex = forwardRef<HTMLDivElement, FlexProps>(function Flex(
48
+ { children, gap = "rec-default", rowGap, columnGap, ...rest },
49
+ ref,
50
+ ) {
51
+ const safeProps = filterSxProp(rest as Record<string, unknown>);
52
+ const resolvedGap =
53
+ typeof gap === "string" && gap in SPACING_MAP
54
+ ? SPACING_MAP[gap as keyof typeof SPACING_MAP]
55
+ : gap;
56
+ const resolvedRowGap =
57
+ typeof rowGap === "string" && rowGap in SPACING_MAP
58
+ ? SPACING_MAP[rowGap as keyof typeof SPACING_MAP]
59
+ : rowGap;
60
+ const resolvedColumnGap =
61
+ typeof columnGap === "string" && columnGap in SPACING_MAP
62
+ ? SPACING_MAP[columnGap as keyof typeof SPACING_MAP]
63
+ : columnGap;
64
+
65
+ return (
66
+ <MUIBox
67
+ ref={ref}
68
+ display="flex"
69
+ {...safeProps}
70
+ gap={resolvedGap}
71
+ rowGap={resolvedRowGap}
72
+ columnGap={resolvedColumnGap}
73
+ >
74
+ {children}
75
+ </MUIBox>
76
+ );
77
+ });
78
+
79
+ Flex.displayName = "Flex";
@@ -6,6 +6,25 @@ const meta: Meta<typeof FormControlLayout> = {
6
6
  title: "UI-Kit/🚧 FormControlLayout",
7
7
  component: FormControlLayout,
8
8
  tags: ["autodocs"],
9
+ parameters: {
10
+ controls: {
11
+ include: [
12
+ "layer",
13
+ "withLayer",
14
+ "children",
15
+ "component",
16
+ "variant",
17
+ "size",
18
+ "icon",
19
+ "disabled",
20
+ "href",
21
+ "onClick",
22
+ "onChange",
23
+ "value",
24
+ "checked",
25
+ ],
26
+ },
27
+ },
9
28
  };
10
29
 
11
30
  export default meta;
@@ -6,6 +6,25 @@ const meta: Meta<typeof FormControlWrapper> = {
6
6
  title: "UI-Kit/🚧 FormControlWrapper",
7
7
  component: FormControlWrapper,
8
8
  tags: ["autodocs"],
9
+ parameters: {
10
+ controls: {
11
+ include: [
12
+ "layer",
13
+ "withLayer",
14
+ "children",
15
+ "component",
16
+ "variant",
17
+ "size",
18
+ "icon",
19
+ "disabled",
20
+ "href",
21
+ "onClick",
22
+ "onChange",
23
+ "value",
24
+ "checked",
25
+ ],
26
+ },
27
+ },
9
28
  };
10
29
 
11
30
  export default meta;
@@ -1,16 +1,144 @@
1
+ import React from "react";
1
2
  import type { Meta, StoryObj } from "@storybook/react";
2
3
  import { Group } from "./Group";
3
- import { ComingSoon } from "@recursica/storybook-template";
4
+ import { Button } from "../Button";
5
+ import { Text } from "../Text/Text";
4
6
 
5
- const meta: Meta<typeof Group> = {
6
- title: "UI-Kit/🚧 Group",
7
+ type GroupStoryProps = React.ComponentProps<typeof Group>;
8
+
9
+ const meta: Meta<GroupStoryProps> = {
10
+ title: "UI-Kit/Group",
7
11
  component: Group,
8
12
  tags: ["autodocs"],
13
+ parameters: {
14
+ docs: {
15
+ description: {
16
+ component:
17
+ "Group is a flex horizontal layout container that maps directly to Mantine's Group component allowing safe layout property passing.",
18
+ },
19
+ },
20
+ controls: {
21
+ include: [
22
+ "layer",
23
+ "withLayer",
24
+ "gap",
25
+ "alignItems",
26
+ "justifyContent",
27
+ "flexWrap",
28
+ "defaultChecked",
29
+ "rowGap",
30
+ "columnGap",
31
+ "children",
32
+ "component",
33
+ "variant",
34
+ "size",
35
+ "icon",
36
+ "disabled",
37
+ "href",
38
+ "onClick",
39
+ "onChange",
40
+ "value",
41
+ "checked",
42
+ ],
43
+ },
44
+ },
45
+ args: {
46
+ gap: "rec-default",
47
+ alignItems: "center",
48
+ justifyContent: "flex-start",
49
+ flexWrap: "wrap",
50
+ },
51
+ argTypes: {
52
+ gap: {
53
+ control: "select",
54
+ options: [
55
+ "rec-none",
56
+ "rec-sm",
57
+ "rec-default",
58
+ "rec-md",
59
+ "rec-lg",
60
+ "rec-xl",
61
+ "rec-2xl",
62
+ "xs",
63
+ "sm",
64
+ "md",
65
+ "lg",
66
+ "xl",
67
+ ],
68
+ description: "Gap between elements",
69
+ },
70
+ alignItems: {
71
+ control: "select",
72
+ options: ["flex-start", "center", "flex-end", "stretch"],
73
+ description: "Align-items property",
74
+ },
75
+ justifyContent: {
76
+ control: "select",
77
+ options: [
78
+ "flex-start",
79
+ "center",
80
+ "flex-end",
81
+ "space-between",
82
+ "space-around",
83
+ ],
84
+ description: "Justify-content property",
85
+ },
86
+ flexWrap: {
87
+ control: "select",
88
+ options: ["wrap", "nowrap", "wrap-reverse"],
89
+ description: "Flex-wrap property",
90
+ },
91
+ defaultChecked: {
92
+ table: { disable: true },
93
+ },
94
+ rowGap: {
95
+ table: { disable: true },
96
+ },
97
+ columnGap: {
98
+ table: { disable: true },
99
+ },
100
+ },
9
101
  };
10
102
 
11
103
  export default meta;
12
- type Story = StoryObj<typeof Group>;
104
+
105
+ type Story = StoryObj<GroupStoryProps>;
13
106
 
14
107
  export const Default: Story = {
15
- render: () => <ComingSoon componentName="Group" />,
108
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
109
+ render: ({ withLayer, layer, ...args }: any) => (
110
+ <Group {...args}>
111
+ <Button variant="solid">Primary</Button>
112
+ <Button variant="outline">Secondary</Button>
113
+ <Text>Text element within Group</Text>
114
+ </Group>
115
+ ),
116
+ };
117
+
118
+ export const StaticGapSmall: Story = {
119
+ args: {
120
+ gap: "rec-sm",
121
+ },
122
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
123
+ render: ({ withLayer, layer, ...args }: any) => (
124
+ <Group {...args}>
125
+ <Button variant="solid">Item 1</Button>
126
+ <Button variant="solid">Item 2</Button>
127
+ <Button variant="solid">Item 3</Button>
128
+ </Group>
129
+ ),
130
+ };
131
+
132
+ export const StaticGapLarge: Story = {
133
+ args: {
134
+ gap: "rec-xl",
135
+ },
136
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any
137
+ render: ({ withLayer, layer, ...args }: any) => (
138
+ <Group {...args}>
139
+ <Button variant="solid">Item 1</Button>
140
+ <Button variant="solid">Item 2</Button>
141
+ <Button variant="solid">Item 3</Button>
142
+ </Group>
143
+ ),
16
144
  };
@@ -1,7 +1,59 @@
1
- import React from "react";
1
+ import React, { forwardRef } from "react";
2
+ import {
3
+ Stack as MUIStack,
4
+ type StackProps as MUIStackProps,
5
+ } from "@mui/material";
6
+ import {
7
+ type RecursicaSpacing,
8
+ SPACING_MAP,
9
+ type OmitSx,
10
+ filterSxProp,
11
+ } from "../../utils/filterStylingProps";
2
12
 
3
- export type GroupProps = React.HTMLAttributes<HTMLDivElement>;
13
+ export interface RecursicaGroupProps {
14
+ children?: React.ReactNode;
15
+ gap?: MUIStackProps["spacing"] | RecursicaSpacing;
16
+ rowGap?: MUIStackProps["spacing"] | RecursicaSpacing;
17
+ columnGap?: MUIStackProps["spacing"] | RecursicaSpacing;
18
+ }
4
19
 
5
- export const Group: React.FC<GroupProps> = (props) => {
6
- return <div {...props}>Group</div>;
7
- };
20
+ export type GroupProps = OmitSx<
21
+ Omit<MUIStackProps, "spacing" | "direction"> & RecursicaGroupProps
22
+ >;
23
+
24
+ export const Group = forwardRef<HTMLDivElement, GroupProps>(function Group(
25
+ { children, gap = "rec-default", rowGap, columnGap, ...rest },
26
+ ref,
27
+ ) {
28
+ const safeProps = filterSxProp(rest as Record<string, unknown>);
29
+ const resolvedGap =
30
+ typeof gap === "string" && gap in SPACING_MAP
31
+ ? SPACING_MAP[gap as keyof typeof SPACING_MAP]
32
+ : gap;
33
+ const resolvedRowGap =
34
+ typeof rowGap === "string" && rowGap in SPACING_MAP
35
+ ? SPACING_MAP[rowGap as keyof typeof SPACING_MAP]
36
+ : rowGap;
37
+ const resolvedColumnGap =
38
+ typeof columnGap === "string" && columnGap in SPACING_MAP
39
+ ? SPACING_MAP[columnGap as keyof typeof SPACING_MAP]
40
+ : columnGap;
41
+
42
+ return (
43
+ <MUIStack
44
+ ref={ref}
45
+ direction="row"
46
+ flexWrap="wrap"
47
+ spacing={resolvedGap}
48
+ sx={{
49
+ ...(resolvedRowGap ? { rowGap: resolvedRowGap } : {}),
50
+ ...(resolvedColumnGap ? { columnGap: resolvedColumnGap } : {}),
51
+ }}
52
+ {...safeProps}
53
+ >
54
+ {children}
55
+ </MUIStack>
56
+ );
57
+ });
58
+
59
+ Group.displayName = "Group";
@@ -6,6 +6,25 @@ const meta: Meta<typeof HoverCard> = {
6
6
  title: "UI-Kit/🚧 HoverCard",
7
7
  component: HoverCard,
8
8
  tags: ["autodocs"],
9
+ parameters: {
10
+ controls: {
11
+ include: [
12
+ "layer",
13
+ "withLayer",
14
+ "children",
15
+ "component",
16
+ "variant",
17
+ "size",
18
+ "icon",
19
+ "disabled",
20
+ "href",
21
+ "onClick",
22
+ "onChange",
23
+ "value",
24
+ "checked",
25
+ ],
26
+ },
27
+ },
9
28
  };
10
29
 
11
30
  export default meta;
@@ -6,6 +6,25 @@ const meta: Meta<typeof Label> = {
6
6
  title: "UI-Kit/🚧 Label",
7
7
  component: Label,
8
8
  tags: ["autodocs"],
9
+ parameters: {
10
+ controls: {
11
+ include: [
12
+ "layer",
13
+ "withLayer",
14
+ "children",
15
+ "component",
16
+ "variant",
17
+ "size",
18
+ "icon",
19
+ "disabled",
20
+ "href",
21
+ "onClick",
22
+ "onChange",
23
+ "value",
24
+ "checked",
25
+ ],
26
+ },
27
+ },
9
28
  };
10
29
 
11
30
  export default meta;
@@ -0,0 +1,133 @@
1
+ /*
2
+ HARDCODED VALUES:
3
+ - display: inline-flex; (Base layout to align icon and text correctly)
4
+ - align-items: center;
5
+ - cursor: pointer;
6
+ - text-decoration: none; (Overridden by our design tokens instead of using Mantine's underline prop)
7
+ - color: inherit; (Reset before applying token)
8
+ */
9
+
10
+ .root {
11
+ /* HARDCODE: base layout to support optional icon gaps */
12
+ display: inline-flex;
13
+ align-items: center;
14
+ cursor: pointer;
15
+
16
+ /* HARDCODE: Reset underline and color so we can explicitly use our tokens */
17
+ text-decoration: none;
18
+ color: inherit;
19
+
20
+ /* Typography Default Tokens */
21
+ font-family: var(
22
+ --recursica_ui-kit_components_link_properties_text_font-family
23
+ );
24
+ font-size: var(--recursica_ui-kit_components_link_properties_text_font-size);
25
+ letter-spacing: var(
26
+ --recursica_ui-kit_components_link_properties_text_letter-spacing
27
+ );
28
+ line-height: var(
29
+ --recursica_ui-kit_components_link_properties_text_line-height
30
+ );
31
+
32
+ /* Default State Tokens */
33
+ font-style: var(
34
+ --recursica_ui-kit_components_link_variants_states_default_properties_text_font-style
35
+ );
36
+ font-weight: var(
37
+ --recursica_ui-kit_components_link_variants_states_default_properties_text_font-weight
38
+ );
39
+ text-decoration: var(
40
+ --recursica_ui-kit_components_link_variants_states_default_properties_text_text-decoration
41
+ );
42
+ color: var(
43
+ --recursica_ui-kit_components_link_variants_states_default_properties_colors_text
44
+ );
45
+ }
46
+
47
+ .root[data-has-icon] {
48
+ gap: var(--recursica_ui-kit_components_link_properties_icon-text-gap);
49
+ }
50
+
51
+ .root:hover {
52
+ /* Reset Mantine's potential hover override */
53
+ text-decoration: var(
54
+ --recursica_ui-kit_components_link_variants_states_hover_properties_text_text-decoration
55
+ );
56
+ font-style: var(
57
+ --recursica_ui-kit_components_link_variants_states_hover_properties_text_font-style
58
+ );
59
+ font-weight: var(
60
+ --recursica_ui-kit_components_link_variants_states_hover_properties_text_font-weight
61
+ );
62
+ color: var(
63
+ --recursica_ui-kit_components_link_variants_states_hover_properties_colors_text
64
+ );
65
+ }
66
+
67
+ .root:visited {
68
+ text-decoration: var(
69
+ --recursica_ui-kit_components_link_variants_states_visited_properties_text_text-decoration
70
+ );
71
+ font-style: var(
72
+ --recursica_ui-kit_components_link_variants_states_visited_properties_text_font-style
73
+ );
74
+ font-weight: var(
75
+ --recursica_ui-kit_components_link_variants_states_visited_properties_text_font-weight
76
+ );
77
+ color: var(
78
+ --recursica_ui-kit_components_link_variants_states_visited_properties_colors_text
79
+ );
80
+ }
81
+
82
+ .root:visited:hover {
83
+ text-decoration: var(
84
+ --recursica_ui-kit_components_link_variants_states_visited-hover_properties_text_text-decoration
85
+ );
86
+ font-style: var(
87
+ --recursica_ui-kit_components_link_variants_states_visited-hover_properties_text_font-style
88
+ );
89
+ font-weight: var(
90
+ --recursica_ui-kit_components_link_variants_states_visited-hover_properties_text_font-weight
91
+ );
92
+ color: var(
93
+ --recursica_ui-kit_components_link_variants_states_visited-hover_properties_colors_text
94
+ );
95
+ }
96
+
97
+ /* Icon Support */
98
+ .iconWrapper {
99
+ /* HARDCODE: maintain intrinsic icon bounds */
100
+ display: flex;
101
+ align-items: center;
102
+ justify-content: center;
103
+
104
+ width: var(--recursica_ui-kit_components_link_properties_icon-size);
105
+ height: var(--recursica_ui-kit_components_link_properties_icon-size);
106
+ color: var(
107
+ --recursica_ui-kit_components_link_variants_states_default_properties_colors_icon
108
+ );
109
+ }
110
+
111
+ .root:hover .iconWrapper {
112
+ color: var(
113
+ --recursica_ui-kit_components_link_variants_states_hover_properties_colors_icon
114
+ );
115
+ }
116
+
117
+ .root:visited .iconWrapper {
118
+ color: var(
119
+ --recursica_ui-kit_components_link_variants_states_visited_properties_colors_icon
120
+ );
121
+ }
122
+
123
+ .root:visited:hover .iconWrapper {
124
+ color: var(
125
+ --recursica_ui-kit_components_link_variants_states_visited-hover_properties_colors_icon
126
+ );
127
+ }
128
+
129
+ /* Label text wrapper */
130
+ .labelText {
131
+ /* HARDCODE: allows text truncation if bounded */
132
+ min-width: 0;
133
+ }
@@ -1,16 +1,64 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ /* eslint-disable @typescript-eslint/no-unused-vars */
1
3
  import type { Meta, StoryObj } from "@storybook/react";
2
4
  import { Link } from "./Link";
3
- import { ComingSoon } from "@recursica/storybook-template";
5
+ import { Text } from "../Text/Text";
4
6
 
5
7
  const meta: Meta<typeof Link> = {
6
- title: "UI-Kit/🚧 Link",
8
+ title: "UI-Kit/Link",
7
9
  component: Link,
8
10
  tags: ["autodocs"],
11
+ parameters: {
12
+ controls: {
13
+ include: ["children", "href", "icon", "component", "onClick"],
14
+ },
15
+ },
16
+ args: {
17
+ children: "Link text",
18
+ href: "#",
19
+ },
20
+ render: ({ withLayer, layer, ...args }: any) => <Link {...args} />,
9
21
  };
10
22
 
11
23
  export default meta;
24
+
12
25
  type Story = StoryObj<typeof Link>;
13
26
 
14
- export const Default: Story = {
15
- render: () => <ComingSoon componentName="Link" />,
27
+ export const Default: Story = {};
28
+
29
+ export const WithIcon: Story = {
30
+ args: {
31
+ icon: (
32
+ <svg
33
+ width="24"
34
+ height="24"
35
+ viewBox="0 0 24 24"
36
+ fill="none"
37
+ stroke="currentColor"
38
+ strokeWidth="2"
39
+ strokeLinecap="round"
40
+ strokeLinejoin="round"
41
+ >
42
+ <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path>
43
+ <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path>
44
+ </svg>
45
+ ),
46
+ children: "Link with Icon",
47
+ },
48
+ };
49
+
50
+ export const Polymorphic: Story = {
51
+ args: {
52
+ component: "button",
53
+ onClick: () => alert("Button clicked!"),
54
+ children: "Rendered as <button>",
55
+ },
56
+ };
57
+
58
+ export const InlineText: Story = {
59
+ render: () => (
60
+ <Text>
61
+ Here is some text with an <Link href="#">inline link</Link> inside it.
62
+ </Text>
63
+ ),
16
64
  };
@@ -1,7 +1,49 @@
1
- import React from "react";
1
+ import React, { forwardRef } from "react";
2
+ import { Link as MUILink, type LinkProps as MUILinkProps } from "@mui/material";
3
+ import {
4
+ filterStylingProps,
5
+ type RecursicaOverStyled,
6
+ } from "../../utils/filterStylingProps";
7
+ import styles from "./Link.module.css";
2
8
 
3
- export type LinkProps = React.HTMLAttributes<HTMLDivElement>;
9
+ export interface RecursicaLinkProps {
10
+ icon?: React.ReactNode;
11
+ children?: React.ReactNode;
12
+ component?: React.ElementType;
13
+ }
4
14
 
5
- export const Link: React.FC<LinkProps> = (props) => {
6
- return <div {...props}>Link</div>;
7
- };
15
+ export type LinkProps = RecursicaOverStyled<
16
+ Omit<MUILinkProps, "underline"> & RecursicaLinkProps
17
+ >;
18
+
19
+ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link(
20
+ { icon, children, overStyled = false, ...rest },
21
+ ref,
22
+ ) {
23
+ const sanitizedProps = filterStylingProps(rest, overStyled);
24
+ const restRecord = sanitizedProps as Record<string, unknown>;
25
+
26
+ const classNameProp = restRecord.className as string | undefined;
27
+ const finalClass = classNameProp
28
+ ? `${styles.root} ${classNameProp}`
29
+ : styles.root;
30
+
31
+ return (
32
+ <MUILink
33
+ ref={ref}
34
+ className={finalClass}
35
+ underline="none"
36
+ {...(icon ? { "data-has-icon": "" } : {})}
37
+ {...sanitizedProps}
38
+ >
39
+ {icon && (
40
+ <span className={styles.iconWrapper} aria-hidden>
41
+ {icon}
42
+ </span>
43
+ )}
44
+ <span className={styles.labelText}>{children}</span>
45
+ </MUILink>
46
+ );
47
+ });
48
+
49
+ Link.displayName = "Link";
@@ -0,0 +1,25 @@
1
+ # Loader Implementation Notes
2
+
3
+ ## Architecture & Integration
4
+
5
+ The `Loader` component for the MUI adapter has been completely hand-coded from scratch using pure CSS and basic HTML `<span>` elements. It explicitly **does not** use MUI's native `<CircularProgress>` component.
6
+
7
+ This was a deliberate architectural decision to ensure 100% feature and visual parity with the `mantine-adapter`.
8
+
9
+ ### Key Decisions:
10
+
11
+ - **Bypassing Native Components:** MUI's native loaders (like `<CircularProgress>`) are built using complex animated SVGs and only support a circular "oval" shape. Since the Recursica design system mandates `oval`, `bars`, and `dots` variants, relying on MUI's primitives would have forced a fragmented architecture where `oval` used MUI but `bars` and `dots` were hand-coded.
12
+ - **Parity with Mantine:** To guarantee identical animation timing, easing curves, and DOM structures across frameworks, the CSS keyframes and layout strategies used internally by Mantine's `<Loader>` were extracted and directly replicated in this adapter's `Loader.module.css`.
13
+
14
+ ### Token Mapping:
15
+
16
+ Sizes are bound through `data-size` attributes (`sm`, `md`, `lg` parsing to target `<div data-size="small">`, etc.).
17
+
18
+ - **Oval Variant:** Uses a CSS spinning `::after` pseudo-element. To avoid CSS border inheritance bugs when computing tokenized border-widths, a custom `--loader-thickness` CSS variable is used to bridge the token into the spinning element.
19
+ - **Bars & Dots Variants:** Render three internal `<span />` elements sequentially, styled via `Loader.module.css` to handle individual keyframe delays for bouncing or fading animations.
20
+
21
+ ### Color Contrast Rules:
22
+
23
+ Loaders are hardcoded to map to their explicitly defined design tokens (e.g., `--recursica_ui-kit_components_loader_properties_indicator-color`). By default, they do **not** inherit `currentColor`.
24
+
25
+ When injected into components like the `Button` (where contrast issues may arise against solid backgrounds), it is the responsibility of the parent component (e.g., `Button.module.css`) to use contextual CSS overrides to force `--loader-color: currentColor !important` if necessary.