@stasho/ds 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/package.json +88 -0
  2. package/src/components/alert/alert.tsx +204 -0
  3. package/src/components/badge/badge.tsx +139 -0
  4. package/src/components/breadcrumb/breadcrumb.tsx +130 -0
  5. package/src/components/button/button.tsx +153 -0
  6. package/src/components/card/card.tsx +48 -0
  7. package/src/components/checkbox/checkbox.tsx +82 -0
  8. package/src/components/combobox/combobox.tsx +166 -0
  9. package/src/components/copyable-text/copyable-text.tsx +194 -0
  10. package/src/components/dialog/dialog.tsx +145 -0
  11. package/src/components/form-field/form-field.tsx +71 -0
  12. package/src/components/input/input.tsx +52 -0
  13. package/src/components/logo/logo.tsx +68 -0
  14. package/src/components/multi-select/multi-select.tsx +312 -0
  15. package/src/components/pagination/pagination.tsx +218 -0
  16. package/src/components/progress-bar/progress-bar.tsx +134 -0
  17. package/src/components/radio-group/radio-group.tsx +87 -0
  18. package/src/components/select/select.tsx +124 -0
  19. package/src/components/slider/slider.tsx +127 -0
  20. package/src/components/status-dot/status-dot.tsx +53 -0
  21. package/src/components/stepper/stepper.tsx +229 -0
  22. package/src/components/switch/switch.tsx +74 -0
  23. package/src/components/table/table.tsx +260 -0
  24. package/src/components/tabs/tabs.tsx +493 -0
  25. package/src/components/textarea/textarea.tsx +56 -0
  26. package/src/components/tooltip/tooltip.tsx +35 -0
  27. package/src/components/ui/skeleton.tsx +21 -0
  28. package/src/components/ui/spinner.tsx +27 -0
  29. package/src/lib/cn.ts +6 -0
  30. package/src/styles/tokens.css +459 -0
