@arcote.tech/arc-ds 0.4.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/package.json +42 -0
- package/src/ds/avatar/avatar.tsx +86 -0
- package/src/ds/badge/badge.tsx +61 -0
- package/src/ds/bento-card/bento-card.tsx +70 -0
- package/src/ds/bento-grid/bento-grid.tsx +52 -0
- package/src/ds/box/box.tsx +96 -0
- package/src/ds/button/button.tsx +191 -0
- package/src/ds/card-modal/card-modal.tsx +161 -0
- package/src/ds/display-mode.tsx +32 -0
- package/src/ds/ds-provider.tsx +85 -0
- package/src/ds/form/field.tsx +124 -0
- package/src/ds/form/fields/checkbox-select-field.tsx +326 -0
- package/src/ds/form/fields/index.ts +14 -0
- package/src/ds/form/fields/search-select-field.tsx +41 -0
- package/src/ds/form/fields/select-field.tsx +42 -0
- package/src/ds/form/fields/suggestion-list-field.tsx +43 -0
- package/src/ds/form/fields/tag-field.tsx +39 -0
- package/src/ds/form/fields/text-field.tsx +35 -0
- package/src/ds/form/fields/textarea-field.tsx +81 -0
- package/src/ds/form/form-part.tsx +79 -0
- package/src/ds/form/form.tsx +299 -0
- package/src/ds/form/index.tsx +5 -0
- package/src/ds/form/message.tsx +14 -0
- package/src/ds/input/input.tsx +115 -0
- package/src/ds/merge-variants.ts +26 -0
- package/src/ds/search-select/search-select.tsx +291 -0
- package/src/ds/separator/separator.tsx +26 -0
- package/src/ds/suggestion-list/suggestion-list.tsx +406 -0
- package/src/ds/tag-list/tag-list.tsx +87 -0
- package/src/ds/tooltip/tooltip.tsx +33 -0
- package/src/ds/transitions.ts +12 -0
- package/src/ds/types.ts +131 -0
- package/src/index.ts +115 -0
- package/src/layout/drag-handle.tsx +117 -0
- package/src/layout/dynamic-slot.tsx +95 -0
- package/src/layout/expandable-panel.tsx +57 -0
- package/src/layout/layout.tsx +323 -0
- package/src/layout/overlay-provider.tsx +103 -0
- package/src/layout/overlay.tsx +33 -0
- package/src/layout/router.tsx +101 -0
- package/src/layout/scroll-nav.tsx +121 -0
- package/src/layout/slot-render-context.tsx +14 -0
- package/src/layout/sub-nav-shell.tsx +41 -0
- package/src/layout/toolbar-expand.tsx +70 -0
- package/src/layout/transitions.ts +12 -0
- package/src/layout/use-expandable.ts +59 -0
- package/src/lib/utils.ts +6 -0
- package/src/ui/tooltip.tsx +59 -0
- package/tsconfig.json +13 -0
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@arcote.tech/arc-ds",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.4.1",
|
|
5
|
+
"private": false,
|
|
6
|
+
"author": "Przemysław Krasiński [arcote.tech]",
|
|
7
|
+
"description": "Design System for Arc framework — CVA-based components with display modes and variant overrides",
|
|
8
|
+
"main": "./src/index.ts",
|
|
9
|
+
"types": "./src/index.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./src/index.ts",
|
|
13
|
+
"import": "./src/index.ts",
|
|
14
|
+
"default": "./src/index.ts"
|
|
15
|
+
},
|
|
16
|
+
"./ui/*": {
|
|
17
|
+
"types": "./src/ui/*.tsx",
|
|
18
|
+
"import": "./src/ui/*.tsx",
|
|
19
|
+
"default": "./src/ui/*.tsx"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"type-check": "tsc --noEmit"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"class-variance-authority": "^0.7.1",
|
|
27
|
+
"clsx": "^2.1.1",
|
|
28
|
+
"tailwind-merge": "^3.5.0"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@arcote.tech/arc": "workspace:*",
|
|
32
|
+
"framer-motion": "^12.0.0",
|
|
33
|
+
"lucide-react": ">=0.400.0",
|
|
34
|
+
"radix-ui": "^1.0.0",
|
|
35
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
36
|
+
"tailwindcss": "^4.0.0",
|
|
37
|
+
"typescript": "^5.0.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"typescript": "~5.9.3"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { cva } from "class-variance-authority";
|
|
2
|
+
import { Avatar as AvatarPrimitive } from "radix-ui";
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
import type { AvatarProps } from "../types";
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// CVA
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
export const avatarVariants = cva(
|
|
11
|
+
"relative flex shrink-0 overflow-hidden rounded-full select-none",
|
|
12
|
+
{
|
|
13
|
+
variants: {
|
|
14
|
+
size: {
|
|
15
|
+
default: "size-9 md:size-8",
|
|
16
|
+
sm: "size-7 md:size-6",
|
|
17
|
+
lg: "size-11 md:size-10",
|
|
18
|
+
xs: "size-6 md:size-5",
|
|
19
|
+
},
|
|
20
|
+
display: {
|
|
21
|
+
default: "",
|
|
22
|
+
compact: "",
|
|
23
|
+
minimal: "",
|
|
24
|
+
expanded: "",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
defaultVariants: {
|
|
28
|
+
size: "default",
|
|
29
|
+
display: "default",
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
export const avatarFallbackVariants = cva(
|
|
35
|
+
"flex size-full items-center justify-center rounded-full bg-muted text-muted-foreground",
|
|
36
|
+
{
|
|
37
|
+
variants: {
|
|
38
|
+
size: {
|
|
39
|
+
default: "text-sm",
|
|
40
|
+
sm: "text-xs",
|
|
41
|
+
lg: "text-base",
|
|
42
|
+
xs: "text-[10px]",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
defaultVariants: {
|
|
46
|
+
size: "default",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Avatar
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
export function Avatar({
|
|
56
|
+
src,
|
|
57
|
+
fallback,
|
|
58
|
+
size = "default",
|
|
59
|
+
display = "default",
|
|
60
|
+
className,
|
|
61
|
+
...props
|
|
62
|
+
}: AvatarProps) {
|
|
63
|
+
return (
|
|
64
|
+
<AvatarPrimitive.Root
|
|
65
|
+
data-slot="avatar"
|
|
66
|
+
className={cn(avatarVariants({ size, display }), className)}
|
|
67
|
+
{...props}
|
|
68
|
+
>
|
|
69
|
+
{src && (
|
|
70
|
+
<AvatarPrimitive.Image
|
|
71
|
+
data-slot="avatar-image"
|
|
72
|
+
src={src}
|
|
73
|
+
className="aspect-square size-full"
|
|
74
|
+
/>
|
|
75
|
+
)}
|
|
76
|
+
{fallback && (
|
|
77
|
+
<AvatarPrimitive.Fallback
|
|
78
|
+
data-slot="avatar-fallback"
|
|
79
|
+
className={avatarFallbackVariants({ size })}
|
|
80
|
+
>
|
|
81
|
+
{fallback}
|
|
82
|
+
</AvatarPrimitive.Fallback>
|
|
83
|
+
)}
|
|
84
|
+
</AvatarPrimitive.Root>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { cva } from "class-variance-authority";
|
|
2
|
+
import { Slot } from "radix-ui";
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
import type { BadgeProps } from "../types";
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// CVA
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
export const badgeVariants = cva(
|
|
11
|
+
"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [&>svg]:pointer-events-none [&>svg]:size-3",
|
|
12
|
+
{
|
|
13
|
+
variants: {
|
|
14
|
+
variant: {
|
|
15
|
+
default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
|
16
|
+
secondary:
|
|
17
|
+
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
|
18
|
+
destructive: "bg-destructive text-white [a&]:hover:bg-destructive/90",
|
|
19
|
+
outline:
|
|
20
|
+
"border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
|
21
|
+
ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
|
22
|
+
},
|
|
23
|
+
display: {
|
|
24
|
+
default: "",
|
|
25
|
+
compact: "px-1.5 py-0 text-[10px]",
|
|
26
|
+
minimal: "size-2 rounded-full p-0 [&>*]:hidden",
|
|
27
|
+
expanded: "",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultVariants: {
|
|
31
|
+
variant: "default",
|
|
32
|
+
display: "default",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Badge
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
export function Badge({
|
|
42
|
+
className,
|
|
43
|
+
variant = "default",
|
|
44
|
+
display = "default",
|
|
45
|
+
asChild = false,
|
|
46
|
+
children,
|
|
47
|
+
...props
|
|
48
|
+
}: BadgeProps) {
|
|
49
|
+
const Comp = asChild ? Slot.Root : "span";
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Comp
|
|
53
|
+
data-slot="badge"
|
|
54
|
+
data-variant={variant}
|
|
55
|
+
className={cn(badgeVariants({ variant, display }), className)}
|
|
56
|
+
{...props}
|
|
57
|
+
>
|
|
58
|
+
{children}
|
|
59
|
+
</Comp>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Box } from "../box/box";
|
|
2
|
+
import { useBentoSpan } from "../bento-grid/bento-grid";
|
|
3
|
+
import { motion } from "framer-motion";
|
|
4
|
+
import { Plus } from "lucide-react";
|
|
5
|
+
import type { ComponentType, ReactNode } from "react";
|
|
6
|
+
|
|
7
|
+
export interface BentoCardProps {
|
|
8
|
+
layoutId: string;
|
|
9
|
+
title: ReactNode;
|
|
10
|
+
icon: ComponentType<{ className?: string }>;
|
|
11
|
+
isEmpty: boolean;
|
|
12
|
+
isModalOpen?: boolean;
|
|
13
|
+
onEdit: () => void;
|
|
14
|
+
emptyLabel?: ReactNode;
|
|
15
|
+
className?: string;
|
|
16
|
+
span?: string;
|
|
17
|
+
children?: ReactNode;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function BentoCard({
|
|
21
|
+
layoutId,
|
|
22
|
+
title,
|
|
23
|
+
icon: Icon,
|
|
24
|
+
isEmpty,
|
|
25
|
+
isModalOpen,
|
|
26
|
+
onEdit,
|
|
27
|
+
emptyLabel,
|
|
28
|
+
className,
|
|
29
|
+
span,
|
|
30
|
+
children,
|
|
31
|
+
}: BentoCardProps) {
|
|
32
|
+
const autoSpan = useBentoSpan();
|
|
33
|
+
const resolvedSpan = span ?? autoSpan;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className={`h-full ${resolvedSpan} ${className ?? ""}`.trim()}>
|
|
37
|
+
<motion.div
|
|
38
|
+
layoutId={layoutId}
|
|
39
|
+
className="h-full"
|
|
40
|
+
style={{ opacity: isModalOpen ? 0 : 1 }}
|
|
41
|
+
>
|
|
42
|
+
<Box
|
|
43
|
+
layout={false}
|
|
44
|
+
className="group relative cursor-pointer p-5 hover:shadow-lg transition-all duration-300 h-full min-h-[140px] hover:scale-[1.01]"
|
|
45
|
+
onClick={onEdit}
|
|
46
|
+
>
|
|
47
|
+
<div className="flex items-center gap-2.5 mb-3">
|
|
48
|
+
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary/10">
|
|
49
|
+
<Icon className="h-4 w-4 text-primary" />
|
|
50
|
+
</div>
|
|
51
|
+
<span className="text-sm font-semibold">{title}</span>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
{isEmpty ? (
|
|
55
|
+
<div className="flex flex-col items-center justify-center gap-2 py-4 border-2 border-dashed border-muted-foreground/15 rounded-xl transition-colors group-hover:border-primary/30">
|
|
56
|
+
<div className="flex h-9 w-9 items-center justify-center rounded-full bg-muted transition-colors group-hover:bg-primary/10">
|
|
57
|
+
<Plus className="h-4 w-4 text-muted-foreground transition-colors group-hover:text-primary" />
|
|
58
|
+
</div>
|
|
59
|
+
{emptyLabel && (
|
|
60
|
+
<span className="text-xs text-muted-foreground">{emptyLabel}</span>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
) : (
|
|
64
|
+
<div className="flex flex-col gap-2">{children}</div>
|
|
65
|
+
)}
|
|
66
|
+
</Box>
|
|
67
|
+
</motion.div>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Children, createContext, useContext, type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export interface BentoGridProps {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
className?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type BentoGridItemContext = {
|
|
9
|
+
index: number;
|
|
10
|
+
count: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const BentoItemContext = createContext<BentoGridItemContext | null>(null);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Hook for children to read their grid span class.
|
|
17
|
+
* Returns the appropriate Tailwind class (e.g., "md:row-span-2", "md:col-span-3")
|
|
18
|
+
* or empty string for normal cells.
|
|
19
|
+
*/
|
|
20
|
+
export function useBentoSpan(): string {
|
|
21
|
+
const ctx = useContext(BentoItemContext);
|
|
22
|
+
if (!ctx) return "";
|
|
23
|
+
const { index, count } = ctx;
|
|
24
|
+
|
|
25
|
+
if (count < 5) return "";
|
|
26
|
+
if (index === 0) return "md:row-span-2";
|
|
27
|
+
if (index === count - 1 && (count === 6 || count === 8)) return "md:col-span-3";
|
|
28
|
+
return "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Responsive bento grid with auto-layout based on child count.
|
|
33
|
+
*
|
|
34
|
+
* Children call useBentoSpan() to get their grid span class.
|
|
35
|
+
* No wrapper divs — children control their own layout.
|
|
36
|
+
*/
|
|
37
|
+
export function BentoGrid({ children, className }: BentoGridProps) {
|
|
38
|
+
const items = Children.toArray(children);
|
|
39
|
+
const count = items.length;
|
|
40
|
+
|
|
41
|
+
if (count === 0) return null;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className={`grid grid-cols-1 md:grid-cols-3 gap-3 ${className ?? ""}`}>
|
|
45
|
+
{items.map((child, index) => (
|
|
46
|
+
<BentoItemContext.Provider key={index} value={{ index, count }}>
|
|
47
|
+
{child}
|
|
48
|
+
</BentoItemContext.Provider>
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { cva } from "class-variance-authority";
|
|
2
|
+
import { type HTMLMotionProps, motion } from "framer-motion";
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import { cn } from "../../lib/utils";
|
|
5
|
+
import { useDisplayMode } from "../display-mode";
|
|
6
|
+
import { useDsVariantOverrides } from "../ds-provider";
|
|
7
|
+
import { dsTransitions } from "../transitions";
|
|
8
|
+
import type { DisplayMode } from "../types";
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// CVA — Box
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
export const boxVariants = cva(
|
|
15
|
+
"rounded-2xl border bg-card text-card-foreground shadow-sm transition-all duration-300",
|
|
16
|
+
{
|
|
17
|
+
variants: {
|
|
18
|
+
variant: {
|
|
19
|
+
default: "",
|
|
20
|
+
ghost:
|
|
21
|
+
"border-transparent bg-transparent shadow-none hover:shadow-none",
|
|
22
|
+
},
|
|
23
|
+
display: {
|
|
24
|
+
default: "",
|
|
25
|
+
compact: "rounded-xl p-2",
|
|
26
|
+
minimal: "rounded-lg p-1",
|
|
27
|
+
expanded: "p-4",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultVariants: {
|
|
31
|
+
variant: "default",
|
|
32
|
+
display: "default",
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Box
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
export interface BoxProps extends HTMLMotionProps<"div"> {
|
|
42
|
+
variant?: "default" | "ghost";
|
|
43
|
+
displayMode?: DisplayMode;
|
|
44
|
+
/** Layout animation ID — dwa Boxy z tym samym layoutId animują przejście. */
|
|
45
|
+
layoutId?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Box — bazowy kontener DS z layout animations.
|
|
50
|
+
*
|
|
51
|
+
* Domyślny wariant: neutralny card (border, bg-card, shadow-sm).
|
|
52
|
+
* `ghost`: przezroczysty, bez border/shadow.
|
|
53
|
+
*
|
|
54
|
+
* Wygląd można nadpisać per-app przez DesignSystemProvider variants:
|
|
55
|
+
* `{ Box: { className: { default: "glass" } } }` — dodaje klasę CSS.
|
|
56
|
+
*/
|
|
57
|
+
export const Box = forwardRef<HTMLDivElement, BoxProps>(
|
|
58
|
+
(
|
|
59
|
+
{
|
|
60
|
+
variant = "default",
|
|
61
|
+
displayMode: displayModeProp,
|
|
62
|
+
layoutId,
|
|
63
|
+
layout = "position",
|
|
64
|
+
className,
|
|
65
|
+
children,
|
|
66
|
+
...rest
|
|
67
|
+
},
|
|
68
|
+
ref,
|
|
69
|
+
) => {
|
|
70
|
+
const contextMode = useDisplayMode();
|
|
71
|
+
const mode: DisplayMode = displayModeProp ?? contextMode;
|
|
72
|
+
|
|
73
|
+
// Extra classes injected by DesignSystemProvider (e.g. NDT adds "glass")
|
|
74
|
+
const overrides = useDsVariantOverrides("Box");
|
|
75
|
+
const extraClass = overrides?.className?.default;
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<motion.div
|
|
79
|
+
ref={ref}
|
|
80
|
+
layout={layout}
|
|
81
|
+
layoutId={layoutId}
|
|
82
|
+
className={cn(
|
|
83
|
+
boxVariants({ variant, display: mode }),
|
|
84
|
+
extraClass,
|
|
85
|
+
className,
|
|
86
|
+
)}
|
|
87
|
+
transition={{ layout: dsTransitions.smooth }}
|
|
88
|
+
{...rest}
|
|
89
|
+
>
|
|
90
|
+
{children}
|
|
91
|
+
</motion.div>
|
|
92
|
+
);
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
Box.displayName = "Box";
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { cva } from "class-variance-authority";
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
import { useDisplayMode } from "../display-mode";
|
|
5
|
+
import { Tooltip } from "../tooltip/tooltip";
|
|
6
|
+
import type { ButtonProps, DisplayMode } from "../types";
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// CVA — root button
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
export const buttonVariants = cva(
|
|
13
|
+
"inline-flex shrink-0 items-center justify-center rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
14
|
+
{
|
|
15
|
+
variants: {
|
|
16
|
+
variant: {
|
|
17
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
18
|
+
secondary:
|
|
19
|
+
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
|
20
|
+
ghost:
|
|
21
|
+
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
|
22
|
+
outline:
|
|
23
|
+
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
|
|
24
|
+
destructive: "bg-destructive text-white hover:bg-destructive/90",
|
|
25
|
+
link: "text-primary underline-offset-4 hover:underline",
|
|
26
|
+
},
|
|
27
|
+
size: {
|
|
28
|
+
default:
|
|
29
|
+
"h-12 gap-2 px-5 py-2.5 text-base md:h-9 md:px-4 md:py-2 md:text-sm",
|
|
30
|
+
sm: "h-11 gap-2 rounded-md px-4 text-base md:h-8 md:gap-1.5 md:px-3 md:text-sm",
|
|
31
|
+
xs: "h-9 gap-1.5 rounded-md px-3 text-sm md:h-6 md:gap-1 md:px-2 md:text-xs",
|
|
32
|
+
lg: "h-14 gap-2.5 rounded-md px-8 text-lg md:h-10 md:px-6 md:text-sm",
|
|
33
|
+
icon: "size-12 md:size-9",
|
|
34
|
+
"icon-sm": "size-11 md:size-8",
|
|
35
|
+
"icon-xs": "size-9 rounded-md md:size-6",
|
|
36
|
+
},
|
|
37
|
+
display: {
|
|
38
|
+
default: "",
|
|
39
|
+
compact:
|
|
40
|
+
"flex-col h-auto min-h-[44px] min-w-[44px] gap-0.5 rounded-lg px-3 py-2 text-xs bg-transparent text-muted-foreground hover:text-foreground data-[active]:text-primary",
|
|
41
|
+
minimal: "",
|
|
42
|
+
expanded: "w-full justify-start gap-2",
|
|
43
|
+
},
|
|
44
|
+
active: {
|
|
45
|
+
true: "data-[active]:font-semibold",
|
|
46
|
+
false: "",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
compoundVariants: [
|
|
50
|
+
{
|
|
51
|
+
display: "compact",
|
|
52
|
+
active: true,
|
|
53
|
+
className: "text-primary bg-transparent",
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
defaultVariants: {
|
|
57
|
+
variant: "ghost",
|
|
58
|
+
size: "sm",
|
|
59
|
+
display: "default",
|
|
60
|
+
active: false,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Sub-CVA — icon
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
export const buttonIconVariants = cva("shrink-0", {
|
|
70
|
+
variants: {
|
|
71
|
+
display: {
|
|
72
|
+
default: "size-4.5 md:size-4",
|
|
73
|
+
compact: "size-5",
|
|
74
|
+
minimal: "size-4.5 md:size-4",
|
|
75
|
+
expanded: "size-4.5 md:size-4",
|
|
76
|
+
},
|
|
77
|
+
active: {
|
|
78
|
+
true: "",
|
|
79
|
+
false: "",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
compoundVariants: [
|
|
83
|
+
{
|
|
84
|
+
display: "compact",
|
|
85
|
+
active: true,
|
|
86
|
+
className: "stroke-[2.5]",
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
defaultVariants: {
|
|
90
|
+
display: "default",
|
|
91
|
+
active: false,
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Sub-CVA — label
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
export const buttonLabelVariants = cva("", {
|
|
100
|
+
variants: {
|
|
101
|
+
display: {
|
|
102
|
+
default: "",
|
|
103
|
+
compact: "",
|
|
104
|
+
minimal: "sr-only",
|
|
105
|
+
expanded: "flex-1 text-left",
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
defaultVariants: {
|
|
109
|
+
display: "default",
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Button
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
118
|
+
(
|
|
119
|
+
{
|
|
120
|
+
icon: Icon,
|
|
121
|
+
label,
|
|
122
|
+
right,
|
|
123
|
+
tooltip,
|
|
124
|
+
isActive,
|
|
125
|
+
displayMode: displayModeProp,
|
|
126
|
+
variant: variantProp,
|
|
127
|
+
size: sizeProp,
|
|
128
|
+
className,
|
|
129
|
+
onClick,
|
|
130
|
+
disabled,
|
|
131
|
+
...rest
|
|
132
|
+
},
|
|
133
|
+
ref,
|
|
134
|
+
) => {
|
|
135
|
+
const contextMode = useDisplayMode();
|
|
136
|
+
const mode: DisplayMode = displayModeProp ?? contextMode;
|
|
137
|
+
|
|
138
|
+
const resolvedVariant = isActive
|
|
139
|
+
? ("secondary" as const)
|
|
140
|
+
: (variantProp ?? "ghost");
|
|
141
|
+
|
|
142
|
+
const resolvedSize = sizeProp ?? (mode === "minimal" ? "icon-sm" : "sm");
|
|
143
|
+
|
|
144
|
+
const tooltipSide =
|
|
145
|
+
mode === "compact" ? "top" : mode === "expanded" ? "right" : "bottom";
|
|
146
|
+
|
|
147
|
+
const resolvedTooltip = mode === "minimal" ? (tooltip ?? label) : tooltip;
|
|
148
|
+
|
|
149
|
+
const content = (
|
|
150
|
+
<button
|
|
151
|
+
ref={ref}
|
|
152
|
+
data-active={isActive || undefined}
|
|
153
|
+
className={cn(
|
|
154
|
+
buttonVariants({
|
|
155
|
+
variant: resolvedVariant,
|
|
156
|
+
size: resolvedSize,
|
|
157
|
+
display: mode,
|
|
158
|
+
active: !!isActive,
|
|
159
|
+
}),
|
|
160
|
+
className,
|
|
161
|
+
)}
|
|
162
|
+
onClick={onClick}
|
|
163
|
+
disabled={disabled}
|
|
164
|
+
{...rest}
|
|
165
|
+
>
|
|
166
|
+
{Icon && (
|
|
167
|
+
<Icon
|
|
168
|
+
className={buttonIconVariants({
|
|
169
|
+
display: mode,
|
|
170
|
+
active: !!isActive,
|
|
171
|
+
})}
|
|
172
|
+
/>
|
|
173
|
+
)}
|
|
174
|
+
{label && (
|
|
175
|
+
<span className={buttonLabelVariants({ display: mode })}>
|
|
176
|
+
{label}
|
|
177
|
+
</span>
|
|
178
|
+
)}
|
|
179
|
+
{mode !== "minimal" && mode !== "compact" && right}
|
|
180
|
+
</button>
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<Tooltip content={resolvedTooltip} side={tooltipSide}>
|
|
185
|
+
{content}
|
|
186
|
+
</Tooltip>
|
|
187
|
+
);
|
|
188
|
+
},
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
Button.displayName = "Button";
|