@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
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Button — generated from Figma "KinetixUI" › UI Components ›
|
|
10
|
+
* 02. Controls & Actions › Button (node 54863:351).
|
|
11
|
+
*
|
|
12
|
+
* Figma variant matrix (mirrored 1:1 below):
|
|
13
|
+
* variant : Primary | Secondary | Outline | Destructive | Ghost | Link
|
|
14
|
+
* size : sm | md | lg | icon
|
|
15
|
+
* state : Default | Hover | Focus | Active | Disabled
|
|
16
|
+
*
|
|
17
|
+
* `state` is expressed idiomatically: Hover/Focus/Active are CSS pseudo-classes
|
|
18
|
+
* (`hover:` / `focus-visible:` / `active:`), Disabled is the native attribute.
|
|
19
|
+
* The optional `state` prop force-pins one visual state for docs / snapshots.
|
|
20
|
+
*
|
|
21
|
+
* Colors resolve to the semantic token contract in
|
|
22
|
+
* @kinetixui/tokens/dist/web/globals.css.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const buttonVariants = cva(
|
|
26
|
+
[
|
|
27
|
+
"inline-flex items-center justify-center gap-1 shrink-0",
|
|
28
|
+
"font-sans font-medium whitespace-nowrap select-none",
|
|
29
|
+
"rounded-md transition-colors outline-none",
|
|
30
|
+
"focus-visible:shadow-focus",
|
|
31
|
+
"disabled:pointer-events-none disabled:opacity-50",
|
|
32
|
+
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:size-[18px]",
|
|
33
|
+
],
|
|
34
|
+
{
|
|
35
|
+
variants: {
|
|
36
|
+
variant: {
|
|
37
|
+
// bg primary / text on-primary; hover+active step down the blue ramp; disabled -> border grey
|
|
38
|
+
Primary:
|
|
39
|
+
"bg-primary text-primary-foreground hover:bg-[--color-blue-600] active:bg-[--color-blue-700] disabled:bg-border disabled:text-muted-foreground",
|
|
40
|
+
// rest = secondaryContainer; hover/active promote to full secondary with on-secondary text
|
|
41
|
+
Secondary:
|
|
42
|
+
"bg-secondary text-secondary-foreground hover:bg-[--color-green-500] hover:text-[--color-green-50] active:bg-[--color-green-600] active:text-[--color-green-50] focus-visible:bg-secondary disabled:bg-border disabled:text-muted-foreground",
|
|
43
|
+
// 1px outline; hover fills with accent (LightBlue); active pins primary border+text
|
|
44
|
+
Outline:
|
|
45
|
+
"border border-input bg-transparent text-foreground hover:bg-accent active:border-primary active:bg-accent active:text-primary disabled:border-input disabled:text-muted-foreground disabled:bg-transparent",
|
|
46
|
+
Destructive:
|
|
47
|
+
"bg-destructive text-destructive-foreground hover:brightness-95 active:brightness-90 disabled:bg-border disabled:text-muted-foreground",
|
|
48
|
+
Ghost:
|
|
49
|
+
"bg-transparent text-foreground hover:bg-accent active:bg-accent active:text-primary disabled:text-muted-foreground disabled:bg-transparent",
|
|
50
|
+
Link: "bg-transparent text-primary underline-offset-4 hover:underline hover:text-foreground rounded-none px-0 disabled:text-muted-foreground disabled:no-underline",
|
|
51
|
+
},
|
|
52
|
+
size: {
|
|
53
|
+
// padding = Figma spacing/3 + spacing/2 ; type = Label Small (11/16, +0.5 tracking)
|
|
54
|
+
sm: "px-3 py-2 text-[11px] leading-4 tracking-[0.5px]",
|
|
55
|
+
// spacing/4 + spacing/3 ; Label Medium (12/16, +0.5)
|
|
56
|
+
md: "px-4 py-3 text-[12px] leading-4 tracking-[0.5px]",
|
|
57
|
+
// spacing/6 + spacing/3 ; Label Large (14/20, +0.1)
|
|
58
|
+
lg: "px-6 py-3 text-[14px] leading-5 tracking-[0.1px]",
|
|
59
|
+
// spacing/3 all round, square
|
|
60
|
+
icon: "p-3 [&>*:not(svg)]:sr-only",
|
|
61
|
+
},
|
|
62
|
+
/** force a static visual state — leave undefined for real interaction */
|
|
63
|
+
state: {
|
|
64
|
+
Default: "",
|
|
65
|
+
Hover: "",
|
|
66
|
+
Focus: "shadow-focus",
|
|
67
|
+
Active: "",
|
|
68
|
+
Disabled: "pointer-events-none opacity-50",
|
|
69
|
+
},
|
|
70
|
+
/** corner style — matches the design source's Corners property */
|
|
71
|
+
corners: {
|
|
72
|
+
sharp: "rounded-none",
|
|
73
|
+
default: "",
|
|
74
|
+
pill: "rounded-full",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
compoundVariants: [
|
|
78
|
+
{ variant: "Destructive", class: "focus-visible:shadow-focus-destructive" },
|
|
79
|
+
{ variant: "Primary", state: "Hover", class: "bg-[--color-blue-600]" },
|
|
80
|
+
{ variant: "Primary", state: "Active", class: "bg-[--color-blue-700]" },
|
|
81
|
+
{ variant: "Secondary", state: "Hover", class: "bg-[--color-green-500] text-[--color-green-50]" },
|
|
82
|
+
{ variant: "Secondary", state: "Active", class: "bg-[--color-green-600] text-[--color-green-50]" },
|
|
83
|
+
{ variant: "Outline", state: "Hover", class: "bg-accent" },
|
|
84
|
+
{ variant: "Outline", state: "Active", class: "border-primary bg-accent text-primary" },
|
|
85
|
+
{ variant: "Ghost", state: "Hover", class: "bg-accent" },
|
|
86
|
+
{ variant: "Ghost", state: "Active", class: "bg-accent text-primary" },
|
|
87
|
+
{ variant: "Link", state: "Hover", class: "underline text-foreground" },
|
|
88
|
+
{ variant: "Link", size: ["sm", "md", "lg"], class: "py-2 px-0 h-auto" },
|
|
89
|
+
],
|
|
90
|
+
defaultVariants: { variant: "Primary", size: "md", state: "Default", corners: "default" },
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
export interface ButtonProps
|
|
95
|
+
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "color">,
|
|
96
|
+
VariantProps<typeof buttonVariants> {
|
|
97
|
+
/** render as the single child element (Radix Slot) instead of <button> */
|
|
98
|
+
asChild?: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|
102
|
+
({ className, variant, size, state, corners, asChild = false, disabled, ...props }, ref) => {
|
|
103
|
+
const Comp: React.ElementType = asChild ? Slot : "button";
|
|
104
|
+
return (
|
|
105
|
+
<Comp
|
|
106
|
+
ref={ref}
|
|
107
|
+
data-slot="button"
|
|
108
|
+
data-variant={variant ?? "Primary"}
|
|
109
|
+
data-size={size ?? "md"}
|
|
110
|
+
disabled={disabled || state === "Disabled"}
|
|
111
|
+
className={cn(buttonVariants({ variant, size, state, corners }), className)}
|
|
112
|
+
{...props}
|
|
113
|
+
/>
|
|
114
|
+
);
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
Button.displayName = "Button";
|
|
118
|
+
|
|
119
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
5
|
+
import { DayPicker } from "react-day-picker";
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
import { buttonVariants } from "./button";
|
|
8
|
+
|
|
9
|
+
export type CalendarProps = React.ComponentProps<typeof DayPicker>;
|
|
10
|
+
|
|
11
|
+
function Calendar({ className, classNames, showOutsideDays = true, ...props }: CalendarProps) {
|
|
12
|
+
return (
|
|
13
|
+
<DayPicker
|
|
14
|
+
showOutsideDays={showOutsideDays}
|
|
15
|
+
className={cn("p-3 font-sans", className)}
|
|
16
|
+
classNames={{
|
|
17
|
+
months: "flex flex-col sm:flex-row gap-2",
|
|
18
|
+
month: "flex flex-col gap-4",
|
|
19
|
+
caption: "flex justify-center pt-1 relative items-center w-full",
|
|
20
|
+
caption_label: "text-sm font-medium",
|
|
21
|
+
nav: "flex items-center gap-1",
|
|
22
|
+
nav_button: cn(
|
|
23
|
+
buttonVariants({ variant: "Outline", size: "icon" }),
|
|
24
|
+
"size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
|
|
25
|
+
),
|
|
26
|
+
nav_button_previous: "absolute left-1",
|
|
27
|
+
nav_button_next: "absolute right-1",
|
|
28
|
+
table: "w-full border-collapse space-x-1",
|
|
29
|
+
head_row: "flex",
|
|
30
|
+
head_cell: "text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
|
|
31
|
+
row: "flex w-full mt-2",
|
|
32
|
+
cell: cn(
|
|
33
|
+
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
|
|
34
|
+
props.mode === "range"
|
|
35
|
+
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
|
|
36
|
+
: "[&:has([aria-selected])]:rounded-md",
|
|
37
|
+
),
|
|
38
|
+
day: cn(buttonVariants({ variant: "Ghost", size: "icon" }), "size-8 p-0 font-normal aria-selected:opacity-100"),
|
|
39
|
+
day_range_start: "day-range-start",
|
|
40
|
+
day_range_end: "day-range-end",
|
|
41
|
+
day_selected:
|
|
42
|
+
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
|
43
|
+
day_today: "bg-accent text-accent-foreground",
|
|
44
|
+
day_outside: "day-outside text-muted-foreground aria-selected:text-muted-foreground",
|
|
45
|
+
day_disabled: "text-muted-foreground opacity-50",
|
|
46
|
+
day_range_middle: "aria-selected:bg-accent aria-selected:text-accent-foreground",
|
|
47
|
+
day_hidden: "invisible",
|
|
48
|
+
...classNames,
|
|
49
|
+
}}
|
|
50
|
+
components={{
|
|
51
|
+
IconLeft: () => <ChevronLeft className="size-4" />,
|
|
52
|
+
IconRight: () => <ChevronRight className="size-4" />,
|
|
53
|
+
}}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
Calendar.displayName = "Calendar";
|
|
59
|
+
|
|
60
|
+
export { Calendar };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
|
|
6
|
+
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
7
|
+
({ className, ...props }, ref) => (
|
|
8
|
+
<div
|
|
9
|
+
ref={ref}
|
|
10
|
+
className={cn("rounded-xl border bg-card text-card-foreground shadow-sm font-sans", className)}
|
|
11
|
+
{...props}
|
|
12
|
+
/>
|
|
13
|
+
),
|
|
14
|
+
);
|
|
15
|
+
Card.displayName = "Card";
|
|
16
|
+
|
|
17
|
+
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
18
|
+
({ className, ...props }, ref) => (
|
|
19
|
+
<div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
|
20
|
+
),
|
|
21
|
+
);
|
|
22
|
+
CardHeader.displayName = "CardHeader";
|
|
23
|
+
|
|
24
|
+
const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
25
|
+
({ className, ...props }, ref) => (
|
|
26
|
+
<div ref={ref} className={cn("font-semibold leading-none tracking-tight", className)} {...props} />
|
|
27
|
+
),
|
|
28
|
+
);
|
|
29
|
+
CardTitle.displayName = "CardTitle";
|
|
30
|
+
|
|
31
|
+
const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
32
|
+
({ className, ...props }, ref) => (
|
|
33
|
+
<div ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
CardDescription.displayName = "CardDescription";
|
|
37
|
+
|
|
38
|
+
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
39
|
+
({ className, ...props }, ref) => <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />,
|
|
40
|
+
);
|
|
41
|
+
CardContent.displayName = "CardContent";
|
|
42
|
+
|
|
43
|
+
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
44
|
+
({ className, ...props }, ref) => (
|
|
45
|
+
<div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
CardFooter.displayName = "CardFooter";
|
|
49
|
+
|
|
50
|
+
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react";
|
|
5
|
+
import { ArrowLeft, ArrowRight } from "lucide-react";
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
import { Button } from "./button";
|
|
8
|
+
|
|
9
|
+
type CarouselApi = UseEmblaCarouselType[1];
|
|
10
|
+
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
|
11
|
+
type CarouselOptions = UseCarouselParameters[0];
|
|
12
|
+
type CarouselPlugin = UseCarouselParameters[1];
|
|
13
|
+
|
|
14
|
+
type CarouselProps = {
|
|
15
|
+
opts?: CarouselOptions;
|
|
16
|
+
plugins?: CarouselPlugin;
|
|
17
|
+
orientation?: "horizontal" | "vertical";
|
|
18
|
+
setApi?: (api: CarouselApi) => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type CarouselContextProps = {
|
|
22
|
+
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
|
23
|
+
api: ReturnType<typeof useEmblaCarousel>[1];
|
|
24
|
+
scrollPrev: () => void;
|
|
25
|
+
scrollNext: () => void;
|
|
26
|
+
canScrollPrev: boolean;
|
|
27
|
+
canScrollNext: boolean;
|
|
28
|
+
} & CarouselProps;
|
|
29
|
+
|
|
30
|
+
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
|
31
|
+
|
|
32
|
+
function useCarousel() {
|
|
33
|
+
const context = React.useContext(CarouselContext);
|
|
34
|
+
if (!context) throw new Error("useCarousel must be used within a <Carousel />");
|
|
35
|
+
return context;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const Carousel = React.forwardRef<
|
|
39
|
+
HTMLDivElement,
|
|
40
|
+
React.HTMLAttributes<HTMLDivElement> & CarouselProps
|
|
41
|
+
>(({ orientation = "horizontal", opts, setApi, plugins, className, children, ...props }, ref) => {
|
|
42
|
+
const [carouselRef, api] = useEmblaCarousel(
|
|
43
|
+
{ ...opts, axis: orientation === "horizontal" ? "x" : "y" },
|
|
44
|
+
plugins,
|
|
45
|
+
);
|
|
46
|
+
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
|
47
|
+
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
|
48
|
+
|
|
49
|
+
const onSelect = React.useCallback((a: CarouselApi) => {
|
|
50
|
+
if (!a) return;
|
|
51
|
+
setCanScrollPrev(a.canScrollPrev());
|
|
52
|
+
setCanScrollNext(a.canScrollNext());
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
const scrollPrev = React.useCallback(() => api?.scrollPrev(), [api]);
|
|
56
|
+
const scrollNext = React.useCallback(() => api?.scrollNext(), [api]);
|
|
57
|
+
|
|
58
|
+
React.useEffect(() => {
|
|
59
|
+
if (api && setApi) setApi(api);
|
|
60
|
+
}, [api, setApi]);
|
|
61
|
+
|
|
62
|
+
React.useEffect(() => {
|
|
63
|
+
if (!api) return;
|
|
64
|
+
onSelect(api);
|
|
65
|
+
api.on("reInit", onSelect);
|
|
66
|
+
api.on("select", onSelect);
|
|
67
|
+
return () => {
|
|
68
|
+
api?.off("select", onSelect);
|
|
69
|
+
};
|
|
70
|
+
}, [api, onSelect]);
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<CarouselContext.Provider
|
|
74
|
+
value={{ carouselRef, api, opts, orientation, scrollPrev, scrollNext, canScrollPrev, canScrollNext }}
|
|
75
|
+
>
|
|
76
|
+
<div
|
|
77
|
+
ref={ref}
|
|
78
|
+
onKeyDownCapture={(e) => {
|
|
79
|
+
if (e.key === "ArrowLeft") {
|
|
80
|
+
e.preventDefault();
|
|
81
|
+
scrollPrev();
|
|
82
|
+
} else if (e.key === "ArrowRight") {
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
scrollNext();
|
|
85
|
+
}
|
|
86
|
+
}}
|
|
87
|
+
className={cn("relative font-sans", className)}
|
|
88
|
+
role="region"
|
|
89
|
+
aria-roledescription="carousel"
|
|
90
|
+
{...props}
|
|
91
|
+
>
|
|
92
|
+
{children}
|
|
93
|
+
</div>
|
|
94
|
+
</CarouselContext.Provider>
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
Carousel.displayName = "Carousel";
|
|
98
|
+
|
|
99
|
+
const CarouselContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
100
|
+
({ className, ...props }, ref) => {
|
|
101
|
+
const { carouselRef, orientation } = useCarousel();
|
|
102
|
+
return (
|
|
103
|
+
<div ref={carouselRef} className="overflow-hidden">
|
|
104
|
+
<div
|
|
105
|
+
ref={ref}
|
|
106
|
+
className={cn("flex", orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col", className)}
|
|
107
|
+
{...props}
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
CarouselContent.displayName = "CarouselContent";
|
|
114
|
+
|
|
115
|
+
const CarouselItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
116
|
+
({ className, ...props }, ref) => {
|
|
117
|
+
const { orientation } = useCarousel();
|
|
118
|
+
return (
|
|
119
|
+
<div
|
|
120
|
+
ref={ref}
|
|
121
|
+
role="group"
|
|
122
|
+
aria-roledescription="slide"
|
|
123
|
+
className={cn("min-w-0 shrink-0 grow-0 basis-full", orientation === "horizontal" ? "pl-4" : "pt-4", className)}
|
|
124
|
+
{...props}
|
|
125
|
+
/>
|
|
126
|
+
);
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
CarouselItem.displayName = "CarouselItem";
|
|
130
|
+
|
|
131
|
+
const CarouselPrevious = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
|
|
132
|
+
({ className, variant = "Outline", ...props }, ref) => {
|
|
133
|
+
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
|
134
|
+
return (
|
|
135
|
+
<Button
|
|
136
|
+
ref={ref}
|
|
137
|
+
variant={variant}
|
|
138
|
+
size="icon"
|
|
139
|
+
className={cn(
|
|
140
|
+
"absolute size-8 rounded-full",
|
|
141
|
+
orientation === "horizontal" ? "-left-12 top-1/2 -translate-y-1/2" : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
|
142
|
+
className,
|
|
143
|
+
)}
|
|
144
|
+
disabled={!canScrollPrev}
|
|
145
|
+
onClick={scrollPrev}
|
|
146
|
+
{...props}
|
|
147
|
+
>
|
|
148
|
+
<ArrowLeft className="size-4" />
|
|
149
|
+
<span className="sr-only">Previous slide</span>
|
|
150
|
+
</Button>
|
|
151
|
+
);
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
CarouselPrevious.displayName = "CarouselPrevious";
|
|
155
|
+
|
|
156
|
+
const CarouselNext = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
|
|
157
|
+
({ className, variant = "Outline", ...props }, ref) => {
|
|
158
|
+
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
|
159
|
+
return (
|
|
160
|
+
<Button
|
|
161
|
+
ref={ref}
|
|
162
|
+
variant={variant}
|
|
163
|
+
size="icon"
|
|
164
|
+
className={cn(
|
|
165
|
+
"absolute size-8 rounded-full",
|
|
166
|
+
orientation === "horizontal" ? "-right-12 top-1/2 -translate-y-1/2" : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
|
167
|
+
className,
|
|
168
|
+
)}
|
|
169
|
+
disabled={!canScrollNext}
|
|
170
|
+
onClick={scrollNext}
|
|
171
|
+
{...props}
|
|
172
|
+
>
|
|
173
|
+
<ArrowRight className="size-4" />
|
|
174
|
+
<span className="sr-only">Next slide</span>
|
|
175
|
+
</Button>
|
|
176
|
+
);
|
|
177
|
+
},
|
|
178
|
+
);
|
|
179
|
+
CarouselNext.displayName = "CarouselNext";
|
|
180
|
+
|
|
181
|
+
export { type CarouselApi, Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext };
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as RechartsPrimitive from "recharts";
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
|
|
7
|
+
const THEMES = { light: "", dark: ".dark" } as const;
|
|
8
|
+
|
|
9
|
+
export type ChartConfig = {
|
|
10
|
+
[k in string]: {
|
|
11
|
+
label?: React.ReactNode;
|
|
12
|
+
icon?: React.ComponentType;
|
|
13
|
+
} & ({ color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> });
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type ChartContextProps = { config: ChartConfig };
|
|
17
|
+
const ChartContext = React.createContext<ChartContextProps | null>(null);
|
|
18
|
+
|
|
19
|
+
function useChart() {
|
|
20
|
+
const context = React.useContext(ChartContext);
|
|
21
|
+
if (!context) throw new Error("useChart must be used within a <ChartContainer />");
|
|
22
|
+
return context;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const ChartContainer = React.forwardRef<
|
|
26
|
+
HTMLDivElement,
|
|
27
|
+
React.ComponentProps<"div"> & {
|
|
28
|
+
config: ChartConfig;
|
|
29
|
+
children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"];
|
|
30
|
+
}
|
|
31
|
+
>(({ id, className, children, config, ...props }, ref) => {
|
|
32
|
+
const uniqueId = React.useId();
|
|
33
|
+
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<ChartContext.Provider value={{ config }}>
|
|
37
|
+
<div
|
|
38
|
+
data-chart={chartId}
|
|
39
|
+
ref={ref}
|
|
40
|
+
className={cn(
|
|
41
|
+
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
|
42
|
+
className,
|
|
43
|
+
)}
|
|
44
|
+
{...props}
|
|
45
|
+
>
|
|
46
|
+
<ChartStyle id={chartId} config={config} />
|
|
47
|
+
<RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
|
|
48
|
+
</div>
|
|
49
|
+
</ChartContext.Provider>
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
ChartContainer.displayName = "Chart";
|
|
53
|
+
|
|
54
|
+
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
|
55
|
+
const colorConfig = Object.entries(config).filter(([, c]) => c.theme || c.color);
|
|
56
|
+
if (!colorConfig.length) return null;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<style
|
|
60
|
+
dangerouslySetInnerHTML={{
|
|
61
|
+
__html: Object.entries(THEMES)
|
|
62
|
+
.map(
|
|
63
|
+
([theme, prefix]) => `
|
|
64
|
+
${prefix} [data-chart=${id}] {
|
|
65
|
+
${colorConfig
|
|
66
|
+
.map(([key, itemConfig]) => {
|
|
67
|
+
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;
|
|
68
|
+
return color ? ` --color-${key}: ${color};` : null;
|
|
69
|
+
})
|
|
70
|
+
.join("\n")}
|
|
71
|
+
}
|
|
72
|
+
`,
|
|
73
|
+
)
|
|
74
|
+
.join("\n"),
|
|
75
|
+
}}
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const ChartTooltip = RechartsPrimitive.Tooltip;
|
|
81
|
+
|
|
82
|
+
const ChartTooltipContent = React.forwardRef<
|
|
83
|
+
HTMLDivElement,
|
|
84
|
+
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
|
85
|
+
React.ComponentProps<"div"> & {
|
|
86
|
+
hideLabel?: boolean;
|
|
87
|
+
hideIndicator?: boolean;
|
|
88
|
+
indicator?: "line" | "dot" | "dashed";
|
|
89
|
+
nameKey?: string;
|
|
90
|
+
labelKey?: string;
|
|
91
|
+
}
|
|
92
|
+
>(
|
|
93
|
+
(
|
|
94
|
+
{ active, payload, className, indicator = "dot", hideLabel = false, hideIndicator = false, label, labelFormatter, formatter, color },
|
|
95
|
+
ref,
|
|
96
|
+
) => {
|
|
97
|
+
const { config } = useChart();
|
|
98
|
+
|
|
99
|
+
if (!active || !payload?.length) return null;
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div
|
|
103
|
+
ref={ref}
|
|
104
|
+
className={cn(
|
|
105
|
+
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
|
106
|
+
className,
|
|
107
|
+
)}
|
|
108
|
+
>
|
|
109
|
+
{!hideLabel && (
|
|
110
|
+
<div className="font-medium">{labelFormatter ? labelFormatter(label, payload) : label}</div>
|
|
111
|
+
)}
|
|
112
|
+
<div className="grid gap-1.5">
|
|
113
|
+
{payload.map((item, i) => {
|
|
114
|
+
const key = `${item.name || item.dataKey || "value"}`;
|
|
115
|
+
const itemConfig = config[key];
|
|
116
|
+
const indicatorColor = color || (item.payload as Record<string, string>)?.fill || item.color;
|
|
117
|
+
return (
|
|
118
|
+
<div key={key + i} className="flex w-full items-center gap-2">
|
|
119
|
+
{!hideIndicator && (
|
|
120
|
+
<div
|
|
121
|
+
className={cn("shrink-0 rounded-[2px]", {
|
|
122
|
+
"size-2.5": indicator === "dot",
|
|
123
|
+
"w-1": indicator === "line",
|
|
124
|
+
"w-0 border-[1.5px] border-dashed bg-transparent": indicator === "dashed",
|
|
125
|
+
})}
|
|
126
|
+
style={{ backgroundColor: indicatorColor, borderColor: indicatorColor }}
|
|
127
|
+
/>
|
|
128
|
+
)}
|
|
129
|
+
<div className="flex flex-1 justify-between leading-none">
|
|
130
|
+
<span className="text-muted-foreground">{itemConfig?.label || item.name}</span>
|
|
131
|
+
{item.value != null && (
|
|
132
|
+
<span className="font-mono font-medium tabular-nums text-foreground">
|
|
133
|
+
{formatter ? formatter(item.value, item.name!, item, i, payload) : item.value.toLocaleString()}
|
|
134
|
+
</span>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
})}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
},
|
|
144
|
+
);
|
|
145
|
+
ChartTooltipContent.displayName = "ChartTooltipContent";
|
|
146
|
+
|
|
147
|
+
const ChartLegend = RechartsPrimitive.Legend;
|
|
148
|
+
|
|
149
|
+
const ChartLegendContent = React.forwardRef<
|
|
150
|
+
HTMLDivElement,
|
|
151
|
+
React.ComponentProps<"div"> &
|
|
152
|
+
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & { hideIcon?: boolean; nameKey?: string }
|
|
153
|
+
>(({ className, payload, verticalAlign = "bottom" }, ref) => {
|
|
154
|
+
const { config } = useChart();
|
|
155
|
+
if (!payload?.length) return null;
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div
|
|
159
|
+
ref={ref}
|
|
160
|
+
className={cn("flex items-center justify-center gap-4", verticalAlign === "top" ? "pb-3" : "pt-3", className)}
|
|
161
|
+
>
|
|
162
|
+
{payload.map((item) => {
|
|
163
|
+
const key = `${item.dataKey || "value"}`;
|
|
164
|
+
const itemConfig = config[key];
|
|
165
|
+
return (
|
|
166
|
+
<div key={item.value} className="flex items-center gap-1.5">
|
|
167
|
+
<div className="size-2 shrink-0 rounded-[2px]" style={{ backgroundColor: item.color }} />
|
|
168
|
+
{itemConfig?.label || item.value}
|
|
169
|
+
</div>
|
|
170
|
+
);
|
|
171
|
+
})}
|
|
172
|
+
</div>
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
ChartLegendContent.displayName = "ChartLegendContent";
|
|
176
|
+
|
|
177
|
+
export { ChartContainer, ChartTooltip, ChartTooltipContent, ChartLegend, ChartLegendContent, ChartStyle };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
|
5
|
+
import { Check, Minus } from "lucide-react";
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Checkbox — reconciled 1:1 with the KinetixUI design source, node 54863:483.
|
|
10
|
+
* 18px box, 4px radius, 2px border. Unchecked border = `--input`; checked /
|
|
11
|
+
* indeterminate fill = `--primary`. Focus = 2px `--ring` offset. `aria-invalid`
|
|
12
|
+
* paints the box (and the composed label/helper) with `--destructive`.
|
|
13
|
+
*/
|
|
14
|
+
const Checkbox = React.forwardRef<
|
|
15
|
+
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
|
16
|
+
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
|
17
|
+
>(({ className, ...props }, ref) => (
|
|
18
|
+
<CheckboxPrimitive.Root
|
|
19
|
+
ref={ref}
|
|
20
|
+
className={cn(
|
|
21
|
+
"peer size-[18px] shrink-0 rounded-sm border-2 border-input outline-none transition-colors",
|
|
22
|
+
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
23
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
24
|
+
"data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
|
25
|
+
"data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground",
|
|
26
|
+
"aria-invalid:border-destructive aria-invalid:data-[state=checked]:border-destructive aria-invalid:data-[state=checked]:bg-destructive",
|
|
27
|
+
className,
|
|
28
|
+
)}
|
|
29
|
+
{...props}
|
|
30
|
+
>
|
|
31
|
+
<CheckboxPrimitive.Indicator className="flex items-center justify-center text-current">
|
|
32
|
+
{props.checked === "indeterminate" ? <Minus className="size-3" /> : <Check className="size-3" strokeWidth={3} />}
|
|
33
|
+
</CheckboxPrimitive.Indicator>
|
|
34
|
+
</CheckboxPrimitive.Root>
|
|
35
|
+
));
|
|
36
|
+
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
|
37
|
+
|
|
38
|
+
export { Checkbox };
|