package/package.json ADDED
@@ -0,0 +1,88 @@
1
+ {
2
+ "name": "@stasho/ds",
3
+ "version": "0.1.0",
4
+ "description": "stasho design system",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/cpascariello/stasho-ds#readme",
8
+ "bugs": {
9
+ "url": "https://github.com/cpascariello/stasho-ds/issues"
10
+ },
11
+ "files": [
12
+ "src/",
13
+ "!src/**/*.test.tsx",
14
+ "!src/**/*.test.ts"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/cpascariello/stasho-ds.git",
22
+ "directory": "packages/ds"
23
+ },
24
+ "exports": {
25
+ "./alert": "./src/components/alert/alert.tsx",
26
+ "./button": "./src/components/button/button.tsx",
27
+ "./checkbox": "./src/components/checkbox/checkbox.tsx",
28
+ "./input": "./src/components/input/input.tsx",
29
+ "./textarea": "./src/components/textarea/textarea.tsx",
30
+ "./select": "./src/components/select/select.tsx",
31
+ "./switch": "./src/components/switch/switch.tsx",
32
+ "./form-field": "./src/components/form-field/form-field.tsx",
33
+ "./badge": "./src/components/badge/badge.tsx",
34
+ "./breadcrumb": "./src/components/breadcrumb/breadcrumb.tsx",
35
+ "./card": "./src/components/card/card.tsx",
36
+ "./status-dot": "./src/components/status-dot/status-dot.tsx",
37
+ "./table": "./src/components/table/table.tsx",
38
+ "./tabs": "./src/components/tabs/tabs.tsx",
39
+ "./tooltip": "./src/components/tooltip/tooltip.tsx",
40
+ "./combobox": "./src/components/combobox/combobox.tsx",
41
+ "./slider": "./src/components/slider/slider.tsx",
42
+ "./radio-group": "./src/components/radio-group/radio-group.tsx",
43
+ "./ui/skeleton": "./src/components/ui/skeleton.tsx",
44
+ "./ui/spinner": "./src/components/ui/spinner.tsx",
45
+ "./multi-select": "./src/components/multi-select/multi-select.tsx",
46
+ "./pagination": "./src/components/pagination/pagination.tsx",
47
+ "./copyable-text": "./src/components/copyable-text/copyable-text.tsx",
48
+ "./dialog": "./src/components/dialog/dialog.tsx",
49
+ "./logo": "./src/components/logo/logo.tsx",
50
+ "./progress-bar": "./src/components/progress-bar/progress-bar.tsx",
51
+ "./stepper": "./src/components/stepper/stepper.tsx",
52
+ "./lib/cn": "./src/lib/cn.ts",
53
+ "./styles/tokens.css": "./src/styles/tokens.css"
54
+ },
55
+ "scripts": {
56
+ "lint": "oxlint --import-plugin src/",
57
+ "typecheck": "tsc --noEmit",
58
+ "test": "vitest run",
59
+ "check": "npm run lint && npm run typecheck && npm run test"
60
+ },
61
+ "peerDependencies": {
62
+ "react": "^19.0.0",
63
+ "react-dom": "^19.0.0"
64
+ },
65
+ "dependencies": {
66
+ "@phosphor-icons/react": "2.1.10",
67
+ "class-variance-authority": "0.7.1",
68
+ "clsx": "2.1.1",
69
+ "cmdk": "1.1.1",
70
+ "radix-ui": "1.4.3",
71
+ "tailwind-merge": "3.5.0"
72
+ },
73
+ "devDependencies": {
74
+ "@testing-library/jest-dom": "6.6.3",
75
+ "@testing-library/react": "16.3.2",
76
+ "@testing-library/user-event": "14.6.1",
77
+ "@types/jsdom": "28.0.0",
78
+ "@types/react": "19.2.14",
79
+ "@types/react-dom": "19.2.3",
80
+ "@vitejs/plugin-react": "5.1.4",
81
+ "jsdom": "28.1.0",
82
+ "oxlint": "1.50.0",
83
+ "react": "19.2.4",
84
+ "react-dom": "19.2.4",
85
+ "typescript": "5.9.3",
86
+ "vitest": "4.0.18"
87
+ }
88
+ }
@@ -0,0 +1,204 @@
1
+ "use client";
2
+
3
+ import {
4
+ forwardRef,
5
+ useEffect,
6
+ useRef,
7
+ useState,
8
+ type HTMLAttributes,
9
+ type ReactNode,
10
+ } from "react";
11
+ import { cva, type VariantProps } from "class-variance-authority";
12
+ import { XCircle } from "@phosphor-icons/react";
13
+ import { cn } from "@ac/lib/cn";
14
+
15
+ type AlertVariant = "warning" | "error" | "info" | "success";
16
+
17
+ const VARIANT_LABELS: Record<AlertVariant, string> = {
18
+ warning: "Warning",
19
+ error: "Error",
20
+ info: "Info",
21
+ success: "Success",
22
+ };
23
+
24
+ const VARIANT_BG_CLASS: Record<AlertVariant, string> = {
25
+ warning: "alert-bg-warning",
26
+ error: "alert-bg-error",
27
+ info: "alert-bg-info",
28
+ success: "alert-bg-success",
29
+ };
30
+
31
+ const alertVariants = cva(
32
+ [
33
+ "relative overflow-hidden rounded-sm border",
34
+ "px-3 py-2",
35
+ "transition-all duration-200",
36
+ "motion-reduce:transition-none",
37
+ ].join(" "),
38
+ {
39
+ variants: {
40
+ variant: {
41
+ warning: "border-warning-400",
42
+ error: "border-error-300",
43
+ info: "border-primary-300",
44
+ success: "border-success-400",
45
+ },
46
+ },
47
+ defaultVariants: {
48
+ variant: "warning",
49
+ },
50
+ },
51
+ );
52
+
53
+ const labelVariants = cva(
54
+ "font-heading font-extrabold italic text-xs uppercase leading-normal pb-1",
55
+ {
56
+ variants: {
57
+ variant: {
58
+ warning: "text-warning-600 dark:text-warning-300",
59
+ error: "text-error-600 dark:text-error-300",
60
+ info: "text-primary-600 dark:text-primary-300",
61
+ success: "text-success-600 dark:text-success-300",
62
+ },
63
+ },
64
+ },
65
+ );
66
+
67
+ const progressVariants = cva("absolute bottom-0 left-0 h-0.5", {
68
+ variants: {
69
+ variant: {
70
+ warning: "bg-warning-400",
71
+ error: "bg-error-400",
72
+ info: "bg-primary-400",
73
+ success: "bg-success-400",
74
+ },
75
+ },
76
+ });
77
+
78
+ type AlertProps = Omit<HTMLAttributes<HTMLDivElement>, "title"> &
79
+ VariantProps<typeof alertVariants> & {
80
+ title?: string;
81
+ onDismiss?: () => void;
82
+ dismissAfter?: number;
83
+ children: ReactNode;
84
+ };
85
+
86
+ const Alert = forwardRef<HTMLDivElement, AlertProps>(
87
+ (
88
+ {
89
+ variant = "warning",
90
+ title,
91
+ onDismiss,
92
+ dismissAfter,
93
+ className,
94
+ children,
95
+ ...rest
96
+ },
97
+ ref,
98
+ ) => {
99
+ const [isDismissing, setIsDismissing] = useState(false);
100
+ const [progressActive, setProgressActive] = useState(false);
101
+ const timerRef = useRef<ReturnType<typeof setTimeout>>(null);
102
+ const resolvedVariant: AlertVariant = variant ?? "warning";
103
+
104
+ const label = VARIANT_LABELS[resolvedVariant];
105
+
106
+ useEffect(() => {
107
+ if (!dismissAfter || !onDismiss) return;
108
+
109
+ requestAnimationFrame(() => {
110
+ setProgressActive(true);
111
+ });
112
+
113
+ timerRef.current = setTimeout(() => {
114
+ setIsDismissing(true);
115
+ }, dismissAfter);
116
+
117
+ return () => {
118
+ if (timerRef.current) clearTimeout(timerRef.current);
119
+ };
120
+ }, [dismissAfter, onDismiss]);
121
+
122
+ function handleDismiss() {
123
+ if (timerRef.current) clearTimeout(timerRef.current);
124
+ setIsDismissing(true);
125
+ }
126
+
127
+ function handleTransitionEnd() {
128
+ if (isDismissing && onDismiss) {
129
+ onDismiss();
130
+ }
131
+ }
132
+
133
+ return (
134
+ <div
135
+ ref={ref}
136
+ role="alert"
137
+ className={cn(
138
+ alertVariants({ variant }),
139
+ VARIANT_BG_CLASS[resolvedVariant],
140
+ isDismissing && "opacity-0 -translate-y-2",
141
+ className,
142
+ )}
143
+ onTransitionEnd={handleTransitionEnd}
144
+ {...rest}
145
+ >
146
+ <span className={labelVariants({ variant })}>
147
+ {label}
148
+ </span>
149
+
150
+ {title ? (
151
+ <p
152
+ data-alert-title=""
153
+ className="font-sans font-bold text-sm text-foreground mt-0.5"
154
+ >
155
+ {title}
156
+ </p>
157
+ ) : null}
158
+
159
+ {/* eslint-disable-next-line -- nested selectors for auto-styled links */}
160
+ <div className={cn(
161
+ "font-sans italic text-xs text-foreground leading-relaxed",
162
+ "[&_a]:font-bold [&_a]:not-italic [&_a]:underline",
163
+ "[&_a]:after:content-['↗'] [&_a]:after:ml-0.5 [&_a]:after:text-[10px] [&_a]:after:no-underline [&_a]:after:inline-block",
164
+ )}>
165
+ {children}
166
+ </div>
167
+
168
+ {onDismiss ? (
169
+ <button
170
+ type="button"
171
+ onClick={handleDismiss}
172
+ className={cn(
173
+ "absolute top-2 right-2 cursor-pointer",
174
+ labelVariants({ variant }),
175
+ )}
176
+ aria-label="Dismiss"
177
+ >
178
+ <XCircle weight="fill" className="size-3.5" />
179
+ </button>
180
+ ) : null}
181
+
182
+ {dismissAfter && onDismiss ? (
183
+ <div
184
+ data-alert-progress=""
185
+ className={cn(
186
+ progressVariants({ variant }),
187
+ "motion-reduce:transition-none",
188
+ )}
189
+ style={{
190
+ width: progressActive ? "0%" : "100%",
191
+ transitionProperty: "width",
192
+ transitionDuration: `${dismissAfter}ms`,
193
+ transitionTimingFunction: "linear",
194
+ }}
195
+ />
196
+ ) : null}
197
+ </div>
198
+ );
199
+ },
200
+ );
201
+
202
+ Alert.displayName = "Alert";
203
+
204
+ export { Alert, alertVariants, type AlertProps };
@@ -0,0 +1,139 @@
1
+ import { forwardRef, type HTMLAttributes, type ReactNode } from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { cn } from "@ac/lib/cn";
4
+
5
+ const badgeVariants = cva(
6
+ [
7
+ "inline-flex items-center justify-center gap-1.5",
8
+ "rounded-md font-heading font-extrabold italic uppercase",
9
+ "whitespace-nowrap select-none",
10
+ ].join(" "),
11
+ {
12
+ variants: {
13
+ variant: {
14
+ default: "",
15
+ success: "",
16
+ warning: "",
17
+ error: "",
18
+ info: "",
19
+ },
20
+ fill: {
21
+ solid: "",
22
+ outline: "border",
23
+ },
24
+ size: {
25
+ sm: "px-3 py-0.5 text-[10px]",
26
+ md: "px-4 py-1 text-xs",
27
+ },
28
+ },
29
+ compoundVariants: [
30
+ {
31
+ fill: "solid",
32
+ variant: "default",
33
+ className: "gradient-fill-info text-neutral-950",
34
+ },
35
+ {
36
+ fill: "solid",
37
+ variant: "success",
38
+ className: "gradient-fill-success text-neutral-950",
39
+ },
40
+ {
41
+ fill: "solid",
42
+ variant: "warning",
43
+ className: "gradient-fill-warning text-neutral-950",
44
+ },
45
+ {
46
+ fill: "solid",
47
+ variant: "error",
48
+ className: "gradient-fill-error text-neutral-950",
49
+ },
50
+ {
51
+ fill: "solid",
52
+ variant: "info",
53
+ className: [
54
+ "bg-neutral-100 text-neutral-700",
55
+ "dark:bg-neutral-800 dark:text-neutral-200",
56
+ ].join(" "),
57
+ },
58
+ {
59
+ fill: "outline",
60
+ variant: "default",
61
+ className: [
62
+ "border-primary-300 bg-primary-100 text-neutral-950",
63
+ "dark:bg-primary-900/20 dark:text-neutral-200",
64
+ ].join(" "),
65
+ },
66
+ {
67
+ fill: "outline",
68
+ variant: "success",
69
+ className: [
70
+ "border-success-400 bg-success-100 text-neutral-950",
71
+ "dark:bg-success-900/20 dark:text-neutral-200",
72
+ ].join(" "),
73
+ },
74
+ {
75
+ fill: "outline",
76
+ variant: "warning",
77
+ className: [
78
+ "border-warning-400 bg-warning-100 text-neutral-950",
79
+ "dark:bg-warning-900/20 dark:text-neutral-200",
80
+ ].join(" "),
81
+ },
82
+ {
83
+ fill: "outline",
84
+ variant: "error",
85
+ className: [
86
+ "border-error-400 bg-error-100 text-neutral-950",
87
+ "dark:bg-error-900/20 dark:text-neutral-200",
88
+ ].join(" "),
89
+ },
90
+ {
91
+ fill: "outline",
92
+ variant: "info",
93
+ className: [
94
+ "border-neutral-400 bg-neutral-100/50 text-neutral-700",
95
+ "dark:border-neutral-600 dark:bg-neutral-800/50 dark:text-neutral-300",
96
+ ].join(" "),
97
+ },
98
+ ],
99
+ defaultVariants: {
100
+ variant: "default",
101
+ fill: "solid",
102
+ size: "md",
103
+ },
104
+ },
105
+ );
106
+
107
+ type BadgeProps = HTMLAttributes<HTMLSpanElement> &
108
+ VariantProps<typeof badgeVariants> & {
109
+ iconLeft?: ReactNode;
110
+ iconRight?: ReactNode;
111
+ };
112
+
113
+ const Badge = forwardRef<HTMLSpanElement, BadgeProps>(
114
+ (
115
+ { variant, fill, size, iconLeft, iconRight, className, children, ...rest },
116
+ ref,
117
+ ) => {
118
+ const iconSize = size === "sm" ? "size-2.5" : "size-3";
119
+ return (
120
+ <span
121
+ ref={ref}
122
+ className={cn(badgeVariants({ variant, fill, size }), className)}
123
+ {...rest}
124
+ >
125
+ {iconLeft && (
126
+ <span className={cn(iconSize, "shrink-0")}>{iconLeft}</span>
127
+ )}
128
+ {children}
129
+ {iconRight && (
130
+ <span className={cn(iconSize, "shrink-0")}>{iconRight}</span>
131
+ )}
132
+ </span>
133
+ );
134
+ },
135
+ );
136
+
137
+ Badge.displayName = "Badge";
138
+
139
+ export { Badge, badgeVariants, type BadgeProps };
@@ -0,0 +1,130 @@
1
+ import { forwardRef, type HTMLAttributes, type ReactNode } from "react";
2
+ import { Slot } from "radix-ui";
3
+ import { cn } from "@ac/lib/cn";
4
+
5
+ /* ── Breadcrumb (nav wrapper) ────────────────── */
6
+
7
+ const Breadcrumb = forwardRef<HTMLElement, HTMLAttributes<HTMLElement>>(
8
+ ({ className, ...rest }, ref) => (
9
+ <nav
10
+ ref={ref}
11
+ aria-label="Breadcrumb"
12
+ className={cn(className)}
13
+ {...rest}
14
+ />
15
+ ),
16
+ );
17
+
18
+ Breadcrumb.displayName = "Breadcrumb";
19
+
20
+ /* ── BreadcrumbList (ol) ─────────────────────── */
21
+
22
+ const BreadcrumbList = forwardRef<
23
+ HTMLOListElement,
24
+ HTMLAttributes<HTMLOListElement>
25
+ >(({ className, ...rest }, ref) => (
26
+ <ol
27
+ ref={ref}
28
+ className={cn(
29
+ "flex flex-wrap items-center gap-1",
30
+ "font-heading font-extrabold italic uppercase text-xs",
31
+ className,
32
+ )}
33
+ {...rest}
34
+ />
35
+ ));
36
+
37
+ BreadcrumbList.displayName = "BreadcrumbList";
38
+
39
+ /* ── BreadcrumbItem (li) ─────────────────────── */
40
+
41
+ const BreadcrumbItem = forwardRef<
42
+ HTMLLIElement,
43
+ HTMLAttributes<HTMLLIElement>
44
+ >(({ className, ...rest }, ref) => (
45
+ <li
46
+ ref={ref}
47
+ className={cn("inline-flex items-center", className)}
48
+ {...rest}
49
+ />
50
+ ));
51
+
52
+ BreadcrumbItem.displayName = "BreadcrumbItem";
53
+
54
+ /* ── BreadcrumbLink (a, with asChild) ────────── */
55
+
56
+ type BreadcrumbLinkProps = HTMLAttributes<HTMLAnchorElement> & {
57
+ asChild?: boolean;
58
+ href?: string;
59
+ };
60
+
61
+ const BreadcrumbLink = forwardRef<HTMLAnchorElement, BreadcrumbLinkProps>(
62
+ ({ asChild, className, ...rest }, ref) => {
63
+ const Comp = asChild ? Slot.Root : "a";
64
+ return (
65
+ <Comp
66
+ ref={ref}
67
+ className={cn(
68
+ "text-foreground",
69
+ "transition-colors duration-150",
70
+ "hover:text-primary-600 dark:hover:text-primary-400",
71
+ "motion-reduce:transition-none",
72
+ className,
73
+ )}
74
+ {...rest}
75
+ />
76
+ );
77
+ },
78
+ );
79
+
80
+ BreadcrumbLink.displayName = "BreadcrumbLink";
81
+
82
+ /* ── BreadcrumbSeparator (li, visual only) ───── */
83
+
84
+ type BreadcrumbSeparatorProps = HTMLAttributes<HTMLLIElement> & {
85
+ children?: ReactNode;
86
+ };
87
+
88
+ const BreadcrumbSeparator = forwardRef<
89
+ HTMLLIElement,
90
+ BreadcrumbSeparatorProps
91
+ >(({ children, className, ...rest }, ref) => (
92
+ <li
93
+ ref={ref}
94
+ aria-hidden="true"
95
+ className={cn("text-primary opacity-40", className)}
96
+ {...rest}
97
+ >
98
+ {children ?? "/"}
99
+ </li>
100
+ ));
101
+
102
+ BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
103
+
104
+ /* ── BreadcrumbPage (current page) ───────────── */
105
+
106
+ const BreadcrumbPage = forwardRef<
107
+ HTMLSpanElement,
108
+ HTMLAttributes<HTMLSpanElement>
109
+ >(({ className, ...rest }, ref) => (
110
+ <span
111
+ ref={ref}
112
+ aria-current="page"
113
+ className={cn("text-neutral opacity-40", className)}
114
+ {...rest}
115
+ />
116
+ ));
117
+
118
+ BreadcrumbPage.displayName = "BreadcrumbPage";
119
+
120
+ /* ── Exports ─────────────────────────────────── */
121
+
122
+ export {
123
+ Breadcrumb,
124
+ BreadcrumbItem,
125
+ BreadcrumbLink,
126
+ type BreadcrumbLinkProps,
127
+ BreadcrumbList,
128
+ BreadcrumbPage,
129
+ BreadcrumbSeparator,
130
+ };