@kinetixui/ui 0.1.0
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/LICENSE +21 -0
- package/dist/index.d.ts +1176 -0
- package/dist/index.js +3939 -0
- package/package.json +106 -0
- package/src/components/accordion.tsx +52 -0
- package/src/components/alert-dialog.tsx +115 -0
- package/src/components/alert.tsx +47 -0
- package/src/components/aspect-ratio.tsx +7 -0
- package/src/components/audio-player.tsx +150 -0
- package/src/components/avatar.tsx +76 -0
- package/src/components/badge.tsx +40 -0
- package/src/components/breadcrumb.tsx +77 -0
- package/src/components/button.tsx +119 -0
- package/src/components/calendar.tsx +60 -0
- package/src/components/card.tsx +50 -0
- package/src/components/carousel.tsx +181 -0
- package/src/components/chart.tsx +177 -0
- package/src/components/checkbox.tsx +38 -0
- package/src/components/circular-progress.tsx +67 -0
- package/src/components/code-block.tsx +96 -0
- package/src/components/collapsible.tsx +9 -0
- package/src/components/command.tsx +123 -0
- package/src/components/context-menu.tsx +132 -0
- package/src/components/data-table.tsx +92 -0
- package/src/components/date-picker.tsx +68 -0
- package/src/components/dialog.tsx +101 -0
- package/src/components/drawer.tsx +82 -0
- package/src/components/dropdown-menu.tsx +132 -0
- package/src/components/fab.tsx +58 -0
- package/src/components/field.tsx +128 -0
- package/src/components/file-upload.tsx +183 -0
- package/src/components/footer.tsx +62 -0
- package/src/components/form.tsx +130 -0
- package/src/components/hover-card.tsx +28 -0
- package/src/components/image.tsx +87 -0
- package/src/components/inform.tsx +82 -0
- package/src/components/input-group.tsx +96 -0
- package/src/components/input-otp.tsx +63 -0
- package/src/components/input.tsx +72 -0
- package/src/components/label.tsx +20 -0
- package/src/components/list.tsx +69 -0
- package/src/components/menubar.tsx +168 -0
- package/src/components/metric.tsx +51 -0
- package/src/components/modal.tsx +126 -0
- package/src/components/navigation-bar.tsx +49 -0
- package/src/components/navigation-menu.tsx +117 -0
- package/src/components/number-input.tsx +86 -0
- package/src/components/pagination.tsx +77 -0
- package/src/components/password-input.tsx +36 -0
- package/src/components/popover.tsx +32 -0
- package/src/components/progress.tsx +24 -0
- package/src/components/quote.tsx +34 -0
- package/src/components/radio-group.tsx +44 -0
- package/src/components/rating.tsx +88 -0
- package/src/components/resizable.tsx +40 -0
- package/src/components/scroll-area.tsx +39 -0
- package/src/components/select.tsx +124 -0
- package/src/components/separator.tsx +25 -0
- package/src/components/sheet.tsx +104 -0
- package/src/components/sidebar.tsx +390 -0
- package/src/components/skeleton.tsx +9 -0
- package/src/components/slider.tsx +24 -0
- package/src/components/sonner.tsx +30 -0
- package/src/components/spinner.tsx +43 -0
- package/src/components/stepper.tsx +75 -0
- package/src/components/switch.tsx +37 -0
- package/src/components/tab-bar.tsx +58 -0
- package/src/components/table-of-contents.tsx +46 -0
- package/src/components/table.tsx +70 -0
- package/src/components/tabs.tsx +54 -0
- package/src/components/tag.tsx +56 -0
- package/src/components/textarea.tsx +56 -0
- package/src/components/toggle-group.tsx +41 -0
- package/src/components/toggle.tsx +34 -0
- package/src/components/tooltip.tsx +31 -0
- package/src/index.ts +305 -0
- package/src/lib/utils.ts +7 -0
- package/src/stories/Button.stories.tsx +155 -0
- package/src/stories/Input.stories.tsx +80 -0
- package/src/stories/Textarea.stories.tsx +69 -0
- package/tailwind.config.ts +107 -0
package/package.json
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kinetixui/ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "KinetixUI React components — CVA + Radix + Tailwind on the KinetixUI token contract. Also distributed via the kinetixui CLI as a component registry.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"react",
|
|
8
|
+
"components",
|
|
9
|
+
"ui",
|
|
10
|
+
"design-system",
|
|
11
|
+
"radix-ui",
|
|
12
|
+
"tailwindcss",
|
|
13
|
+
"kinetixui"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "KinetixUI",
|
|
17
|
+
"homepage": "https://kinetixui.com",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/ziadfteha/kinetixui.git",
|
|
21
|
+
"directory": "packages/ui"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"exports": {
|
|
26
|
+
".": {
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"import": "./dist/index.js"
|
|
29
|
+
},
|
|
30
|
+
"./tailwind.config": "./tailwind.config.ts",
|
|
31
|
+
"./package.json": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"src",
|
|
39
|
+
"tailwind.config.ts"
|
|
40
|
+
],
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@hookform/resolvers": "^3.9.1",
|
|
43
|
+
"@radix-ui/react-accordion": "^1.2.2",
|
|
44
|
+
"@radix-ui/react-alert-dialog": "^1.1.4",
|
|
45
|
+
"@radix-ui/react-aspect-ratio": "^1.1.1",
|
|
46
|
+
"@radix-ui/react-avatar": "^1.1.2",
|
|
47
|
+
"@radix-ui/react-checkbox": "^1.1.3",
|
|
48
|
+
"@radix-ui/react-collapsible": "^1.1.2",
|
|
49
|
+
"@radix-ui/react-context-menu": "^2.2.4",
|
|
50
|
+
"@radix-ui/react-dialog": "^1.1.4",
|
|
51
|
+
"@radix-ui/react-dropdown-menu": "^2.1.4",
|
|
52
|
+
"@radix-ui/react-hover-card": "^1.1.4",
|
|
53
|
+
"@radix-ui/react-label": "^2.1.1",
|
|
54
|
+
"@radix-ui/react-menubar": "^1.1.4",
|
|
55
|
+
"@radix-ui/react-navigation-menu": "^1.2.3",
|
|
56
|
+
"@radix-ui/react-popover": "^1.1.4",
|
|
57
|
+
"@radix-ui/react-progress": "^1.1.1",
|
|
58
|
+
"@radix-ui/react-radio-group": "^1.2.2",
|
|
59
|
+
"@radix-ui/react-scroll-area": "^1.2.2",
|
|
60
|
+
"@radix-ui/react-select": "^2.1.4",
|
|
61
|
+
"@radix-ui/react-separator": "^1.1.1",
|
|
62
|
+
"@radix-ui/react-slider": "^1.2.2",
|
|
63
|
+
"@radix-ui/react-slot": "^1.1.1",
|
|
64
|
+
"@radix-ui/react-switch": "^1.1.2",
|
|
65
|
+
"@radix-ui/react-tabs": "^1.1.2",
|
|
66
|
+
"@radix-ui/react-toggle": "^1.1.1",
|
|
67
|
+
"@radix-ui/react-toggle-group": "^1.1.1",
|
|
68
|
+
"@radix-ui/react-tooltip": "^1.1.6",
|
|
69
|
+
"@tanstack/react-table": "^8.20.6",
|
|
70
|
+
"class-variance-authority": "^0.7.1",
|
|
71
|
+
"clsx": "^2.1.1",
|
|
72
|
+
"cmdk": "^1.0.4",
|
|
73
|
+
"date-fns": "^3.6.0",
|
|
74
|
+
"embla-carousel-react": "^8.5.1",
|
|
75
|
+
"input-otp": "^1.4.1",
|
|
76
|
+
"lucide-react": "^0.468.0",
|
|
77
|
+
"react-day-picker": "8.10.1",
|
|
78
|
+
"react-hook-form": "^7.54.1",
|
|
79
|
+
"react-resizable-panels": "^2.1.7",
|
|
80
|
+
"recharts": "^2.15.0",
|
|
81
|
+
"sonner": "^1.7.1",
|
|
82
|
+
"tailwind-merge": "^2.6.0",
|
|
83
|
+
"tailwindcss-animate": "^1.0.7",
|
|
84
|
+
"vaul": "^1.1.2",
|
|
85
|
+
"zod": "^3.24.1"
|
|
86
|
+
},
|
|
87
|
+
"peerDependencies": {
|
|
88
|
+
"react": ">=18",
|
|
89
|
+
"react-dom": ">=18"
|
|
90
|
+
},
|
|
91
|
+
"devDependencies": {
|
|
92
|
+
"@kinetixui/tokens": "0.1.0",
|
|
93
|
+
"@types/react": "^18.3.17",
|
|
94
|
+
"@types/react-dom": "^18.3.5",
|
|
95
|
+
"react": "^18.3.1",
|
|
96
|
+
"react-dom": "^18.3.1",
|
|
97
|
+
"tailwindcss": "^3.4.17",
|
|
98
|
+
"tsup": "^8.3.5",
|
|
99
|
+
"typescript": "^5.7.2"
|
|
100
|
+
},
|
|
101
|
+
"scripts": {
|
|
102
|
+
"build": "tsup src/index.ts --format esm --dts --external react --external react-dom",
|
|
103
|
+
"lint": "eslint src",
|
|
104
|
+
"typecheck": "tsc --noEmit"
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
|
5
|
+
import { ChevronDown } from "lucide-react";
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
|
|
8
|
+
const Accordion = AccordionPrimitive.Root;
|
|
9
|
+
|
|
10
|
+
const AccordionItem = React.forwardRef<
|
|
11
|
+
React.ElementRef<typeof AccordionPrimitive.Item>,
|
|
12
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
|
13
|
+
>(({ className, ...props }, ref) => (
|
|
14
|
+
<AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />
|
|
15
|
+
));
|
|
16
|
+
AccordionItem.displayName = "AccordionItem";
|
|
17
|
+
|
|
18
|
+
const AccordionTrigger = React.forwardRef<
|
|
19
|
+
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
|
20
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
|
21
|
+
>(({ className, children, ...props }, ref) => (
|
|
22
|
+
<AccordionPrimitive.Header className="flex">
|
|
23
|
+
<AccordionPrimitive.Trigger
|
|
24
|
+
ref={ref}
|
|
25
|
+
className={cn(
|
|
26
|
+
"flex flex-1 items-center justify-between py-4 text-sm font-medium font-sans transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
|
27
|
+
className,
|
|
28
|
+
)}
|
|
29
|
+
{...props}
|
|
30
|
+
>
|
|
31
|
+
{children}
|
|
32
|
+
<ChevronDown className="size-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
|
33
|
+
</AccordionPrimitive.Trigger>
|
|
34
|
+
</AccordionPrimitive.Header>
|
|
35
|
+
));
|
|
36
|
+
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
|
|
37
|
+
|
|
38
|
+
const AccordionContent = React.forwardRef<
|
|
39
|
+
React.ElementRef<typeof AccordionPrimitive.Content>,
|
|
40
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
|
41
|
+
>(({ className, children, ...props }, ref) => (
|
|
42
|
+
<AccordionPrimitive.Content
|
|
43
|
+
ref={ref}
|
|
44
|
+
className="overflow-hidden text-sm font-sans data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
|
45
|
+
{...props}
|
|
46
|
+
>
|
|
47
|
+
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
|
48
|
+
</AccordionPrimitive.Content>
|
|
49
|
+
));
|
|
50
|
+
AccordionContent.displayName = AccordionPrimitive.Content.displayName;
|
|
51
|
+
|
|
52
|
+
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
import { buttonVariants } from "./button";
|
|
7
|
+
|
|
8
|
+
const AlertDialog = AlertDialogPrimitive.Root;
|
|
9
|
+
const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
|
|
10
|
+
const AlertDialogPortal = AlertDialogPrimitive.Portal;
|
|
11
|
+
|
|
12
|
+
const AlertDialogOverlay = React.forwardRef<
|
|
13
|
+
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
|
|
14
|
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
|
15
|
+
>(({ className, ...props }, ref) => (
|
|
16
|
+
<AlertDialogPrimitive.Overlay
|
|
17
|
+
ref={ref}
|
|
18
|
+
className={cn(
|
|
19
|
+
"fixed inset-0 z-50 bg-foreground/40 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
20
|
+
className,
|
|
21
|
+
)}
|
|
22
|
+
{...props}
|
|
23
|
+
/>
|
|
24
|
+
));
|
|
25
|
+
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
|
|
26
|
+
|
|
27
|
+
const AlertDialogContent = React.forwardRef<
|
|
28
|
+
React.ElementRef<typeof AlertDialogPrimitive.Content>,
|
|
29
|
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
|
30
|
+
>(({ className, ...props }, ref) => (
|
|
31
|
+
<AlertDialogPortal>
|
|
32
|
+
<AlertDialogOverlay />
|
|
33
|
+
<AlertDialogPrimitive.Content
|
|
34
|
+
ref={ref}
|
|
35
|
+
className={cn(
|
|
36
|
+
"fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg sm:rounded-xl font-sans",
|
|
37
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95",
|
|
38
|
+
className,
|
|
39
|
+
)}
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
</AlertDialogPortal>
|
|
43
|
+
));
|
|
44
|
+
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
|
|
45
|
+
|
|
46
|
+
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
47
|
+
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
|
|
48
|
+
);
|
|
49
|
+
AlertDialogHeader.displayName = "AlertDialogHeader";
|
|
50
|
+
|
|
51
|
+
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
|
52
|
+
<div
|
|
53
|
+
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
AlertDialogFooter.displayName = "AlertDialogFooter";
|
|
58
|
+
|
|
59
|
+
const AlertDialogTitle = React.forwardRef<
|
|
60
|
+
React.ElementRef<typeof AlertDialogPrimitive.Title>,
|
|
61
|
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
|
62
|
+
>(({ className, ...props }, ref) => (
|
|
63
|
+
<AlertDialogPrimitive.Title ref={ref} className={cn("text-lg font-semibold", className)} {...props} />
|
|
64
|
+
));
|
|
65
|
+
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
|
|
66
|
+
|
|
67
|
+
const AlertDialogDescription = React.forwardRef<
|
|
68
|
+
React.ElementRef<typeof AlertDialogPrimitive.Description>,
|
|
69
|
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
|
70
|
+
>(({ className, ...props }, ref) => (
|
|
71
|
+
<AlertDialogPrimitive.Description
|
|
72
|
+
ref={ref}
|
|
73
|
+
className={cn("text-sm text-muted-foreground", className)}
|
|
74
|
+
{...props}
|
|
75
|
+
/>
|
|
76
|
+
));
|
|
77
|
+
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
|
|
78
|
+
|
|
79
|
+
const AlertDialogAction = React.forwardRef<
|
|
80
|
+
React.ElementRef<typeof AlertDialogPrimitive.Action>,
|
|
81
|
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
|
82
|
+
>(({ className, ...props }, ref) => (
|
|
83
|
+
<AlertDialogPrimitive.Action
|
|
84
|
+
ref={ref}
|
|
85
|
+
className={cn(buttonVariants({ variant: "Primary", size: "md" }), className)}
|
|
86
|
+
{...props}
|
|
87
|
+
/>
|
|
88
|
+
));
|
|
89
|
+
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
|
|
90
|
+
|
|
91
|
+
const AlertDialogCancel = React.forwardRef<
|
|
92
|
+
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
|
|
93
|
+
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
|
94
|
+
>(({ className, ...props }, ref) => (
|
|
95
|
+
<AlertDialogPrimitive.Cancel
|
|
96
|
+
ref={ref}
|
|
97
|
+
className={cn(buttonVariants({ variant: "Outline", size: "md" }), "mt-2 sm:mt-0", className)}
|
|
98
|
+
{...props}
|
|
99
|
+
/>
|
|
100
|
+
));
|
|
101
|
+
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
|
|
102
|
+
|
|
103
|
+
export {
|
|
104
|
+
AlertDialog,
|
|
105
|
+
AlertDialogPortal,
|
|
106
|
+
AlertDialogOverlay,
|
|
107
|
+
AlertDialogTrigger,
|
|
108
|
+
AlertDialogContent,
|
|
109
|
+
AlertDialogHeader,
|
|
110
|
+
AlertDialogFooter,
|
|
111
|
+
AlertDialogTitle,
|
|
112
|
+
AlertDialogDescription,
|
|
113
|
+
AlertDialogAction,
|
|
114
|
+
AlertDialogCancel,
|
|
115
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
|
|
7
|
+
const alertVariants = cva(
|
|
8
|
+
"relative w-full rounded-lg border px-4 py-3 text-sm font-sans [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: "bg-background text-foreground",
|
|
13
|
+
destructive:
|
|
14
|
+
"border-destructive/50 text-destructive [&>svg]:text-destructive",
|
|
15
|
+
success: "border-success/50 text-success [&>svg]:text-success",
|
|
16
|
+
warning: "border-warning/50 text-warning [&>svg]:text-warning",
|
|
17
|
+
info: "border-info/50 text-info [&>svg]:text-info",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultVariants: { variant: "default" },
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const Alert = React.forwardRef<
|
|
25
|
+
HTMLDivElement,
|
|
26
|
+
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
|
|
27
|
+
>(({ className, variant, ...props }, ref) => (
|
|
28
|
+
<div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
|
|
29
|
+
));
|
|
30
|
+
Alert.displayName = "Alert";
|
|
31
|
+
|
|
32
|
+
const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
|
|
33
|
+
({ className, ...props }, ref) => (
|
|
34
|
+
<h5 ref={ref} className={cn("mb-1 font-medium leading-none tracking-tight", className)} {...props} />
|
|
35
|
+
),
|
|
36
|
+
);
|
|
37
|
+
AlertTitle.displayName = "AlertTitle";
|
|
38
|
+
|
|
39
|
+
const AlertDescription = React.forwardRef<
|
|
40
|
+
HTMLParagraphElement,
|
|
41
|
+
React.HTMLAttributes<HTMLParagraphElement>
|
|
42
|
+
>(({ className, ...props }, ref) => (
|
|
43
|
+
<div ref={ref} className={cn("text-sm [&_p]:leading-relaxed", className)} {...props} />
|
|
44
|
+
));
|
|
45
|
+
AlertDescription.displayName = "AlertDescription";
|
|
46
|
+
|
|
47
|
+
export { Alert, AlertTitle, AlertDescription };
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Pause, Play, SkipBack, SkipForward } from "lucide-react";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
import { Slider } from "./slider";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* AudioPlayer — interactive playback controls, full or mini. Wraps a native
|
|
10
|
+
* <audio> element: play / pause, scrubber with time, ±10s skip, and optional
|
|
11
|
+
* prev / next track callbacks.
|
|
12
|
+
*/
|
|
13
|
+
export interface AudioPlayerProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
14
|
+
src: string;
|
|
15
|
+
variant?: "full" | "mini";
|
|
16
|
+
title?: string;
|
|
17
|
+
artist?: string;
|
|
18
|
+
coverSrc?: string;
|
|
19
|
+
/** ±10s skip step, seconds */
|
|
20
|
+
skipBy?: number;
|
|
21
|
+
onPrev?: () => void;
|
|
22
|
+
onNext?: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function fmt(t: number) {
|
|
26
|
+
if (!Number.isFinite(t)) return "0:00";
|
|
27
|
+
const m = Math.floor(t / 60);
|
|
28
|
+
const s = Math.floor(t % 60);
|
|
29
|
+
return `${m}:${s.toString().padStart(2, "0")}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const AudioPlayer = React.forwardRef<HTMLDivElement, AudioPlayerProps>(
|
|
33
|
+
({ src, variant = "full", title, artist, coverSrc, skipBy = 10, onPrev, onNext, className, ...props }, ref) => {
|
|
34
|
+
const audioRef = React.useRef<HTMLAudioElement>(null);
|
|
35
|
+
const [playing, setPlaying] = React.useState(false);
|
|
36
|
+
const [current, setCurrent] = React.useState(0);
|
|
37
|
+
const [duration, setDuration] = React.useState(0);
|
|
38
|
+
|
|
39
|
+
const toggle = () => {
|
|
40
|
+
const a = audioRef.current;
|
|
41
|
+
if (!a) return;
|
|
42
|
+
if (a.paused) void a.play();
|
|
43
|
+
else a.pause();
|
|
44
|
+
};
|
|
45
|
+
const seek = (t: number) => {
|
|
46
|
+
const a = audioRef.current;
|
|
47
|
+
if (a) a.currentTime = Math.max(0, Math.min(duration || 0, t));
|
|
48
|
+
};
|
|
49
|
+
const skip = (delta: number) => seek(current + delta);
|
|
50
|
+
|
|
51
|
+
const audio = (
|
|
52
|
+
<audio
|
|
53
|
+
ref={audioRef}
|
|
54
|
+
src={src}
|
|
55
|
+
preload="metadata"
|
|
56
|
+
onPlay={() => setPlaying(true)}
|
|
57
|
+
onPause={() => setPlaying(false)}
|
|
58
|
+
onTimeUpdate={(e) => setCurrent(e.currentTarget.currentTime)}
|
|
59
|
+
onLoadedMetadata={(e) => setDuration(e.currentTarget.duration)}
|
|
60
|
+
onEnded={() => setPlaying(false)}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const PlayPause = ({ size = 44 }: { size?: number }) => (
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
onClick={toggle}
|
|
68
|
+
aria-label={playing ? "Pause" : "Play"}
|
|
69
|
+
className="grid shrink-0 place-items-center rounded-full bg-primary text-primary-foreground outline-none transition-transform hover:scale-105 focus-visible:shadow-focus"
|
|
70
|
+
style={{ width: size, height: size }}
|
|
71
|
+
>
|
|
72
|
+
{playing ? <Pause className="size-1/3 fill-current" /> : <Play className="size-1/3 translate-x-px fill-current" />}
|
|
73
|
+
</button>
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const iconBtn =
|
|
77
|
+
"grid size-8 shrink-0 place-items-center rounded-full text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:text-foreground disabled:opacity-40 [&>svg]:size-4";
|
|
78
|
+
|
|
79
|
+
if (variant === "mini") {
|
|
80
|
+
const pct = duration ? (current / duration) * 100 : 0;
|
|
81
|
+
return (
|
|
82
|
+
<div
|
|
83
|
+
ref={ref}
|
|
84
|
+
className={cn(
|
|
85
|
+
"relative flex w-full max-w-[360px] items-center gap-3 overflow-hidden rounded-full border border-border bg-background p-2 pl-3 font-sans",
|
|
86
|
+
className,
|
|
87
|
+
)}
|
|
88
|
+
{...props}
|
|
89
|
+
>
|
|
90
|
+
{audio}
|
|
91
|
+
{coverSrc ? (
|
|
92
|
+
<img src={coverSrc} alt="" className="size-10 shrink-0 rounded-full object-cover" />
|
|
93
|
+
) : (
|
|
94
|
+
<div className="size-10 shrink-0 rounded-full bg-muted" />
|
|
95
|
+
)}
|
|
96
|
+
<div className="min-w-0 flex-1">
|
|
97
|
+
<p className="truncate text-label-md font-semibold text-foreground">{title ?? "Audio"}</p>
|
|
98
|
+
{artist && <p className="truncate text-body-sm text-muted-foreground">{artist}</p>}
|
|
99
|
+
</div>
|
|
100
|
+
<button type="button" onClick={() => skip(skipBy)} aria-label={`Forward ${skipBy}s`} className={iconBtn}>
|
|
101
|
+
<SkipForward />
|
|
102
|
+
</button>
|
|
103
|
+
<PlayPause size={36} />
|
|
104
|
+
<div className="absolute inset-x-0 bottom-0 h-1 bg-muted">
|
|
105
|
+
<div className="h-full bg-primary transition-[width]" style={{ width: `${pct}%` }} />
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div
|
|
113
|
+
ref={ref}
|
|
114
|
+
className={cn("flex w-full max-w-[360px] flex-col gap-2 font-sans", className)}
|
|
115
|
+
{...props}
|
|
116
|
+
>
|
|
117
|
+
{audio}
|
|
118
|
+
<Slider
|
|
119
|
+
value={[current]}
|
|
120
|
+
max={duration || 1}
|
|
121
|
+
step={0.5}
|
|
122
|
+
onValueChange={(vals) => seek(vals[0] ?? 0)}
|
|
123
|
+
aria-label="Seek"
|
|
124
|
+
/>
|
|
125
|
+
<div className="flex justify-between text-body-sm text-muted-foreground">
|
|
126
|
+
<span>{fmt(current)}</span>
|
|
127
|
+
<span>{fmt(duration)}</span>
|
|
128
|
+
</div>
|
|
129
|
+
<div className="mt-1 flex items-center justify-center gap-3">
|
|
130
|
+
<button type="button" onClick={onPrev} disabled={!onPrev} aria-label="Previous track" className={iconBtn}>
|
|
131
|
+
<SkipBack className="fill-current" />
|
|
132
|
+
</button>
|
|
133
|
+
<button type="button" onClick={() => skip(-skipBy)} aria-label={`Back ${skipBy}s`} className={iconBtn}>
|
|
134
|
+
<SkipBack />
|
|
135
|
+
</button>
|
|
136
|
+
<PlayPause />
|
|
137
|
+
<button type="button" onClick={() => skip(skipBy)} aria-label={`Forward ${skipBy}s`} className={iconBtn}>
|
|
138
|
+
<SkipForward />
|
|
139
|
+
</button>
|
|
140
|
+
<button type="button" onClick={onNext} disabled={!onNext} aria-label="Next track" className={iconBtn}>
|
|
141
|
+
<SkipForward className="fill-current" />
|
|
142
|
+
</button>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
);
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
AudioPlayer.displayName = "AudioPlayer";
|
|
149
|
+
|
|
150
|
+
export { AudioPlayer };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as AvatarPrimitive from "@radix-ui/react-avatar";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
|
|
7
|
+
const Avatar = React.forwardRef<
|
|
8
|
+
React.ElementRef<typeof AvatarPrimitive.Root>,
|
|
9
|
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
|
|
10
|
+
>(({ className, ...props }, ref) => (
|
|
11
|
+
<AvatarPrimitive.Root
|
|
12
|
+
ref={ref}
|
|
13
|
+
className={cn("relative flex size-10 shrink-0 overflow-hidden rounded-full", className)}
|
|
14
|
+
{...props}
|
|
15
|
+
/>
|
|
16
|
+
));
|
|
17
|
+
Avatar.displayName = AvatarPrimitive.Root.displayName;
|
|
18
|
+
|
|
19
|
+
const AvatarImage = React.forwardRef<
|
|
20
|
+
React.ElementRef<typeof AvatarPrimitive.Image>,
|
|
21
|
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
|
|
22
|
+
>(({ className, ...props }, ref) => (
|
|
23
|
+
<AvatarPrimitive.Image ref={ref} className={cn("aspect-square size-full", className)} {...props} />
|
|
24
|
+
));
|
|
25
|
+
AvatarImage.displayName = AvatarPrimitive.Image.displayName;
|
|
26
|
+
|
|
27
|
+
const AvatarFallback = React.forwardRef<
|
|
28
|
+
React.ElementRef<typeof AvatarPrimitive.Fallback>,
|
|
29
|
+
React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
|
|
30
|
+
>(({ className, ...props }, ref) => (
|
|
31
|
+
<AvatarPrimitive.Fallback
|
|
32
|
+
ref={ref}
|
|
33
|
+
className={cn(
|
|
34
|
+
"flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground",
|
|
35
|
+
className,
|
|
36
|
+
)}
|
|
37
|
+
{...props}
|
|
38
|
+
/>
|
|
39
|
+
));
|
|
40
|
+
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* AvatarGroup — stacked, overlapping avatars with a "+N" overflow marker.
|
|
44
|
+
*/
|
|
45
|
+
export interface AvatarGroupProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
46
|
+
/** cap the number of visible avatars, folding the rest into a "+N" marker */
|
|
47
|
+
max?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const AvatarGroup = React.forwardRef<HTMLDivElement, AvatarGroupProps>(
|
|
51
|
+
({ className, max, children, ...props }, ref) => {
|
|
52
|
+
const items = React.Children.toArray(children);
|
|
53
|
+
const visible = max ? items.slice(0, max) : items;
|
|
54
|
+
const overflow = max ? items.length - max : 0;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div ref={ref} className={cn("flex -space-x-2", className)} {...props}>
|
|
58
|
+
{visible.map((child, i) => (
|
|
59
|
+
<div key={i} className="rounded-full ring-2 ring-background">
|
|
60
|
+
{child}
|
|
61
|
+
</div>
|
|
62
|
+
))}
|
|
63
|
+
{overflow > 0 && (
|
|
64
|
+
<div className="rounded-full ring-2 ring-background">
|
|
65
|
+
<Avatar>
|
|
66
|
+
<AvatarFallback>+{overflow}</AvatarFallback>
|
|
67
|
+
</Avatar>
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
},
|
|
73
|
+
);
|
|
74
|
+
AvatarGroup.displayName = "AvatarGroup";
|
|
75
|
+
|
|
76
|
+
export { Avatar, AvatarImage, AvatarFallback, AvatarGroup };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Badge — reconciled 1:1 with the KinetixUI design source, node 54855:13995.
|
|
9
|
+
* Solid pill status marker. Label Medium type (12 / 16, +0.5). Variants:
|
|
10
|
+
* default | secondary | destructive | outline | subtle.
|
|
11
|
+
*
|
|
12
|
+
* Note: the design's "Secondary" badge is the saturated sage (`#748873`), which
|
|
13
|
+
* in this token contract is `--secondary-foreground`; `--secondary` is its light
|
|
14
|
+
* container. Hence the swapped bg/text on that one variant.
|
|
15
|
+
*/
|
|
16
|
+
const badgeVariants = cva(
|
|
17
|
+
"inline-flex items-center rounded-full px-2.5 py-1 text-xs font-medium font-sans tracking-[0.5px] transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
|
18
|
+
{
|
|
19
|
+
variants: {
|
|
20
|
+
variant: {
|
|
21
|
+
default: "bg-primary text-primary-foreground",
|
|
22
|
+
secondary: "bg-secondary-foreground text-secondary",
|
|
23
|
+
destructive: "bg-destructive text-destructive-foreground",
|
|
24
|
+
outline: "border border-input text-foreground",
|
|
25
|
+
subtle: "bg-accent text-accent-foreground",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
defaultVariants: { variant: "default" },
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
export interface BadgeProps
|
|
33
|
+
extends React.HTMLAttributes<HTMLDivElement>,
|
|
34
|
+
VariantProps<typeof badgeVariants> {}
|
|
35
|
+
|
|
36
|
+
function Badge({ className, variant, ...props }: BadgeProps) {
|
|
37
|
+
return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export { Badge, badgeVariants };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
5
|
+
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
|
|
8
|
+
const Breadcrumb = React.forwardRef<HTMLElement, React.ComponentPropsWithoutRef<"nav">>(
|
|
9
|
+
({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" className="font-sans" {...props} />,
|
|
10
|
+
);
|
|
11
|
+
Breadcrumb.displayName = "Breadcrumb";
|
|
12
|
+
|
|
13
|
+
const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<"ol">>(
|
|
14
|
+
({ className, ...props }, ref) => (
|
|
15
|
+
<ol
|
|
16
|
+
ref={ref}
|
|
17
|
+
className={cn("flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5", className)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
),
|
|
21
|
+
);
|
|
22
|
+
BreadcrumbList.displayName = "BreadcrumbList";
|
|
23
|
+
|
|
24
|
+
const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>(
|
|
25
|
+
({ className, ...props }, ref) => (
|
|
26
|
+
<li ref={ref} className={cn("inline-flex items-center gap-1.5", className)} {...props} />
|
|
27
|
+
),
|
|
28
|
+
);
|
|
29
|
+
BreadcrumbItem.displayName = "BreadcrumbItem";
|
|
30
|
+
|
|
31
|
+
const BreadcrumbLink = React.forwardRef<
|
|
32
|
+
HTMLAnchorElement,
|
|
33
|
+
React.ComponentPropsWithoutRef<"a"> & { asChild?: boolean }
|
|
34
|
+
>(({ asChild, className, ...props }, ref) => {
|
|
35
|
+
const Comp = asChild ? Slot : "a";
|
|
36
|
+
return <Comp ref={ref} className={cn("transition-colors hover:text-foreground", className)} {...props} />;
|
|
37
|
+
});
|
|
38
|
+
BreadcrumbLink.displayName = "BreadcrumbLink";
|
|
39
|
+
|
|
40
|
+
const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<"span">>(
|
|
41
|
+
({ className, ...props }, ref) => (
|
|
42
|
+
<span
|
|
43
|
+
ref={ref}
|
|
44
|
+
role="link"
|
|
45
|
+
aria-disabled="true"
|
|
46
|
+
aria-current="page"
|
|
47
|
+
className={cn("font-normal text-foreground", className)}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
BreadcrumbPage.displayName = "BreadcrumbPage";
|
|
53
|
+
|
|
54
|
+
const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
|
|
55
|
+
<li role="presentation" aria-hidden className={cn("[&>svg]:size-3.5", className)} {...props}>
|
|
56
|
+
{children ?? <ChevronRight />}
|
|
57
|
+
</li>
|
|
58
|
+
);
|
|
59
|
+
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
|
|
60
|
+
|
|
61
|
+
const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
|
|
62
|
+
<span role="presentation" aria-hidden className={cn("flex size-9 items-center justify-center", className)} {...props}>
|
|
63
|
+
<MoreHorizontal className="size-4" />
|
|
64
|
+
<span className="sr-only">More</span>
|
|
65
|
+
</span>
|
|
66
|
+
);
|
|
67
|
+
BreadcrumbEllipsis.displayName = "BreadcrumbEllipsis";
|
|
68
|
+
|
|
69
|
+
export {
|
|
70
|
+
Breadcrumb,
|
|
71
|
+
BreadcrumbList,
|
|
72
|
+
BreadcrumbItem,
|
|
73
|
+
BreadcrumbLink,
|
|
74
|
+
BreadcrumbPage,
|
|
75
|
+
BreadcrumbSeparator,
|
|
76
|
+
BreadcrumbEllipsis,
|
|
77
|
+
};
|