@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 +171 -0
- package/components/accordion.tsx +64 -0
- package/components/alert-dialog.tsx +179 -0
- package/components/avatar.tsx +96 -0
- package/components/badge.tsx +46 -0
- package/components/button.tsx +62 -0
- package/components/card.tsx +75 -0
- package/components/checkbox.tsx +29 -0
- package/components/dialog.tsx +144 -0
- package/components/dropdown-menu.tsx +228 -0
- package/components/input.tsx +21 -0
- package/components/label.tsx +21 -0
- package/components/pill-tabs.tsx +66 -0
- package/components/progress.tsx +28 -0
- package/components/radio-group.tsx +45 -0
- package/components/scroll-area.tsx +56 -0
- package/components/select.tsx +175 -0
- package/components/separator.tsx +28 -0
- package/components/sheet.tsx +134 -0
- package/components/skeleton.tsx +13 -0
- package/components/sonner.tsx +40 -0
- package/components/switch.tsx +28 -0
- package/components/table.tsx +92 -0
- package/components/tabs.tsx +81 -0
- package/components/textarea.tsx +18 -0
- package/components/tooltip.tsx +53 -0
- package/lib/utils.ts +12 -0
- package/package.json +85 -0
- package/tokens.css +91 -0
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 };
|