@ponti-studios/ui 0.1.2
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/README.md +27 -0
- package/dist/index.js +2118 -0
- package/dist/index.js.map +1 -0
- package/package.json +130 -0
- package/src/animations.css +437 -0
- package/src/components/data-display/accordion.stories.tsx +57 -0
- package/src/components/data-display/accordion.tsx +65 -0
- package/src/components/data-display/filter-chip.tsx +41 -0
- package/src/components/data-display/index.ts +13 -0
- package/src/components/data-display/table.stories.tsx +75 -0
- package/src/components/data-display/table.tsx +84 -0
- package/src/components/feedback/alert.stories.tsx +51 -0
- package/src/components/feedback/alert.tsx +60 -0
- package/src/components/feedback/empty-state.tsx +66 -0
- package/src/components/feedback/index.ts +6 -0
- package/src/components/feedback/progress.stories.tsx +37 -0
- package/src/components/feedback/progress.tsx +30 -0
- package/src/components/feedback/spinner.stories.tsx +52 -0
- package/src/components/feedback/spinner.tsx +48 -0
- package/src/components/forms/calendar.stories.tsx +30 -0
- package/src/components/forms/calendar.tsx +67 -0
- package/src/components/forms/date-picker.stories.tsx +48 -0
- package/src/components/forms/date-picker.tsx +60 -0
- package/src/components/forms/field.stories.tsx +62 -0
- package/src/components/forms/field.tsx +65 -0
- package/src/components/forms/field.types.ts +7 -0
- package/src/components/forms/index.ts +24 -0
- package/src/components/forms/input.stories.tsx +47 -0
- package/src/components/forms/input.tsx +20 -0
- package/src/components/forms/radio-group.stories.tsx +48 -0
- package/src/components/forms/radio-group.tsx +38 -0
- package/src/components/forms/select.stories.tsx +88 -0
- package/src/components/forms/select.tsx +147 -0
- package/src/components/forms/slider.stories.tsx +67 -0
- package/src/components/forms/slider.tsx +25 -0
- package/src/components/forms/stepper.stories.tsx +111 -0
- package/src/components/forms/stepper.tsx +66 -0
- package/src/components/forms/switch.stories.tsx +144 -0
- package/src/components/forms/switch.tsx +36 -0
- package/src/components/forms/text-field.stories.tsx +87 -0
- package/src/components/forms/text-field.tsx +40 -0
- package/src/components/forms/text-field.types.ts +9 -0
- package/src/components/forms/textarea.stories.tsx +150 -0
- package/src/components/forms/textarea.tsx +18 -0
- package/src/components/layout/index.ts +2 -0
- package/src/components/layout/scroll-area.stories.tsx +83 -0
- package/src/components/layout/scroll-area.tsx +40 -0
- package/src/components/navigation/app-navigation.stories.tsx +81 -0
- package/src/components/navigation/app-navigation.tsx +155 -0
- package/src/components/navigation/index.ts +3 -0
- package/src/components/navigation/tabs.stories.tsx +69 -0
- package/src/components/navigation/tabs.tsx +49 -0
- package/src/components/overlays/alert-dialog.stories.tsx +45 -0
- package/src/components/overlays/alert-dialog.tsx +119 -0
- package/src/components/overlays/command.stories.tsx +65 -0
- package/src/components/overlays/command.tsx +130 -0
- package/src/components/overlays/dialog.stories.tsx +111 -0
- package/src/components/overlays/dialog.tsx +162 -0
- package/src/components/overlays/dropdown-menu.stories.tsx +79 -0
- package/src/components/overlays/dropdown-menu.tsx +214 -0
- package/src/components/overlays/index.ts +65 -0
- package/src/components/overlays/popover.stories.tsx +39 -0
- package/src/components/overlays/popover.tsx +40 -0
- package/src/components/overlays/sheet.stories.tsx +68 -0
- package/src/components/overlays/sheet.tsx +98 -0
- package/src/components/primitives/avatar.stories.tsx +71 -0
- package/src/components/primitives/avatar.tsx +132 -0
- package/src/components/primitives/badge.stories.tsx +33 -0
- package/src/components/primitives/badge.tsx +44 -0
- package/src/components/primitives/button.stories.tsx +113 -0
- package/src/components/primitives/button.tsx +95 -0
- package/src/components/primitives/button.types.ts +10 -0
- package/src/components/primitives/card.stories.tsx +59 -0
- package/src/components/primitives/card.tsx +69 -0
- package/src/components/primitives/index.ts +13 -0
- package/src/components/primitives/label.stories.tsx +47 -0
- package/src/components/primitives/label.tsx +21 -0
- package/src/constants/chart-colors.ts +39 -0
- package/src/env.d.ts +9 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/sort.types.ts +12 -0
- package/src/hooks/use-api-client.ts +122 -0
- package/src/hooks/use-debounced-value.ts +12 -0
- package/src/hooks/use-filter-state.ts +68 -0
- package/src/hooks/use-media-query.ts +65 -0
- package/src/hooks/use-mobile.tsx +19 -0
- package/src/index.ts +25 -0
- package/src/lib/auth.ts +5 -0
- package/src/lib/clipboard.ts +8 -0
- package/src/lib/create-memory-storage.ts +24 -0
- package/src/lib/index.ts +4 -0
- package/src/lib/utils.ts +6 -0
- package/src/storybook/controls.ts +117 -0
- package/src/storybook/options.ts +52 -0
- package/src/styles.css +1052 -0
- package/src/tokens/color-systems.stories.tsx +133 -0
- package/src/tokens/colors.ts +156 -0
- package/src/tokens/foundations.stories.tsx +151 -0
- package/src/tokens/index.ts +16 -0
- package/src/tokens/layout.stories.tsx +118 -0
- package/src/tokens/motion.ts +51 -0
- package/src/tokens/radii.ts +10 -0
- package/src/tokens/shadows.ts +34 -0
- package/src/tokens/spacing.ts +30 -0
- package/src/tokens/typography.ts +42 -0
- package/src/translations/en.ts +34 -0
- package/src/translations/index.ts +32 -0
- package/src/types/upload.ts +42 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
|
2
|
+
import type { ClassValue } from "clsx";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
|
|
7
|
+
type AvatarSize = "sm" | "default" | "lg";
|
|
8
|
+
|
|
9
|
+
const AvatarContext = React.createContext<{ size: AvatarSize }>({ size: "default" });
|
|
10
|
+
|
|
11
|
+
const avatarSize: Record<AvatarSize, ClassValue> = {
|
|
12
|
+
sm: "size-6",
|
|
13
|
+
default: "size-6",
|
|
14
|
+
lg: "size-10",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const badgeDotSize: Record<AvatarSize, ClassValue> = {
|
|
18
|
+
sm: "size-2",
|
|
19
|
+
default: "size-2.5",
|
|
20
|
+
lg: "size-3",
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const fallbackText: Record<AvatarSize, ClassValue> = {
|
|
24
|
+
sm: "text-xs",
|
|
25
|
+
default: "text-sm",
|
|
26
|
+
lg: "text-sm",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const groupCountSvg: Record<AvatarSize, ClassValue> = {
|
|
30
|
+
sm: "[&>svg]:size-3",
|
|
31
|
+
default: "[&>svg]:size-4",
|
|
32
|
+
lg: "[&>svg]:size-5",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function Avatar({
|
|
36
|
+
className,
|
|
37
|
+
size = "default",
|
|
38
|
+
statusBadge = false,
|
|
39
|
+
children,
|
|
40
|
+
...props
|
|
41
|
+
}: React.ComponentProps<typeof AvatarPrimitive.Root> & {
|
|
42
|
+
size?: AvatarSize;
|
|
43
|
+
statusBadge?: boolean;
|
|
44
|
+
}) {
|
|
45
|
+
return (
|
|
46
|
+
<AvatarContext.Provider value={{ size }}>
|
|
47
|
+
<AvatarPrimitive.Root
|
|
48
|
+
data-slot="avatar"
|
|
49
|
+
className={cn("relative shrink-0 rounded-full select-none", avatarSize[size], className)}
|
|
50
|
+
{...props}
|
|
51
|
+
>
|
|
52
|
+
{children}
|
|
53
|
+
{statusBadge ? (
|
|
54
|
+
<span
|
|
55
|
+
data-slot="avatar-badge"
|
|
56
|
+
className={cn(
|
|
57
|
+
"bg-accent ring-background absolute right-0 bottom-0 z-10 rounded-full ring-2",
|
|
58
|
+
badgeDotSize[size],
|
|
59
|
+
)}
|
|
60
|
+
/>
|
|
61
|
+
) : null}
|
|
62
|
+
</AvatarPrimitive.Root>
|
|
63
|
+
</AvatarContext.Provider>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function AvatarImage({ className, ...props }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
|
68
|
+
return (
|
|
69
|
+
<AvatarPrimitive.Image
|
|
70
|
+
data-slot="avatar-image"
|
|
71
|
+
className={cn("absolute inset-0 size-full rounded-full object-cover", className)}
|
|
72
|
+
{...props}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function AvatarFallback({
|
|
78
|
+
className,
|
|
79
|
+
...props
|
|
80
|
+
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
|
81
|
+
const { size } = React.useContext(AvatarContext);
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<AvatarPrimitive.Fallback
|
|
85
|
+
data-slot="avatar-fallback"
|
|
86
|
+
className={cn(
|
|
87
|
+
"bg-inset text-secondary absolute inset-0 flex items-center justify-center rounded-full",
|
|
88
|
+
fallbackText[size],
|
|
89
|
+
className,
|
|
90
|
+
)}
|
|
91
|
+
{...props}
|
|
92
|
+
/>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function AvatarGroup({
|
|
97
|
+
className,
|
|
98
|
+
size = "default",
|
|
99
|
+
...props
|
|
100
|
+
}: React.ComponentProps<"div"> & { size?: AvatarSize }) {
|
|
101
|
+
return (
|
|
102
|
+
<AvatarContext.Provider value={{ size }}>
|
|
103
|
+
<div
|
|
104
|
+
data-slot="avatar-group"
|
|
105
|
+
className={cn(
|
|
106
|
+
"*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2",
|
|
107
|
+
className,
|
|
108
|
+
)}
|
|
109
|
+
{...props}
|
|
110
|
+
/>
|
|
111
|
+
</AvatarContext.Provider>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function AvatarGroupCount({ className, ...props }: React.ComponentProps<"div">) {
|
|
116
|
+
const { size } = React.useContext(AvatarContext);
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div
|
|
120
|
+
data-slot="avatar-group-count"
|
|
121
|
+
className={cn(
|
|
122
|
+
"bg-inset text-secondary ring-background relative flex shrink-0 items-center justify-center rounded-full text-sm ring-2",
|
|
123
|
+
avatarSize[size],
|
|
124
|
+
groupCountSvg[size],
|
|
125
|
+
className,
|
|
126
|
+
)}
|
|
127
|
+
{...props}
|
|
128
|
+
/>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export { Avatar, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarImage };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
import { Badge } from "./badge";
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Primitives/Badge",
|
|
7
|
+
component: Badge,
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
argTypes: {
|
|
10
|
+
variant: {
|
|
11
|
+
control: "select",
|
|
12
|
+
options: ["default", "secondary", "destructive", "outline"],
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
} satisfies Meta<typeof Badge>;
|
|
16
|
+
|
|
17
|
+
export default meta;
|
|
18
|
+
type Story = StoryObj<typeof meta>;
|
|
19
|
+
|
|
20
|
+
export const Default: Story = {
|
|
21
|
+
args: { children: "Badge" },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const Variants: Story = {
|
|
25
|
+
render: () => (
|
|
26
|
+
<div className="flex flex-wrap gap-3">
|
|
27
|
+
<Badge variant="default">Default</Badge>
|
|
28
|
+
<Badge variant="secondary">Secondary</Badge>
|
|
29
|
+
<Badge variant="destructive">Destructive</Badge>
|
|
30
|
+
<Badge variant="outline">Outline</Badge>
|
|
31
|
+
</div>
|
|
32
|
+
),
|
|
33
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
|
|
7
|
+
const badgeVariants = cva(
|
|
8
|
+
"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden whitespace-nowrap rounded border border-transparent px-1.5 py-px text-xs font-medium [&>svg]:size-3 [&>svg]:pointer-events-none aria-invalid:border-destructive aria-invalid:ring-destructive/20",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "bg-accent text-on-accent [a&]:[a&]: [a&]: [a&]:",
|
|
13
|
+
secondary: "bg-panel text-primary [a&]:[a&]: [a&]: [a&]:",
|
|
14
|
+
destructive: "bg-destructive text-on-destructive [a&]:[a&]: [a&]: [a&]:",
|
|
15
|
+
outline: "border-default text-primary [a&]:[a&]: [a&]: [a&]:",
|
|
16
|
+
ghost: "[a&]:[a&]: [a&]: [a&]:",
|
|
17
|
+
link: "text-primary underline-offset-4 [a&]:[a&]:hover:underline [a&]: [a&]: [a&]:",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultVariants: {
|
|
21
|
+
variant: "default",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
function Badge({
|
|
27
|
+
className,
|
|
28
|
+
variant = "default",
|
|
29
|
+
asChild = false,
|
|
30
|
+
...props
|
|
31
|
+
}: React.ComponentProps<"span"> & VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
|
32
|
+
const Comp = asChild ? Slot : "span";
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Comp
|
|
36
|
+
data-slot="badge"
|
|
37
|
+
data-variant={variant}
|
|
38
|
+
className={cn(badgeVariants({ variant }), className)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { Badge, badgeVariants };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { Plus, Trash2 } from "lucide-react";
|
|
3
|
+
import { expect, userEvent, within } from "storybook/test";
|
|
4
|
+
|
|
5
|
+
import { booleanControl, hiddenControl, selectControl } from "../../storybook/controls";
|
|
6
|
+
import { buttonVariantOptions } from "../../storybook/options";
|
|
7
|
+
import { Button } from "./button";
|
|
8
|
+
|
|
9
|
+
const meta: Meta = {
|
|
10
|
+
title: "Primitives/Button",
|
|
11
|
+
component: Button,
|
|
12
|
+
tags: ["autodocs"],
|
|
13
|
+
argTypes: {
|
|
14
|
+
variant: selectControl(buttonVariantOptions, "Visual style variant of the button", {
|
|
15
|
+
defaultValue: "default",
|
|
16
|
+
}),
|
|
17
|
+
disabled: booleanControl("Prevents user interaction and applies disabled styling", false),
|
|
18
|
+
asChild: hiddenControl,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
23
|
+
type Story = StoryObj<typeof meta>;
|
|
24
|
+
|
|
25
|
+
export const Default: Story = {
|
|
26
|
+
args: { children: "Button" },
|
|
27
|
+
play: async ({ canvasElement }) => {
|
|
28
|
+
const canvas = within(canvasElement);
|
|
29
|
+
const button = canvas.getByRole("button", { name: "Button" });
|
|
30
|
+
|
|
31
|
+
await userEvent.hover(button);
|
|
32
|
+
await userEvent.click(button);
|
|
33
|
+
|
|
34
|
+
await expect(button).toHaveFocus();
|
|
35
|
+
await expect(button).toBeEnabled();
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const Variants: Story = {
|
|
40
|
+
render: () => (
|
|
41
|
+
<div className="flex flex-wrap gap-3">
|
|
42
|
+
<Button variant="default">Default</Button>
|
|
43
|
+
<Button variant="secondary">Secondary</Button>
|
|
44
|
+
<Button variant="ghost">Ghost</Button>
|
|
45
|
+
<Button variant="destructive">Destructive</Button>
|
|
46
|
+
<Button variant="ghost" size="icon" aria-label="Add">
|
|
47
|
+
<Plus className="size-4" />
|
|
48
|
+
</Button>
|
|
49
|
+
<Button variant="link">Link</Button>
|
|
50
|
+
</div>
|
|
51
|
+
),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const Icon: Story = {
|
|
55
|
+
render: () => (
|
|
56
|
+
<div className="flex items-center gap-3">
|
|
57
|
+
<Button>Standard</Button>
|
|
58
|
+
<Button size="icon" aria-label="Add">
|
|
59
|
+
<Plus className="size-4" />
|
|
60
|
+
</Button>
|
|
61
|
+
</div>
|
|
62
|
+
),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const WithIcon: Story = {
|
|
66
|
+
render: () => (
|
|
67
|
+
<div className="flex gap-3">
|
|
68
|
+
<Button>
|
|
69
|
+
<Plus className="size-4" />
|
|
70
|
+
Create
|
|
71
|
+
</Button>
|
|
72
|
+
<Button variant="destructive">
|
|
73
|
+
<Trash2 className="size-4" />
|
|
74
|
+
Delete
|
|
75
|
+
</Button>
|
|
76
|
+
</div>
|
|
77
|
+
),
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const Disabled: Story = {
|
|
81
|
+
render: () => (
|
|
82
|
+
<div className="flex gap-3">
|
|
83
|
+
<Button disabled>Default</Button>
|
|
84
|
+
<Button variant="outline" disabled>
|
|
85
|
+
Outline
|
|
86
|
+
</Button>
|
|
87
|
+
<Button variant="destructive" disabled>
|
|
88
|
+
Destructive
|
|
89
|
+
</Button>
|
|
90
|
+
</div>
|
|
91
|
+
),
|
|
92
|
+
play: async ({ canvasElement }) => {
|
|
93
|
+
const canvas = within(canvasElement);
|
|
94
|
+
|
|
95
|
+
await expect(canvas.getByRole("button", { name: "Default" })).toBeDisabled();
|
|
96
|
+
await expect(canvas.getByRole("button", { name: "Outline" })).toBeDisabled();
|
|
97
|
+
await expect(canvas.getByRole("button", { name: "Destructive" })).toBeDisabled();
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const Loading: Story = {
|
|
102
|
+
render: () => (
|
|
103
|
+
<Button isLoading loadingLabel="Saving">
|
|
104
|
+
Save
|
|
105
|
+
</Button>
|
|
106
|
+
),
|
|
107
|
+
play: async ({ canvasElement }) => {
|
|
108
|
+
const canvas = within(canvasElement);
|
|
109
|
+
const button = canvas.getByRole("button", { name: "Saving" });
|
|
110
|
+
|
|
111
|
+
await expect(button).toBeDisabled();
|
|
112
|
+
},
|
|
113
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
2
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
import { Spinner } from "../feedback/spinner";
|
|
7
|
+
|
|
8
|
+
const buttonVariants = cva(
|
|
9
|
+
"inline-flex min-h-11 !cursor-pointer items-center justify-center gap-1.5 whitespace-nowrap rounded-md border px-3 py-1.5 text-sm font-medium leading-5 transition-colors duration-150 focus-visible:ring-2 focus-visible:ring-focus focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:!cursor-default [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
|
10
|
+
{
|
|
11
|
+
variants: {
|
|
12
|
+
variant: {
|
|
13
|
+
default:
|
|
14
|
+
"border-transparent bg-accent text-on-accent hover:bg-accent-hover active:bg-accent-pressed disabled:border-subtle disabled:bg-panel disabled:text-disabled",
|
|
15
|
+
destructive:
|
|
16
|
+
"border-transparent bg-destructive text-on-destructive hover:bg-destructive-hover active:bg-destructive-pressed disabled:border-subtle disabled:bg-panel disabled:text-disabled",
|
|
17
|
+
outline:
|
|
18
|
+
"border-default bg-transparent text-primary hover:bg-surface-hover active:bg-surface-pressed disabled:border-subtle disabled:text-disabled",
|
|
19
|
+
secondary:
|
|
20
|
+
"border-default bg-panel text-primary hover:bg-surface-hover active:bg-surface-pressed disabled:border-subtle disabled:bg-panel disabled:text-disabled",
|
|
21
|
+
ghost:
|
|
22
|
+
"border-transparent bg-transparent text-secondary hover:bg-surface-hover hover:text-primary active:bg-surface-pressed disabled:text-disabled",
|
|
23
|
+
link: "border-transparent bg-transparent px-0 text-accent hover:text-accent-hover disabled:text-disabled underline-offset-4 hover:underline",
|
|
24
|
+
},
|
|
25
|
+
size: {
|
|
26
|
+
sm: "min-h-11 px-2.5 text-sm",
|
|
27
|
+
md: "min-h-11 px-3 text-sm",
|
|
28
|
+
lg: "min-h-12 px-4 text-base",
|
|
29
|
+
default: "",
|
|
30
|
+
icon: "size-11 rounded-md p-0",
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
defaultVariants: {
|
|
34
|
+
variant: "default",
|
|
35
|
+
size: "md",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
interface ButtonProps
|
|
41
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
|
42
|
+
asChild?: boolean;
|
|
43
|
+
isLoading?: boolean;
|
|
44
|
+
loadingLabel?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
48
|
+
(
|
|
49
|
+
{
|
|
50
|
+
children,
|
|
51
|
+
className,
|
|
52
|
+
disabled,
|
|
53
|
+
isLoading = false,
|
|
54
|
+
loadingLabel = "Loading",
|
|
55
|
+
variant,
|
|
56
|
+
size,
|
|
57
|
+
asChild = false,
|
|
58
|
+
"aria-label": ariaLabel,
|
|
59
|
+
...props
|
|
60
|
+
},
|
|
61
|
+
ref,
|
|
62
|
+
) => {
|
|
63
|
+
const Comp = asChild ? Slot : "button";
|
|
64
|
+
return (
|
|
65
|
+
<Comp
|
|
66
|
+
className={cn(buttonVariants({ variant, size, className }), isLoading && "relative")}
|
|
67
|
+
style={{
|
|
68
|
+
...props.style,
|
|
69
|
+
cursor: disabled || isLoading ? "default" : "pointer",
|
|
70
|
+
}}
|
|
71
|
+
type="button"
|
|
72
|
+
ref={ref}
|
|
73
|
+
disabled={disabled || isLoading}
|
|
74
|
+
aria-busy={isLoading}
|
|
75
|
+
aria-label={isLoading ? loadingLabel : ariaLabel}
|
|
76
|
+
{...props}
|
|
77
|
+
>
|
|
78
|
+
{isLoading ? (
|
|
79
|
+
<>
|
|
80
|
+
<span className="opacity-0">{children}</span>
|
|
81
|
+
<span className="absolute inset-0 inline-flex items-center justify-center">
|
|
82
|
+
<Spinner size="sm" aria-hidden="true" />
|
|
83
|
+
</span>
|
|
84
|
+
<span className="sr-only">{loadingLabel}</span>
|
|
85
|
+
</>
|
|
86
|
+
) : (
|
|
87
|
+
children
|
|
88
|
+
)}
|
|
89
|
+
</Comp>
|
|
90
|
+
);
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
Button.displayName = "Button";
|
|
94
|
+
|
|
95
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type ButtonVariant = "default" | "destructive" | "ghost" | "link" | "outline" | "secondary";
|
|
2
|
+
|
|
3
|
+
export type ButtonSize = "sm" | "md" | "lg" | "default" | "icon";
|
|
4
|
+
|
|
5
|
+
export interface ButtonBaseProps {
|
|
6
|
+
isLoading?: boolean | undefined;
|
|
7
|
+
size?: ButtonSize;
|
|
8
|
+
title?: string | undefined;
|
|
9
|
+
variant?: ButtonVariant;
|
|
10
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import { expect, userEvent, within } from "storybook/test";
|
|
3
|
+
|
|
4
|
+
import { Button } from "./button";
|
|
5
|
+
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "./card";
|
|
6
|
+
|
|
7
|
+
const meta = {
|
|
8
|
+
title: "Primitives/Card",
|
|
9
|
+
tags: ["autodocs"],
|
|
10
|
+
component: Card,
|
|
11
|
+
} satisfies Meta<typeof Card>;
|
|
12
|
+
|
|
13
|
+
export default meta;
|
|
14
|
+
type Story = StoryObj<typeof meta>;
|
|
15
|
+
|
|
16
|
+
export const Default: Story = {
|
|
17
|
+
render: () => (
|
|
18
|
+
<Card>
|
|
19
|
+
<CardHeader>
|
|
20
|
+
<CardTitle>Card Title</CardTitle>
|
|
21
|
+
<CardDescription>A short description of the card's content.</CardDescription>
|
|
22
|
+
</CardHeader>
|
|
23
|
+
<CardContent>
|
|
24
|
+
<p className="body-2 text-secondary">
|
|
25
|
+
Card body content goes here. It can be any React node.
|
|
26
|
+
</p>
|
|
27
|
+
</CardContent>
|
|
28
|
+
<CardFooter className="flex justify-end gap-2">
|
|
29
|
+
<Button variant="outline">Cancel</Button>
|
|
30
|
+
<Button>Confirm</Button>
|
|
31
|
+
</CardFooter>
|
|
32
|
+
</Card>
|
|
33
|
+
),
|
|
34
|
+
play: async ({ canvasElement }) => {
|
|
35
|
+
const canvas = within(canvasElement);
|
|
36
|
+
const cancelButton = canvas.getByRole("button", { name: "Cancel" });
|
|
37
|
+
|
|
38
|
+
await userEvent.tab();
|
|
39
|
+
|
|
40
|
+
await expect(canvas.getByText("Card Title")).toBeInTheDocument();
|
|
41
|
+
await expect(cancelButton).toHaveFocus();
|
|
42
|
+
await expect(canvas.getByRole("button", { name: "Confirm" })).toBeInTheDocument();
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const Minimal: Story = {
|
|
47
|
+
render: () => (
|
|
48
|
+
<Card className="w-80">
|
|
49
|
+
<CardContent>
|
|
50
|
+
<p className="body-2 text-secondary">Minimal card with only content.</p>
|
|
51
|
+
</CardContent>
|
|
52
|
+
</Card>
|
|
53
|
+
),
|
|
54
|
+
play: async ({ canvasElement }) => {
|
|
55
|
+
const canvas = within(canvasElement);
|
|
56
|
+
|
|
57
|
+
await expect(canvas.getByText("Minimal card with only content.")).toBeInTheDocument();
|
|
58
|
+
},
|
|
59
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
|
|
5
|
+
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
|
6
|
+
return (
|
|
7
|
+
<div
|
|
8
|
+
data-slot="card"
|
|
9
|
+
className={cn(
|
|
10
|
+
"bg-panel text-primary border-default flex flex-col rounded-md border",
|
|
11
|
+
className,
|
|
12
|
+
)}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
data-slot="card-header"
|
|
22
|
+
className={cn(
|
|
23
|
+
"flex items-center justify-between gap-2 border-b px-4 py-3 has-data-[slot=card-action]:grid has-data-[slot=card-action]:grid-cols-[1fr_auto]",
|
|
24
|
+
className,
|
|
25
|
+
)}
|
|
26
|
+
{...props}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
32
|
+
return <div data-slot="card-title" className={cn(className)} {...props} />;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
data-slot="card-description"
|
|
39
|
+
className={cn("body-4 text-secondary", className)}
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
data-slot="card-action"
|
|
49
|
+
className={cn("col-start-2 self-start justify-self-end", className)}
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
56
|
+
return <div data-slot="card-content" className={cn("p-4", className)} {...props} />;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
data-slot="card-footer"
|
|
63
|
+
className={cn("flex items-center border-t px-4 py-3", className)}
|
|
64
|
+
{...props}
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export { Avatar, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarImage } from "./avatar";
|
|
2
|
+
export { Badge, badgeVariants } from "./badge";
|
|
3
|
+
export { Button, buttonVariants } from "./button";
|
|
4
|
+
export {
|
|
5
|
+
Card,
|
|
6
|
+
CardAction,
|
|
7
|
+
CardContent,
|
|
8
|
+
CardDescription,
|
|
9
|
+
CardFooter,
|
|
10
|
+
CardHeader,
|
|
11
|
+
CardTitle,
|
|
12
|
+
} from "./card";
|
|
13
|
+
export { Label } from "./label";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
|
|
4
|
+
import { Input } from "../forms/input";
|
|
5
|
+
import { Label } from "../primitives/label";
|
|
6
|
+
|
|
7
|
+
const meta: Meta<typeof Label> = {
|
|
8
|
+
title: "Primitives/Label",
|
|
9
|
+
component: Label,
|
|
10
|
+
tags: ["autodocs"],
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default meta;
|
|
14
|
+
type Story = StoryObj<typeof meta>;
|
|
15
|
+
|
|
16
|
+
export const Default: Story = {
|
|
17
|
+
args: {
|
|
18
|
+
children: "Email address",
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const WithInput: Story = {
|
|
23
|
+
render: () => (
|
|
24
|
+
<div className="flex w-full max-w-sm flex-col gap-2">
|
|
25
|
+
<Label htmlFor="email">Email address</Label>
|
|
26
|
+
<Input id="email" type="email" placeholder="you@example.com" />
|
|
27
|
+
</div>
|
|
28
|
+
),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const Disabled: Story = {
|
|
32
|
+
render: () => (
|
|
33
|
+
<div className="flex w-full max-w-sm flex-col gap-2">
|
|
34
|
+
<Label htmlFor="disabled-field">Subscription plan</Label>
|
|
35
|
+
<div className="flex items-center gap-3">
|
|
36
|
+
<Input id="disabled-field" className="peer" disabled placeholder="Enterprise" />
|
|
37
|
+
<Label htmlFor="disabled-field" className="shrink-0">
|
|
38
|
+
(locked)
|
|
39
|
+
</Label>
|
|
40
|
+
</div>
|
|
41
|
+
<p className="text-secondary text-xs">
|
|
42
|
+
The <code className="font-mono">peer-disabled</code> variant mutes the label when its paired
|
|
43
|
+
input is disabled.
|
|
44
|
+
</p>
|
|
45
|
+
</div>
|
|
46
|
+
),
|
|
47
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as LabelPrimitive from "@radix-ui/react-label";
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
|
+
import * as React from "react";
|
|
6
|
+
|
|
7
|
+
import { cn } from "../../lib/utils";
|
|
8
|
+
|
|
9
|
+
const labelVariants = cva(
|
|
10
|
+
"text-sm font-medium leading-none text-primary peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
const Label = React.forwardRef<
|
|
14
|
+
React.ComponentRef<typeof LabelPrimitive.Root>,
|
|
15
|
+
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
|
|
16
|
+
>(({ className, ...props }, ref) => (
|
|
17
|
+
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
|
|
18
|
+
));
|
|
19
|
+
Label.displayName = LabelPrimitive.Root.displayName;
|
|
20
|
+
|
|
21
|
+
export { Label };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chart colors are CSS-variable-backed so they stay in sync with the active
|
|
3
|
+
* color system and mode at runtime.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const CHART_COLORS = {
|
|
7
|
+
chart1: "var(--color-chart-1)",
|
|
8
|
+
chart2: "var(--color-chart-2)",
|
|
9
|
+
chart3: "var(--color-chart-3)",
|
|
10
|
+
chart4: "var(--color-chart-4)",
|
|
11
|
+
chart5: "var(--color-chart-5)",
|
|
12
|
+
|
|
13
|
+
positive: "var(--color-chart-positive)",
|
|
14
|
+
negative: "var(--color-chart-negative)",
|
|
15
|
+
neutral: "var(--color-chart-neutral)",
|
|
16
|
+
|
|
17
|
+
background: "var(--color-surface-canvas)",
|
|
18
|
+
grid: "var(--color-border-subtle)",
|
|
19
|
+
|
|
20
|
+
axis: "var(--color-text-tertiary)",
|
|
21
|
+
label: "var(--color-text-secondary)",
|
|
22
|
+
|
|
23
|
+
tooltip: {
|
|
24
|
+
background: "var(--color-surface-panel)",
|
|
25
|
+
text: "var(--color-text-primary)",
|
|
26
|
+
border: "var(--color-border-default)",
|
|
27
|
+
},
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
export const CHART_CSS_VARS = {
|
|
31
|
+
positive: "var(--color-chart-positive)",
|
|
32
|
+
negative: "var(--color-chart-negative)",
|
|
33
|
+
neutral: "var(--color-chart-neutral)",
|
|
34
|
+
chart1: "var(--color-chart-1)",
|
|
35
|
+
chart2: "var(--color-chart-2)",
|
|
36
|
+
chart3: "var(--color-chart-3)",
|
|
37
|
+
chart4: "var(--color-chart-4)",
|
|
38
|
+
chart5: "var(--color-chart-5)",
|
|
39
|
+
} as const;
|