@usefui/components 1.5.1 → 1.5.2
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 +6 -0
- package/dist/index.d.mts +230 -9
- package/dist/index.d.ts +230 -9
- package/dist/index.js +1131 -309
- package/dist/index.mjs +1085 -276
- package/package.json +6 -6
- package/src/avatar/index.tsx +1 -1
- package/src/badge/index.tsx +3 -3
- package/src/breadcrumb/Breadcrumb.stories.tsx +36 -0
- package/src/breadcrumb/index.tsx +117 -0
- package/src/breadcrumb/styles/index.ts +19 -0
- package/src/button/Button.stories.tsx +80 -8
- package/src/button/index.tsx +67 -5
- package/src/button/styles/index.ts +82 -10
- package/src/card/Card.stories.tsx +57 -0
- package/src/card/index.tsx +55 -0
- package/src/card/styles/index.ts +72 -0
- package/src/copy-button/CopyButton.stories.tsx +29 -0
- package/src/copy-button/index.tsx +101 -0
- package/src/dialog/index.tsx +1 -1
- package/src/dropdown/index.tsx +1 -1
- package/src/field/Field.stories.tsx +39 -8
- package/src/field/index.tsx +5 -0
- package/src/field/styles/index.ts +37 -12
- package/src/index.ts +8 -0
- package/src/page/index.tsx +1 -1
- package/src/privacy-field/PrivacyField.stories.tsx +29 -0
- package/src/privacy-field/index.tsx +56 -0
- package/src/privacy-field/styles/index.ts +17 -0
- package/src/resizable/Resizable.stories.tsx +40 -0
- package/src/resizable/index.tsx +108 -0
- package/src/resizable/styles/index.ts +65 -0
- package/src/skeleton/Skeleton.stories.tsx +32 -0
- package/src/skeleton/index.tsx +43 -0
- package/src/skeleton/styles/index.ts +56 -0
- package/src/spinner/Spinner.stories.tsx +27 -0
- package/src/spinner/index.tsx +19 -0
- package/src/spinner/styles/index.ts +43 -0
- package/src/text-area/Textarea.stories.tsx +27 -0
- package/src/text-area/index.tsx +62 -0
- package/src/text-area/styles/index.ts +124 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usefui/components",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"description": "Open Source React components library",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -24,16 +24,16 @@
|
|
|
24
24
|
"styled-components": "^5.1.34"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@types/react": "^18.3.
|
|
27
|
+
"@types/react": "^18.3.26",
|
|
28
28
|
"@types/react-dom": "^18.3.7",
|
|
29
29
|
"@types/styled-components": "^5.1.34",
|
|
30
30
|
"react": "^18.3.1",
|
|
31
31
|
"react-dom": "^18.3.1",
|
|
32
32
|
"styled-components": "^5.3.11",
|
|
33
|
-
"typescript": "^5.9.
|
|
34
|
-
"@usefui/core": "^1.3.
|
|
35
|
-
"@usefui/hooks": "^1.3.
|
|
36
|
-
"@usefui/tokens": "^1.5.
|
|
33
|
+
"typescript": "^5.9.3",
|
|
34
|
+
"@usefui/core": "^1.3.4",
|
|
35
|
+
"@usefui/hooks": "^1.3.4",
|
|
36
|
+
"@usefui/tokens": "^1.5.3"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
39
|
"test": "vitest run --coverage --logHeapUsage",
|
package/src/avatar/index.tsx
CHANGED
|
@@ -34,7 +34,7 @@ export interface IAvatarProperties
|
|
|
34
34
|
*
|
|
35
35
|
* @param {IAvatarProperties} props - The props for the Avatar component.
|
|
36
36
|
* @param {boolean} props.raw - Whether the avatar is styled or not.
|
|
37
|
-
* @param {ComponentSizeEnum} props.sizing - The size of the avatar. Defaults to
|
|
37
|
+
* @param {ComponentSizeEnum} props.sizing - The size of the avatar. Defaults to "medium".
|
|
38
38
|
* @param {string} props.status - The status of the user represented by the avatar.
|
|
39
39
|
* @param {string} props.src - The source URL of the image to be displayed in the avatar.
|
|
40
40
|
* @param {string} props.alt - The alternative text for the image in the avatar.
|
package/src/badge/index.tsx
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { BadgeWrapper } from "./styles";
|
|
5
5
|
|
|
6
|
-
import { IComponentStyling } from "../../../../types";
|
|
6
|
+
import type { IComponentStyling, TComponentShape } from "../../../../types";
|
|
7
7
|
|
|
8
8
|
interface IBadgeProperties
|
|
9
9
|
extends IComponentStyling,
|
|
@@ -16,7 +16,7 @@ interface IBadgeProperties
|
|
|
16
16
|
| "warning"
|
|
17
17
|
| "success"
|
|
18
18
|
| "meta";
|
|
19
|
-
shape?:
|
|
19
|
+
shape?: TComponentShape;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/**
|
|
@@ -29,7 +29,7 @@ interface IBadgeProperties
|
|
|
29
29
|
*
|
|
30
30
|
* @param {IBadgeProperties} props - The props for the Badge component.
|
|
31
31
|
* @param {boolean} props.raw - Define whether the component is styled or not.
|
|
32
|
-
* @param {
|
|
32
|
+
* @param {TComponentShape} props.shape - The shape of the component. Defaults to `smooth`.
|
|
33
33
|
* @param {string} props.variant - The style definition used by the component.
|
|
34
34
|
* @param {ReactNode} props.children - The content to be rendered inside the Badge.
|
|
35
35
|
* @returns {ReactElement} The Badge component.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Page, Breadcrumb } from "..";
|
|
3
|
+
|
|
4
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
*
|
|
8
|
+
*/
|
|
9
|
+
const meta = {
|
|
10
|
+
title: "Components/Breadcrumb",
|
|
11
|
+
component: Breadcrumb,
|
|
12
|
+
tags: ["autodocs"],
|
|
13
|
+
} satisfies Meta<typeof Breadcrumb>;
|
|
14
|
+
export default meta;
|
|
15
|
+
|
|
16
|
+
type Story = StoryObj<typeof meta>;
|
|
17
|
+
|
|
18
|
+
const navigate = (href: string) => {
|
|
19
|
+
console.log(href);
|
|
20
|
+
};
|
|
21
|
+
export const Default: Story = {
|
|
22
|
+
args: {
|
|
23
|
+
path: "",
|
|
24
|
+
},
|
|
25
|
+
render: ({ ...args }) => (
|
|
26
|
+
<Page>
|
|
27
|
+
<Page.Content>
|
|
28
|
+
<div className="flex align-center justify-center h-100">
|
|
29
|
+
<Breadcrumb path={"/test/demo-path/current-path"} navigate={navigate}>
|
|
30
|
+
<Breadcrumb.Item>Home</Breadcrumb.Item>
|
|
31
|
+
</Breadcrumb>
|
|
32
|
+
</div>
|
|
33
|
+
</Page.Content>
|
|
34
|
+
</Page>
|
|
35
|
+
),
|
|
36
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
|
|
5
|
+
import { ItemWrapper, SeparatorItem } from "./styles";
|
|
6
|
+
import { Button, type IButtonProperties } from "../";
|
|
7
|
+
|
|
8
|
+
const SEGMENT_SEPARATOR = "/";
|
|
9
|
+
|
|
10
|
+
interface BreadcrumbProps extends React.ComponentProps<"div"> {
|
|
11
|
+
navigate?: (href: string) => void;
|
|
12
|
+
capitalizeItems?: boolean;
|
|
13
|
+
path: string;
|
|
14
|
+
}
|
|
15
|
+
interface BreadcrumbItemProps extends IButtonProperties {
|
|
16
|
+
isLastItem?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface BreadcrumbComposition {
|
|
19
|
+
Item: typeof BreadcrumbItem;
|
|
20
|
+
Separator: typeof BreadcrumbSeparator;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Breadcrumb Item are used to display the path to the current resource.
|
|
25
|
+
*
|
|
26
|
+
* @param {BreadcrumbItemProps} props - The props for the Breadcrumb Item component.
|
|
27
|
+
* @param {boolean} props.isLastItem - Render the item as latest on the links hierarchy.
|
|
28
|
+
* @returns {ReactElement} The Breadcrumb Item component.
|
|
29
|
+
*/
|
|
30
|
+
const BreadcrumbItem = (props: BreadcrumbItemProps) => {
|
|
31
|
+
const { isLastItem = false, children, ...restProps } = props;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<Button sizing="small" variant="ghost" {...restProps}>
|
|
35
|
+
<ItemWrapper data-current={isLastItem}>{children}</ItemWrapper>
|
|
36
|
+
</Button>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
BreadcrumbItem.displayName = "Breadcrumb.Item";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Breadcrumb Separators are used to display hierarchy of links.
|
|
43
|
+
*
|
|
44
|
+
* @param {React.ComponentProps<"span">} props - The props for the Breadcrumb Separators component.
|
|
45
|
+
* @returns {ReactElement} The Breadcrumb Separators component.
|
|
46
|
+
*/
|
|
47
|
+
const BreadcrumbSeparator = (props: React.ComponentProps<"span">) => {
|
|
48
|
+
const { children } = props;
|
|
49
|
+
|
|
50
|
+
return <SeparatorItem>{children ?? SEGMENT_SEPARATOR}</SeparatorItem>;
|
|
51
|
+
};
|
|
52
|
+
BreadcrumbSeparator.displayName = "Breadcrumb.Separator";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Breadcrumbs are used to display the path to the current resource using a hierarchy of links.
|
|
56
|
+
*
|
|
57
|
+
* @param {BreadcrumbProps} props - The props for the Breadcrumb component.
|
|
58
|
+
* @param {boolean} props.capitalizeItems - Render the current path with a capitalized case. Defaults to 'true'.
|
|
59
|
+
* @param {string} props.path - The current path.
|
|
60
|
+
* @param {function} props.navigate - The function used to redirect to the path route.
|
|
61
|
+
* @returns {ReactElement} The Breadcrumb component.
|
|
62
|
+
*/
|
|
63
|
+
const Breadcrumb = (props: BreadcrumbProps) => {
|
|
64
|
+
const { path, navigate, capitalizeItems = true, children } = props;
|
|
65
|
+
|
|
66
|
+
const segments = path.split(SEGMENT_SEPARATOR).filter(Boolean);
|
|
67
|
+
const items = segments.map((segment, index) => {
|
|
68
|
+
const href = `/${segments.slice(0, index + 1).join(SEGMENT_SEPARATOR)}`;
|
|
69
|
+
const isLastItem = index === segments.length - 1;
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
href,
|
|
73
|
+
label: segment,
|
|
74
|
+
isLastItem,
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const breadcrumbLabel = (segment: string) => {
|
|
79
|
+
return capitalizeItems
|
|
80
|
+
? segment.charAt(0).toUpperCase() + segment.slice(1).replace(/-/g, " ")
|
|
81
|
+
: segment.replace(/-/g, " ");
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const handleNavigate = React.useCallback((href: string) => {
|
|
85
|
+
if (navigate) navigate(href);
|
|
86
|
+
}, []);
|
|
87
|
+
|
|
88
|
+
if (path === SEGMENT_SEPARATOR) return null;
|
|
89
|
+
return (
|
|
90
|
+
<div className="flex align-center g-medium-30">
|
|
91
|
+
{children && (
|
|
92
|
+
<React.Fragment>
|
|
93
|
+
{children}
|
|
94
|
+
<Breadcrumb.Separator />
|
|
95
|
+
</React.Fragment>
|
|
96
|
+
)}
|
|
97
|
+
{items.map((item, key) => (
|
|
98
|
+
<React.Fragment key={key}>
|
|
99
|
+
<Breadcrumb.Item
|
|
100
|
+
isLastItem={item.isLastItem}
|
|
101
|
+
disabled={!navigate}
|
|
102
|
+
onClick={() => handleNavigate(item.href)}
|
|
103
|
+
>
|
|
104
|
+
{breadcrumbLabel(item.label)}
|
|
105
|
+
</Breadcrumb.Item>
|
|
106
|
+
{!item.isLastItem && <Breadcrumb.Separator />}
|
|
107
|
+
</React.Fragment>
|
|
108
|
+
))}
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
Breadcrumb.displayName = "Breadcrumb";
|
|
113
|
+
|
|
114
|
+
Breadcrumb.Item = BreadcrumbItem;
|
|
115
|
+
Breadcrumb.Separator = BreadcrumbSeparator;
|
|
116
|
+
|
|
117
|
+
export { Breadcrumb, BreadcrumbItem, BreadcrumbSeparator };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import styled from "styled-components";
|
|
4
|
+
|
|
5
|
+
export const ItemWrapper = styled.span`
|
|
6
|
+
width: 100%;
|
|
7
|
+
line-clamp: 2;
|
|
8
|
+
word-break: break-all;
|
|
9
|
+
|
|
10
|
+
&[data-current="true"] {
|
|
11
|
+
opacity: 1 !important;
|
|
12
|
+
}
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
export const SeparatorItem = styled.span`
|
|
16
|
+
color: var(--font-color-alpha-10) !important;
|
|
17
|
+
user-select: none;
|
|
18
|
+
pointer-events: none;
|
|
19
|
+
`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
3
|
-
import { Button } from "..";
|
|
3
|
+
import { Button, Page } from "..";
|
|
4
4
|
|
|
5
5
|
const meta = {
|
|
6
6
|
title: "Components/Button",
|
|
@@ -31,17 +31,89 @@ export const Sizes: Story = {
|
|
|
31
31
|
);
|
|
32
32
|
},
|
|
33
33
|
};
|
|
34
|
-
export const
|
|
34
|
+
export const Shapes: Story = {
|
|
35
35
|
render: ({ ...args }) => {
|
|
36
36
|
return (
|
|
37
37
|
<div className="flex g-medium-30">
|
|
38
|
-
<Button
|
|
39
|
-
<Button
|
|
40
|
-
<Button
|
|
41
|
-
<Button variant="mono">Mono</Button>
|
|
42
|
-
<Button variant="border">Border</Button>
|
|
43
|
-
<Button variant="ghost">Ghost</Button>
|
|
38
|
+
<Button shape="square">Square</Button>
|
|
39
|
+
<Button shape="smooth">Smooth</Button>
|
|
40
|
+
<Button shape="round">Round</Button>
|
|
44
41
|
</div>
|
|
45
42
|
);
|
|
46
43
|
},
|
|
47
44
|
};
|
|
45
|
+
export const Variants: Story = {
|
|
46
|
+
render: ({ ...args }) => {
|
|
47
|
+
return (
|
|
48
|
+
<Page>
|
|
49
|
+
<Page.Content className="flex align-center justify-center">
|
|
50
|
+
<div className="flex align-center justify-center g-medium-30 flex-wrap">
|
|
51
|
+
<Button variant="primary">Primary</Button>
|
|
52
|
+
<Button variant="secondary">Secondary</Button>
|
|
53
|
+
<Button variant="tertiary">Tertiary</Button>
|
|
54
|
+
<Button variant="danger">Danger</Button>
|
|
55
|
+
<Button variant="warning">Warning</Button>
|
|
56
|
+
<Button variant="mono">Mono</Button>
|
|
57
|
+
<Button variant="border">Border</Button>
|
|
58
|
+
<Button variant="ghost">Ghost</Button>
|
|
59
|
+
</div>
|
|
60
|
+
</Page.Content>
|
|
61
|
+
</Page>
|
|
62
|
+
);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
export const Animations: Story = {
|
|
66
|
+
render: ({ ...args }) => {
|
|
67
|
+
return (
|
|
68
|
+
<Page>
|
|
69
|
+
<Page.Content className="flex flex-column g-medium-30 align-center justify-center">
|
|
70
|
+
<div className="flex align-center justify-center g-medium-30 flex-wrap">
|
|
71
|
+
<Button animation="reflective" variant="primary">
|
|
72
|
+
Primary
|
|
73
|
+
</Button>
|
|
74
|
+
<Button animation="reflective" variant="secondary">
|
|
75
|
+
Secondary
|
|
76
|
+
</Button>
|
|
77
|
+
<Button animation="reflective" variant="tertiary">
|
|
78
|
+
Tertiary
|
|
79
|
+
</Button>
|
|
80
|
+
<Button animation="reflective" variant="danger">
|
|
81
|
+
Danger
|
|
82
|
+
</Button>
|
|
83
|
+
<Button animation="reflective" variant="warning">
|
|
84
|
+
Warning
|
|
85
|
+
</Button>
|
|
86
|
+
<Button animation="reflective" variant="mono">
|
|
87
|
+
Mono
|
|
88
|
+
</Button>
|
|
89
|
+
<Button animation="reflective" variant="border">
|
|
90
|
+
Border
|
|
91
|
+
</Button>
|
|
92
|
+
</div>
|
|
93
|
+
<div className="flex align-center justify-center g-medium-30 flex-wrap">
|
|
94
|
+
<Button animation="reflective" variant="border" sizing="small">
|
|
95
|
+
Small
|
|
96
|
+
</Button>
|
|
97
|
+
<Button animation="reflective" variant="border" sizing="medium">
|
|
98
|
+
Medium
|
|
99
|
+
</Button>
|
|
100
|
+
<Button animation="reflective" variant="border" sizing="large">
|
|
101
|
+
Large
|
|
102
|
+
</Button>
|
|
103
|
+
</div>
|
|
104
|
+
<div className="flex align-center justify-center g-medium-30 flex-wrap">
|
|
105
|
+
<Button animation="reflective" variant="border" shape="square">
|
|
106
|
+
Square
|
|
107
|
+
</Button>
|
|
108
|
+
<Button animation="reflective" variant="border" shape="smooth">
|
|
109
|
+
Smooth
|
|
110
|
+
</Button>
|
|
111
|
+
<Button animation="reflective" variant="border" shape="round">
|
|
112
|
+
Round
|
|
113
|
+
</Button>
|
|
114
|
+
</div>
|
|
115
|
+
</Page.Content>
|
|
116
|
+
</Page>
|
|
117
|
+
);
|
|
118
|
+
},
|
|
119
|
+
};
|
package/src/button/index.tsx
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import React from "react";
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
import { ButtonWrapper, ButtonMaskElement, ButtonOverlay } from "./styles";
|
|
5
6
|
import {
|
|
6
7
|
IComponentStyling,
|
|
7
8
|
ComponentSizeEnum,
|
|
8
9
|
IComponentSize,
|
|
9
10
|
ComponentVariantEnum,
|
|
10
|
-
|
|
11
|
+
TComponentVariant,
|
|
12
|
+
TComponentShape,
|
|
11
13
|
} from "../../../../types";
|
|
12
14
|
|
|
13
15
|
export interface IButtonProperties
|
|
14
16
|
extends IComponentStyling,
|
|
15
17
|
IComponentSize,
|
|
16
|
-
IComponentVariant,
|
|
17
18
|
React.ComponentPropsWithRef<"button"> {
|
|
18
19
|
rawicon?: boolean;
|
|
20
|
+
variant?: TComponentVariant | "danger" | "warning";
|
|
21
|
+
shape?: TComponentShape;
|
|
22
|
+
animation?: "reflective";
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
/**
|
|
@@ -29,8 +33,10 @@ export interface IButtonProperties
|
|
|
29
33
|
* @param {IButtonProperties} props - The props for the Button component.
|
|
30
34
|
* @param {boolean} props.raw - Define whether the component is styled or not.
|
|
31
35
|
* @param {boolean} props.rawicon - Define whether the component is styles its svg children.
|
|
32
|
-
* @param {ComponentSizeEnum} props.sizing - The size of the component. Defaults to
|
|
36
|
+
* @param {ComponentSizeEnum} props.sizing - The size of the component. Defaults to "medium".
|
|
37
|
+
* @param {TComponentShape} props.shape - The size of the component. Defaults to `smooth`.
|
|
33
38
|
* @param {string} props.variant - The style definition used by the component.
|
|
39
|
+
* @param {string} props.animation - The animation that comes with the variant.
|
|
34
40
|
* @param {ReactNode} props.children - The content to be rendered inside the button.
|
|
35
41
|
* @returns {ReactElement} The Button component.
|
|
36
42
|
*/
|
|
@@ -38,10 +44,15 @@ export const Button = React.forwardRef<HTMLButtonElement, IButtonProperties>(
|
|
|
38
44
|
(props, forwardedRef): React.ReactElement => {
|
|
39
45
|
const {
|
|
40
46
|
name,
|
|
41
|
-
variant = ComponentVariantEnum.
|
|
47
|
+
variant = ComponentVariantEnum.Primary,
|
|
42
48
|
sizing = ComponentSizeEnum.Medium,
|
|
49
|
+
shape = "smooth",
|
|
50
|
+
animation,
|
|
43
51
|
raw,
|
|
44
52
|
rawicon,
|
|
53
|
+
onMouseMove,
|
|
54
|
+
onMouseEnter,
|
|
55
|
+
onMouseLeave,
|
|
45
56
|
children,
|
|
46
57
|
...restProps
|
|
47
58
|
} = props;
|
|
@@ -55,6 +66,43 @@ export const Button = React.forwardRef<HTMLButtonElement, IButtonProperties>(
|
|
|
55
66
|
const buttonStateDescription = `disabled:${disabledState}`;
|
|
56
67
|
const ButtonFullDesc = `${buttonDescription}/${buttonStateDescription}`;
|
|
57
68
|
|
|
69
|
+
const isReflective = animation === "reflective" && variant !== "ghost";
|
|
70
|
+
|
|
71
|
+
const [position, setPosition] = React.useState({ x: 0, y: 0 });
|
|
72
|
+
const [isHovering, setIsHovering] = React.useState(false);
|
|
73
|
+
|
|
74
|
+
const handleMouseMove = React.useCallback(
|
|
75
|
+
(e: React.MouseEvent<HTMLButtonElement>) => {
|
|
76
|
+
if (onMouseMove) onMouseMove(e);
|
|
77
|
+
if (!isReflective) return;
|
|
78
|
+
|
|
79
|
+
const rect = e.currentTarget.getBoundingClientRect();
|
|
80
|
+
setPosition({
|
|
81
|
+
x: ((e.clientX - rect.left) / rect.width) * 100,
|
|
82
|
+
y: ((e.clientY - rect.top) / rect.height) * 100,
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
[]
|
|
86
|
+
);
|
|
87
|
+
const handleMouseEnter = React.useCallback(
|
|
88
|
+
(e: React.MouseEvent<HTMLButtonElement>) => {
|
|
89
|
+
if (onMouseEnter) onMouseEnter(e);
|
|
90
|
+
if (!isReflective) return;
|
|
91
|
+
|
|
92
|
+
setIsHovering(true);
|
|
93
|
+
},
|
|
94
|
+
[]
|
|
95
|
+
);
|
|
96
|
+
const handleMouseLeave = React.useCallback(
|
|
97
|
+
(e: React.MouseEvent<HTMLButtonElement>) => {
|
|
98
|
+
if (onMouseLeave) onMouseLeave(e);
|
|
99
|
+
if (!isReflective) return;
|
|
100
|
+
|
|
101
|
+
setIsHovering(false);
|
|
102
|
+
},
|
|
103
|
+
[]
|
|
104
|
+
);
|
|
105
|
+
|
|
58
106
|
return (
|
|
59
107
|
<ButtonWrapper
|
|
60
108
|
ref={forwardedRef}
|
|
@@ -66,11 +114,25 @@ export const Button = React.forwardRef<HTMLButtonElement, IButtonProperties>(
|
|
|
66
114
|
aria-disabled={disabledState}
|
|
67
115
|
data-variant={variant}
|
|
68
116
|
data-size={sizing}
|
|
117
|
+
data-shape={shape}
|
|
118
|
+
data-animation={animation}
|
|
69
119
|
data-raw={Boolean(raw)}
|
|
70
120
|
data-rawicon={Boolean(rawicon)}
|
|
71
121
|
tabIndex={0}
|
|
122
|
+
onMouseMove={handleMouseMove}
|
|
123
|
+
onMouseEnter={handleMouseEnter}
|
|
124
|
+
onMouseLeave={handleMouseLeave}
|
|
72
125
|
{...restProps}
|
|
73
126
|
>
|
|
127
|
+
{isReflective && (
|
|
128
|
+
<ButtonOverlay $isHovering={isHovering} data-shape={shape}>
|
|
129
|
+
<ButtonMaskElement
|
|
130
|
+
$mouseX={position.x}
|
|
131
|
+
$mouseY={position.y}
|
|
132
|
+
data-shape={shape}
|
|
133
|
+
/>
|
|
134
|
+
</ButtonOverlay>
|
|
135
|
+
)}
|
|
74
136
|
{children}
|
|
75
137
|
</ButtonWrapper>
|
|
76
138
|
);
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import styled, { css } from "styled-components";
|
|
2
2
|
|
|
3
|
+
interface OverlayProps {
|
|
4
|
+
$isHovering: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface MaskProps {
|
|
8
|
+
$mouseX: number;
|
|
9
|
+
$mouseY: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
3
12
|
const ButtonDefaultStyles = css`
|
|
4
13
|
cursor: pointer;
|
|
5
14
|
position: relative;
|
|
@@ -26,6 +35,11 @@ const ButtonDefaultStyles = css`
|
|
|
26
35
|
opacity: 0.6;
|
|
27
36
|
}
|
|
28
37
|
|
|
38
|
+
svg,
|
|
39
|
+
span {
|
|
40
|
+
transition: all 0.2s ease-in-out;
|
|
41
|
+
}
|
|
42
|
+
|
|
29
43
|
&:hover,
|
|
30
44
|
&:focus,
|
|
31
45
|
&:active {
|
|
@@ -78,19 +92,19 @@ const ButtonVariantsStyles = css`
|
|
|
78
92
|
}
|
|
79
93
|
&[data-variant="secondary"] {
|
|
80
94
|
color: var(--font-color-alpha-60);
|
|
81
|
-
background-color:
|
|
95
|
+
background-color: transparent;
|
|
82
96
|
border-color: var(--font-color-alpha-10);
|
|
83
97
|
|
|
84
98
|
&:hover,
|
|
85
99
|
&:focus,
|
|
86
100
|
&:active {
|
|
87
101
|
color: var(--font-color);
|
|
88
|
-
|
|
102
|
+
background-color: var(--font-color-alpha-10);
|
|
103
|
+
border-color: transparent;
|
|
89
104
|
}
|
|
90
105
|
}
|
|
91
106
|
&[data-variant="tertiary"] {
|
|
92
|
-
color: var(--font-color-alpha-
|
|
93
|
-
border-color: var(--font-color-alpha-10);
|
|
107
|
+
color: var(--font-color-alpha-80);
|
|
94
108
|
background-color: transparent;
|
|
95
109
|
|
|
96
110
|
&:hover,
|
|
@@ -98,7 +112,6 @@ const ButtonVariantsStyles = css`
|
|
|
98
112
|
&:active {
|
|
99
113
|
color: var(--font-color);
|
|
100
114
|
background-color: var(--font-color-alpha-10);
|
|
101
|
-
border-color: transparent;
|
|
102
115
|
}
|
|
103
116
|
}
|
|
104
117
|
&[data-variant="border"] {
|
|
@@ -110,6 +123,27 @@ const ButtonVariantsStyles = css`
|
|
|
110
123
|
&:focus,
|
|
111
124
|
&:active {
|
|
112
125
|
color: var(--font-color);
|
|
126
|
+
border-color: var(--font-color-alpha-20);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
&[data-variant="danger"] {
|
|
130
|
+
color: var(--color-mono-white);
|
|
131
|
+
background-color: var(--color-red);
|
|
132
|
+
|
|
133
|
+
&:hover,
|
|
134
|
+
&:focus,
|
|
135
|
+
&:active {
|
|
136
|
+
background-color: var(--shade-red-10);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
&[data-variant="warning"] {
|
|
140
|
+
color: var(--color-mono-dark);
|
|
141
|
+
background-color: var(--color-orange);
|
|
142
|
+
|
|
143
|
+
&:hover,
|
|
144
|
+
&:focus,
|
|
145
|
+
&:active {
|
|
146
|
+
background-color: var(--shade-orange-10);
|
|
113
147
|
}
|
|
114
148
|
}
|
|
115
149
|
&[data-variant="mono"] {
|
|
@@ -140,8 +174,7 @@ const ButtonVariantsStyles = css`
|
|
|
140
174
|
`;
|
|
141
175
|
const ButtonSizeStyles = css`
|
|
142
176
|
&[data-size="small"] {
|
|
143
|
-
|
|
144
|
-
font-size: var(--fontsize-medium-10);
|
|
177
|
+
font-size: var(--fontsize-small-60);
|
|
145
178
|
|
|
146
179
|
gap: var(--measurement-medium-10);
|
|
147
180
|
padding: var(--measurement-medium-10) var(--measurement-medium-30);
|
|
@@ -154,27 +187,66 @@ const ButtonSizeStyles = css`
|
|
|
154
187
|
}
|
|
155
188
|
}
|
|
156
189
|
&[data-size="medium"] {
|
|
157
|
-
border-radius: var(--measurement-medium-30);
|
|
158
190
|
padding: var(--measurement-medium-10) var(--measurement-medium-60);
|
|
159
191
|
min-width: var(--measurement-medium-90);
|
|
160
192
|
min-height: var(--measurement-medium-80);
|
|
161
193
|
}
|
|
162
194
|
&[data-size="large"] {
|
|
163
|
-
border-radius: var(--measurement-medium-30);
|
|
164
195
|
padding: var(--measurement-medium-10) var(--measurement-medium-60);
|
|
165
196
|
min-width: var(--measurement-medium-90);
|
|
166
197
|
min-height: var(--measurement-medium-90);
|
|
167
198
|
}
|
|
168
199
|
`;
|
|
200
|
+
const ButtonShapeStyles = css`
|
|
201
|
+
&[data-shape="square"] {
|
|
202
|
+
border-radius: 0;
|
|
203
|
+
}
|
|
204
|
+
&[data-shape="smooth"] {
|
|
205
|
+
border-radius: var(--measurement-medium-20);
|
|
206
|
+
}
|
|
207
|
+
&[data-shape="round"] {
|
|
208
|
+
border-radius: var(--measurement-large-90);
|
|
209
|
+
}
|
|
210
|
+
`;
|
|
169
211
|
|
|
170
212
|
export const ButtonWrapper = styled.button`
|
|
171
213
|
&[data-raw="false"] {
|
|
172
214
|
${ButtonDefaultStyles}
|
|
173
215
|
${ButtonSizeStyles}
|
|
216
|
+
${ButtonShapeStyles}
|
|
174
217
|
${ButtonVariantsStyles}
|
|
175
|
-
|
|
176
218
|
&[data-rawIcon="false"] {
|
|
177
219
|
${ButtonIconStyles}
|
|
178
220
|
}
|
|
179
221
|
}
|
|
180
222
|
`;
|
|
223
|
+
export const ButtonOverlay = styled.div<OverlayProps>`
|
|
224
|
+
position: absolute;
|
|
225
|
+
inset: -1px;
|
|
226
|
+
pointer-events: none;
|
|
227
|
+
opacity: ${(props) => (props.$isHovering ? 1 : 0)};
|
|
228
|
+
background: transparent;
|
|
229
|
+
transition: opacity 0.2s ease-in-out;
|
|
230
|
+
|
|
231
|
+
${ButtonShapeStyles}
|
|
232
|
+
`;
|
|
233
|
+
export const ButtonMaskElement = styled.div<MaskProps>`
|
|
234
|
+
position: absolute;
|
|
235
|
+
inset: 0;
|
|
236
|
+
background: transparent;
|
|
237
|
+
border: var(--measurement-small-10) solid var(--font-color-alpha-20);
|
|
238
|
+
clip-path: inset(0 round var(--measurement-medium-30));
|
|
239
|
+
|
|
240
|
+
mask-image: radial-gradient(
|
|
241
|
+
circle at ${(props) => props.$mouseX}% ${(props) => props.$mouseY}%,
|
|
242
|
+
var(--body-color),
|
|
243
|
+
transparent 100%
|
|
244
|
+
);
|
|
245
|
+
-webkit-mask-image: radial-gradient(
|
|
246
|
+
circle at ${(props) => props.$mouseX}% ${(props) => props.$mouseY}%,
|
|
247
|
+
var(--body-color),
|
|
248
|
+
transparent 100%
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
${ButtonShapeStyles}
|
|
252
|
+
`;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Page, Card, Button } from "..";
|
|
3
|
+
|
|
4
|
+
import type { Meta } from "@storybook/react";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
*
|
|
8
|
+
*/
|
|
9
|
+
const meta = {
|
|
10
|
+
title: "Components/Card",
|
|
11
|
+
component: Card,
|
|
12
|
+
tags: ["autodocs"],
|
|
13
|
+
} satisfies Meta<typeof Card>;
|
|
14
|
+
export default meta;
|
|
15
|
+
|
|
16
|
+
export const Default = {
|
|
17
|
+
render: () => (
|
|
18
|
+
<Page>
|
|
19
|
+
<Page.Content className="p-medium-60">
|
|
20
|
+
<Card.Grid sizing="medium">
|
|
21
|
+
<Card>
|
|
22
|
+
<Card.Meta>
|
|
23
|
+
<p className="fs-medium-10 opacity-default-30">Header</p>
|
|
24
|
+
</Card.Meta>
|
|
25
|
+
<Card.Body>
|
|
26
|
+
<header className="flex align-start justify-between g-medium-30 w-100">
|
|
27
|
+
<div>
|
|
28
|
+
<p className="fs-medium-30">Title</p>
|
|
29
|
+
<p className="fs-medium-20 opacity-default-30">
|
|
30
|
+
Lorem ipsum dolor sit amet consectetur.
|
|
31
|
+
</p>
|
|
32
|
+
</div>
|
|
33
|
+
<div>
|
|
34
|
+
<Button variant="border" sizing="small">
|
|
35
|
+
Action
|
|
36
|
+
</Button>
|
|
37
|
+
</div>
|
|
38
|
+
</header>
|
|
39
|
+
|
|
40
|
+
<p className="fs-medium-20">
|
|
41
|
+
Lorem ipsum dolor sit amet consectetur, adipisicing elit.
|
|
42
|
+
Repellat a corporis exercitationem.
|
|
43
|
+
</p>
|
|
44
|
+
|
|
45
|
+
<div className="w-100 grid g-medium-10">
|
|
46
|
+
<Button sizing="large">Primary</Button>
|
|
47
|
+
</div>
|
|
48
|
+
</Card.Body>
|
|
49
|
+
<Card.Meta>
|
|
50
|
+
<p className="fs-medium-10 opacity-default-30">Footer</p>
|
|
51
|
+
</Card.Meta>
|
|
52
|
+
</Card>
|
|
53
|
+
</Card.Grid>
|
|
54
|
+
</Page.Content>
|
|
55
|
+
</Page>
|
|
56
|
+
),
|
|
57
|
+
};
|