@clip-how/ui 0.1.6

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 ADDED
@@ -0,0 +1,171 @@
1
+ # @clip-how/ui
2
+
3
+ ClipHow design tokens + shadcn-based UI primitives. **Single source of truth** for visual identity across:
4
+
5
+ - `ClipHow` — the main app at `app.cliphow.com`
6
+ - `cliphow-website` — the marketing site at `cliphow.com`
7
+ - Any future ClipHow surface (mobile, embed widget, etc.)
8
+
9
+ ## What's in here
10
+
11
+ ```
12
+ @clip-how/ui
13
+ ├── tokens.css ← Design tokens (colors, spacing, typography). Figma → here.
14
+ ├── components/ ← 25 shadcn primitives (Button, Dialog, Input, …)
15
+ └── lib/
16
+ └── utils.ts ← cn() helper (clsx + tailwind-merge)
17
+ ```
18
+
19
+ ## Install (consumer side)
20
+
21
+ ### One-time per machine — auth to GitHub Packages
22
+
23
+ ```bash
24
+ # Generate a GitHub Personal Access Token (classic) with `read:packages` scope:
25
+ # https://github.com/settings/tokens/new?scopes=read:packages&description=clip-how-ui
26
+ # Then either:
27
+ export NPM_AUTH_TOKEN="ghp_yourTokenHere"
28
+
29
+ # Or persist via your shell profile (~/.zshrc, ~/.bashrc):
30
+ echo 'export NPM_AUTH_TOKEN="ghp_yourTokenHere"' >> ~/.zshrc
31
+ ```
32
+
33
+ ### Per-project — add `.npmrc` to the consumer repo
34
+
35
+ ```
36
+ @clip-how:registry=https://npm.pkg.github.com
37
+ //npm.pkg.github.com/:_authToken=${NPM_AUTH_TOKEN}
38
+ ```
39
+
40
+ ### Install
41
+
42
+ ```bash
43
+ npm install @clip-how/ui
44
+ ```
45
+
46
+ ## Use in code
47
+
48
+ ```tsx
49
+ // 1. Import tokens once (in globals.css or app layout)
50
+ import "@clip-how/ui/tokens.css";
51
+
52
+ // 2. Import components per-file
53
+ import { Button } from "@clip-how/ui/components/button";
54
+ import { Dialog, DialogContent } from "@clip-how/ui/components/dialog";
55
+
56
+ // 3. Use them like any other component
57
+ <Button variant="primary">Save</Button>
58
+ ```
59
+
60
+ ## Publishing (maintainer side)
61
+
62
+ Releases are cut by pushing a git tag — GitHub Actions publishes to GitHub Packages automatically:
63
+
64
+ ```bash
65
+ # Bump version + tag in one step
66
+ npm version patch # or minor / major
67
+ git push --follow-tags
68
+
69
+ # CI publishes within ~30s, visible at:
70
+ # https://github.com/orgs/Clip-How/packages
71
+ ```
72
+
73
+ ### Semver policy
74
+
75
+ - **Patch** (0.1.0 → 0.1.1) — bug fixes, CSS-only tweaks, internal refactors
76
+ - **Minor** (0.1.0 → 0.2.0) — new components, new tokens, additive prop changes
77
+ - **Major** (0.1.0 → 1.0.0) — breaking API or token rename, removing a component
78
+
79
+ ## Why GitHub Packages and not public npm?
80
+
81
+ - ClipHow is a closed-source SaaS. Internal design choices stay internal.
82
+ - Free for unlimited private packages on the Free plan since 2020.
83
+ - Same GitHub PAT covers package read + repo access — one credential.
84
+
85
+ ## When updating tokens (Figma → here flow)
86
+
87
+ > **For Pauline** — this is your playbook for getting a Figma token
88
+ > change live across `cliphow.com` AND `app.cliphow.com` in one go.
89
+
90
+ ### Step 1 — Update the source Figma file
91
+ Update the variable (color, spacing, font weight, etc.) in the master
92
+ ClipHow design system Figma file.
93
+
94
+ ### Step 2 — Pull the matching CSS variable name
95
+ The CSS variable name in `tokens.css` matches the Figma variable name
96
+ 1-for-1 (kebab-case). Examples:
97
+
98
+ | Figma variable | CSS variable in tokens.css |
99
+ |---|---|
100
+ | `Primary / 500` | `--primary-500` |
101
+ | `Foreground / Default` | `--foreground` |
102
+ | `Spacing / 8` | `--spacing-8` |
103
+ | `Font / Body / Size` | `--font-body-size` |
104
+
105
+ ### Step 3 — Open a PR with the diff
106
+
107
+ ```bash
108
+ git clone https://github.com/Clip-How/cliphow-design-system.git
109
+ cd cliphow-design-system
110
+ git checkout -b tokens/$(date +%Y-%m-%d)-rebrand-primary
111
+
112
+ # Edit tokens.css — change the value of the variable(s)
113
+ # Save
114
+
115
+ # Verify it parses
116
+ npm install
117
+ npx tsc --noEmit
118
+
119
+ git add tokens.css
120
+ git commit -m "feat: update primary 500 from #c4e8a0 to #b8e0a8 (rebrand)"
121
+ git push -u origin tokens/$(date +%Y-%m-%d)-rebrand-primary
122
+ gh pr create --fill
123
+ ```
124
+
125
+ ### Step 4 — Bump version + tag (after PR is merged)
126
+
127
+ ```bash
128
+ git checkout main && git pull
129
+
130
+ # Patch for a single token tweak, minor for "a few tokens changed",
131
+ # major if you're removing/renaming variables.
132
+ npm version patch
133
+
134
+ git push --follow-tags
135
+ ```
136
+
137
+ The publish workflow auto-fires on the tag push. ~30s later the new
138
+ version is live on GitHub Packages.
139
+
140
+ ### Step 5 — Bump consumers
141
+
142
+ Pull the new version into BOTH consumer repos:
143
+
144
+ ```bash
145
+ # In cliphow-website
146
+ cd ~/Desktop/GitHub/cliphow-website
147
+ git checkout -b chore/bump-clip-how-ui
148
+ npm update @clip-how/ui
149
+ git add package.json package-lock.json
150
+ git commit -m "chore: bump @clip-how/ui (rebrand primary)"
151
+ git push -u origin chore/bump-clip-how-ui
152
+ gh pr create --fill
153
+
154
+ # Then in cliphow-app (same flow)
155
+ cd ~/Desktop/GitHub/cliphow-app
156
+ # ... same commands
157
+ ```
158
+
159
+ Vercel auto-deploys both as soon as the PRs merge. Total time from
160
+ "Figma updated" → "live on cliphow.com + app.cliphow.com" ≈ 10 minutes.
161
+
162
+ ---
163
+
164
+ ### Stuck?
165
+
166
+ | Symptom | Fix |
167
+ |---|---|
168
+ | `npm install` says 401 | The `.npmrc` PAT expired — ping Charles |
169
+ | Color didn't change after `npm update` | Hard refresh the browser (CDN cache) or wait ~1 min |
170
+ | Tag push didn't trigger publish | Check https://github.com/Clip-How/cliphow-design-system/actions — workflow logs explain why |
171
+ | New variable doesn't show in autocomplete | The consuming repo needs to re-import `tokens.css` — the dev server hot-reload usually handles it; if not, restart `npm run dev` |
@@ -0,0 +1,64 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { ChevronDownIcon } from "lucide-react";
5
+ import { Accordion as AccordionPrimitive } from "radix-ui";
6
+
7
+ import { cn } from "../lib/utils";
8
+
9
+ function Accordion({ ...props }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
10
+ return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
11
+ }
12
+
13
+ function AccordionItem({
14
+ className,
15
+ ...props
16
+ }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
17
+ return (
18
+ <AccordionPrimitive.Item
19
+ data-slot="accordion-item"
20
+ className={cn("border-b last:border-b-0", className)}
21
+ {...props}
22
+ />
23
+ );
24
+ }
25
+
26
+ function AccordionTrigger({
27
+ className,
28
+ children,
29
+ ...props
30
+ }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
31
+ return (
32
+ <AccordionPrimitive.Header className="flex">
33
+ <AccordionPrimitive.Trigger
34
+ data-slot="accordion-trigger"
35
+ className={cn(
36
+ "flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
37
+ className,
38
+ )}
39
+ {...props}
40
+ >
41
+ {children}
42
+ <ChevronDownIcon className="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" />
43
+ </AccordionPrimitive.Trigger>
44
+ </AccordionPrimitive.Header>
45
+ );
46
+ }
47
+
48
+ function AccordionContent({
49
+ className,
50
+ children,
51
+ ...props
52
+ }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
53
+ return (
54
+ <AccordionPrimitive.Content
55
+ data-slot="accordion-content"
56
+ className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
57
+ {...props}
58
+ >
59
+ <div className={cn("pt-0 pb-4", className)}>{children}</div>
60
+ </AccordionPrimitive.Content>
61
+ );
62
+ }
63
+
64
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
@@ -0,0 +1,179 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { AlertDialog as AlertDialogPrimitive } from "radix-ui";
5
+
6
+ import { cn } from "../lib/utils";
7
+ import { Button } from "./button";
8
+
9
+ function AlertDialog({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
10
+ return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
11
+ }
12
+
13
+ function AlertDialogTrigger({
14
+ ...props
15
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
16
+ return <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />;
17
+ }
18
+
19
+ function AlertDialogPortal({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
20
+ return <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />;
21
+ }
22
+
23
+ function AlertDialogOverlay({
24
+ className,
25
+ ...props
26
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
27
+ return (
28
+ <AlertDialogPrimitive.Overlay
29
+ data-slot="alert-dialog-overlay"
30
+ className={cn(
31
+ "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
32
+ className,
33
+ )}
34
+ {...props}
35
+ />
36
+ );
37
+ }
38
+
39
+ function AlertDialogContent({
40
+ className,
41
+ size = "default",
42
+ ...props
43
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Content> & {
44
+ size?: "default" | "sm";
45
+ }) {
46
+ return (
47
+ <AlertDialogPortal>
48
+ <AlertDialogOverlay />
49
+ <AlertDialogPrimitive.Content
50
+ data-slot="alert-dialog-content"
51
+ data-size={size}
52
+ className={cn(
53
+ "group/alert-dialog-content fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-2xl border bg-background p-6 shadow-lg duration-200 data-[size=sm]:max-w-xs data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[size=default]:sm:max-w-lg",
54
+ className,
55
+ )}
56
+ {...props}
57
+ />
58
+ </AlertDialogPortal>
59
+ );
60
+ }
61
+
62
+ function AlertDialogHeader({ className, ...props }: React.ComponentProps<"div">) {
63
+ return (
64
+ <div
65
+ data-slot="alert-dialog-header"
66
+ className={cn(
67
+ "grid grid-rows-[auto_1fr] place-items-center gap-1.5 text-center has-data-[slot=alert-dialog-media]:grid-rows-[auto_auto_1fr] has-data-[slot=alert-dialog-media]:gap-x-6 sm:group-data-[size=default]/alert-dialog-content:place-items-start sm:group-data-[size=default]/alert-dialog-content:text-left sm:group-data-[size=default]/alert-dialog-content:has-data-[slot=alert-dialog-media]:grid-rows-[auto_1fr]",
68
+ className,
69
+ )}
70
+ {...props}
71
+ />
72
+ );
73
+ }
74
+
75
+ function AlertDialogFooter({ className, ...props }: React.ComponentProps<"div">) {
76
+ return (
77
+ <div
78
+ data-slot="alert-dialog-footer"
79
+ className={cn(
80
+ "flex flex-col-reverse gap-2 group-data-[size=sm]/alert-dialog-content:grid group-data-[size=sm]/alert-dialog-content:grid-cols-2 sm:flex-row sm:justify-end",
81
+ className,
82
+ )}
83
+ {...props}
84
+ />
85
+ );
86
+ }
87
+
88
+ function AlertDialogTitle({
89
+ className,
90
+ ...props
91
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
92
+ return (
93
+ <AlertDialogPrimitive.Title
94
+ data-slot="alert-dialog-title"
95
+ className={cn(
96
+ "text-lg font-semibold sm:group-data-[size=default]/alert-dialog-content:group-has-data-[slot=alert-dialog-media]/alert-dialog-content:col-start-2",
97
+ className,
98
+ )}
99
+ {...props}
100
+ />
101
+ );
102
+ }
103
+
104
+ function AlertDialogDescription({
105
+ className,
106
+ ...props
107
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
108
+ return (
109
+ <AlertDialogPrimitive.Description
110
+ data-slot="alert-dialog-description"
111
+ className={cn("text-sm text-muted-foreground", className)}
112
+ {...props}
113
+ />
114
+ );
115
+ }
116
+
117
+ function AlertDialogMedia({ className, ...props }: React.ComponentProps<"div">) {
118
+ return (
119
+ <div
120
+ data-slot="alert-dialog-media"
121
+ className={cn(
122
+ "mb-2 inline-flex size-16 items-center justify-center rounded-md bg-muted sm:group-data-[size=default]/alert-dialog-content:row-span-2 *:[svg:not([class*='size-'])]:size-8",
123
+ className,
124
+ )}
125
+ {...props}
126
+ />
127
+ );
128
+ }
129
+
130
+ function AlertDialogAction({
131
+ className,
132
+ variant = "default",
133
+ size = "default",
134
+ ...props
135
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Action> &
136
+ Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
137
+ return (
138
+ <Button variant={variant} size={size} asChild>
139
+ <AlertDialogPrimitive.Action
140
+ data-slot="alert-dialog-action"
141
+ className={cn(className)}
142
+ {...props}
143
+ />
144
+ </Button>
145
+ );
146
+ }
147
+
148
+ function AlertDialogCancel({
149
+ className,
150
+ variant = "outline",
151
+ size = "default",
152
+ ...props
153
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Cancel> &
154
+ Pick<React.ComponentProps<typeof Button>, "variant" | "size">) {
155
+ return (
156
+ <Button variant={variant} size={size} asChild>
157
+ <AlertDialogPrimitive.Cancel
158
+ data-slot="alert-dialog-cancel"
159
+ className={cn(className)}
160
+ {...props}
161
+ />
162
+ </Button>
163
+ );
164
+ }
165
+
166
+ export {
167
+ AlertDialog,
168
+ AlertDialogAction,
169
+ AlertDialogCancel,
170
+ AlertDialogContent,
171
+ AlertDialogDescription,
172
+ AlertDialogFooter,
173
+ AlertDialogHeader,
174
+ AlertDialogMedia,
175
+ AlertDialogOverlay,
176
+ AlertDialogPortal,
177
+ AlertDialogTitle,
178
+ AlertDialogTrigger,
179
+ };
@@ -0,0 +1,96 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Avatar as AvatarPrimitive } from "radix-ui";
5
+
6
+ import { cn } from "../lib/utils";
7
+
8
+ function Avatar({
9
+ className,
10
+ size = "default",
11
+ ...props
12
+ }: React.ComponentProps<typeof AvatarPrimitive.Root> & {
13
+ size?: "default" | "sm" | "lg";
14
+ }) {
15
+ return (
16
+ <AvatarPrimitive.Root
17
+ data-slot="avatar"
18
+ data-size={size}
19
+ className={cn(
20
+ "group/avatar relative flex size-8 shrink-0 overflow-hidden rounded-full select-none data-[size=lg]:size-10 data-[size=sm]:size-6",
21
+ className,
22
+ )}
23
+ {...props}
24
+ />
25
+ );
26
+ }
27
+
28
+ function AvatarImage({ className, ...props }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
29
+ return (
30
+ <AvatarPrimitive.Image
31
+ data-slot="avatar-image"
32
+ className={cn("aspect-square size-full", className)}
33
+ {...props}
34
+ />
35
+ );
36
+ }
37
+
38
+ function AvatarFallback({
39
+ className,
40
+ ...props
41
+ }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
42
+ return (
43
+ <AvatarPrimitive.Fallback
44
+ data-slot="avatar-fallback"
45
+ className={cn(
46
+ "flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs",
47
+ className,
48
+ )}
49
+ {...props}
50
+ />
51
+ );
52
+ }
53
+
54
+ function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) {
55
+ return (
56
+ <span
57
+ data-slot="avatar-badge"
58
+ className={cn(
59
+ "absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground ring-2 ring-background select-none",
60
+ "group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden",
61
+ "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2",
62
+ "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2",
63
+ className,
64
+ )}
65
+ {...props}
66
+ />
67
+ );
68
+ }
69
+
70
+ function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) {
71
+ return (
72
+ <div
73
+ data-slot="avatar-group"
74
+ className={cn(
75
+ "group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background",
76
+ className,
77
+ )}
78
+ {...props}
79
+ />
80
+ );
81
+ }
82
+
83
+ function AvatarGroupCount({ className, ...props }: React.ComponentProps<"div">) {
84
+ return (
85
+ <div
86
+ data-slot="avatar-group-count"
87
+ className={cn(
88
+ "relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3",
89
+ className,
90
+ )}
91
+ {...props}
92
+ />
93
+ );
94
+ }
95
+
96
+ export { Avatar, AvatarImage, AvatarFallback, AvatarBadge, AvatarGroup, AvatarGroupCount };
@@ -0,0 +1,46 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { Slot } from "radix-ui";
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 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 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
13
+ secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
14
+ destructive:
15
+ "bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
16
+ outline:
17
+ "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
18
+ ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
19
+ link: "text-primary underline-offset-4 [a&]:hover:underline",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ variant: "default",
24
+ },
25
+ },
26
+ );
27
+
28
+ function Badge({
29
+ className,
30
+ variant = "default",
31
+ asChild = false,
32
+ ...props
33
+ }: React.ComponentProps<"span"> & VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
34
+ const Comp = asChild ? Slot.Root : "span";
35
+
36
+ return (
37
+ <Comp
38
+ data-slot="badge"
39
+ data-variant={variant}
40
+ className={cn(badgeVariants({ variant }), className)}
41
+ {...props}
42
+ />
43
+ );
44
+ }
45
+
46
+ export { Badge, badgeVariants };
@@ -0,0 +1,62 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { Slot } from "radix-ui";
4
+
5
+ import { cn } from "../lib/utils";
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex shrink-0 items-center justify-center gap-2 rounded-xl text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
15
+ outline:
16
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
17
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
18
+ ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
19
+ link: "text-primary underline-offset-4 hover:underline",
20
+ },
21
+ size: {
22
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
23
+ xs: "h-6 gap-1 rounded-xl px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
24
+ sm: "h-8 gap-1.5 rounded-xl px-3 has-[>svg]:px-2.5",
25
+ lg: "h-10 rounded-xl px-6 has-[>svg]:px-4",
26
+ icon: "size-9",
27
+ "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
28
+ "icon-sm": "size-8",
29
+ "icon-lg": "size-10",
30
+ },
31
+ },
32
+ defaultVariants: {
33
+ variant: "default",
34
+ size: "default",
35
+ },
36
+ },
37
+ );
38
+
39
+ function Button({
40
+ className,
41
+ variant = "default",
42
+ size = "default",
43
+ asChild = false,
44
+ ...props
45
+ }: React.ComponentProps<"button"> &
46
+ VariantProps<typeof buttonVariants> & {
47
+ asChild?: boolean;
48
+ }) {
49
+ const Comp = asChild ? Slot.Root : "button";
50
+
51
+ return (
52
+ <Comp
53
+ data-slot="button"
54
+ data-variant={variant}
55
+ data-size={size}
56
+ className={cn(buttonVariants({ variant, size, className }))}
57
+ {...props}
58
+ />
59
+ );
60
+ }
61
+
62
+ export { Button, buttonVariants };
@@ -0,0 +1,75 @@
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
+ "flex flex-col gap-6 rounded-2xl border bg-card py-6 text-card-foreground shadow-sm",
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
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
24
+ className,
25
+ )}
26
+ {...props}
27
+ />
28
+ );
29
+ }
30
+
31
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32
+ return (
33
+ <div
34
+ data-slot="card-title"
35
+ className={cn("leading-none font-semibold", className)}
36
+ {...props}
37
+ />
38
+ );
39
+ }
40
+
41
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42
+ return (
43
+ <div
44
+ data-slot="card-description"
45
+ className={cn("text-sm text-muted-foreground", className)}
46
+ {...props}
47
+ />
48
+ );
49
+ }
50
+
51
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52
+ return (
53
+ <div
54
+ data-slot="card-action"
55
+ className={cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className)}
56
+ {...props}
57
+ />
58
+ );
59
+ }
60
+
61
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
62
+ return <div data-slot="card-content" className={cn("px-6", className)} {...props} />;
63
+ }
64
+
65
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
66
+ return (
67
+ <div
68
+ data-slot="card-footer"
69
+ className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
70
+ {...props}
71
+ />
72
+ );
73
+ }
74
+
75
+ export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };