@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.
- package/package.json +63 -4
- package/src/components/avatar.tsx +30 -0
- package/src/components/badge.tsx +26 -0
- package/src/components/breadcrumb.tsx +37 -0
- package/src/components/button.tsx +58 -0
- package/src/components/calendar.tsx +244 -0
- package/src/components/card.tsx +18 -0
- package/src/components/chart.tsx +387 -0
- package/src/components/checkbox.tsx +26 -0
- package/src/components/code-block.tsx +118 -0
- package/src/components/collapsible.tsx +31 -0
- package/src/components/command.tsx +111 -0
- package/src/components/context-menu.tsx +150 -0
- package/src/components/counter.tsx +56 -0
- package/src/components/dialog.tsx +36 -0
- package/src/components/dropdown-menu.tsx +54 -0
- package/src/components/file-upload.tsx +381 -0
- package/src/components/glide.tsx +82 -0
- package/src/components/icon.tsx +28 -0
- package/src/components/image-viewer.tsx +370 -0
- package/src/components/input.tsx +187 -0
- package/src/components/kbd.tsx +14 -0
- package/src/components/logo-mark.ts +8 -0
- package/src/components/logo.tsx +36 -0
- package/src/components/navbar.tsx +144 -0
- package/src/components/otp.tsx +93 -0
- package/src/components/page-loader.tsx +37 -0
- package/src/components/pophover.tsx +28 -0
- package/src/components/popover.tsx +36 -0
- package/src/components/popup.tsx +75 -0
- package/src/components/progress.tsx +26 -0
- package/src/components/result.tsx +22 -0
- package/src/components/scroll-area.tsx +38 -0
- package/src/components/select.tsx +52 -0
- package/src/components/separator.tsx +14 -0
- package/src/components/sheet.tsx +96 -0
- package/src/components/sidebar.tsx +195 -0
- package/src/components/skeleton.tsx +14 -0
- package/src/components/slider.tsx +31 -0
- package/src/components/smooth-scroll.tsx +174 -0
- package/src/components/spinner.tsx +18 -0
- package/src/components/switch.tsx +22 -0
- package/src/components/table.tsx +52 -0
- package/src/components/tabs.tsx +48 -0
- package/src/components/theme-toggle.tsx +214 -0
- package/src/components/toaster.tsx +38 -0
- package/src/components/tooltip.tsx +43 -0
- package/src/components/video-player.tsx +559 -0
- package/src/components/video-settings.tsx +233 -0
- package/src/index.ts +102 -0
- package/src/lib/cn.ts +6 -0
- package/src/lib/jsx-highlight.ts +128 -0
- package/src/styles.css +443 -0
- package/src/theme/theme-script.ts +13 -0
- package/src/theme/theme.tsx +77 -0
- package/styles.css +1 -0
- package/README.md +0 -3
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const LOGO_VIEWBOX = "4 4 24 24";
|
|
2
|
+
|
|
3
|
+
export const LOGO_PATH =
|
|
4
|
+
"M6.85515 12.0105C7.05812 11.9096 7.28021 11.8477 7.49009 11.7637C7.79246 11.6429 8.0954 11.5136 8.39337 11.3823C9.99543 10.68 11.5859 9.95158 13.1641 9.19723C14.3228 8.63695 15.3771 8.15564 16.4511 7.40511C17.306 6.82373 17.9976 6.03223 18.6683 5.25433C18.7978 5.11236 18.8985 4.96726 19.0008 4.8057C19.0621 4.70895 19.1264 4.54717 19.2511 4.56367L19.2747 4.59692C19.2832 4.6733 19.257 4.76217 19.2211 4.82773C19.0247 5.18651 18.8069 5.54126 18.6086 5.89861C18.2257 6.59823 17.8559 7.30505 17.4996 8.01861C17.1747 8.67511 16.8888 9.37811 16.5726 10.0409C16.4517 10.294 16.344 10.5527 16.2124 10.8014C16.7828 10.4335 17.4011 10.1246 17.9538 9.72323C18.4506 9.36255 18.9207 8.95255 19.3495 8.51342C19.6011 8.2572 19.8404 7.9892 20.0667 7.7103C20.3385 7.36895 21.5875 5.57114 21.8676 5.5078L21.8909 5.52573C21.8963 5.56073 21.8876 5.60764 21.8714 5.63892C21.5412 6.27583 21.2734 6.92767 21.0043 7.5922C20.4488 8.96633 19.9528 10.3637 19.5176 11.7805C19.2502 12.6542 19.0194 13.5353 18.7763 14.4146C19.0179 14.3631 19.6183 14.071 19.8671 13.9567C21.1747 13.3441 22.4644 12.694 23.7347 12.0072L24.5558 11.5639C24.6942 11.489 25.0695 11.2449 25.2057 11.3116C25.2225 11.3451 25.2281 11.3708 25.2151 11.4077C25.1754 11.52 24.4489 11.9701 24.3065 12.068C23.3141 12.7522 22.3586 13.4885 21.4441 14.2738C19.6499 15.8032 17.8687 17.7296 16.5837 19.701C16.0131 20.5928 15.4963 21.5179 15.036 22.4713C14.9305 22.692 14.831 22.9155 14.7375 23.1415C14.6755 23.2929 14.5728 23.6333 14.4293 23.7068C14.3768 23.706 14.3965 23.7165 14.3639 23.6848C14.3351 23.5084 14.4843 22.9123 14.5257 22.7072C14.6923 21.8458 14.88 20.9886 15.0886 20.1363C15.3216 19.1981 15.5661 18.2628 15.822 17.3305C15.9787 16.8116 16.107 16.2866 16.2643 15.7726C16.4889 15.0388 16.7466 14.2946 16.9927 13.5665C17.1502 13.1108 17.3102 12.656 17.4727 12.202C17.5402 12.0164 17.6657 11.7217 17.7099 11.5449C17.063 11.979 16.2704 12.4224 15.7573 13.0159C15.2112 13.6475 14.6937 14.4864 14.3055 15.2273C14.2039 15.4212 14.0827 15.7217 13.9932 15.9272C13.8192 16.3229 13.6492 16.7202 13.4833 17.1194C12.5103 19.482 11.6951 21.9067 11.0431 24.3773C10.864 25.0674 10.7016 25.7617 10.556 26.4597C10.5162 26.6461 10.4248 27.2123 10.378 27.3347C10.3492 27.3628 10.3175 27.3811 10.2765 27.3844C10.2413 27.3873 10.2143 27.3674 10.1887 27.3455C10.0139 27.1963 9.82834 26.8456 9.70743 26.6432C8.80699 25.1357 8.30909 23.4184 8.1564 21.6733C8.0884 20.8961 8.07781 20.1268 8.08665 19.3474C8.09071 18.9883 8.08328 18.6193 8.11481 18.2615C8.19015 17.4073 8.34599 16.5762 8.52578 15.739C8.56634 15.5501 8.6709 14.9509 8.78696 14.8316C8.81565 14.8021 8.85012 14.7871 8.89215 14.7931C8.92515 14.7978 8.94831 14.8159 8.96656 14.8422C9.0389 14.9468 8.96684 15.2039 8.95003 15.3276C8.79862 16.4423 8.85028 17.7454 9.30406 18.7878C9.36893 18.9369 9.43634 19.1037 9.53821 19.2312C9.60778 19.3183 9.69584 19.3805 9.80937 19.389C10.0665 19.4082 10.3401 19.2211 10.5265 19.0635C11.8179 17.9714 13.079 15.5979 13.6447 14.015C13.7969 13.5818 13.9211 13.1393 14.0165 12.6903C14.2049 11.842 14.4357 10.5691 13.1614 10.5664C10.443 10.5607 11.9101 11.7971 13.279 12.5124C13.6905 12.7275 13.5201 12.9958 13.1149 12.8436C13.0391 12.8157 12.891 12.7736 12.8071 12.748C12.5423 12.6641 12.2752 12.5878 12.0061 12.5191C10.5974 12.1623 9.08934 12.0222 7.64137 12.1625C7.47015 12.1791 6.89987 12.299 6.81424 12.1371C6.80596 12.084 6.82843 12.0584 6.85515 12.0105Z";
|
|
5
|
+
|
|
6
|
+
export function logoSvg(light: string, dark: string) {
|
|
7
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${LOGO_VIEWBOX}"><style>path{fill:${light}}@media (prefers-color-scheme: dark){path{fill:${dark}}}</style><path d="${LOGO_PATH}"/></svg>`;
|
|
8
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
|
+
import type { ComponentProps } from "react";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
import { LOGO_PATH, LOGO_VIEWBOX } from "./logo-mark";
|
|
5
|
+
|
|
6
|
+
const logoVariants = cva("inline-flex shrink-0 items-center justify-center text-current transition-colors", {
|
|
7
|
+
variants: {
|
|
8
|
+
variant: {
|
|
9
|
+
primary: "bg-accent text-accent-fg",
|
|
10
|
+
secondary: "bg-bg-elevated text-fg border-border border",
|
|
11
|
+
ghost: "bg-transparent text-fg",
|
|
12
|
+
outline: "border-border text-fg border bg-transparent",
|
|
13
|
+
},
|
|
14
|
+
size: {
|
|
15
|
+
sm: "size-8 rounded-[var(--radius-sm)] p-1",
|
|
16
|
+
md: "size-10 rounded-[var(--radius-md)] p-1.5",
|
|
17
|
+
lg: "size-12 rounded-[var(--radius-md)] p-2",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultVariants: { variant: "primary", size: "sm" },
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export function Logo({
|
|
24
|
+
variant,
|
|
25
|
+
size,
|
|
26
|
+
className,
|
|
27
|
+
...props
|
|
28
|
+
}: ComponentProps<"span"> & VariantProps<typeof logoVariants>) {
|
|
29
|
+
return (
|
|
30
|
+
<span className={cn(logoVariants({ variant, size }), className)} {...props}>
|
|
31
|
+
<svg viewBox={LOGO_VIEWBOX} className="size-full" aria-hidden>
|
|
32
|
+
<path fill="currentColor" d={LOGO_PATH} />
|
|
33
|
+
</svg>
|
|
34
|
+
</span>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button as BaseButton } from "@base-ui/react/button";
|
|
4
|
+
import { NavigationMenu } from "@base-ui/react/navigation-menu";
|
|
5
|
+
import { ArrowDown01Icon } from "@hugeicons-pro/core-stroke-rounded";
|
|
6
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
7
|
+
import { cn } from "../lib/cn";
|
|
8
|
+
import { Glide, glideActive } from "./glide";
|
|
9
|
+
import type { AppIcon } from "./icon";
|
|
10
|
+
import { Icon } from "./icon";
|
|
11
|
+
|
|
12
|
+
const itemClass =
|
|
13
|
+
"text-fg-muted hover:bg-bg-subtle hover:text-fg relative inline-flex h-8 cursor-pointer items-center gap-1.5 rounded-[var(--radius-sm)] px-2.5 text-sm outline-none transition-colors duration-[var(--motion-duration)] focus-visible:ring-2 focus-visible:ring-ring";
|
|
14
|
+
|
|
15
|
+
const motion = "duration-[calc(var(--motion-duration)*3)] ease-[var(--motion-ease)] motion-reduce:transition-none";
|
|
16
|
+
|
|
17
|
+
export function Navbar({ className, children, ...props }: ComponentProps<"header">) {
|
|
18
|
+
return (
|
|
19
|
+
<header
|
|
20
|
+
className={cn(
|
|
21
|
+
"sticky top-0 z-[var(--z-sticky)] h-14 border-0 bg-transparent px-3 shadow-none backdrop-blur-none",
|
|
22
|
+
className,
|
|
23
|
+
)}
|
|
24
|
+
{...props}
|
|
25
|
+
>
|
|
26
|
+
<div className="relative flex h-full min-w-0 items-center gap-1 overflow-x-auto">
|
|
27
|
+
<Glide selector='[aria-current="page"]' className="bg-accent-muted/60" />
|
|
28
|
+
{children}
|
|
29
|
+
</div>
|
|
30
|
+
</header>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function NavbarItem({
|
|
35
|
+
icon,
|
|
36
|
+
active,
|
|
37
|
+
className,
|
|
38
|
+
children,
|
|
39
|
+
render,
|
|
40
|
+
...props
|
|
41
|
+
}: ComponentProps<typeof BaseButton> & { icon?: AppIcon; active?: boolean }) {
|
|
42
|
+
return (
|
|
43
|
+
<BaseButton
|
|
44
|
+
nativeButton={render ? false : true}
|
|
45
|
+
render={render}
|
|
46
|
+
aria-current={active ? "page" : undefined}
|
|
47
|
+
className={cn(itemClass, active && cn("bg-accent-muted text-accent hover:text-accent hover:bg-transparent", glideActive), className)}
|
|
48
|
+
{...props}
|
|
49
|
+
>
|
|
50
|
+
{icon ? <Icon icon={icon} size={16} /> : null}
|
|
51
|
+
{children}
|
|
52
|
+
</BaseButton>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function NavbarMenus({ className, children, ...props }: ComponentProps<typeof NavigationMenu.Root>) {
|
|
57
|
+
return (
|
|
58
|
+
<NavigationMenu.Root className={cn("relative", className)} {...props}>
|
|
59
|
+
<NavigationMenu.List className="flex items-center gap-1">{children}</NavigationMenu.List>
|
|
60
|
+
<NavigationMenu.Portal>
|
|
61
|
+
<NavigationMenu.Positioner
|
|
62
|
+
sideOffset={8}
|
|
63
|
+
collisionPadding={12}
|
|
64
|
+
align="start"
|
|
65
|
+
className={cn(
|
|
66
|
+
"z-[var(--z-popover)] h-[var(--positioner-height)] w-[var(--positioner-width)] max-w-[var(--available-width)] transition-[top,left,right,bottom] data-[instant]:transition-none",
|
|
67
|
+
motion,
|
|
68
|
+
)}
|
|
69
|
+
>
|
|
70
|
+
<NavigationMenu.Popup
|
|
71
|
+
className={cn(
|
|
72
|
+
"border-border bg-bg-elevated/60 relative h-[var(--popup-height)] w-[var(--popup-width)] origin-[var(--transform-origin)] overflow-hidden rounded-[var(--radius-lg)] border shadow-[var(--shadow-lg)] backdrop-blur-2xl transition-[opacity,width,height] data-[ending-style]:opacity-0 data-[starting-style]:opacity-0",
|
|
73
|
+
motion,
|
|
74
|
+
)}
|
|
75
|
+
>
|
|
76
|
+
<NavigationMenu.Viewport className="relative size-full overflow-hidden" />
|
|
77
|
+
</NavigationMenu.Popup>
|
|
78
|
+
</NavigationMenu.Positioner>
|
|
79
|
+
</NavigationMenu.Portal>
|
|
80
|
+
</NavigationMenu.Root>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function NavbarMenu({
|
|
85
|
+
label,
|
|
86
|
+
icon,
|
|
87
|
+
className,
|
|
88
|
+
children,
|
|
89
|
+
}: {
|
|
90
|
+
label: ReactNode;
|
|
91
|
+
icon?: AppIcon;
|
|
92
|
+
className?: string;
|
|
93
|
+
children: ReactNode;
|
|
94
|
+
}) {
|
|
95
|
+
return (
|
|
96
|
+
<NavigationMenu.Item>
|
|
97
|
+
<NavigationMenu.Trigger className={cn(itemClass, "data-[popup-open]:bg-bg-subtle data-[popup-open]:text-fg")}>
|
|
98
|
+
{icon ? <Icon icon={icon} size={16} /> : null}
|
|
99
|
+
{label}
|
|
100
|
+
<NavigationMenu.Icon className="transition-transform duration-[var(--motion-duration)] data-[popup-open]:rotate-180">
|
|
101
|
+
<Icon icon={ArrowDown01Icon} variant="stroke" size={14} />
|
|
102
|
+
</NavigationMenu.Icon>
|
|
103
|
+
</NavigationMenu.Trigger>
|
|
104
|
+
<NavigationMenu.Content
|
|
105
|
+
className={cn(
|
|
106
|
+
"grid w-max max-w-[calc(100vw-1.5rem)] gap-0.5 p-2 transition-[opacity,translate] data-[ending-style]:translate-x-2 data-[ending-style]:opacity-0 data-[starting-style]:-translate-x-2 data-[starting-style]:opacity-0",
|
|
107
|
+
motion,
|
|
108
|
+
className,
|
|
109
|
+
)}
|
|
110
|
+
>
|
|
111
|
+
{children}
|
|
112
|
+
</NavigationMenu.Content>
|
|
113
|
+
</NavigationMenu.Item>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function NavbarMenuLink({
|
|
118
|
+
icon,
|
|
119
|
+
title,
|
|
120
|
+
description,
|
|
121
|
+
className,
|
|
122
|
+
children,
|
|
123
|
+
...props
|
|
124
|
+
}: Omit<ComponentProps<typeof NavigationMenu.Link>, "title"> & { icon?: AppIcon; title?: ReactNode; description?: ReactNode }) {
|
|
125
|
+
return (
|
|
126
|
+
<NavigationMenu.Link
|
|
127
|
+
className={cn(
|
|
128
|
+
"text-fg hover:bg-bg-subtle flex cursor-pointer items-start gap-3 rounded-[var(--radius-md)] px-3 py-2 text-sm outline-none transition-colors duration-[var(--motion-duration)] focus-visible:ring-2 focus-visible:ring-ring",
|
|
129
|
+
className,
|
|
130
|
+
)}
|
|
131
|
+
{...props}
|
|
132
|
+
>
|
|
133
|
+
{icon ? (
|
|
134
|
+
<span className="bg-bg-muted text-fg grid size-9 shrink-0 place-items-center rounded-[var(--radius-sm)]">
|
|
135
|
+
<Icon icon={icon} size={18} />
|
|
136
|
+
</span>
|
|
137
|
+
) : null}
|
|
138
|
+
<span className="grid min-w-0 gap-0.5">
|
|
139
|
+
<span className="font-medium">{title ?? children}</span>
|
|
140
|
+
{description ? <span className="text-fg-muted text-xs leading-snug">{description}</span> : null}
|
|
141
|
+
</span>
|
|
142
|
+
</NavigationMenu.Link>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { OTPField } from "@base-ui/react/otp-field";
|
|
4
|
+
import { useRef, useState, type ComponentProps } from "react";
|
|
5
|
+
import { cn } from "../lib/cn";
|
|
6
|
+
|
|
7
|
+
const slotClass =
|
|
8
|
+
"border-border bg-bg-elevated focus:border-accent focus:ring-ring size-10 cursor-text rounded-[var(--radius-md)] border text-center text-sm text-transparent caret-transparent outline-none selection:bg-transparent selection:text-transparent focus:ring-2 disabled:cursor-not-allowed disabled:opacity-50";
|
|
9
|
+
|
|
10
|
+
const glyphClass = "text-fg absolute inset-0 flex items-center justify-center text-sm";
|
|
11
|
+
|
|
12
|
+
interface Glyph {
|
|
13
|
+
id: number;
|
|
14
|
+
text: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let seq = 0;
|
|
18
|
+
|
|
19
|
+
function OtpSlot({ text }: { text: string }) {
|
|
20
|
+
const [glyph, setGlyph] = useState<Glyph | null>(null);
|
|
21
|
+
const [leaving, setLeaving] = useState<Glyph[]>([]);
|
|
22
|
+
|
|
23
|
+
if ((glyph?.text ?? "") !== text) {
|
|
24
|
+
if (glyph) setLeaving((list) => [...list, glyph]);
|
|
25
|
+
setGlyph(text ? { id: (seq += 1), text } : null);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<span className="relative">
|
|
30
|
+
<OTPField.Input className={slotClass} />
|
|
31
|
+
<span aria-hidden className="pointer-events-none absolute inset-px overflow-hidden rounded-[var(--radius-md)] select-none">
|
|
32
|
+
{glyph ? (
|
|
33
|
+
<span key={glyph.id} className={cn(glyphClass, "animate-[otp-in_calc(var(--motion-duration)*2)_var(--motion-ease)]")}>
|
|
34
|
+
{glyph.text}
|
|
35
|
+
</span>
|
|
36
|
+
) : null}
|
|
37
|
+
{leaving.map((item) => (
|
|
38
|
+
<span
|
|
39
|
+
key={item.id}
|
|
40
|
+
className={cn(glyphClass, "animate-[otp-out_calc(var(--motion-duration)*2)_var(--motion-ease)_forwards]")}
|
|
41
|
+
onAnimationEnd={() => setLeaving((list) => list.filter((entry) => entry.id !== item.id))}
|
|
42
|
+
>
|
|
43
|
+
{item.text}
|
|
44
|
+
</span>
|
|
45
|
+
))}
|
|
46
|
+
</span>
|
|
47
|
+
</span>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type OtpProps = Omit<ComponentProps<typeof OTPField.Root>, "length" | "mask"> & {
|
|
52
|
+
length?: number;
|
|
53
|
+
mask?: boolean;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function Otp({
|
|
57
|
+
length = 6,
|
|
58
|
+
className,
|
|
59
|
+
mask = false,
|
|
60
|
+
value,
|
|
61
|
+
defaultValue = "",
|
|
62
|
+
onValueChange,
|
|
63
|
+
onValueComplete,
|
|
64
|
+
...props
|
|
65
|
+
}: OtpProps) {
|
|
66
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
67
|
+
const [inner, setInner] = useState(defaultValue);
|
|
68
|
+
const current = value ?? inner;
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<OTPField.Root
|
|
72
|
+
ref={rootRef}
|
|
73
|
+
length={length}
|
|
74
|
+
mask={mask}
|
|
75
|
+
value={current}
|
|
76
|
+
className={cn("flex gap-2", className)}
|
|
77
|
+
onValueChange={(next, eventDetails) => {
|
|
78
|
+
setInner(next);
|
|
79
|
+
onValueChange?.(next, eventDetails);
|
|
80
|
+
}}
|
|
81
|
+
onValueComplete={(next, eventDetails) => {
|
|
82
|
+
rootRef.current?.querySelectorAll("input").forEach((input) => input.blur());
|
|
83
|
+
onValueComplete?.(next, eventDetails);
|
|
84
|
+
}}
|
|
85
|
+
{...props}
|
|
86
|
+
>
|
|
87
|
+
{Array.from({ length }, (_, index) => {
|
|
88
|
+
const char = current[index] ?? "";
|
|
89
|
+
return <OtpSlot key={index} text={char && mask ? "•" : char} />;
|
|
90
|
+
})}
|
|
91
|
+
</OTPField.Root>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, type ComponentProps } from "react";
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
|
|
6
|
+
export function PageLoader({ className, ...props }: ComponentProps<"div">) {
|
|
7
|
+
return (
|
|
8
|
+
<div role="status" aria-label="Loading" className={cn("grid size-full min-h-40 place-items-center", className)} {...props}>
|
|
9
|
+
<span className="border-ring size-10 animate-[loader-pulse_1.2s_var(--motion-ease)_infinite] rounded-full border-2 motion-reduce:animate-none" />
|
|
10
|
+
</div>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function BootScreen() {
|
|
15
|
+
const [phase, setPhase] = useState<"on" | "out" | "off">("on");
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const hide = () => setPhase("out");
|
|
19
|
+
if (document.readyState === "complete") hide();
|
|
20
|
+
else window.addEventListener("load", hide, { once: true });
|
|
21
|
+
return () => window.removeEventListener("load", hide);
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
if (phase === "off") return null;
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
className={cn(
|
|
29
|
+
"bg-bg fixed inset-0 z-[calc(var(--z-toast)+10)] transition-opacity duration-[calc(var(--motion-duration)*2)] ease-[var(--motion-ease)]",
|
|
30
|
+
phase === "out" && "pointer-events-none opacity-0",
|
|
31
|
+
)}
|
|
32
|
+
onTransitionEnd={() => setPhase("off")}
|
|
33
|
+
>
|
|
34
|
+
<PageLoader className="size-full min-h-0" />
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { PreviewCard } from "@base-ui/react/preview-card";
|
|
4
|
+
import type { ComponentProps } from "react";
|
|
5
|
+
import { cn } from "../lib/cn";
|
|
6
|
+
import { popupClass } from "./popup";
|
|
7
|
+
|
|
8
|
+
export const Pophover = PreviewCard.Root;
|
|
9
|
+
|
|
10
|
+
export function PophoverTrigger({ delay = 150, closeDelay = 200, ...props }: ComponentProps<typeof PreviewCard.Trigger>) {
|
|
11
|
+
return <PreviewCard.Trigger delay={delay} closeDelay={closeDelay} {...props} />;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function PophoverContent({
|
|
15
|
+
className,
|
|
16
|
+
side = "bottom",
|
|
17
|
+
align = "center",
|
|
18
|
+
...props
|
|
19
|
+
}: ComponentProps<typeof PreviewCard.Popup> &
|
|
20
|
+
Pick<ComponentProps<typeof PreviewCard.Positioner>, "side" | "align">) {
|
|
21
|
+
return (
|
|
22
|
+
<PreviewCard.Portal>
|
|
23
|
+
<PreviewCard.Positioner side={side} align={align} sideOffset={6} className="z-[var(--z-popover)]">
|
|
24
|
+
<PreviewCard.Popup className={cn(popupClass, "p-3", className)} {...props} />
|
|
25
|
+
</PreviewCard.Positioner>
|
|
26
|
+
</PreviewCard.Portal>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Popover as BasePopover } from "@base-ui/react/popover";
|
|
4
|
+
import type { ComponentProps } from "react";
|
|
5
|
+
import { cn } from "../lib/cn";
|
|
6
|
+
import { popupClass, useAnchoredPopup } from "./popup";
|
|
7
|
+
|
|
8
|
+
export const Popover = BasePopover.Root;
|
|
9
|
+
export const PopoverTrigger = BasePopover.Trigger;
|
|
10
|
+
|
|
11
|
+
export function PopoverContent({
|
|
12
|
+
className,
|
|
13
|
+
children,
|
|
14
|
+
container,
|
|
15
|
+
side,
|
|
16
|
+
align,
|
|
17
|
+
...props
|
|
18
|
+
}: ComponentProps<typeof BasePopover.Popup> &
|
|
19
|
+
Pick<ComponentProps<typeof BasePopover.Portal>, "container"> &
|
|
20
|
+
Pick<ComponentProps<typeof BasePopover.Positioner>, "side" | "align">) {
|
|
21
|
+
const { popupClassName: _popupClassName, ...anchor } = useAnchoredPopup();
|
|
22
|
+
return (
|
|
23
|
+
<BasePopover.Portal container={container}>
|
|
24
|
+
<BasePopover.Positioner
|
|
25
|
+
className="z-[var(--z-popover)]"
|
|
26
|
+
{...anchor}
|
|
27
|
+
{...(side && { side })}
|
|
28
|
+
{...(align && { align })}
|
|
29
|
+
>
|
|
30
|
+
<BasePopover.Popup className={cn(popupClass, "p-3", className)} {...props}>
|
|
31
|
+
{children}
|
|
32
|
+
</BasePopover.Popup>
|
|
33
|
+
</BasePopover.Positioner>
|
|
34
|
+
</BasePopover.Portal>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, type ComponentType, type ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
const transition =
|
|
6
|
+
"transition-[opacity,transform,translate,scale] duration-[var(--motion-duration)] ease-[var(--motion-ease)] motion-reduce:transition-none";
|
|
7
|
+
|
|
8
|
+
const backdropTransition =
|
|
9
|
+
"transition-[opacity,backdrop-filter] duration-[calc(var(--motion-duration)*2.2)] ease-[var(--motion-ease)] motion-reduce:transition-none";
|
|
10
|
+
|
|
11
|
+
export const backdropMotion = `${backdropTransition} opacity-100 backdrop-blur-2xl data-[starting-style]:opacity-0 data-[starting-style]:backdrop-blur-[0px] data-[ending-style]:opacity-0 data-[ending-style]:backdrop-blur-[0px]`;
|
|
12
|
+
|
|
13
|
+
export const popupMotion = `${transition} origin-[var(--transform-origin)] scale-100 opacity-100 data-[starting-style]:scale-[0.96] data-[starting-style]:opacity-0 data-[ending-style]:scale-[0.96] data-[ending-style]:opacity-0`;
|
|
14
|
+
|
|
15
|
+
export const popupClass =
|
|
16
|
+
"border-border bg-bg-elevated/60 text-fg relative z-[var(--z-popover)] min-w-40 overflow-visible rounded-[var(--radius-md)] border p-1 shadow-[var(--shadow-md)] backdrop-blur-2xl outline-none " +
|
|
17
|
+
popupMotion;
|
|
18
|
+
|
|
19
|
+
export const popupItemClass =
|
|
20
|
+
"relative flex cursor-pointer items-center gap-2 rounded-[var(--radius-sm)] px-2 py-1.5 text-sm outline-none data-[disabled]:pointer-events-none data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 data-[highlighted]:text-fg";
|
|
21
|
+
|
|
22
|
+
export const HIGHLIGHTED = "[data-highlighted]";
|
|
23
|
+
|
|
24
|
+
export const popupArrowClass =
|
|
25
|
+
"pointer-events-none data-[side=bottom]:top-[-5px] data-[side=left]:right-[-8px] data-[side=left]:rotate-90 data-[side=right]:left-[-8px] data-[side=right]:-rotate-90 data-[side=top]:bottom-[-5px] data-[side=top]:rotate-180";
|
|
26
|
+
|
|
27
|
+
export function PopupArrowSvg({ inverted = false }: { inverted?: boolean }) {
|
|
28
|
+
return (
|
|
29
|
+
<svg width="12" height="6" viewBox="0 0 12 6" aria-hidden>
|
|
30
|
+
<path
|
|
31
|
+
d="M0.8 5.4 L6 1 L11.2 5.4"
|
|
32
|
+
className={inverted ? "fill-fg stroke-fg" : "fill-bg-elevated stroke-border"}
|
|
33
|
+
strokeWidth="1"
|
|
34
|
+
strokeLinejoin="round"
|
|
35
|
+
strokeLinecap="round"
|
|
36
|
+
/>
|
|
37
|
+
</svg>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function PopupArrow({
|
|
42
|
+
arrow: Arrow,
|
|
43
|
+
inverted = false,
|
|
44
|
+
}: {
|
|
45
|
+
arrow: ComponentType<{ className?: string; children?: ReactNode }>;
|
|
46
|
+
inverted?: boolean;
|
|
47
|
+
}) {
|
|
48
|
+
return (
|
|
49
|
+
<Arrow className={popupArrowClass}>
|
|
50
|
+
<PopupArrowSvg inverted={inverted} />
|
|
51
|
+
</Arrow>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function useAnchoredPopup() {
|
|
56
|
+
const [desktop, setDesktop] = useState(true);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
const query = window.matchMedia("(min-width: 768px)");
|
|
60
|
+
const apply = () => setDesktop(query.matches);
|
|
61
|
+
apply();
|
|
62
|
+
query.addEventListener("change", apply);
|
|
63
|
+
return () => query.removeEventListener("change", apply);
|
|
64
|
+
}, []);
|
|
65
|
+
|
|
66
|
+
if (!desktop) {
|
|
67
|
+
return {
|
|
68
|
+
side: "bottom" as const,
|
|
69
|
+
align: "center" as const,
|
|
70
|
+
sideOffset: 8,
|
|
71
|
+
popupClassName: "w-[calc(100vw-1.5rem)] max-w-[calc(100vw-1.5rem)]",
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return { side: "bottom" as const, align: "start" as const, sideOffset: 6, popupClassName: "" };
|
|
75
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
|
|
4
|
+
export function Progress({
|
|
5
|
+
value = 0,
|
|
6
|
+
max = 100,
|
|
7
|
+
className,
|
|
8
|
+
...props
|
|
9
|
+
}: ComponentProps<"div"> & { value?: number; max?: number }) {
|
|
10
|
+
const ratio = max <= 0 ? 0 : Math.min(1, Math.max(0, value / max));
|
|
11
|
+
return (
|
|
12
|
+
<div
|
|
13
|
+
role="progressbar"
|
|
14
|
+
aria-valuemin={0}
|
|
15
|
+
aria-valuemax={max}
|
|
16
|
+
aria-valuenow={Math.round(value)}
|
|
17
|
+
className={cn("bg-bg-muted h-2 overflow-hidden rounded-full", className)}
|
|
18
|
+
{...props}
|
|
19
|
+
>
|
|
20
|
+
<div
|
|
21
|
+
className="bg-accent h-full rounded-full transition-[width] duration-[calc(var(--motion-duration)*1.6)] ease-[var(--motion-ease)] motion-reduce:transition-none"
|
|
22
|
+
style={{ width: `${ratio * 100}%` }}
|
|
23
|
+
/>
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
|
|
4
|
+
export function Result({
|
|
5
|
+
title,
|
|
6
|
+
description,
|
|
7
|
+
children,
|
|
8
|
+
className,
|
|
9
|
+
}: {
|
|
10
|
+
title: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
children?: ReactNode;
|
|
13
|
+
className?: string;
|
|
14
|
+
}) {
|
|
15
|
+
return (
|
|
16
|
+
<div className={cn("mx-auto grid min-h-64 max-w-md place-content-center justify-items-center gap-3 px-4 py-16 text-center", className)}>
|
|
17
|
+
<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
|
|
18
|
+
{description ? <p className="text-fg-muted text-sm">{description}</p> : null}
|
|
19
|
+
{children ? <div className="mt-2 flex flex-wrap items-center justify-center gap-2">{children}</div> : null}
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ScrollArea as BaseScrollArea } from "@base-ui/react/scroll-area";
|
|
4
|
+
import type { ComponentProps } from "react";
|
|
5
|
+
import { cn } from "../lib/cn";
|
|
6
|
+
|
|
7
|
+
const scrollbarClass =
|
|
8
|
+
"flex touch-none p-px opacity-0 transition-opacity duration-[var(--motion-duration)] ease-[var(--motion-ease)] data-[scrolling]:opacity-100";
|
|
9
|
+
|
|
10
|
+
export function ScrollArea({
|
|
11
|
+
className,
|
|
12
|
+
children,
|
|
13
|
+
contentClassName,
|
|
14
|
+
hideScrollbar = false,
|
|
15
|
+
...props
|
|
16
|
+
}: ComponentProps<typeof BaseScrollArea.Root> & { contentClassName?: string; hideScrollbar?: boolean }) {
|
|
17
|
+
return (
|
|
18
|
+
<BaseScrollArea.Root className={cn("relative overflow-hidden", className)} {...props}>
|
|
19
|
+
<BaseScrollArea.Viewport className="size-full overscroll-contain rounded-[inherit]">
|
|
20
|
+
<BaseScrollArea.Content className={contentClassName}>{children}</BaseScrollArea.Content>
|
|
21
|
+
</BaseScrollArea.Viewport>
|
|
22
|
+
{hideScrollbar ? null : (
|
|
23
|
+
<>
|
|
24
|
+
<BaseScrollArea.Scrollbar orientation="vertical" className={cn(scrollbarClass, "w-[var(--scrollbar-size)]")}>
|
|
25
|
+
<BaseScrollArea.Thumb className="bg-[var(--scrollbar-thumb)] flex-1 rounded-full" />
|
|
26
|
+
</BaseScrollArea.Scrollbar>
|
|
27
|
+
<BaseScrollArea.Scrollbar
|
|
28
|
+
orientation="horizontal"
|
|
29
|
+
className={cn(scrollbarClass, "h-[var(--scrollbar-size)] flex-col")}
|
|
30
|
+
>
|
|
31
|
+
<BaseScrollArea.Thumb className="bg-[var(--scrollbar-thumb)] flex-1 rounded-full" />
|
|
32
|
+
</BaseScrollArea.Scrollbar>
|
|
33
|
+
</>
|
|
34
|
+
)}
|
|
35
|
+
<BaseScrollArea.Corner />
|
|
36
|
+
</BaseScrollArea.Root>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Select as BaseSelect } from "@base-ui/react/select";
|
|
4
|
+
import { ArrowDown01Icon } from "@hugeicons-pro/core-stroke-rounded";
|
|
5
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
6
|
+
import { cn } from "../lib/cn";
|
|
7
|
+
import { Icon } from "./icon";
|
|
8
|
+
import { Glide } from "./glide";
|
|
9
|
+
import { HIGHLIGHTED, popupClass, popupItemClass, useAnchoredPopup } from "./popup";
|
|
10
|
+
|
|
11
|
+
export const Select = BaseSelect.Root;
|
|
12
|
+
|
|
13
|
+
export function SelectTrigger({ className, children, ...props }: ComponentProps<typeof BaseSelect.Trigger>) {
|
|
14
|
+
return (
|
|
15
|
+
<BaseSelect.Trigger
|
|
16
|
+
className={cn(
|
|
17
|
+
"border-border bg-bg-elevated text-fg hover:border-border-strong focus-visible:border-accent focus-visible:ring-ring flex w-full cursor-pointer items-center justify-between rounded-[var(--radius-md)] border px-3 text-sm outline-none focus-visible:ring-2 disabled:cursor-not-allowed disabled:opacity-50 data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 [height:var(--row-h)]",
|
|
18
|
+
className,
|
|
19
|
+
)}
|
|
20
|
+
{...props}
|
|
21
|
+
>
|
|
22
|
+
{children}
|
|
23
|
+
<BaseSelect.Icon className="text-fg-muted">
|
|
24
|
+
<Icon icon={ArrowDown01Icon} variant="stroke" size={16} />
|
|
25
|
+
</BaseSelect.Icon>
|
|
26
|
+
</BaseSelect.Trigger>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const SelectValue = BaseSelect.Value;
|
|
31
|
+
|
|
32
|
+
export function SelectContent({ className, children, ...props }: ComponentProps<typeof BaseSelect.Popup>) {
|
|
33
|
+
const { popupClassName: _popupClassName, ...anchor } = useAnchoredPopup();
|
|
34
|
+
return (
|
|
35
|
+
<BaseSelect.Portal>
|
|
36
|
+
<BaseSelect.Positioner className="z-[var(--z-popover)]" {...anchor}>
|
|
37
|
+
<BaseSelect.Popup className={cn(popupClass, "w-[var(--anchor-width)] min-w-[var(--anchor-width)]", className)} {...props}>
|
|
38
|
+
<Glide selector={HIGHLIGHTED} />
|
|
39
|
+
<BaseSelect.List>{children}</BaseSelect.List>
|
|
40
|
+
</BaseSelect.Popup>
|
|
41
|
+
</BaseSelect.Positioner>
|
|
42
|
+
</BaseSelect.Portal>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function SelectItem({ className, children, ...props }: ComponentProps<typeof BaseSelect.Item>) {
|
|
47
|
+
return (
|
|
48
|
+
<BaseSelect.Item className={cn(popupItemClass, className)} {...props}>
|
|
49
|
+
<BaseSelect.ItemText>{children as ReactNode}</BaseSelect.ItemText>
|
|
50
|
+
</BaseSelect.Item>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Separator as BaseSeparator } from "@base-ui/react/separator";
|
|
4
|
+
import type { ComponentProps } from "react";
|
|
5
|
+
import { cn } from "../lib/cn";
|
|
6
|
+
|
|
7
|
+
export function Separator({ className, ...props }: ComponentProps<typeof BaseSeparator>) {
|
|
8
|
+
return (
|
|
9
|
+
<BaseSeparator
|
|
10
|
+
className={cn("bg-border h-px w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", className)}
|
|
11
|
+
{...props}
|
|
12
|
+
/>
|
|
13
|
+
);
|
|
14
|
+
}
|