@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
@@ -0,0 +1,153 @@
1
+ import {
2
+ cloneElement,
3
+ forwardRef,
4
+ isValidElement,
5
+ type ButtonHTMLAttributes,
6
+ type ReactElement,
7
+ type ReactNode,
8
+ } from "react";
9
+ import { cva, type VariantProps } from "class-variance-authority";
10
+ import { cn } from "@ac/lib/cn";
11
+ import { Spinner } from "@ac/components/ui/spinner";
12
+
13
+ const buttonVariants = cva(
14
+ [
15
+ "inline-flex items-center justify-center",
16
+ "font-heading font-bold",
17
+ "rounded-full border-3 transition-colors",
18
+ "focus-visible:outline-none focus-visible:ring-2",
19
+ "focus-visible:ring-primary-400 focus-visible:ring-offset-2",
20
+ "disabled:pointer-events-none",
21
+ ].join(" "),
22
+ {
23
+ variants: {
24
+ variant: {
25
+ primary: [
26
+ "gradient-fill-main text-white border-transparent",
27
+ "disabled:opacity-50",
28
+ ].join(" "),
29
+ secondary: [
30
+ "gradient-fill-lime text-neutral-950 border-neutral-950",
31
+ "disabled:opacity-50",
32
+ ].join(" "),
33
+ outline: [
34
+ "border-gradient-main text-primary-700",
35
+ "hover:text-primary-800",
36
+ "active:text-primary-800",
37
+ "disabled:opacity-50",
38
+ ].join(" "),
39
+ text: [
40
+ "bg-transparent text-primary-600 dark:text-primary-300 border-transparent",
41
+ "hover:bg-primary-100 hover:text-primary-700",
42
+ "dark:hover:bg-primary-200/10 dark:hover:text-primary-200",
43
+ "active:bg-primary-200 active:text-primary-800",
44
+ "dark:active:bg-primary-700 dark:active:text-primary-100",
45
+ "disabled:bg-transparent disabled:text-primary-600/50",
46
+ "dark:disabled:text-primary-300/50",
47
+ ].join(" "),
48
+ destructive: [
49
+ "bg-error-600/20 text-error-700 dark:text-error-300 border-error-600",
50
+ "hover:bg-error-600/30 hover:border-error-700",
51
+ "active:bg-error-600/40 active:border-error-800",
52
+ "disabled:bg-error-600/10 disabled:text-error-700/50 disabled:border-error-600/50",
53
+ ].join(" "),
54
+ warning: [
55
+ "bg-warning-500/20 text-warning-800 dark:text-warning-200 border-warning-500",
56
+ "hover:bg-warning-500/30 hover:border-warning-600",
57
+ "active:bg-warning-500/40 active:border-warning-700",
58
+ "disabled:bg-warning-500/10 disabled:text-warning-800/50 disabled:border-warning-500/50",
59
+ ].join(" "),
60
+ },
61
+ size: {
62
+ xs: "py-1 px-4 text-sm gap-1",
63
+ sm: "py-1.5 px-5 text-base gap-1.5",
64
+ md: "py-2 px-6 text-base gap-2",
65
+ lg: "py-2.5 px-8 text-lg gap-2",
66
+ },
67
+ },
68
+ defaultVariants: {
69
+ variant: "primary",
70
+ size: "md",
71
+ },
72
+ },
73
+ );
74
+
75
+ const iconSize = {
76
+ xs: "size-3.5",
77
+ sm: "size-4",
78
+ md: "size-4",
79
+ lg: "size-5",
80
+ } as const;
81
+
82
+ type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> &
83
+ VariantProps<typeof buttonVariants> & {
84
+ iconLeft?: ReactNode;
85
+ iconRight?: ReactNode;
86
+ loading?: boolean;
87
+ asChild?: boolean;
88
+ };
89
+
90
+ const Button = forwardRef<HTMLButtonElement, ButtonProps>(
91
+ (
92
+ {
93
+ variant,
94
+ size,
95
+ iconLeft,
96
+ iconRight,
97
+ loading = false,
98
+ disabled = false,
99
+ asChild = false,
100
+ className,
101
+ children,
102
+ ...rest
103
+ },
104
+ ref,
105
+ ) => {
106
+ const sizeKey = size ?? "md";
107
+ const classes = cn(
108
+ buttonVariants({ variant, size }),
109
+ loading && "pointer-events-none",
110
+ className,
111
+ );
112
+
113
+ const iconClass = cn("shrink-0", iconSize[sizeKey], "[&>svg]:size-full");
114
+
115
+ const content = (
116
+ <>
117
+ {loading ? (
118
+ <Spinner className={cn("shrink-0", iconSize[sizeKey])} />
119
+ ) : iconLeft ? (
120
+ <span className={iconClass}>{iconLeft}</span>
121
+ ) : null}
122
+ <span>{children}</span>
123
+ {!loading && iconRight ? (
124
+ <span className={iconClass}>{iconRight}</span>
125
+ ) : null}
126
+ </>
127
+ );
128
+
129
+ if (asChild && isValidElement(children)) {
130
+ return cloneElement(children as ReactElement<Record<string, unknown>>, {
131
+ className: classes,
132
+ ref,
133
+ ...rest,
134
+ });
135
+ }
136
+
137
+ return (
138
+ <button
139
+ ref={ref}
140
+ className={classes}
141
+ disabled={disabled}
142
+ aria-busy={loading || undefined}
143
+ {...rest}
144
+ >
145
+ {content}
146
+ </button>
147
+ );
148
+ },
149
+ );
150
+
151
+ Button.displayName = "Button";
152
+
153
+ export { Button, buttonVariants, type ButtonProps };
@@ -0,0 +1,48 @@
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 cardVariants = cva("rounded-md", {
6
+ variants: {
7
+ variant: {
8
+ default: "bg-surface text-surface-foreground",
9
+ noise: "fx-grain-lg text-surface-foreground",
10
+ ghost: "bg-transparent",
11
+ },
12
+ padding: {
13
+ sm: "p-4",
14
+ md: "p-6",
15
+ lg: "p-8",
16
+ },
17
+ },
18
+ defaultVariants: {
19
+ variant: "default",
20
+ padding: "md",
21
+ },
22
+ });
23
+
24
+ type CardProps = HTMLAttributes<HTMLDivElement> &
25
+ VariantProps<typeof cardVariants> & {
26
+ title?: ReactNode;
27
+ };
28
+
29
+ const Card = forwardRef<HTMLDivElement, CardProps>(
30
+ ({ variant, padding, title, className, children, ...rest }, ref) => {
31
+ return (
32
+ <div
33
+ ref={ref}
34
+ className={cn(cardVariants({ variant, padding }), className)}
35
+ {...rest}
36
+ >
37
+ {title ? (
38
+ <h3 className="mb-4 text-lg font-heading font-bold">{title}</h3>
39
+ ) : null}
40
+ {children}
41
+ </div>
42
+ );
43
+ },
44
+ );
45
+
46
+ Card.displayName = "Card";
47
+
48
+ export { Card, cardVariants, type CardProps };
@@ -0,0 +1,82 @@
1
+ import { forwardRef, type ComponentPropsWithoutRef } from "react";
2
+ import { Checkbox as CheckboxPrimitive } from "radix-ui";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import { cn } from "@ac/lib/cn";
5
+
6
+ const checkboxVariants = cva(
7
+ [
8
+ "peer shrink-0 bg-surface",
9
+ "border-3 border-edge",
10
+ "hover:border-edge-hover",
11
+ "focus-visible:outline-none focus-visible:ring-3",
12
+ "focus-visible:ring-primary-500",
13
+ "disabled:opacity-50 disabled:pointer-events-none",
14
+ "data-[state=checked]:bg-primary data-[state=checked]:border-primary",
15
+ "data-[state=checked]:text-primary-foreground",
16
+ "transition-colors",
17
+ ].join(" "),
18
+ {
19
+ variants: {
20
+ size: {
21
+ xs: "size-4 rounded",
22
+ sm: "size-5 rounded-md",
23
+ md: "size-6 rounded-md",
24
+ },
25
+ },
26
+ defaultVariants: {
27
+ size: "md",
28
+ },
29
+ },
30
+ );
31
+
32
+ type CheckboxProps = Omit<
33
+ ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>,
34
+ "size"
35
+ > &
36
+ VariantProps<typeof checkboxVariants> & {
37
+ error?: boolean;
38
+ };
39
+
40
+ const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(
41
+ ({ size, error = false, className, ...rest }, ref) => {
42
+ return (
43
+ <CheckboxPrimitive.Root
44
+ ref={ref}
45
+ className={cn(
46
+ checkboxVariants({ size }),
47
+ error && "border-3 border-error-400 hover:border-error-500",
48
+ className,
49
+ )}
50
+ aria-invalid={error || undefined}
51
+ {...rest}
52
+ >
53
+ <CheckboxPrimitive.Indicator
54
+ forceMount
55
+ className={cn(
56
+ "flex size-full items-center justify-center text-current",
57
+ "[clip-path:circle(0%_at_0%_75%)]",
58
+ "data-[state=checked]:[clip-path:circle(100%_at_50%_50%)]",
59
+ "transition-[clip-path] duration-200 ease-in-out motion-reduce:transition-none",
60
+ )}
61
+ >
62
+ <svg
63
+ xmlns="http://www.w3.org/2000/svg"
64
+ viewBox="0 0 24 24"
65
+ fill="none"
66
+ stroke="currentColor"
67
+ strokeWidth={3}
68
+ strokeLinecap="round"
69
+ strokeLinejoin="round"
70
+ className="size-[90%]"
71
+ >
72
+ <polyline points="20 6 9 17 4 12" />
73
+ </svg>
74
+ </CheckboxPrimitive.Indicator>
75
+ </CheckboxPrimitive.Root>
76
+ );
77
+ },
78
+ );
79
+
80
+ Checkbox.displayName = "Checkbox";
81
+
82
+ export { Checkbox, checkboxVariants, type CheckboxProps };
@@ -0,0 +1,166 @@
1
+ import { forwardRef, useState } from "react";
2
+ import { Popover } from "radix-ui";
3
+ import { Command } from "cmdk";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+ import { CaretDown, Check } from "@phosphor-icons/react";
6
+ import { cn } from "@ac/lib/cn";
7
+
8
+ const triggerVariants = cva(
9
+ [
10
+ "inline-flex items-center justify-between",
11
+ "w-full font-sans text-foreground bg-primary-100 dark:bg-base-700",
12
+ "border-0 rounded-full",
13
+ "focus-visible:outline-none focus-visible:ring-3",
14
+ "focus-visible:ring-primary-500",
15
+ "disabled:opacity-50 disabled:pointer-events-none",
16
+ "ring-0 transition-colors",
17
+ ].join(" "),
18
+ {
19
+ variants: {
20
+ size: {
21
+ sm: "py-1.5 px-4 text-sm",
22
+ md: "py-2 px-5 text-base",
23
+ },
24
+ },
25
+ defaultVariants: {
26
+ size: "md",
27
+ },
28
+ },
29
+ );
30
+
31
+ type ComboboxOption = {
32
+ value: string;
33
+ label: string;
34
+ disabled?: boolean;
35
+ };
36
+
37
+ type ComboboxProps = VariantProps<typeof triggerVariants> & {
38
+ options: ComboboxOption[];
39
+ value?: string;
40
+ onValueChange?: (value: string) => void;
41
+ placeholder?: string;
42
+ searchPlaceholder?: string;
43
+ emptyMessage?: string;
44
+ size?: "sm" | "md";
45
+ error?: boolean;
46
+ disabled?: boolean;
47
+ className?: string;
48
+ id?: string;
49
+ "aria-describedby"?: string;
50
+ "aria-invalid"?: boolean;
51
+ };
52
+
53
+ const Combobox = forwardRef<HTMLButtonElement, ComboboxProps>(
54
+ (
55
+ {
56
+ options,
57
+ value,
58
+ onValueChange,
59
+ placeholder = "Select...",
60
+ searchPlaceholder = "Search...",
61
+ emptyMessage = "No results found.",
62
+ size,
63
+ error = false,
64
+ disabled = false,
65
+ className,
66
+ id,
67
+ "aria-describedby": ariaDescribedBy,
68
+ ...rest
69
+ },
70
+ ref,
71
+ ) => {
72
+ const [open, setOpen] = useState(false);
73
+ const selectedLabel = options.find((o) => o.value === value)?.label;
74
+
75
+ return (
76
+ <Popover.Root open={open} onOpenChange={setOpen}>
77
+ <Popover.Trigger
78
+ ref={ref}
79
+ id={id}
80
+ disabled={disabled}
81
+ aria-describedby={ariaDescribedBy}
82
+ aria-invalid={error || undefined}
83
+ className={cn(
84
+ triggerVariants({ size }),
85
+ error && "border-3 border-error-400 hover:border-error-500",
86
+ !selectedLabel && "text-muted-foreground",
87
+ className,
88
+ )}
89
+ {...rest}
90
+ >
91
+ <span className="truncate">
92
+ {selectedLabel ?? placeholder}
93
+ </span>
94
+ <CaretDown
95
+ weight="bold"
96
+ className={cn(
97
+ "ml-2 size-4 shrink-0 text-muted-foreground",
98
+ "transition-transform motion-reduce:transition-none",
99
+ open && "rotate-180",
100
+ )}
101
+ aria-hidden="true"
102
+ />
103
+ </Popover.Trigger>
104
+ <Popover.Portal>
105
+ <Popover.Content
106
+ className={cn(
107
+ "z-50 w-[var(--radix-popover-trigger-width)]",
108
+ "overflow-hidden rounded-2xl",
109
+ "bg-surface border border-edge shadow-brand",
110
+ )}
111
+ sideOffset={4}
112
+ align="start"
113
+ >
114
+ <Command>
115
+ <Command.Input
116
+ placeholder={searchPlaceholder}
117
+ className={cn(
118
+ "w-full border-b border-edge bg-transparent px-4 py-2.5",
119
+ "text-sm text-foreground placeholder:text-muted-foreground",
120
+ "outline-none",
121
+ )}
122
+ />
123
+ <Command.List className="max-h-60 overflow-y-auto p-1">
124
+ <Command.Empty className="px-4 py-6 text-center text-sm text-muted-foreground">
125
+ {emptyMessage}
126
+ </Command.Empty>
127
+ {options.map((option) => (
128
+ <Command.Item
129
+ key={option.value}
130
+ value={option.label}
131
+ {...(option.disabled ? { disabled: true } : {})}
132
+ onSelect={() => {
133
+ onValueChange?.(option.value);
134
+ setOpen(false);
135
+ }}
136
+ className={cn(
137
+ "relative flex items-center rounded-xl px-4 py-2",
138
+ "text-sm text-foreground cursor-pointer select-none",
139
+ "outline-none",
140
+ "data-[selected=true]:bg-muted",
141
+ "data-[disabled=true]:opacity-50",
142
+ "data-[disabled=true]:pointer-events-none",
143
+ )}
144
+ >
145
+ <span className="flex-1">{option.label}</span>
146
+ {value === option.value && (
147
+ <Check
148
+ weight="bold"
149
+ className="ml-auto size-4"
150
+ aria-hidden="true"
151
+ />
152
+ )}
153
+ </Command.Item>
154
+ ))}
155
+ </Command.List>
156
+ </Command>
157
+ </Popover.Content>
158
+ </Popover.Portal>
159
+ </Popover.Root>
160
+ );
161
+ },
162
+ );
163
+
164
+ Combobox.displayName = "Combobox";
165
+
166
+ export { Combobox, triggerVariants, type ComboboxProps, type ComboboxOption };
@@ -0,0 +1,194 @@
1
+ "use client";
2
+
3
+ import {
4
+ forwardRef,
5
+ useCallback,
6
+ useEffect,
7
+ useRef,
8
+ useState,
9
+ type HTMLAttributes,
10
+ } from "react";
11
+ import { cva, type VariantProps } from "class-variance-authority";
12
+ import { ArrowUpRight, Copy } from "@phosphor-icons/react";
13
+ import { cn } from "@ac/lib/cn";
14
+
15
+ const copyableTextVariants = cva(
16
+ "inline-flex items-center font-mono select-none",
17
+ {
18
+ variants: {
19
+ size: {
20
+ sm: "text-xs gap-1",
21
+ md: "text-sm gap-1.5",
22
+ },
23
+ },
24
+ defaultVariants: {
25
+ size: "md",
26
+ },
27
+ },
28
+ );
29
+
30
+ const iconSize: Record<"sm" | "md", string> = {
31
+ sm: "size-3.5",
32
+ md: "size-4",
33
+ };
34
+
35
+ const buttonSize: Record<"sm" | "md", string> = {
36
+ sm: "size-4",
37
+ md: "size-5",
38
+ };
39
+
40
+ function isExternalUrl(url: string): boolean {
41
+ return /^https?:\/\//.test(url) || url.startsWith("//");
42
+ }
43
+
44
+ function truncateMiddle(
45
+ text: string,
46
+ startChars: number,
47
+ endChars: number,
48
+ ): string {
49
+ if (text.length <= startChars + endChars) return text;
50
+ return `${text.slice(0, startChars)}...${text.slice(-endChars)}`;
51
+ }
52
+
53
+ type CopyableTextProps = Omit<HTMLAttributes<HTMLSpanElement>, "children"> &
54
+ VariantProps<typeof copyableTextVariants> & {
55
+ text: string;
56
+ startChars?: number;
57
+ endChars?: number;
58
+ href?: string;
59
+ };
60
+
61
+ const CopyableText = forwardRef<HTMLSpanElement, CopyableTextProps>(
62
+ (
63
+ {
64
+ text,
65
+ startChars = 6,
66
+ endChars = 4,
67
+ href,
68
+ size = "md",
69
+ className,
70
+ ...rest
71
+ },
72
+ ref,
73
+ ) => {
74
+ const [copied, setCopied] = useState(false);
75
+ const timerRef = useRef<ReturnType<typeof setTimeout>>(null);
76
+
77
+ useEffect(() => {
78
+ return () => {
79
+ if (timerRef.current) clearTimeout(timerRef.current);
80
+ };
81
+ }, []);
82
+
83
+ const handleCopy = useCallback((e: React.MouseEvent) => {
84
+ e.stopPropagation();
85
+ void navigator.clipboard.writeText(text).then(() => {
86
+ setCopied(true);
87
+ if (timerRef.current) clearTimeout(timerRef.current);
88
+ timerRef.current = setTimeout(() => setCopied(false), 1500);
89
+ });
90
+ }, [text]);
91
+
92
+ const resolvedSize = size ?? "md";
93
+ const iconCn = iconSize[resolvedSize];
94
+ const btnCn = buttonSize[resolvedSize];
95
+
96
+ return (
97
+ <span
98
+ ref={ref}
99
+ className={cn(
100
+ copyableTextVariants({ size }),
101
+ href && "text-primary-500 dark:text-primary-300",
102
+ className,
103
+ )}
104
+ {...rest}
105
+ >
106
+ {href ? (
107
+ <a
108
+ href={href}
109
+ {...(isExternalUrl(href)
110
+ ? { target: "_blank", rel: "noopener noreferrer" }
111
+ : {})}
112
+ onClick={(e) => e.stopPropagation()}
113
+ className="cursor-pointer hover:underline"
114
+ >
115
+ {truncateMiddle(text, startChars, endChars)}
116
+ </a>
117
+ ) : (
118
+ <span className="cursor-default">
119
+ {truncateMiddle(text, startChars, endChars)}
120
+ </span>
121
+ )}
122
+
123
+ <button
124
+ type="button"
125
+ onClick={handleCopy}
126
+ className={cn(
127
+ "relative inline-flex items-center justify-center",
128
+ "rounded-md cursor-pointer",
129
+ "hover:bg-foreground/10 transition-colors",
130
+ btnCn,
131
+ )}
132
+ aria-label={copied ? "Copied" : "Copy to clipboard"}
133
+ >
134
+ <Copy
135
+ weight="bold"
136
+ className={cn(
137
+ iconCn,
138
+ "text-muted-foreground",
139
+ "transition-opacity duration-100",
140
+ "motion-reduce:transition-none",
141
+ copied && "opacity-0",
142
+ )}
143
+ aria-hidden="true"
144
+ />
145
+ <svg
146
+ viewBox="0 0 24 24"
147
+ fill="none"
148
+ stroke="currentColor"
149
+ strokeWidth={3}
150
+ strokeLinecap="round"
151
+ strokeLinejoin="round"
152
+ className={cn(
153
+ iconCn,
154
+ "text-muted-foreground absolute",
155
+ "[stroke-dasharray:20] [stroke-dashoffset:20]",
156
+ "transition-[stroke-dashoffset] duration-300 delay-75 ease-out",
157
+ "motion-reduce:transition-none",
158
+ copied && "[stroke-dashoffset:0]",
159
+ )}
160
+ aria-hidden="true"
161
+ >
162
+ <polyline points="4 12 9 17 20 6" />
163
+ </svg>
164
+ </button>
165
+
166
+ {href && isExternalUrl(href) ? (
167
+ <a
168
+ href={href}
169
+ target="_blank"
170
+ rel="noopener noreferrer"
171
+ onClick={(e) => e.stopPropagation()}
172
+ className={cn(
173
+ "inline-flex items-center justify-center rounded-md",
174
+ "text-muted-foreground hover:text-foreground",
175
+ "hover:bg-foreground/10 transition-colors",
176
+ btnCn,
177
+ )}
178
+ aria-label="Open in new tab"
179
+ >
180
+ <ArrowUpRight
181
+ weight="bold"
182
+ className={iconCn}
183
+ aria-hidden="true"
184
+ />
185
+ </a>
186
+ ) : null}
187
+ </span>
188
+ );
189
+ },
190
+ );
191
+
192
+ CopyableText.displayName = "CopyableText";
193
+
194
+ export { CopyableText, copyableTextVariants, type CopyableTextProps };