@hogoshi/ui 0.0.0-stage → 0.1.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.
Files changed (57) hide show
  1. package/package.json +63 -4
  2. package/src/components/avatar.tsx +30 -0
  3. package/src/components/badge.tsx +26 -0
  4. package/src/components/breadcrumb.tsx +37 -0
  5. package/src/components/button.tsx +58 -0
  6. package/src/components/calendar.tsx +244 -0
  7. package/src/components/card.tsx +18 -0
  8. package/src/components/chart.tsx +387 -0
  9. package/src/components/checkbox.tsx +26 -0
  10. package/src/components/code-block.tsx +118 -0
  11. package/src/components/collapsible.tsx +31 -0
  12. package/src/components/command.tsx +111 -0
  13. package/src/components/context-menu.tsx +150 -0
  14. package/src/components/counter.tsx +56 -0
  15. package/src/components/dialog.tsx +36 -0
  16. package/src/components/dropdown-menu.tsx +54 -0
  17. package/src/components/file-upload.tsx +381 -0
  18. package/src/components/glide.tsx +82 -0
  19. package/src/components/icon.tsx +28 -0
  20. package/src/components/image-viewer.tsx +370 -0
  21. package/src/components/input.tsx +187 -0
  22. package/src/components/kbd.tsx +14 -0
  23. package/src/components/logo-mark.ts +8 -0
  24. package/src/components/logo.tsx +36 -0
  25. package/src/components/navbar.tsx +144 -0
  26. package/src/components/otp.tsx +93 -0
  27. package/src/components/page-loader.tsx +37 -0
  28. package/src/components/pophover.tsx +28 -0
  29. package/src/components/popover.tsx +36 -0
  30. package/src/components/popup.tsx +75 -0
  31. package/src/components/progress.tsx +26 -0
  32. package/src/components/result.tsx +22 -0
  33. package/src/components/scroll-area.tsx +38 -0
  34. package/src/components/select.tsx +52 -0
  35. package/src/components/separator.tsx +14 -0
  36. package/src/components/sheet.tsx +96 -0
  37. package/src/components/sidebar.tsx +195 -0
  38. package/src/components/skeleton.tsx +14 -0
  39. package/src/components/slider.tsx +31 -0
  40. package/src/components/smooth-scroll.tsx +174 -0
  41. package/src/components/spinner.tsx +18 -0
  42. package/src/components/switch.tsx +22 -0
  43. package/src/components/table.tsx +52 -0
  44. package/src/components/tabs.tsx +48 -0
  45. package/src/components/theme-toggle.tsx +214 -0
  46. package/src/components/toaster.tsx +38 -0
  47. package/src/components/tooltip.tsx +43 -0
  48. package/src/components/video-player.tsx +559 -0
  49. package/src/components/video-settings.tsx +233 -0
  50. package/src/index.ts +102 -0
  51. package/src/lib/cn.ts +6 -0
  52. package/src/lib/jsx-highlight.ts +128 -0
  53. package/src/styles.css +443 -0
  54. package/src/theme/theme-script.ts +13 -0
  55. package/src/theme/theme.tsx +77 -0
  56. package/styles.css +1 -0
  57. package/README.md +0 -3
