cooterlabs 1.0.1 → 2.0.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/README.md +77 -14
- package/bin/index.js +510 -75
- package/package.json +12 -4
- package/templates/action-sheet/action-sheet.tsx +113 -0
- package/templates/alert/alert.tsx +96 -0
- package/templates/avatar/avatar.tsx +21 -4
- package/templates/badge/badge.tsx +106 -15
- package/templates/breadcrumb/breadcrumb.tsx +113 -0
- package/templates/button/button.tsx +40 -2
- package/templates/button-group/button-group.tsx +93 -0
- package/templates/card/card.tsx +31 -19
- package/templates/checkbox/checkbox.tsx +4 -3
- package/templates/context-menu/context-menu.tsx +177 -0
- package/templates/dialog/dialog.tsx +127 -0
- package/templates/dropdown/dropdown.tsx +180 -0
- package/templates/input/input.tsx +30 -3
- package/templates/label/label.tsx +2 -2
- package/templates/list/list.tsx +96 -0
- package/templates/loader/loader.tsx +38 -0
- package/templates/navbar-bottom/navbar-bottom.tsx +76 -0
- package/templates/navbar-top/navbar-top.tsx +84 -0
- package/templates/page-control/page-control.tsx +55 -0
- package/templates/pagination/pagination.tsx +109 -0
- package/templates/progress/progress.tsx +69 -0
- package/templates/radio/radio.tsx +41 -0
- package/templates/stepper/stepper.tsx +93 -0
- package/templates/tabs/tabs.tsx +57 -0
- package/templates/textarea/textarea.tsx +1 -1
- package/templates/toggle/toggle.tsx +27 -0
- package/templates/tooltip/tooltip.tsx +37 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { clsx, type ClassValue } from "clsx"
|
|
3
|
+
import { twMerge } from "tailwind-merge"
|
|
4
|
+
|
|
5
|
+
export function cn(...inputs: ClassValue[]) {
|
|
6
|
+
return twMerge(clsx(inputs))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const List = React.forwardRef<
|
|
10
|
+
HTMLDivElement,
|
|
11
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
12
|
+
>(({ className, ...props }, ref) => (
|
|
13
|
+
<div
|
|
14
|
+
ref={ref}
|
|
15
|
+
role="list"
|
|
16
|
+
className={cn("w-full divide-y divide-grey-100", className)}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
))
|
|
20
|
+
List.displayName = "List"
|
|
21
|
+
|
|
22
|
+
const ListHeader = React.forwardRef<
|
|
23
|
+
HTMLDivElement,
|
|
24
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
25
|
+
>(({ className, ...props }, ref) => (
|
|
26
|
+
<div
|
|
27
|
+
ref={ref}
|
|
28
|
+
className={cn("px-4 py-2 text-label text-muted-foreground", className)}
|
|
29
|
+
{...props}
|
|
30
|
+
/>
|
|
31
|
+
))
|
|
32
|
+
ListHeader.displayName = "ListHeader"
|
|
33
|
+
|
|
34
|
+
export interface ListItemProps
|
|
35
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
|
|
36
|
+
leading?: React.ReactNode
|
|
37
|
+
title?: React.ReactNode
|
|
38
|
+
subtitle?: React.ReactNode
|
|
39
|
+
trailing?: React.ReactNode
|
|
40
|
+
interactive?: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ListItem = React.forwardRef<HTMLDivElement, ListItemProps>(
|
|
44
|
+
(
|
|
45
|
+
{
|
|
46
|
+
className,
|
|
47
|
+
leading,
|
|
48
|
+
title,
|
|
49
|
+
subtitle,
|
|
50
|
+
trailing,
|
|
51
|
+
interactive = false,
|
|
52
|
+
children,
|
|
53
|
+
...props
|
|
54
|
+
},
|
|
55
|
+
ref
|
|
56
|
+
) => (
|
|
57
|
+
<div
|
|
58
|
+
ref={ref}
|
|
59
|
+
role="listitem"
|
|
60
|
+
className={cn(
|
|
61
|
+
"flex min-h-12 items-center gap-3 px-4 py-2",
|
|
62
|
+
interactive &&
|
|
63
|
+
"cursor-pointer transition-colors hover:bg-secondary active:bg-muted",
|
|
64
|
+
className
|
|
65
|
+
)}
|
|
66
|
+
{...props}
|
|
67
|
+
>
|
|
68
|
+
{leading && (
|
|
69
|
+
<span className="flex shrink-0 items-center justify-center text-muted-foreground [&_svg]:size-5">
|
|
70
|
+
{leading}
|
|
71
|
+
</span>
|
|
72
|
+
)}
|
|
73
|
+
<span className="flex min-w-0 flex-1 flex-col">
|
|
74
|
+
{title && (
|
|
75
|
+
<span className="truncate text-b2 text-foreground">
|
|
76
|
+
{title}
|
|
77
|
+
</span>
|
|
78
|
+
)}
|
|
79
|
+
{subtitle && (
|
|
80
|
+
<span className="truncate text-c1 text-muted-foreground">
|
|
81
|
+
{subtitle}
|
|
82
|
+
</span>
|
|
83
|
+
)}
|
|
84
|
+
{children}
|
|
85
|
+
</span>
|
|
86
|
+
{trailing && (
|
|
87
|
+
<span className="flex shrink-0 items-center gap-2 text-c1 text-muted-foreground [&_svg]:size-5">
|
|
88
|
+
{trailing}
|
|
89
|
+
</span>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
ListItem.displayName = "ListItem"
|
|
95
|
+
|
|
96
|
+
export { List, ListHeader, ListItem }
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { clsx, type ClassValue } from "clsx"
|
|
3
|
+
import { twMerge } from "tailwind-merge"
|
|
4
|
+
|
|
5
|
+
export function cn(...inputs: ClassValue[]) {
|
|
6
|
+
return twMerge(clsx(inputs))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const loaderSizes = {
|
|
10
|
+
tiny: "h-6 w-6 border-2",
|
|
11
|
+
small: "h-8 w-8 border-2",
|
|
12
|
+
medium: "h-10 w-10 border-4",
|
|
13
|
+
large: "h-12 w-12 border-4",
|
|
14
|
+
giant: "h-14 w-14 border-4",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LoaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
18
|
+
size?: keyof typeof loaderSizes
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const Loader = React.forwardRef<HTMLDivElement, LoaderProps>(
|
|
22
|
+
({ className, size = "medium", ...props }, ref) => (
|
|
23
|
+
<div
|
|
24
|
+
ref={ref}
|
|
25
|
+
role="status"
|
|
26
|
+
aria-label="Loading"
|
|
27
|
+
className={cn(
|
|
28
|
+
"inline-block animate-spin rounded-full border-grey-200 border-t-primary",
|
|
29
|
+
loaderSizes[size],
|
|
30
|
+
className
|
|
31
|
+
)}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
Loader.displayName = "Loader"
|
|
37
|
+
|
|
38
|
+
export { Loader }
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { clsx, type ClassValue } from "clsx"
|
|
3
|
+
import { twMerge } from "tailwind-merge"
|
|
4
|
+
|
|
5
|
+
function cn(...inputs: ClassValue[]) {
|
|
6
|
+
return twMerge(clsx(inputs))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const NavbarBottom = React.forwardRef<
|
|
10
|
+
HTMLElement,
|
|
11
|
+
React.HTMLAttributes<HTMLElement>
|
|
12
|
+
>(({ className, ...props }, ref) => (
|
|
13
|
+
<nav
|
|
14
|
+
ref={ref}
|
|
15
|
+
// box-content keeps the tab row a full h-12 above the safe-area padding
|
|
16
|
+
className={cn(
|
|
17
|
+
"box-content flex h-12 items-stretch border-t border-grey-100 bg-background pb-[env(safe-area-inset-bottom)]",
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
))
|
|
23
|
+
NavbarBottom.displayName = "NavbarBottom"
|
|
24
|
+
|
|
25
|
+
export interface NavbarBottomItemProps
|
|
26
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
27
|
+
label?: string
|
|
28
|
+
active?: boolean
|
|
29
|
+
badge?: boolean
|
|
30
|
+
// Figma Style="Line Selector + Icon": active top indicator line, no label
|
|
31
|
+
lineSelector?: boolean
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const NavbarBottomItem = React.forwardRef<
|
|
35
|
+
HTMLButtonElement,
|
|
36
|
+
NavbarBottomItemProps
|
|
37
|
+
>(
|
|
38
|
+
(
|
|
39
|
+
{
|
|
40
|
+
className,
|
|
41
|
+
label,
|
|
42
|
+
active = false,
|
|
43
|
+
badge = false,
|
|
44
|
+
lineSelector = false,
|
|
45
|
+
children,
|
|
46
|
+
...props
|
|
47
|
+
},
|
|
48
|
+
ref
|
|
49
|
+
) => (
|
|
50
|
+
<button
|
|
51
|
+
ref={ref}
|
|
52
|
+
type="button"
|
|
53
|
+
aria-current={active ? "page" : undefined}
|
|
54
|
+
className={cn(
|
|
55
|
+
"relative flex flex-1 flex-col items-center justify-center gap-1 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:text-grey-400",
|
|
56
|
+
active ? "text-primary" : "text-grey-400 hover:bg-secondary",
|
|
57
|
+
className
|
|
58
|
+
)}
|
|
59
|
+
{...props}
|
|
60
|
+
>
|
|
61
|
+
{lineSelector && active && (
|
|
62
|
+
<span className="absolute inset-x-3 top-0 h-0.5 rounded-full bg-primary" />
|
|
63
|
+
)}
|
|
64
|
+
<span className="relative [&_svg]:size-6 [&_svg]:shrink-0">
|
|
65
|
+
{children}
|
|
66
|
+
{badge && (
|
|
67
|
+
<span className="absolute -right-1 -top-1 size-2 rounded-full bg-error-500" />
|
|
68
|
+
)}
|
|
69
|
+
</span>
|
|
70
|
+
{label && !lineSelector && <span className="text-c3">{label}</span>}
|
|
71
|
+
</button>
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
NavbarBottomItem.displayName = "NavbarBottomItem"
|
|
75
|
+
|
|
76
|
+
export { NavbarBottom, NavbarBottomItem }
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { ChevronLeft } from "lucide-react"
|
|
3
|
+
import { clsx, type ClassValue } from "clsx"
|
|
4
|
+
import { twMerge } from "tailwind-merge"
|
|
5
|
+
|
|
6
|
+
function cn(...inputs: ClassValue[]) {
|
|
7
|
+
return twMerge(clsx(inputs))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface NavbarTopProps extends React.HTMLAttributes<HTMLElement> {
|
|
11
|
+
bordered?: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const NavbarTop = React.forwardRef<HTMLElement, NavbarTopProps>(
|
|
15
|
+
({ className, bordered = false, ...props }, ref) => (
|
|
16
|
+
<header
|
|
17
|
+
ref={ref}
|
|
18
|
+
className={cn(
|
|
19
|
+
"relative flex h-11 items-center justify-between gap-2 bg-background px-4",
|
|
20
|
+
bordered && "border-b border-grey-100",
|
|
21
|
+
className
|
|
22
|
+
)}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
)
|
|
26
|
+
)
|
|
27
|
+
NavbarTop.displayName = "NavbarTop"
|
|
28
|
+
|
|
29
|
+
const NavbarTopLeft = React.forwardRef<
|
|
30
|
+
HTMLButtonElement,
|
|
31
|
+
React.ButtonHTMLAttributes<HTMLButtonElement>
|
|
32
|
+
>(({ className, children, ...props }, ref) => (
|
|
33
|
+
<button
|
|
34
|
+
ref={ref}
|
|
35
|
+
type="button"
|
|
36
|
+
aria-label="Back"
|
|
37
|
+
className={cn(
|
|
38
|
+
"-ml-2 inline-flex items-center gap-1 rounded-xs p-2 text-b2 text-foreground transition-colors hover:bg-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:text-grey-400 [&_svg]:size-6 [&_svg]:shrink-0",
|
|
39
|
+
className
|
|
40
|
+
)}
|
|
41
|
+
{...props}
|
|
42
|
+
>
|
|
43
|
+
{children ?? <ChevronLeft />}
|
|
44
|
+
</button>
|
|
45
|
+
))
|
|
46
|
+
NavbarTopLeft.displayName = "NavbarTopLeft"
|
|
47
|
+
|
|
48
|
+
export interface NavbarTopTitleProps
|
|
49
|
+
extends React.HTMLAttributes<HTMLDivElement> {
|
|
50
|
+
subtitle?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const NavbarTopTitle = React.forwardRef<HTMLDivElement, NavbarTopTitleProps>(
|
|
54
|
+
({ className, subtitle, children, ...props }, ref) => (
|
|
55
|
+
<div
|
|
56
|
+
ref={ref}
|
|
57
|
+
className={cn(
|
|
58
|
+
"pointer-events-none absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-center text-s2 text-foreground",
|
|
59
|
+
className
|
|
60
|
+
)}
|
|
61
|
+
{...props}
|
|
62
|
+
>
|
|
63
|
+
{children}
|
|
64
|
+
{subtitle && (
|
|
65
|
+
<div className="text-c1 text-muted-foreground">{subtitle}</div>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
NavbarTopTitle.displayName = "NavbarTopTitle"
|
|
71
|
+
|
|
72
|
+
const NavbarTopRight = React.forwardRef<
|
|
73
|
+
HTMLDivElement,
|
|
74
|
+
React.HTMLAttributes<HTMLDivElement>
|
|
75
|
+
>(({ className, ...props }, ref) => (
|
|
76
|
+
<div
|
|
77
|
+
ref={ref}
|
|
78
|
+
className={cn("ml-auto flex items-center gap-2", className)}
|
|
79
|
+
{...props}
|
|
80
|
+
/>
|
|
81
|
+
))
|
|
82
|
+
NavbarTopRight.displayName = "NavbarTopRight"
|
|
83
|
+
|
|
84
|
+
export { NavbarTop, NavbarTopLeft, NavbarTopTitle, NavbarTopRight }
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { clsx, type ClassValue } from "clsx"
|
|
3
|
+
import { twMerge } from "tailwind-merge"
|
|
4
|
+
|
|
5
|
+
export function cn(...inputs: ClassValue[]) {
|
|
6
|
+
return twMerge(clsx(inputs))
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface PageControlProps
|
|
10
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
|
|
11
|
+
count: number
|
|
12
|
+
index: number
|
|
13
|
+
onChange?: (index: number) => void
|
|
14
|
+
orientation?: "horizontal" | "vertical"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const PageControl = React.forwardRef<HTMLDivElement, PageControlProps>(
|
|
18
|
+
(
|
|
19
|
+
{ className, count, index, onChange, orientation = "horizontal", ...props },
|
|
20
|
+
ref
|
|
21
|
+
) => (
|
|
22
|
+
<div
|
|
23
|
+
ref={ref}
|
|
24
|
+
role="tablist"
|
|
25
|
+
aria-label="Page control"
|
|
26
|
+
aria-orientation={orientation}
|
|
27
|
+
className={cn(
|
|
28
|
+
"flex items-center gap-2 p-2",
|
|
29
|
+
orientation === "vertical" && "flex-col",
|
|
30
|
+
className
|
|
31
|
+
)}
|
|
32
|
+
{...props}
|
|
33
|
+
>
|
|
34
|
+
{Array.from({ length: count }, (_, i) => (
|
|
35
|
+
<button
|
|
36
|
+
key={i}
|
|
37
|
+
type="button"
|
|
38
|
+
role="tab"
|
|
39
|
+
aria-label={`Page ${i + 1}`}
|
|
40
|
+
aria-selected={i === index}
|
|
41
|
+
tabIndex={i === index ? 0 : -1}
|
|
42
|
+
disabled={!onChange}
|
|
43
|
+
onClick={() => onChange?.(i)}
|
|
44
|
+
className={cn(
|
|
45
|
+
"size-2 rounded-full ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none",
|
|
46
|
+
i === index ? "bg-primary" : "bg-grey-200 hover:bg-grey-300"
|
|
47
|
+
)}
|
|
48
|
+
/>
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
PageControl.displayName = "PageControl"
|
|
54
|
+
|
|
55
|
+
export { PageControl }
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
|
|
3
|
+
import { clsx, type ClassValue } from "clsx"
|
|
4
|
+
import { twMerge } from "tailwind-merge"
|
|
5
|
+
|
|
6
|
+
export function cn(...inputs: ClassValue[]) {
|
|
7
|
+
return twMerge(clsx(inputs))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const Pagination = ({
|
|
11
|
+
className,
|
|
12
|
+
...props
|
|
13
|
+
}: React.HTMLAttributes<HTMLElement>) => (
|
|
14
|
+
<nav
|
|
15
|
+
role="navigation"
|
|
16
|
+
aria-label="pagination"
|
|
17
|
+
className={cn("mx-auto flex w-full justify-center", className)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
)
|
|
21
|
+
Pagination.displayName = "Pagination"
|
|
22
|
+
|
|
23
|
+
const PaginationContent = React.forwardRef<
|
|
24
|
+
HTMLUListElement,
|
|
25
|
+
React.HTMLAttributes<HTMLUListElement>
|
|
26
|
+
>(({ className, ...props }, ref) => (
|
|
27
|
+
<ul
|
|
28
|
+
ref={ref}
|
|
29
|
+
className={cn("flex flex-row items-center gap-1", className)}
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
))
|
|
33
|
+
PaginationContent.displayName = "PaginationContent"
|
|
34
|
+
|
|
35
|
+
const PaginationItem = React.forwardRef<
|
|
36
|
+
HTMLLIElement,
|
|
37
|
+
React.LiHTMLAttributes<HTMLLIElement>
|
|
38
|
+
>(({ ...props }, ref) => <li ref={ref} {...props} />)
|
|
39
|
+
PaginationItem.displayName = "PaginationItem"
|
|
40
|
+
|
|
41
|
+
export interface PaginationLinkProps
|
|
42
|
+
extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
43
|
+
isActive?: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const PaginationLink = React.forwardRef<HTMLAnchorElement, PaginationLinkProps>(
|
|
47
|
+
({ className, isActive = false, ...props }, ref) => (
|
|
48
|
+
<a
|
|
49
|
+
ref={ref}
|
|
50
|
+
aria-current={isActive ? "page" : undefined}
|
|
51
|
+
className={cn(
|
|
52
|
+
"inline-flex size-12 items-center justify-center whitespace-nowrap rounded-xs text-b4 ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 aria-disabled:pointer-events-none aria-disabled:text-grey-400 [&_svg]:size-4",
|
|
53
|
+
isActive
|
|
54
|
+
? "bg-primary text-primary-foreground"
|
|
55
|
+
: "text-foreground hover:bg-secondary",
|
|
56
|
+
className
|
|
57
|
+
)}
|
|
58
|
+
{...props}
|
|
59
|
+
/>
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
PaginationLink.displayName = "PaginationLink"
|
|
63
|
+
|
|
64
|
+
const PaginationPrevious = React.forwardRef<
|
|
65
|
+
HTMLAnchorElement,
|
|
66
|
+
PaginationLinkProps
|
|
67
|
+
>(({ ...props }, ref) => (
|
|
68
|
+
<PaginationLink ref={ref} aria-label="Go to previous page" {...props}>
|
|
69
|
+
<ChevronLeft />
|
|
70
|
+
</PaginationLink>
|
|
71
|
+
))
|
|
72
|
+
PaginationPrevious.displayName = "PaginationPrevious"
|
|
73
|
+
|
|
74
|
+
const PaginationNext = React.forwardRef<HTMLAnchorElement, PaginationLinkProps>(
|
|
75
|
+
({ ...props }, ref) => (
|
|
76
|
+
<PaginationLink ref={ref} aria-label="Go to next page" {...props}>
|
|
77
|
+
<ChevronRight />
|
|
78
|
+
</PaginationLink>
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
PaginationNext.displayName = "PaginationNext"
|
|
82
|
+
|
|
83
|
+
const PaginationEllipsis = ({
|
|
84
|
+
className,
|
|
85
|
+
...props
|
|
86
|
+
}: React.HTMLAttributes<HTMLSpanElement>) => (
|
|
87
|
+
<span
|
|
88
|
+
aria-hidden="true"
|
|
89
|
+
className={cn(
|
|
90
|
+
"flex size-12 items-center justify-center text-muted-foreground",
|
|
91
|
+
className
|
|
92
|
+
)}
|
|
93
|
+
{...props}
|
|
94
|
+
>
|
|
95
|
+
<MoreHorizontal className="size-4" />
|
|
96
|
+
<span className="sr-only">More pages</span>
|
|
97
|
+
</span>
|
|
98
|
+
)
|
|
99
|
+
PaginationEllipsis.displayName = "PaginationEllipsis"
|
|
100
|
+
|
|
101
|
+
export {
|
|
102
|
+
Pagination,
|
|
103
|
+
PaginationContent,
|
|
104
|
+
PaginationItem,
|
|
105
|
+
PaginationLink,
|
|
106
|
+
PaginationPrevious,
|
|
107
|
+
PaginationNext,
|
|
108
|
+
PaginationEllipsis,
|
|
109
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
|
3
|
+
import { clsx, type ClassValue } from "clsx"
|
|
4
|
+
import { twMerge } from "tailwind-merge"
|
|
5
|
+
|
|
6
|
+
export function cn(...inputs: ClassValue[]) {
|
|
7
|
+
return twMerge(clsx(inputs))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ProgressProps
|
|
11
|
+
extends React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root> {
|
|
12
|
+
label?: "left" | "right"
|
|
13
|
+
disabled?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const Progress = React.forwardRef<
|
|
17
|
+
React.ElementRef<typeof ProgressPrimitive.Root>,
|
|
18
|
+
ProgressProps
|
|
19
|
+
>(({ className, value, label, disabled = false, ...props }, ref) => {
|
|
20
|
+
const percent = value ?? 0
|
|
21
|
+
const bar = (
|
|
22
|
+
<ProgressPrimitive.Root
|
|
23
|
+
ref={ref}
|
|
24
|
+
className={cn(
|
|
25
|
+
"relative h-2 w-full overflow-hidden rounded-full bg-grey-100",
|
|
26
|
+
!label && className
|
|
27
|
+
)}
|
|
28
|
+
value={value}
|
|
29
|
+
{...props}
|
|
30
|
+
>
|
|
31
|
+
<ProgressPrimitive.Indicator
|
|
32
|
+
className={cn(
|
|
33
|
+
"h-full w-full flex-1 rounded-full transition-all",
|
|
34
|
+
disabled ? "bg-grey-200" : "bg-primary"
|
|
35
|
+
)}
|
|
36
|
+
style={{ transform: `translateX(-${100 - percent}%)` }}
|
|
37
|
+
/>
|
|
38
|
+
</ProgressPrimitive.Root>
|
|
39
|
+
)
|
|
40
|
+
if (!label) return bar
|
|
41
|
+
return (
|
|
42
|
+
<div className={cn("flex items-center gap-3", className)}>
|
|
43
|
+
{label === "left" && (
|
|
44
|
+
<span
|
|
45
|
+
className={cn(
|
|
46
|
+
"text-c2 font-medium",
|
|
47
|
+
disabled ? "text-grey-400" : "text-muted-foreground"
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
50
|
+
{percent}%
|
|
51
|
+
</span>
|
|
52
|
+
)}
|
|
53
|
+
{bar}
|
|
54
|
+
{label === "right" && (
|
|
55
|
+
<span
|
|
56
|
+
className={cn(
|
|
57
|
+
"text-c2 font-medium",
|
|
58
|
+
disabled ? "text-grey-400" : "text-muted-foreground"
|
|
59
|
+
)}
|
|
60
|
+
>
|
|
61
|
+
{percent}%
|
|
62
|
+
</span>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
Progress.displayName = ProgressPrimitive.Root.displayName
|
|
68
|
+
|
|
69
|
+
export { Progress }
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
|
|
3
|
+
import { clsx, type ClassValue } from "clsx"
|
|
4
|
+
import { twMerge } from "tailwind-merge"
|
|
5
|
+
|
|
6
|
+
export function cn(...inputs: ClassValue[]) {
|
|
7
|
+
return twMerge(clsx(inputs))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const RadioGroup = React.forwardRef<
|
|
11
|
+
React.ElementRef<typeof RadioGroupPrimitive.Root>,
|
|
12
|
+
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
|
|
13
|
+
>(({ className, ...props }, ref) => (
|
|
14
|
+
<RadioGroupPrimitive.Root
|
|
15
|
+
ref={ref}
|
|
16
|
+
className={cn("grid gap-2", className)}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
))
|
|
20
|
+
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
|
|
21
|
+
|
|
22
|
+
const RadioGroupItem = React.forwardRef<
|
|
23
|
+
React.ElementRef<typeof RadioGroupPrimitive.Item>,
|
|
24
|
+
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
|
|
25
|
+
>(({ className, ...props }, ref) => (
|
|
26
|
+
<RadioGroupPrimitive.Item
|
|
27
|
+
ref={ref}
|
|
28
|
+
className={cn(
|
|
29
|
+
"aspect-square size-5 shrink-0 rounded-full border border-grey-300 bg-background ring-offset-background transition-colors hover:border-grey-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:border-grey-200 disabled:bg-muted data-[state=checked]:border-primary",
|
|
30
|
+
className
|
|
31
|
+
)}
|
|
32
|
+
{...props}
|
|
33
|
+
>
|
|
34
|
+
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
|
|
35
|
+
<span className="block size-2.5 rounded-full bg-primary" />
|
|
36
|
+
</RadioGroupPrimitive.Indicator>
|
|
37
|
+
</RadioGroupPrimitive.Item>
|
|
38
|
+
))
|
|
39
|
+
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
|
|
40
|
+
|
|
41
|
+
export { RadioGroup, RadioGroupItem }
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { Check } from "lucide-react"
|
|
3
|
+
import { clsx, type ClassValue } from "clsx"
|
|
4
|
+
import { twMerge } from "tailwind-merge"
|
|
5
|
+
|
|
6
|
+
export function cn(...inputs: ClassValue[]) {
|
|
7
|
+
return twMerge(clsx(inputs))
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const stepperCircleStates = {
|
|
11
|
+
completed: "bg-primary text-primary-foreground",
|
|
12
|
+
current: "border border-primary bg-background text-primary",
|
|
13
|
+
upcoming: "border border-grey-200 bg-background text-grey-400",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const stepperLabelStates = {
|
|
17
|
+
completed: "text-foreground",
|
|
18
|
+
current: "text-primary",
|
|
19
|
+
upcoming: "text-grey-400",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const Stepper = React.forwardRef<
|
|
23
|
+
HTMLOListElement,
|
|
24
|
+
React.OlHTMLAttributes<HTMLOListElement>
|
|
25
|
+
>(({ className, ...props }, ref) => (
|
|
26
|
+
<ol
|
|
27
|
+
ref={ref}
|
|
28
|
+
className={cn("flex w-full items-center", className)}
|
|
29
|
+
{...props}
|
|
30
|
+
/>
|
|
31
|
+
))
|
|
32
|
+
Stepper.displayName = "Stepper"
|
|
33
|
+
|
|
34
|
+
export interface StepperItemProps
|
|
35
|
+
extends React.LiHTMLAttributes<HTMLLIElement> {
|
|
36
|
+
index: number
|
|
37
|
+
state?: keyof typeof stepperCircleStates
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const StepperItem = React.forwardRef<HTMLLIElement, StepperItemProps>(
|
|
41
|
+
({ className, index, state = "upcoming", children, ...props }, ref) => (
|
|
42
|
+
<li
|
|
43
|
+
ref={ref}
|
|
44
|
+
aria-current={state === "current" ? "step" : undefined}
|
|
45
|
+
className={cn("flex items-center gap-2", className)}
|
|
46
|
+
{...props}
|
|
47
|
+
>
|
|
48
|
+
<span
|
|
49
|
+
className={cn(
|
|
50
|
+
"flex size-8 shrink-0 items-center justify-center rounded-full text-c2 transition-colors",
|
|
51
|
+
stepperCircleStates[state]
|
|
52
|
+
)}
|
|
53
|
+
>
|
|
54
|
+
{state === "completed" ? (
|
|
55
|
+
<Check className="size-4" aria-hidden="true" />
|
|
56
|
+
) : (
|
|
57
|
+
index
|
|
58
|
+
)}
|
|
59
|
+
</span>
|
|
60
|
+
{children != null && (
|
|
61
|
+
<span className={cn("text-c1", stepperLabelStates[state])}>
|
|
62
|
+
{children}
|
|
63
|
+
</span>
|
|
64
|
+
)}
|
|
65
|
+
</li>
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
StepperItem.displayName = "StepperItem"
|
|
69
|
+
|
|
70
|
+
export interface StepperSeparatorProps
|
|
71
|
+
extends React.HTMLAttributes<HTMLLIElement> {
|
|
72
|
+
completed?: boolean
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const StepperSeparator = ({
|
|
76
|
+
className,
|
|
77
|
+
completed = false,
|
|
78
|
+
...props
|
|
79
|
+
}: StepperSeparatorProps) => (
|
|
80
|
+
<li
|
|
81
|
+
role="presentation"
|
|
82
|
+
aria-hidden="true"
|
|
83
|
+
className={cn(
|
|
84
|
+
"mx-2 h-px flex-1",
|
|
85
|
+
completed ? "bg-primary" : "bg-grey-200",
|
|
86
|
+
className
|
|
87
|
+
)}
|
|
88
|
+
{...props}
|
|
89
|
+
/>
|
|
90
|
+
)
|
|
91
|
+
StepperSeparator.displayName = "StepperSeparator"
|
|
92
|
+
|
|
93
|
+
export { Stepper, StepperItem, StepperSeparator }
|