@windstream/react-shared-components 0.0.1
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/LICENSE +21 -0
- package/README.md +360 -0
- package/dist/core.d.ts +389 -0
- package/dist/index.d.ts +389 -0
- package/dist/index.esm.js +35 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/styles.css +3 -0
- package/dist/utils/index.d.ts +11 -0
- package/dist/utils/index.esm.js +2 -0
- package/dist/utils/index.esm.js.map +1 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/index.js.map +1 -0
- package/package.json +151 -0
- package/src/components/accordion/Accordion.stories.tsx +225 -0
- package/src/components/accordion/index.tsx +36 -0
- package/src/components/accordion/types.ts +10 -0
- package/src/components/alert-card/AlertCard.stories.tsx +172 -0
- package/src/components/alert-card/index.tsx +35 -0
- package/src/components/alert-card/types.ts +9 -0
- package/src/components/brand-button/BrandButton.stories.tsx +221 -0
- package/src/components/brand-button/helpers.ts +35 -0
- package/src/components/brand-button/index.tsx +90 -0
- package/src/components/brand-button/types.ts +27 -0
- package/src/components/button/Button.stories.tsx +108 -0
- package/src/components/button/index.tsx +23 -0
- package/src/components/button/types.ts +14 -0
- package/src/components/call-button/CallButton.stories.tsx +324 -0
- package/src/components/call-button/index.tsx +80 -0
- package/src/components/call-button/types.ts +9 -0
- package/src/components/checkbox/Checkbox.stories.tsx +248 -0
- package/src/components/checkbox/index.tsx +185 -0
- package/src/components/checkbox/types.ts +24 -0
- package/src/components/checklist/Checklist.stories.tsx +151 -0
- package/src/components/checklist/index.tsx +33 -0
- package/src/components/checklist/types.ts +6 -0
- package/src/components/collapse/Collapse.stories.tsx +256 -0
- package/src/components/collapse/index.tsx +44 -0
- package/src/components/collapse/types.ts +6 -0
- package/src/components/divider/Divider.stories.tsx +206 -0
- package/src/components/divider/index.tsx +23 -0
- package/src/components/divider/type.ts +3 -0
- package/src/components/input/Input.stories.tsx +358 -0
- package/src/components/input/index.tsx +174 -0
- package/src/components/input/types.ts +37 -0
- package/src/components/link/Link.stories.tsx +163 -0
- package/src/components/link/index.tsx +96 -0
- package/src/components/link/types.ts +25 -0
- package/src/components/list/List.stories.tsx +272 -0
- package/src/components/list/index.tsx +86 -0
- package/src/components/list/list-item/index.tsx +36 -0
- package/src/components/list/list-item/types.ts +13 -0
- package/src/components/list/types.ts +29 -0
- package/src/components/material-icon/MaterialIcon.stories.tsx +330 -0
- package/src/components/material-icon/constants.ts +95 -0
- package/src/components/material-icon/index.tsx +44 -0
- package/src/components/material-icon/types.ts +31 -0
- package/src/components/modal/Modal.stories.tsx +171 -0
- package/src/components/modal/index.tsx +168 -0
- package/src/components/modal/types.ts +24 -0
- package/src/components/radio-button/RadioButton.stories.tsx +307 -0
- package/src/components/radio-button/index.tsx +73 -0
- package/src/components/radio-button/types.ts +22 -0
- package/src/components/see-more/SeeMore.stories.tsx +182 -0
- package/src/components/see-more/index.tsx +38 -0
- package/src/components/see-more/types.ts +4 -0
- package/src/components/select/Select.stories.tsx +410 -0
- package/src/components/select/index.tsx +150 -0
- package/src/components/select/types.ts +34 -0
- package/src/components/select-plan-button/SelectPlanButton.stories.tsx +160 -0
- package/src/components/select-plan-button/index.tsx +21 -0
- package/src/components/select-plan-button/types.ts +4 -0
- package/src/components/skeleton/Skeleton.stories.tsx +180 -0
- package/src/components/skeleton/index.tsx +61 -0
- package/src/components/skeleton/types.ts +4 -0
- package/src/components/spinner/Spinner.stories.tsx +335 -0
- package/src/components/spinner/index.tsx +44 -0
- package/src/components/spinner/types.ts +5 -0
- package/src/components/text/Text.stories.tsx +302 -0
- package/src/components/text/index.tsx +26 -0
- package/src/components/text/types.ts +45 -0
- package/src/components/tooltip/Tooltip.stories.tsx +220 -0
- package/src/components/tooltip/index.tsx +78 -0
- package/src/components/tooltip/types.ts +7 -0
- package/src/components/view-cart-button/ViewCartButton.stories.tsx +241 -0
- package/src/components/view-cart-button/index.tsx +39 -0
- package/src/components/view-cart-button/types.ts +5 -0
- package/src/hooks/use-body-scroll-lock.ts +31 -0
- package/src/index.ts +81 -0
- package/src/setupTests.ts +46 -0
- package/src/stories/DocsTemplate.tsx +26 -0
- package/src/stories/assets/accessibility.png +0 -0
- package/src/stories/assets/accessibility.svg +1 -0
- package/src/stories/assets/addon-library.png +0 -0
- package/src/stories/assets/assets.png +0 -0
- package/src/stories/assets/avif-test-image.avif +0 -0
- package/src/stories/assets/context.png +0 -0
- package/src/stories/assets/discord.svg +1 -0
- package/src/stories/assets/docs.png +0 -0
- package/src/stories/assets/figma-plugin.png +0 -0
- package/src/stories/assets/github.svg +1 -0
- package/src/stories/assets/share.png +0 -0
- package/src/stories/assets/styling.png +0 -0
- package/src/stories/assets/testing.png +0 -0
- package/src/stories/assets/theming.png +0 -0
- package/src/stories/assets/tutorials.svg +1 -0
- package/src/stories/assets/youtube.svg +1 -0
- package/src/styles/globals.css +50 -0
- package/src/types/global.d.ts +9 -0
- package/src/utils/index.ts +49 -0
- package/tailwind.config.cjs +524 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import ReactSelect, { createFilter } from "react-select";
|
|
3
|
+
|
|
4
|
+
import { cx } from "../../utils";
|
|
5
|
+
import { Text } from "../text";
|
|
6
|
+
import { FilterOptions, SelectOption, SelectProps } from "./types";
|
|
7
|
+
|
|
8
|
+
export const Select: React.FC<SelectProps> = React.memo(
|
|
9
|
+
({
|
|
10
|
+
options,
|
|
11
|
+
className,
|
|
12
|
+
controlClassName,
|
|
13
|
+
value,
|
|
14
|
+
isCustomStyle = false,
|
|
15
|
+
placeholder,
|
|
16
|
+
required,
|
|
17
|
+
onChange,
|
|
18
|
+
filterOptions,
|
|
19
|
+
hasError,
|
|
20
|
+
error,
|
|
21
|
+
helperText,
|
|
22
|
+
label,
|
|
23
|
+
labelClassName,
|
|
24
|
+
size = "md",
|
|
25
|
+
variant = "default",
|
|
26
|
+
"data-cy": dataCy,
|
|
27
|
+
"data-testid": dataTestId,
|
|
28
|
+
...rest
|
|
29
|
+
}) => {
|
|
30
|
+
const memoizedFilterOption = useMemo(
|
|
31
|
+
() => createFilter(filterOptions),
|
|
32
|
+
[filterOptions]
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const getSizeClasses = () => {
|
|
36
|
+
switch (size) {
|
|
37
|
+
case "sm":
|
|
38
|
+
return "h-[48px] px-3 rounded-lg";
|
|
39
|
+
case "md":
|
|
40
|
+
default:
|
|
41
|
+
return "h-[56px] px-3 rounded-xl";
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const memoizedClassNames = useMemo(
|
|
46
|
+
() => ({
|
|
47
|
+
control: ({ isFocused }: any) =>
|
|
48
|
+
cx(
|
|
49
|
+
getSizeClasses(),
|
|
50
|
+
"border border-input-border bg-white hover:not-focus:border-input-border-hover",
|
|
51
|
+
{
|
|
52
|
+
"border-input-border-selected outline outline-2 outline-input-border-selected":
|
|
53
|
+
isFocused && !hasError,
|
|
54
|
+
"border-input-border-critical shadow-none": hasError,
|
|
55
|
+
},
|
|
56
|
+
controlClassName
|
|
57
|
+
),
|
|
58
|
+
indicatorSeparator: () => "hidden",
|
|
59
|
+
dropdownIndicator: ({ selectProps }: any) =>
|
|
60
|
+
isCustomStyle ? (selectProps.value ? "hidden" : "block") : `block text-icon-default ${hasError ? "text-icon-critical":""}`,
|
|
61
|
+
singleValue: () =>
|
|
62
|
+
cx(
|
|
63
|
+
isCustomStyle
|
|
64
|
+
? "text-text text-body1 whitespace-normal"
|
|
65
|
+
: "text-text-secondary"
|
|
66
|
+
),
|
|
67
|
+
menu: () => "z-20 mt-2",
|
|
68
|
+
option: ({ isFocused, isSelected, label }: any) =>
|
|
69
|
+
cx(
|
|
70
|
+
"min-h-[40px] px-4 py-2 cursor-pointer",
|
|
71
|
+
isSelected
|
|
72
|
+
? "bg-gray-200 text-text font-semibold"
|
|
73
|
+
: "bg-white text-text-secondary",
|
|
74
|
+
!isSelected && "hover:bg-bg-surface-hover",
|
|
75
|
+
!isSelected && isFocused && "bg-bg-surface-hover",
|
|
76
|
+
isCustomStyle
|
|
77
|
+
? "text-body1 text-text"
|
|
78
|
+
: "",
|
|
79
|
+
!isCustomStyle || options[options.length - 1]?.label === label
|
|
80
|
+
? ""
|
|
81
|
+
: "border-b border-border-brand"
|
|
82
|
+
),
|
|
83
|
+
placeholder: () => "text-input-text-placeholder",
|
|
84
|
+
input: () => "text-text",
|
|
85
|
+
valueContainer: () => "px-0",
|
|
86
|
+
}),
|
|
87
|
+
[isCustomStyle, controlClassName, options, hasError, size]
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
if (variant === "unstyled") {
|
|
91
|
+
return (
|
|
92
|
+
<ReactSelect
|
|
93
|
+
options={options}
|
|
94
|
+
value={value}
|
|
95
|
+
placeholder={required ? `${placeholder}*` : placeholder}
|
|
96
|
+
onChange={onChange}
|
|
97
|
+
filterOption={memoizedFilterOption}
|
|
98
|
+
className={cx("rounded-xl", className)}
|
|
99
|
+
classNames={memoizedClassNames}
|
|
100
|
+
data-cy={dataCy}
|
|
101
|
+
data-testid={dataTestId}
|
|
102
|
+
{...rest}
|
|
103
|
+
/>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div className="w-full">
|
|
109
|
+
{label && (
|
|
110
|
+
<label className={cx("block text-sm font-medium text-text-secondary mb-1", labelClassName)}>
|
|
111
|
+
{label}
|
|
112
|
+
</label>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
<ReactSelect
|
|
116
|
+
options={options}
|
|
117
|
+
value={value}
|
|
118
|
+
placeholder={required ? `${placeholder}*` : placeholder}
|
|
119
|
+
onChange={onChange}
|
|
120
|
+
filterOption={memoizedFilterOption}
|
|
121
|
+
className={cx("rounded-xl", className)}
|
|
122
|
+
classNames={memoizedClassNames}
|
|
123
|
+
data-cy={dataCy}
|
|
124
|
+
data-testid={dataTestId}
|
|
125
|
+
{...rest}
|
|
126
|
+
/>
|
|
127
|
+
|
|
128
|
+
{error && (
|
|
129
|
+
<Text
|
|
130
|
+
className="mt-1 footnote text-text-critical"
|
|
131
|
+
>
|
|
132
|
+
{error}
|
|
133
|
+
</Text>
|
|
134
|
+
)}
|
|
135
|
+
|
|
136
|
+
{!error && helperText && (
|
|
137
|
+
<Text
|
|
138
|
+
className="mt-1 footnote text-text-info"
|
|
139
|
+
>
|
|
140
|
+
{helperText}
|
|
141
|
+
</Text>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
Select.displayName = "Select";
|
|
149
|
+
|
|
150
|
+
export type { SelectOption, SelectProps, FilterOptions };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Props as ReactSelectProps } from "react-select";
|
|
2
|
+
|
|
3
|
+
export interface SelectOption<T = any> {
|
|
4
|
+
label: string;
|
|
5
|
+
value: string;
|
|
6
|
+
data?: T;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type FilterOptions = {
|
|
10
|
+
ignoreCase?: boolean;
|
|
11
|
+
ignoreAccents?: boolean;
|
|
12
|
+
trim?: boolean;
|
|
13
|
+
matchFrom?: "start" | "any";
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type OwnProps = {
|
|
17
|
+
options: SelectOption[];
|
|
18
|
+
value?: SelectOption;
|
|
19
|
+
className?: string;
|
|
20
|
+
isCustomStyle?: boolean;
|
|
21
|
+
controlClassName?: string;
|
|
22
|
+
filterOptions?: FilterOptions;
|
|
23
|
+
hasError?: boolean;
|
|
24
|
+
error?: string;
|
|
25
|
+
helperText?: string;
|
|
26
|
+
label?: string;
|
|
27
|
+
labelClassName?: string;
|
|
28
|
+
size?: "sm" | "md";
|
|
29
|
+
variant?: "default" | "unstyled";
|
|
30
|
+
"data-cy"?: string;
|
|
31
|
+
"data-testid"?: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type SelectProps = OwnProps & Omit<ReactSelectProps<SelectOption>, "options" | "value" | "className">;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { SelectPlanButton } from "./index";
|
|
2
|
+
import { DocsPage } from "@shared/stories/DocsTemplate";
|
|
3
|
+
|
|
4
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof SelectPlanButton> = {
|
|
7
|
+
title: "Components/SelectPlanButton",
|
|
8
|
+
component: SelectPlanButton,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: "centered",
|
|
12
|
+
docs: {
|
|
13
|
+
page: DocsPage,
|
|
14
|
+
description: {
|
|
15
|
+
component:
|
|
16
|
+
"A button component specifically designed for selecting plans, with speed information and interactive states.",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
argTypes: {
|
|
21
|
+
speed: {
|
|
22
|
+
control: { type: "text" },
|
|
23
|
+
description: "Speed value for the plan",
|
|
24
|
+
},
|
|
25
|
+
onSelect: {
|
|
26
|
+
action: "selected",
|
|
27
|
+
description: "Callback function when button is clicked",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
args: {
|
|
31
|
+
speed: "100 Mbps",
|
|
32
|
+
onSelect: () => {},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default meta;
|
|
37
|
+
type Story = StoryObj<typeof meta>;
|
|
38
|
+
|
|
39
|
+
// Default button
|
|
40
|
+
export const Default: Story = {
|
|
41
|
+
args: {},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// With speed prop
|
|
45
|
+
export const WithSpeed: Story = {
|
|
46
|
+
args: {
|
|
47
|
+
speed: "500 Mbps",
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Different speeds
|
|
52
|
+
export const DifferentSpeeds: Story = {
|
|
53
|
+
render: () => (
|
|
54
|
+
<div className="w-full max-w-md space-y-3">
|
|
55
|
+
<SelectPlanButton speed="100 Mbps" onSelect={() => {}} />
|
|
56
|
+
<SelectPlanButton speed="250 Mbps" onSelect={() => {}} />
|
|
57
|
+
<SelectPlanButton speed="500 Mbps" onSelect={() => {}} />
|
|
58
|
+
<SelectPlanButton speed="1 Gbps" onSelect={() => {}} />
|
|
59
|
+
</div>
|
|
60
|
+
),
|
|
61
|
+
parameters: {
|
|
62
|
+
docs: {
|
|
63
|
+
description: {
|
|
64
|
+
story: "SelectPlanButton with different speed values.",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Interactive state
|
|
71
|
+
export const Interactive: Story = {
|
|
72
|
+
render: () => {
|
|
73
|
+
const handleSelect = (speed: string) => {
|
|
74
|
+
alert(`Selected plan: ${speed}`);
|
|
75
|
+
};
|
|
76
|
+
return (
|
|
77
|
+
<div className="w-full max-w-md space-y-3">
|
|
78
|
+
<SelectPlanButton speed="100 Mbps" onSelect={() => handleSelect("100 Mbps")} />
|
|
79
|
+
<SelectPlanButton speed="500 Mbps" onSelect={() => handleSelect("500 Mbps")} />
|
|
80
|
+
<SelectPlanButton speed="1 Gbps" onSelect={() => handleSelect("1 Gbps")} />
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
},
|
|
84
|
+
parameters: {
|
|
85
|
+
docs: {
|
|
86
|
+
description: {
|
|
87
|
+
story: "Interactive SelectPlanButton examples that trigger callbacks.",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// In plan card
|
|
94
|
+
export const InPlanCard: Story = {
|
|
95
|
+
render: () => (
|
|
96
|
+
<div className="w-full max-w-sm rounded-lg border border-border-default p-6 space-y-4">
|
|
97
|
+
<div>
|
|
98
|
+
<h3 className="heading3 text-text">Standard Plan</h3>
|
|
99
|
+
<p className="body2 text-text-secondary mt-1">Perfect for families</p>
|
|
100
|
+
</div>
|
|
101
|
+
<div>
|
|
102
|
+
<p className="heading2 text-text">$49.99</p>
|
|
103
|
+
<p className="body3 text-text-secondary">per month</p>
|
|
104
|
+
</div>
|
|
105
|
+
<ul className="space-y-2">
|
|
106
|
+
<li className="body3 text-text">Up to 500 Mbps</li>
|
|
107
|
+
<li className="body3 text-text">Unlimited data</li>
|
|
108
|
+
<li className="body3 text-text">Free installation</li>
|
|
109
|
+
</ul>
|
|
110
|
+
<SelectPlanButton speed="500 Mbps" onSelect={() => {}} />
|
|
111
|
+
</div>
|
|
112
|
+
),
|
|
113
|
+
parameters: {
|
|
114
|
+
docs: {
|
|
115
|
+
description: {
|
|
116
|
+
story: "SelectPlanButton used within a plan card layout.",
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// All variants showcase
|
|
123
|
+
export const AllVariants: Story = {
|
|
124
|
+
render: () => (
|
|
125
|
+
<div className="w-full max-w-2xl space-y-6">
|
|
126
|
+
<div>
|
|
127
|
+
<h3 className="mb-3 text-lg font-semibold">Different Speeds</h3>
|
|
128
|
+
<div className="space-y-3">
|
|
129
|
+
<SelectPlanButton speed="100 Mbps" onSelect={() => {}} />
|
|
130
|
+
<SelectPlanButton speed="250 Mbps" onSelect={() => {}} />
|
|
131
|
+
<SelectPlanButton speed="500 Mbps" onSelect={() => {}} />
|
|
132
|
+
<SelectPlanButton speed="1 Gbps" onSelect={() => {}} />
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
<div>
|
|
136
|
+
<h3 className="mb-3 text-lg font-semibold">In Context</h3>
|
|
137
|
+
<div className="grid grid-cols-2 gap-4">
|
|
138
|
+
<div className="rounded-lg border border-border-default p-4 space-y-3">
|
|
139
|
+
<h4 className="subheading1 text-text">Basic Plan</h4>
|
|
140
|
+
<p className="body2 text-text-secondary">100 Mbps</p>
|
|
141
|
+
<SelectPlanButton speed="100 Mbps" onSelect={() => {}} />
|
|
142
|
+
</div>
|
|
143
|
+
<div className="rounded-lg border border-border-default p-4 space-y-3">
|
|
144
|
+
<h4 className="subheading1 text-text">Premium Plan</h4>
|
|
145
|
+
<p className="body2 text-text-secondary">1 Gbps</p>
|
|
146
|
+
<SelectPlanButton speed="1 Gbps" onSelect={() => {}} />
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
),
|
|
152
|
+
parameters: {
|
|
153
|
+
docs: {
|
|
154
|
+
description: {
|
|
155
|
+
story: "Showcase of all SelectPlanButton variants and use cases.",
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { cx } from "@shared/utils"
|
|
2
|
+
import { Button } from "../button"
|
|
3
|
+
import { MaterialIcon } from "../material-icon"
|
|
4
|
+
import { SelectPlanButtonProps } from "./types"
|
|
5
|
+
const className = "btn-medium rounded-2xl pl-15 pr-15 inline-flex gap-2 items-center justify-center outline-none focus:ring-2 focus:ring-offset-2 cursor-pointer transition-colors duration-200 align-top bg-bg-fill-brand text-text-brand-on-bg-fill enabled:hover:bg-bg-fill-brand-hover focus:ring-bg-fill-brand"
|
|
6
|
+
export const SelectPlanButton:React.FC<SelectPlanButtonProps> = ({ onSelect, speed }) => {
|
|
7
|
+
return <Button
|
|
8
|
+
onClick={onSelect}
|
|
9
|
+
className={cx(className,`w-full justify-between pl-5 pr-5`)}
|
|
10
|
+
data-track-element-name="speed_plan_select_button"
|
|
11
|
+
data-track-click-text={`Select plan speed ${speed}`}
|
|
12
|
+
data-track-element-clicked="speed_plan_card"
|
|
13
|
+
>
|
|
14
|
+
<span>Select plan</span>
|
|
15
|
+
<span className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-bg text-icon-brand">
|
|
16
|
+
<MaterialIcon name="chevron_right" fill={1} size={24} />
|
|
17
|
+
</span>
|
|
18
|
+
</Button>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type { SelectPlanButtonProps } from "./types";
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { Skeleton, PageSkeleton } from "./index";
|
|
2
|
+
import { DocsPage } from "@shared/stories/DocsTemplate";
|
|
3
|
+
|
|
4
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof Skeleton> = {
|
|
7
|
+
title: "Components/Skeleton",
|
|
8
|
+
component: Skeleton,
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
parameters: {
|
|
11
|
+
layout: "centered",
|
|
12
|
+
docs: {
|
|
13
|
+
page: DocsPage,
|
|
14
|
+
description: {
|
|
15
|
+
component:
|
|
16
|
+
"Loading skeleton components for displaying placeholder content while data is being loaded.",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
argTypes: {
|
|
21
|
+
count: {
|
|
22
|
+
control: { type: "number" },
|
|
23
|
+
description: "Number of skeleton lines to display",
|
|
24
|
+
},
|
|
25
|
+
className: {
|
|
26
|
+
control: { type: "text" },
|
|
27
|
+
description: "Additional CSS classes",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
args: {
|
|
31
|
+
count: 1,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default meta;
|
|
36
|
+
type Story = StoryObj<typeof meta>;
|
|
37
|
+
|
|
38
|
+
// Default skeleton
|
|
39
|
+
export const Default: Story = {
|
|
40
|
+
args: {},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Multiple counts
|
|
44
|
+
export const Multiple: Story = {
|
|
45
|
+
args: {
|
|
46
|
+
count: 3,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Custom styling
|
|
51
|
+
export const CustomStyling: Story = {
|
|
52
|
+
args: {
|
|
53
|
+
count: 2,
|
|
54
|
+
className: "h-12 rounded-lg",
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Different counts
|
|
59
|
+
export const DifferentCounts: Story = {
|
|
60
|
+
render: () => (
|
|
61
|
+
<div className="w-full max-w-md space-y-4">
|
|
62
|
+
<div>
|
|
63
|
+
<h4 className="mb-2 text-sm font-medium">Single Line</h4>
|
|
64
|
+
<Skeleton count={1} />
|
|
65
|
+
</div>
|
|
66
|
+
<div>
|
|
67
|
+
<h4 className="mb-2 text-sm font-medium">Three Lines</h4>
|
|
68
|
+
<Skeleton count={3} />
|
|
69
|
+
</div>
|
|
70
|
+
<div>
|
|
71
|
+
<h4 className="mb-2 text-sm font-medium">Five Lines</h4>
|
|
72
|
+
<Skeleton count={5} />
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
),
|
|
76
|
+
parameters: {
|
|
77
|
+
docs: {
|
|
78
|
+
description: {
|
|
79
|
+
story: "Skeleton with different line counts.",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Page skeleton
|
|
86
|
+
export const PageSkeletonExample: Story = {
|
|
87
|
+
render: () => (
|
|
88
|
+
<div className="w-full">
|
|
89
|
+
<PageSkeleton />
|
|
90
|
+
</div>
|
|
91
|
+
),
|
|
92
|
+
parameters: {
|
|
93
|
+
layout: "fullscreen",
|
|
94
|
+
docs: {
|
|
95
|
+
description: {
|
|
96
|
+
story: "PageSkeleton component for full page loading states.",
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// In card
|
|
103
|
+
export const InCard: Story = {
|
|
104
|
+
render: () => (
|
|
105
|
+
<div className="w-full max-w-sm rounded-lg border border-border-default p-6 space-y-4">
|
|
106
|
+
<Skeleton count={1} className="h-6 w-3/4" />
|
|
107
|
+
<Skeleton count={2} />
|
|
108
|
+
<Skeleton count={1} className="h-10 w-full" />
|
|
109
|
+
</div>
|
|
110
|
+
),
|
|
111
|
+
parameters: {
|
|
112
|
+
docs: {
|
|
113
|
+
description: {
|
|
114
|
+
story: "Skeleton used within a card layout.",
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// List skeleton
|
|
121
|
+
export const ListSkeleton: Story = {
|
|
122
|
+
render: () => (
|
|
123
|
+
<div className="w-full max-w-md space-y-3">
|
|
124
|
+
{[1, 2, 3, 4, 5].map((i) => (
|
|
125
|
+
<div key={i} className="flex items-center gap-3">
|
|
126
|
+
<Skeleton count={1} className="h-12 w-12 rounded-full" />
|
|
127
|
+
<div className="flex-1 space-y-2">
|
|
128
|
+
<Skeleton count={1} className="h-4 w-3/4" />
|
|
129
|
+
<Skeleton count={1} className="h-3 w-1/2" />
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
))}
|
|
133
|
+
</div>
|
|
134
|
+
),
|
|
135
|
+
parameters: {
|
|
136
|
+
docs: {
|
|
137
|
+
description: {
|
|
138
|
+
story: "Skeleton used for loading list items.",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// All variants showcase
|
|
145
|
+
export const AllVariants: Story = {
|
|
146
|
+
render: () => (
|
|
147
|
+
<div className="w-full max-w-2xl space-y-8">
|
|
148
|
+
<div>
|
|
149
|
+
<h3 className="mb-3 text-lg font-semibold">Basic Skeleton</h3>
|
|
150
|
+
<Skeleton count={3} />
|
|
151
|
+
</div>
|
|
152
|
+
<div>
|
|
153
|
+
<h3 className="mb-3 text-lg font-semibold">Custom Styled</h3>
|
|
154
|
+
<Skeleton count={2} className="h-8 rounded-lg" />
|
|
155
|
+
</div>
|
|
156
|
+
<div>
|
|
157
|
+
<h3 className="mb-3 text-lg font-semibold">In Card</h3>
|
|
158
|
+
<div className="rounded-lg border border-border-default p-6 space-y-4">
|
|
159
|
+
<Skeleton count={1} className="h-6 w-3/4" />
|
|
160
|
+
<Skeleton count={2} />
|
|
161
|
+
<Skeleton count={1} className="h-10 w-full" />
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
<div>
|
|
165
|
+
<h3 className="mb-3 text-lg font-semibold">Page Skeleton</h3>
|
|
166
|
+
<div className="border border-border-default rounded p-4">
|
|
167
|
+
<PageSkeleton />
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
),
|
|
172
|
+
parameters: {
|
|
173
|
+
docs: {
|
|
174
|
+
description: {
|
|
175
|
+
story: "Showcase of all skeleton variants and use cases.",
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { FC } from "react";
|
|
2
|
+
import { SkeletonProps } from "./types";
|
|
3
|
+
|
|
4
|
+
export const Skeleton: FC<SkeletonProps> = ({ className = "", count = 1 }) => {
|
|
5
|
+
return (
|
|
6
|
+
<div className="animate-pulse space-y-4">
|
|
7
|
+
{Array.from({ length: count }).map((_, index) => (
|
|
8
|
+
<div
|
|
9
|
+
key={index}
|
|
10
|
+
className={`h-8 rounded bg-skeleton-bg-start ${className}`}
|
|
11
|
+
style={{ opacity: 1 - index * 0.1 }}
|
|
12
|
+
/>
|
|
13
|
+
))}
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
Skeleton.displayName = "Skeleton";
|
|
19
|
+
|
|
20
|
+
export type { SkeletonProps };
|
|
21
|
+
|
|
22
|
+
export const PageSkeleton: FC = () => {
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
className="item-center mx-auto mt-4 flex animate-pulse flex-col justify-center gap-10 px-1 md:max-w-[48.375rem] md:gap-16 md:px-0"
|
|
26
|
+
data-testid="generic-skeleton"
|
|
27
|
+
>
|
|
28
|
+
{/* Title */}
|
|
29
|
+
<div
|
|
30
|
+
className="h-12 w-full rounded-lg bg-skeleton-bg-start"
|
|
31
|
+
data-testid="title"
|
|
32
|
+
/>
|
|
33
|
+
|
|
34
|
+
{/* Subtitle */}
|
|
35
|
+
<div className="item-center flex flex-col gap-6" data-testid="subtitle">
|
|
36
|
+
<div className="h-5 w-full rounded-lg bg-skeleton-bg-start" />
|
|
37
|
+
<div className="h-5 w-full rounded-lg bg-skeleton-bg-start" />
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
{/* Content */}
|
|
41
|
+
<div className="item-center flex flex-col gap-3" data-testid="content">
|
|
42
|
+
<div className="h-12 w-full rounded-lg bg-skeleton-bg-start" />
|
|
43
|
+
<div className="h-12 w-full rounded-lg bg-skeleton-bg-start" />
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
{/* Additional Information */}
|
|
47
|
+
<div
|
|
48
|
+
className="item-center flex flex-col gap-6"
|
|
49
|
+
data-testid="additional-info"
|
|
50
|
+
>
|
|
51
|
+
<div className="h-5 w-full rounded-lg bg-skeleton-bg-end" />
|
|
52
|
+
<div className="h-5 w-full rounded-lg bg-skeleton-bg-end" />
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
{/* CTA button */}
|
|
56
|
+
<div className="item-center flex justify-center" data-testid="cta-button">
|
|
57
|
+
<div className="h-12 w-full rounded-lg bg-skeleton-bg-end md:w-[22.125rem]" />
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
};
|