package/package.json CHANGED
@@ -1,6 +1,65 @@
1
1
  {
2
2
  "name": "@hogoshi/ui",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.1",
4
+ "type": "module",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/zhogoshi/ui.git",
8
+ "directory": "packages/ui"
9
+ },
10
+ "publishConfig": {
11
+ "access": "public",
12
+ "registry": "https://registry.npmjs.org/"
13
+ },
14
+ "exports": {
15
+ ".": {
16
+ "types": "./src/index.ts",
17
+ "default": "./src/index.ts"
18
+ },
19
+ "./styles.css": {
20
+ "style": "./styles.css",
21
+ "default": "./styles.css"
22
+ },
23
+ "./src/styles.css": {
24
+ "style": "./src/styles.css",
25
+ "default": "./src/styles.css"
26
+ },
27
+ "./brand": {
28
+ "types": "./src/components/logo-mark.ts",
29
+ "default": "./src/components/logo-mark.ts"
30
+ }
31
+ },
32
+ "files": [
33
+ "src",
34
+ "styles.css"
35
+ ],
36
+ "scripts": {
37
+ "typecheck": "tsc --noEmit"
38
+ },
39
+ "dependencies": {
40
+ "@base-ui/react": "^1.8.0",
41
+ "@hogoshi/tokens": "workspace:^",
42
+ "@hugeicons-pro/core-duotone-rounded": "^4.2.2",
43
+ "@hugeicons-pro/core-solid-rounded": "^4.2.2",
44
+ "@hugeicons-pro/core-stroke-rounded": "^4.2.2",
45
+ "@hugeicons/react": "^1.1.10",
46
+ "class-variance-authority": "^0.7.1",
47
+ "clsx": "^2.1.1",
48
+ "cmdk": "^1.1.1",
49
+ "highlight.js": "^11.11.1",
50
+ "recharts": "^3.10.1",
51
+ "sonner": "^2.0.7",
52
+ "tailwind-merge": "^2.6.0"
53
+ },
54
+ "peerDependencies": {
55
+ "react": "^19.0.0",
56
+ "react-dom": "^19.0.0"
57
+ },
58
+ "devDependencies": {
59
+ "@types/react": "^19.0.7",
60
+ "@types/react-dom": "^19.0.3",
61
+ "react": "^19.0.0",
62
+ "react-dom": "^19.0.0",
63
+ "typescript": "^5.7.3"
64
+ }
65
+ }
@@ -0,0 +1,30 @@
1
+ "use client";
2
+
3
+ import { Avatar as BaseAvatar } from "@base-ui/react/avatar";
4
+ import type { ComponentProps } from "react";
5
+ import { cn } from "../lib/cn";
6
+
7
+ export function Avatar({ className, ...props }: ComponentProps<typeof BaseAvatar.Root>) {
8
+ return (
9
+ <BaseAvatar.Root
10
+ className={cn(
11
+ "bg-bg-muted relative inline-flex size-8 shrink-0 overflow-hidden rounded-full",
12
+ className,
13
+ )}
14
+ {...props}
15
+ />
16
+ );
17
+ }
18
+
19
+ export function AvatarImage({ className, ...props }: ComponentProps<typeof BaseAvatar.Image>) {
20
+ return <BaseAvatar.Image className={cn("size-full object-cover", className)} {...props} />;
21
+ }
22
+
23
+ export function AvatarFallback({ className, ...props }: ComponentProps<typeof BaseAvatar.Fallback>) {
24
+ return (
25
+ <BaseAvatar.Fallback
26
+ className={cn("text-fg-muted flex size-full items-center justify-center text-xs font-medium", className)}
27
+ {...props}
28
+ />
29
+ );
30
+ }
@@ -0,0 +1,26 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+ import type { ComponentProps } from "react";
3
+ import { cn } from "../lib/cn";
4
+
5
+ const badgeVariants = cva(
6
+ "inline-flex items-center gap-1 rounded-[var(--radius-sm)] px-1.5 py-0.5 text-[11px] leading-tight font-medium",
7
+ {
8
+ variants: {
9
+ tone: {
10
+ neutral: "bg-bg-muted text-fg-muted",
11
+ accent: "bg-accent-muted text-accent",
12
+ success: "bg-success/15 text-success",
13
+ warning: "bg-warning/15 text-warning",
14
+ danger: "bg-danger/15 text-danger",
15
+ outline: "border-border text-fg-muted border bg-transparent",
16
+ },
17
+ },
18
+ defaultVariants: { tone: "neutral" },
19
+ },
20
+ );
21
+
22
+ export interface BadgeProps extends ComponentProps<"span">, VariantProps<typeof badgeVariants> {}
23
+
24
+ export function Badge({ className, tone, ...props }: BadgeProps) {
25
+ return <span className={cn(badgeVariants({ tone }), className)} {...props} />;
26
+ }
@@ -0,0 +1,37 @@
1
+ import type { ReactNode } from "react";
2
+ import { cn } from "../lib/cn";
3
+
4
+ export interface BreadcrumbItem {
5
+ label: string;
6
+ href?: string;
7
+ }
8
+
9
+ export function Breadcrumb({
10
+ items,
11
+ separator = "/",
12
+ className,
13
+ }: {
14
+ items: BreadcrumbItem[];
15
+ separator?: ReactNode;
16
+ className?: string;
17
+ }) {
18
+ return (
19
+ <nav aria-label="Breadcrumb" className={cn("text-fg-muted flex min-w-0 items-center gap-1.5 text-sm", className)}>
20
+ {items.map((item, index) => {
21
+ const current = index === items.length - 1;
22
+ return (
23
+ <span key={`${item.label}-${index}`} className="flex min-w-0 items-center gap-1.5">
24
+ {index > 0 ? <span className="text-fg-subtle shrink-0">{separator}</span> : null}
25
+ {item.href && !current ? (
26
+ <a href={item.href} className="hover:text-fg truncate rounded-[var(--radius-sm)] outline-none focus-visible:ring-2 focus-visible:ring-ring">
27
+ {item.label}
28
+ </a>
29
+ ) : (
30
+ <span className={cn("truncate", current && "text-fg font-medium")}>{item.label}</span>
31
+ )}
32
+ </span>
33
+ );
34
+ })}
35
+ </nav>
36
+ );
37
+ }
@@ -0,0 +1,58 @@
1
+ "use client";
2
+
3
+ import { Button as BaseButton } from "@base-ui/react/button";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+ import type { ComponentProps } from "react";
6
+ import { cn } from "../lib/cn";
7
+ import { Spinner } from "./spinner";
8
+
9
+ export const buttonVariants = cva(
10
+ "inline-flex cursor-pointer items-center justify-center gap-1.5 font-medium whitespace-nowrap select-none transition-[background-color,border-color,color,box-shadow,transform] duration-[var(--motion-duration)] ease-[var(--motion-ease)] focus-visible:ring-ring focus-visible:ring-offset-0 focus-visible:ring-2 focus-visible:outline-none active:scale-[0.98] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 motion-reduce:transform-none",
11
+ {
12
+ variants: {
13
+ variant: {
14
+ primary: "bg-accent text-accent-fg hover:bg-accent-hover",
15
+ secondary:
16
+ "border-border bg-bg-elevated text-fg hover:border-border-strong hover:bg-bg-subtle border",
17
+ ghost: "text-fg-muted hover:bg-bg-subtle hover:text-fg",
18
+ danger: "bg-danger text-[oklch(99%_0_0)] hover:opacity-90",
19
+ outline: "border-border-strong text-fg hover:bg-bg-subtle border bg-transparent",
20
+ link: "text-accent h-auto px-0 underline-offset-4 hover:underline",
21
+ },
22
+ size: {
23
+ sm: "h-7 rounded-[var(--radius-sm)] px-2.5 text-xs",
24
+ md: "rounded-[var(--radius-md)] px-3.5 text-sm [height:var(--row-h)]",
25
+ lg: "h-10 rounded-[var(--radius-md)] px-5 text-sm",
26
+ icon: "size-8 rounded-[var(--radius-md)]",
27
+ "icon-sm": "size-7 rounded-[var(--radius-sm)]",
28
+ },
29
+ },
30
+ defaultVariants: { variant: "secondary", size: "md" },
31
+ },
32
+ );
33
+
34
+ export interface ButtonProps
35
+ extends ComponentProps<typeof BaseButton>,
36
+ VariantProps<typeof buttonVariants> {
37
+ loading?: boolean;
38
+ }
39
+
40
+ export function Button({ className, variant, size, loading, disabled, render, children, ...props }: ButtonProps) {
41
+ return (
42
+ <BaseButton
43
+ className={cn(buttonVariants({ variant, size }), className)}
44
+ disabled={disabled || loading}
45
+ aria-busy={loading || undefined}
46
+ {...props}
47
+ render={render}
48
+ nativeButton={render ? false : true}
49
+ >
50
+ <span className="grid place-items-center">
51
+ <span aria-hidden={loading || undefined} className={loading ? "invisible col-start-1 row-start-1" : "col-start-1 row-start-1"}>
52
+ {children}
53
+ </span>
54
+ {loading ? <Spinner size="sm" className="col-start-1 row-start-1" /> : null}
55
+ </span>
56
+ </BaseButton>
57
+ );
58
+ }
@@ -0,0 +1,244 @@
1
+ "use client";
2
+
3
+ import { ArrowLeft01Icon, ArrowRight01Icon } from "@hugeicons-pro/core-stroke-rounded";
4
+ import { useMemo, useState } from "react";
5
+ import { cn } from "../lib/cn";
6
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./dropdown-menu";
7
+ import { Icon } from "./icon";
8
+
9
+ const WEEKDAYS = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
10
+ const MONTHS = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
11
+ const YEAR_MIN = 1920;
12
+ const FUTURE_YEAR = 2030;
13
+
14
+ export interface CalendarRange {
15
+ from?: Date;
16
+ to?: Date;
17
+ }
18
+
19
+ function startOfMonth(date: Date) {
20
+ return new Date(date.getFullYear(), date.getMonth(), 1);
21
+ }
22
+
23
+ function dayStamp(date: Date) {
24
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
25
+ }
26
+
27
+ function sameDay(a: Date, b: Date) {
28
+ return dayStamp(a) === dayStamp(b);
29
+ }
30
+
31
+ const CELL = 100 / 7;
32
+ const glide =
33
+ "absolute h-8 rounded-[var(--radius-sm)] transition-[left,top,width,opacity] duration-[calc(var(--motion-duration)*1.6)] ease-[var(--motion-ease)] motion-reduce:transition-none";
34
+
35
+ const rowTop = (index: number) => `calc(${Math.floor(index / 7)} * 2.25rem + 0.125rem)`;
36
+
37
+ function Selection({ marks, band }: { marks: number[]; band: readonly [number, number] | null }) {
38
+ return (
39
+ <div aria-hidden className="pointer-events-none absolute inset-0">
40
+ {Array.from({ length: 6 }, (_, row) => {
41
+ const start = band ? Math.floor(band[0] / 7) : -1;
42
+ const end = band ? Math.floor(band[1] / 7) : -1;
43
+ const on = Boolean(band) && row >= start && row <= end;
44
+ const from = on ? Math.max(band![0], row * 7) % 7 : 0;
45
+ const to = on ? Math.min(band![1], row * 7 + 6) % 7 : 0;
46
+ return (
47
+ <span
48
+ key={row}
49
+ className={cn(glide, "bg-accent-muted")}
50
+ style={{
51
+ top: rowTop(row * 7),
52
+ left: `${from * CELL}%`,
53
+ width: on ? `${(to - from + 1) * CELL}%` : 0,
54
+ opacity: on ? 1 : 0,
55
+ }}
56
+ />
57
+ );
58
+ })}
59
+ {[0, 1].map((slot) => {
60
+ const mark = marks[slot] ?? marks[0] ?? 0;
61
+ const on = marks[slot] != null;
62
+ return (
63
+ <span
64
+ key={slot}
65
+ className={cn(glide, "bg-accent w-8")}
66
+ style={{
67
+ top: rowTop(mark),
68
+ left: `calc(${(mark % 7) * CELL}% + (${CELL}% - 2rem) / 2)`,
69
+ opacity: on ? 1 : 0,
70
+ }}
71
+ />
72
+ );
73
+ })}
74
+ </div>
75
+ );
76
+ }
77
+
78
+ export function Calendar({
79
+ value,
80
+ onChange,
81
+ mode = "single",
82
+ range,
83
+ onRangeChange,
84
+ allowPast = true,
85
+ allowFuture = true,
86
+ className,
87
+ }: {
88
+ value?: Date;
89
+ onChange?: (date: Date) => void;
90
+ mode?: "single" | "range";
91
+ range?: CalendarRange;
92
+ onRangeChange?: (range: CalendarRange) => void;
93
+ allowPast?: boolean;
94
+ allowFuture?: boolean;
95
+ className?: string;
96
+ }) {
97
+ const [cursor, setCursor] = useState(() => startOfMonth(value ?? range?.from ?? new Date()));
98
+ const [direction, setDirection] = useState<1 | -1>(1);
99
+ const today = dayStamp(new Date());
100
+ const yearCap = allowFuture ? FUTURE_YEAR : new Date().getFullYear();
101
+ const years = useMemo(() => {
102
+ const list: number[] = [];
103
+ for (let year = yearCap; year >= YEAR_MIN; year -= 1) list.push(year);
104
+ return list;
105
+ }, [yearCap]);
106
+
107
+ const cells = useMemo(() => {
108
+ const first = (startOfMonth(cursor).getDay() + 6) % 7;
109
+ const count = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 0).getDate();
110
+ return [...Array.from({ length: first }, () => null), ...Array.from({ length: count }, (_, index) => index + 1)];
111
+ }, [cursor]);
112
+
113
+ const selection = useMemo(() => {
114
+ const first = cells.indexOf(1);
115
+ const start = dayStamp(cursor);
116
+ const end = dayStamp(new Date(cursor.getFullYear(), cursor.getMonth() + 1, 0));
117
+ const indexOf = (stamp: number) => first + new Date(stamp).getDate() - 1;
118
+ const visible = (date?: Date) => (date && dayStamp(date) >= start && dayStamp(date) <= end ? indexOf(dayStamp(date)) : null);
119
+ const picked = mode === "single" ? [value] : [range?.from, range?.to];
120
+ const marks = [...new Set(picked.map(visible).filter((mark): mark is number => mark !== null))];
121
+ if (mode !== "range" || !range?.from || !range.to || sameDay(range.from, range.to)) return { marks, band: null };
122
+ const from = Math.max(dayStamp(range.from), start);
123
+ const to = Math.min(dayStamp(range.to), end);
124
+ if (from > to) return { marks, band: null };
125
+ return { marks, band: [indexOf(from), indexOf(to)] as const };
126
+ }, [cells, cursor, mode, range, value]);
127
+
128
+ const move = (next: Date, dir: 1 | -1) => {
129
+ setDirection(dir);
130
+ setCursor(startOfMonth(next));
131
+ };
132
+
133
+ const blocked = (day: number) => {
134
+ const stamp = dayStamp(new Date(cursor.getFullYear(), cursor.getMonth(), day));
135
+ if (!allowPast && stamp < today) return true;
136
+ if (!allowFuture && stamp > today) return true;
137
+ return false;
138
+ };
139
+
140
+ const pick = (day: number) => {
141
+ if (blocked(day)) return;
142
+ const date = new Date(cursor.getFullYear(), cursor.getMonth(), day);
143
+ if (mode === "single") {
144
+ onChange?.(date);
145
+ return;
146
+ }
147
+ const current = range ?? {};
148
+ if (!current.from || current.to) {
149
+ onRangeChange?.({ from: date, to: undefined });
150
+ return;
151
+ }
152
+ if (dayStamp(date) < dayStamp(current.from)) onRangeChange?.({ from: date, to: current.from });
153
+ else onRangeChange?.({ from: current.from, to: date });
154
+ };
155
+
156
+ return (
157
+ <div className={cn("bg-bg-elevated border-border w-72 rounded-[var(--radius-lg)] border p-3", className)}>
158
+ <div className="mb-2 flex items-center justify-between gap-1">
159
+ <button
160
+ type="button"
161
+ className="text-fg-muted hover:bg-bg-subtle hover:text-fg inline-flex size-7 items-center justify-center rounded-[var(--radius-sm)]"
162
+ onClick={() => move(new Date(cursor.getFullYear(), cursor.getMonth() - 1, 1), -1)}
163
+ aria-label="Previous month"
164
+ >
165
+ <Icon icon={ArrowLeft01Icon} variant="stroke" />
166
+ </button>
167
+ <div className="flex items-center gap-1">
168
+ <DropdownMenu>
169
+ <DropdownMenuTrigger className="hover:bg-bg-subtle rounded-[var(--radius-sm)] px-2 py-1 text-sm font-semibold">
170
+ {MONTHS[cursor.getMonth()]}
171
+ </DropdownMenuTrigger>
172
+ <DropdownMenuContent className="max-h-64 overflow-y-auto">
173
+ {MONTHS.map((month, index) => (
174
+ <DropdownMenuItem key={month} onClick={() => move(new Date(cursor.getFullYear(), index, 1), index >= cursor.getMonth() ? 1 : -1)}>
175
+ {month}
176
+ </DropdownMenuItem>
177
+ ))}
178
+ </DropdownMenuContent>
179
+ </DropdownMenu>
180
+ <DropdownMenu>
181
+ <DropdownMenuTrigger className="hover:bg-bg-subtle rounded-[var(--radius-sm)] px-2 py-1 text-sm font-semibold">
182
+ {cursor.getFullYear()}
183
+ </DropdownMenuTrigger>
184
+ <DropdownMenuContent className="max-h-64 overflow-y-auto">
185
+ {years.map((year) => (
186
+ <DropdownMenuItem key={year} onClick={() => move(new Date(year, cursor.getMonth(), 1), year >= cursor.getFullYear() ? 1 : -1)}>
187
+ {year}
188
+ </DropdownMenuItem>
189
+ ))}
190
+ </DropdownMenuContent>
191
+ </DropdownMenu>
192
+ </div>
193
+ <button
194
+ type="button"
195
+ className="text-fg-muted hover:bg-bg-subtle hover:text-fg inline-flex size-7 items-center justify-center rounded-[var(--radius-sm)]"
196
+ onClick={() => move(new Date(cursor.getFullYear(), cursor.getMonth() + 1, 1), 1)}
197
+ aria-label="Next month"
198
+ >
199
+ <Icon icon={ArrowRight01Icon} variant="stroke" />
200
+ </button>
201
+ </div>
202
+ <div
203
+ key={`${cursor.getFullYear()}-${cursor.getMonth()}`}
204
+ className={cn(
205
+ "text-center",
206
+ direction > 0 ? "animate-[month-next_var(--motion-duration)_var(--motion-ease)]" : "animate-[month-prev_var(--motion-duration)_var(--motion-ease)]",
207
+ )}
208
+ >
209
+ <div className="grid grid-cols-7">
210
+ {WEEKDAYS.map((day) => (
211
+ <span key={day} className="text-fg-muted text-[11px]">
212
+ {day}
213
+ </span>
214
+ ))}
215
+ </div>
216
+ <div className="relative grid grid-cols-7">
217
+ <Selection
218
+ marks={selection.marks}
219
+ band={selection.band}
220
+ />
221
+ {cells.map((day, index) => {
222
+ if (day === null) return <span key={`empty-${index}`} />;
223
+ const marked = selection.marks.includes(index);
224
+ return (
225
+ <div key={day} className="flex h-9 items-center justify-center">
226
+ <button
227
+ type="button"
228
+ disabled={blocked(day)}
229
+ className={cn(
230
+ "relative flex size-8 cursor-pointer items-center justify-center rounded-[var(--radius-sm)] text-sm transition-colors duration-[var(--motion-duration)] disabled:cursor-not-allowed disabled:opacity-40",
231
+ marked ? "text-accent-fg" : "hover:bg-bg-subtle",
232
+ )}
233
+ onClick={() => pick(day)}
234
+ >
235
+ {day}
236
+ </button>
237
+ </div>
238
+ );
239
+ })}
240
+ </div>
241
+ </div>
242
+ </div>
243
+ );
244
+ }
@@ -0,0 +1,18 @@
1
+ import type { ComponentProps } from "react";
2
+ import { cn } from "../lib/cn";
3
+
4
+ export function Card({ className, ...props }: ComponentProps<"div">) {
5
+ return <div className={cn("bg-bg-elevated border-border rounded-[var(--radius-lg)] border p-4", className)} {...props} />;
6
+ }
7
+
8
+ export function CardHeader({ className, ...props }: ComponentProps<"div">) {
9
+ return <div className={cn("mb-3 grid gap-1", className)} {...props} />;
10
+ }
11
+
12
+ export function CardTitle({ className, ...props }: ComponentProps<"h3">) {
13
+ return <h3 className={cn("text-sm font-semibold", className)} {...props} />;
14
+ }
15
+
16
+ export function CardDescription({ className, ...props }: ComponentProps<"p">) {
17
+ return <p className={cn("text-fg-muted text-xs", className)} {...props} />;
18
+ }