@stackshift-ui/button 6.0.2 → 6.0.4-beta.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/package.json +8 -7
- package/src/button.test.tsx +13 -0
- package/src/button.tsx +135 -0
- package/src/helper/index.ts +25 -0
- package/src/index.ts +4 -0
- package/src/types.ts +16 -0
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackshift-ui/button",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.4-beta.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"module": "./dist/index.mjs",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"files": [
|
|
10
|
-
"dist/**"
|
|
10
|
+
"dist/**",
|
|
11
|
+
"src"
|
|
11
12
|
],
|
|
12
13
|
"devDependencies": {
|
|
13
14
|
"@testing-library/react": "^16.0.1",
|
|
@@ -26,15 +27,15 @@
|
|
|
26
27
|
"typescript": "^5.6.2",
|
|
27
28
|
"vite-tsconfig-paths": "^5.0.1",
|
|
28
29
|
"vitest": "^2.1.1",
|
|
29
|
-
"@stackshift-ui/eslint-config": "6.0.
|
|
30
|
-
"@stackshift-ui/typescript-config": "6.0.
|
|
30
|
+
"@stackshift-ui/eslint-config": "6.0.3-beta.0",
|
|
31
|
+
"@stackshift-ui/typescript-config": "6.0.3-beta.0"
|
|
31
32
|
},
|
|
32
33
|
"dependencies": {
|
|
33
34
|
"classnames": "^2.5.1",
|
|
34
35
|
"react-icons": "^5.3.0",
|
|
35
|
-
"@stackshift-ui/
|
|
36
|
-
"@stackshift-ui/
|
|
37
|
-
"@stackshift-ui/
|
|
36
|
+
"@stackshift-ui/scripts": "6.0.3-beta.0",
|
|
37
|
+
"@stackshift-ui/system": "6.0.4-beta.0",
|
|
38
|
+
"@stackshift-ui/link": "6.0.4-beta.0"
|
|
38
39
|
},
|
|
39
40
|
"peerDependencies": {
|
|
40
41
|
"@types/react": "16.8 - 19",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { cleanup, render, screen } from "@testing-library/react";
|
|
2
|
+
import { afterEach, describe, test } from "vitest";
|
|
3
|
+
import { Button } from "./button";
|
|
4
|
+
|
|
5
|
+
describe.concurrent("button", () => {
|
|
6
|
+
afterEach(cleanup);
|
|
7
|
+
|
|
8
|
+
test("Common: Button - test if renders without errors", ({ expect }) => {
|
|
9
|
+
const clx = "button-class";
|
|
10
|
+
render(<Button className={clx} />);
|
|
11
|
+
expect(screen.getByTestId("button").classList).toContain(clx);
|
|
12
|
+
});
|
|
13
|
+
});
|
package/src/button.tsx
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Link } from "@stackshift-ui/link";
|
|
2
|
+
import { useStackShiftUIComponents } from "@stackshift-ui/system";
|
|
3
|
+
import cn from "classnames";
|
|
4
|
+
import type { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactNode } from "react";
|
|
5
|
+
import { FaSpinner } from "react-icons/fa";
|
|
6
|
+
import { extractLink } from "./helper";
|
|
7
|
+
import { type LabeledRoute, type StyleVariants } from "./types";
|
|
8
|
+
|
|
9
|
+
type Variant =
|
|
10
|
+
| "outline"
|
|
11
|
+
| "ghost"
|
|
12
|
+
| "link"
|
|
13
|
+
| "custom"
|
|
14
|
+
| "solid"
|
|
15
|
+
| "addToWishlist"
|
|
16
|
+
| "unstyled"
|
|
17
|
+
| "swiper_pagination"
|
|
18
|
+
| "tab";
|
|
19
|
+
type TextSize = "xs" | "sm" | "md" | "lg";
|
|
20
|
+
type RadiusSize = "none" | "sm" | "md" | "base" | "lg" | "xl" | "2xl" | "full";
|
|
21
|
+
|
|
22
|
+
interface BaseType {
|
|
23
|
+
children?: ReactNode;
|
|
24
|
+
variant?: Variant;
|
|
25
|
+
radius?: RadiusSize;
|
|
26
|
+
size?: TextSize;
|
|
27
|
+
ariaLabel?: string;
|
|
28
|
+
isActive?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface LinkProps extends BaseType, Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "type"> {
|
|
32
|
+
link: LabeledRoute;
|
|
33
|
+
as: "link";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ButtonProps extends BaseType, ButtonHTMLAttributes<HTMLButtonElement> {
|
|
37
|
+
as?: "button";
|
|
38
|
+
loading?: boolean;
|
|
39
|
+
disabled?: boolean;
|
|
40
|
+
loadingComponent?: React.ReactNode;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type Props = ButtonProps | LinkProps;
|
|
44
|
+
|
|
45
|
+
const displayName = "Button";
|
|
46
|
+
|
|
47
|
+
export function Button({ children, ...props }: Props) {
|
|
48
|
+
const { [displayName]: Component = "button" } = useStackShiftUIComponents();
|
|
49
|
+
|
|
50
|
+
const sizes = {
|
|
51
|
+
xs: "py-1 px-3 text-xs",
|
|
52
|
+
sm: "py-2 px-4 text-sm",
|
|
53
|
+
default: "py-3 px-6 text-default",
|
|
54
|
+
md: "py-3 px-6 text-base",
|
|
55
|
+
lg: "py-4 px-7 text-lg",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const borderRadiusMap = {
|
|
59
|
+
none: "rounded-none",
|
|
60
|
+
sm: "rounded-sm",
|
|
61
|
+
md: "rounded-md",
|
|
62
|
+
base: "rounded-base",
|
|
63
|
+
lg: "rounded-lg",
|
|
64
|
+
xl: "rounded-xl",
|
|
65
|
+
"2xl": "rounded-2xl",
|
|
66
|
+
full: "rounded-full",
|
|
67
|
+
global: "rounded-global",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const { radius, size, variant, ariaLabel, className, isActive } = props;
|
|
71
|
+
|
|
72
|
+
const buttonRadius = radius ? borderRadiusMap[radius] : "rounded-global";
|
|
73
|
+
const buttonSize = sizes[size ?? "default"];
|
|
74
|
+
|
|
75
|
+
const commonStyles = "inline-block font-default text-default transition duration-200";
|
|
76
|
+
const solid = `${commonStyles} ${buttonSize} ${buttonRadius} bg-primary hover:bg-primary/50 text-gray-50`;
|
|
77
|
+
const custom = `inline-block bg-primary hover:bg-primary/50 ${buttonSize} ${buttonRadius} text-gray-50 font-bold transition duration-200`;
|
|
78
|
+
const outline = `${commonStyles} ${buttonSize} ${buttonRadius} bg-white hover:bg-primary/50 outline outline-1 text-primary outline-primary`;
|
|
79
|
+
const ghost = `${commonStyles} ${buttonRadius} ${buttonSize} bg-transparent hover:bg-primary/50 text-primary`;
|
|
80
|
+
const linkType = `transition-200 text-primary hover:text-primary/50 underline ${buttonRadius} ${cn(buttonSize, "px-0 py-0")}`;
|
|
81
|
+
const unstyled = ``;
|
|
82
|
+
const swiper_pagination = `mr-1 ${isActive ? "bg-primary" : "bg-gray-200"} rounded-full p-1 focus:outline-none`;
|
|
83
|
+
const tab = `mx-auto mb-1 w-auto px-4 py-2 rounded duration-200 transition focus:outline-none font-bold ${
|
|
84
|
+
isActive
|
|
85
|
+
? "bg-gray-50 text-primary shadow"
|
|
86
|
+
: "text-gray-700 hover:bg-secondary/50 hover:text-primary hover:shadow"
|
|
87
|
+
}`;
|
|
88
|
+
const addToWishlist = `${commonStyles} ${buttonRadius} ${buttonSize} ml-auto sm:ml-0 flex-shrink-0 inline-flex items-center justify-center w-full rounded-md border hover:border-primary`;
|
|
89
|
+
|
|
90
|
+
const variants: StyleVariants<Variant> = {
|
|
91
|
+
outline,
|
|
92
|
+
ghost,
|
|
93
|
+
link: linkType,
|
|
94
|
+
custom,
|
|
95
|
+
solid,
|
|
96
|
+
addToWishlist,
|
|
97
|
+
unstyled,
|
|
98
|
+
swiper_pagination,
|
|
99
|
+
tab,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const variantClass = variants[variant ?? "solid"];
|
|
103
|
+
|
|
104
|
+
if (props.as === "link") {
|
|
105
|
+
const { link, ...rest } = props as LinkProps;
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<Link
|
|
109
|
+
className={cn(variantClass, className)}
|
|
110
|
+
aria-label={ariaLabel}
|
|
111
|
+
href={extractLink(link)}
|
|
112
|
+
target={link.linkTarget}
|
|
113
|
+
rel={link.linkTarget === "_blank" ? "noopener noreferrer" : ""}
|
|
114
|
+
{...rest}>
|
|
115
|
+
{children}
|
|
116
|
+
</Link>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const { loadingComponent, onClick, loading, disabled, type } = props;
|
|
121
|
+
|
|
122
|
+
const Loader = loadingComponent ?? <FaSpinner className="animate-spin" size={30} />;
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<Component
|
|
126
|
+
onClick={onClick}
|
|
127
|
+
disabled={disabled ?? loading}
|
|
128
|
+
className={cn(variantClass, className)}
|
|
129
|
+
aria-label={ariaLabel}
|
|
130
|
+
data-testid={displayName.toLowerCase()}
|
|
131
|
+
type={type}>
|
|
132
|
+
{loading ? Loader : children}
|
|
133
|
+
</Component>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { LabeledRoute } from "../types";
|
|
2
|
+
|
|
3
|
+
export const extractLink = (link: LabeledRoute) => {
|
|
4
|
+
//not found page
|
|
5
|
+
if (!link?.internalLink && !link?.externalLink) {
|
|
6
|
+
return "/page-not-found";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
//home page
|
|
10
|
+
if (link?.type === "linkInternal" && link?.internalLink?.toLowerCase()?.includes("home")) {
|
|
11
|
+
return "/";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
//internal link
|
|
15
|
+
if (link?.type === "linkInternal") {
|
|
16
|
+
return `/${link?.internalLink}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
//external link
|
|
20
|
+
if (link?.type === "linkExternal") {
|
|
21
|
+
return `${link?.externalLink}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return "/";
|
|
25
|
+
};
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type StyleVariants<T extends string> = Record<T, string>;
|
|
2
|
+
|
|
3
|
+
export interface LabeledRoute extends ConditionalLink {
|
|
4
|
+
ariaLabel?: string;
|
|
5
|
+
label?: string;
|
|
6
|
+
linkTarget?: string;
|
|
7
|
+
linkType?: string;
|
|
8
|
+
_type?: string;
|
|
9
|
+
linkInternal?: any;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ConditionalLink {
|
|
13
|
+
type?: string;
|
|
14
|
+
internalLink?: string | null;
|
|
15
|
+
externalLink?: string | null;
|
|
16
|
+
}
|