@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.
- package/CHANGELOG.md +12 -0
- package/dist/mui-adapter.cjs +1 -1
- package/dist/mui-adapter.cjs.map +1 -1
- package/dist/mui-adapter.css +1 -1
- package/dist/mui-adapter.js +286 -91
- package/dist/mui-adapter.js.map +1 -1
- package/dist/src/components/Box/Box.d.ts +2 -3
- package/dist/src/components/Button/Button.d.ts +40 -2
- package/dist/src/components/Container/Container.d.ts +8 -2
- package/dist/src/components/Flex/Flex.d.ts +25 -2
- package/dist/src/components/Group/Group.d.ts +10 -2
- package/dist/src/components/Link/Link.d.ts +13 -2
- package/dist/src/components/Loader/Loader.d.ts +9 -2
- package/dist/src/components/Stack/Stack.d.ts +8 -2
- package/dist/src/components/Text/Text.d.ts +18 -2
- package/dist/src/components/Title/Title.d.ts +17 -2
- package/dist/src/components/Typography/Typography.d.ts +27 -0
- package/dist/src/utils/filterStylingProps.d.ts +3 -0
- package/package.json +2 -1
- package/src/Introduction.stories.tsx +113 -110
- package/src/OverStyling.tsx +18 -23
- package/src/Version.tsx +14 -21
- package/src/components/Accordion/Accordion.stories.tsx +19 -0
- package/src/components/AssistiveElement/AssistiveElement.stories.tsx +19 -0
- package/src/components/Avatar/Avatar.stories.tsx +19 -0
- package/src/components/Badge/Badge.stories.tsx +19 -0
- package/src/components/Box/Box.stories.tsx +19 -10
- package/src/components/Box/Box.tsx +11 -14
- package/src/components/Box/IMPLEMENTATION_NOTES.md +5 -0
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +19 -0
- package/src/components/Button/BUTTON_IMPLEMENTATION_NOTES.md +33 -0
- package/src/components/Button/Button.module.css +343 -0
- package/src/components/Button/Button.stories.tsx +181 -5
- package/src/components/Button/Button.tsx +141 -5
- package/src/components/Card/Card.stories.tsx +19 -0
- package/src/components/Checkbox/Checkbox.stories.tsx +19 -0
- package/src/components/Chip/Chip.stories.tsx +19 -0
- package/src/components/Container/Container.stories.tsx +19 -0
- package/src/components/Container/Container.tsx +56 -5
- package/src/components/Container/IMPLEMENTATION_NOTES.md +5 -0
- package/src/components/DatePicker/DatePicker.stories.tsx +19 -0
- package/src/components/Dropdown/Dropdown.stories.tsx +19 -0
- package/src/components/FileInput/FileInput.stories.tsx +19 -0
- package/src/components/FileUpload/FileUpload.stories.tsx +19 -0
- package/src/components/Flex/Flex.stories.tsx +19 -0
- package/src/components/Flex/Flex.tsx +77 -5
- package/src/components/FormControlLayout/FormControlLayout.stories.tsx +19 -0
- package/src/components/FormControlWrapper/FormControlWrapper.stories.tsx +19 -0
- package/src/components/Group/Group.stories.tsx +133 -5
- package/src/components/Group/Group.tsx +57 -5
- package/src/components/HoverCard/HoverCard.stories.tsx +19 -0
- package/src/components/Label/Label.stories.tsx +19 -0
- package/src/components/Link/Link.module.css +133 -0
- package/src/components/Link/Link.stories.tsx +52 -4
- package/src/components/Link/Link.tsx +47 -5
- package/src/components/Loader/LOADER_IMPLEMENTATION_NOTES.md +25 -0
- package/src/components/Loader/Loader.module.css +166 -0
- package/src/components/Loader/Loader.stories.tsx +93 -6
- package/src/components/Loader/Loader.tsx +69 -5
- package/src/components/Menu/Menu.stories.tsx +19 -0
- package/src/components/Modal/Modal.stories.tsx +19 -0
- package/src/components/NumberInput/NumberInput.stories.tsx +19 -0
- package/src/components/Pagination/Pagination.stories.tsx +19 -0
- package/src/components/Panel/Panel.stories.tsx +19 -0
- package/src/components/Radio/Radio.stories.tsx +19 -0
- package/src/components/ReadOnlyField/ReadOnlyField.stories.tsx +19 -0
- package/src/components/SegmentedControl/SegmentedControl.stories.tsx +19 -0
- package/src/components/Slider/Slider.stories.tsx +19 -0
- package/src/components/Stack/Stack.stories.tsx +118 -5
- package/src/components/Stack/Stack.tsx +37 -5
- package/src/components/Stepper/Stepper.stories.tsx +19 -0
- package/src/components/Switch/Switch.stories.tsx +19 -0
- package/src/components/Table/Table.stories.tsx +19 -0
- package/src/components/Tabs/Tabs.stories.tsx +19 -0
- package/src/components/Text/Text.stories.tsx +87 -3
- package/src/components/Text/Text.tsx +27 -4
- package/src/components/TextArea/TextArea.stories.tsx +19 -0
- package/src/components/TextField/TextField.stories.tsx +19 -0
- package/src/components/TimePicker/TimePicker.stories.tsx +19 -0
- package/src/components/Timeline/Timeline.stories.tsx +19 -0
- package/src/components/Title/Title.stories.tsx +68 -3
- package/src/components/Title/Title.tsx +29 -5
- package/src/components/Toast/Toast.stories.tsx +19 -0
- package/src/components/Tooltip/Tooltip.stories.tsx +19 -0
- package/src/components/TransferList/TransferList.stories.tsx +19 -0
- package/src/components/Typography/Typography.tsx +42 -0
- package/src/utils/ColorSchemeWrapper.tsx +1 -5
- package/src/utils/filterStylingProps.ts +13 -1
|
@@ -1,7 +1,79 @@
|
|
|
1
|
-
|
|
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
|
|
20
|
+
export interface RecursicaFlexProps {
|
|
21
|
+
/**
|
|
22
|
+
* Content to render inside the flex container.
|
|
23
|
+
*/
|
|
24
|
+
children?: React.ReactNode;
|
|
4
25
|
|
|
5
|
-
|
|
6
|
-
|
|
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 {
|
|
4
|
+
import { Button } from "../Button";
|
|
5
|
+
import { Text } from "../Text/Text";
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
104
|
+
|
|
105
|
+
type Story = StoryObj<GroupStoryProps>;
|
|
13
106
|
|
|
14
107
|
export const Default: Story = {
|
|
15
|
-
|
|
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
|
|
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
|
|
6
|
-
|
|
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 {
|
|
5
|
+
import { Text } from "../Text/Text";
|
|
4
6
|
|
|
5
7
|
const meta: Meta<typeof Link> = {
|
|
6
|
-
title: "UI-Kit
|
|
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
|
-
|
|
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
|
|
9
|
+
export interface RecursicaLinkProps {
|
|
10
|
+
icon?: React.ReactNode;
|
|
11
|
+
children?: React.ReactNode;
|
|
12
|
+
component?: React.ElementType;
|
|
13
|
+
}
|
|
4
14
|
|
|
5
|
-
export
|
|
6
|
-
|
|
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.
|