@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,96 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Drawer as BaseDrawer } from "@base-ui/react/drawer";
|
|
4
|
+
import { Cancel01Icon } from "@hugeicons-pro/core-stroke-rounded";
|
|
5
|
+
import { createContext, useContext, type ComponentProps } from "react";
|
|
6
|
+
import { cn } from "../lib/cn";
|
|
7
|
+
import { Icon } from "./icon";
|
|
8
|
+
|
|
9
|
+
export const SheetTrigger = BaseDrawer.Trigger;
|
|
10
|
+
export const SheetClose = BaseDrawer.Close;
|
|
11
|
+
export const SheetTitle = BaseDrawer.Title;
|
|
12
|
+
export const SheetDescription = BaseDrawer.Description;
|
|
13
|
+
|
|
14
|
+
export type SheetSide = "top" | "right" | "bottom" | "left";
|
|
15
|
+
|
|
16
|
+
const SideContext = createContext<SheetSide>("right");
|
|
17
|
+
|
|
18
|
+
const direction = { top: "up", right: "right", bottom: "down", left: "left" } as const;
|
|
19
|
+
|
|
20
|
+
const edge = {
|
|
21
|
+
top: "top-[calc(0.75rem+var(--safe-top))]",
|
|
22
|
+
right: "right-[calc(0.75rem+var(--safe-right))]",
|
|
23
|
+
bottom: "bottom-[calc(0.75rem+var(--safe-bottom))]",
|
|
24
|
+
left: "left-[calc(0.75rem+var(--safe-left))]",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const frame = {
|
|
28
|
+
right: `${edge.top} ${edge.bottom} ${edge.right} w-[min(20rem,calc(100vw-1.5rem))]`,
|
|
29
|
+
left: `${edge.top} ${edge.bottom} ${edge.left} w-[min(20rem,calc(100vw-1.5rem))]`,
|
|
30
|
+
bottom: `${edge.left} ${edge.right} ${edge.bottom} max-h-[min(90dvh,calc(100dvh-1.5rem-var(--safe-top)-var(--safe-bottom)))] md:max-h-[min(32rem,70dvh)]`,
|
|
31
|
+
top: `${edge.left} ${edge.right} ${edge.top} max-h-[min(90dvh,calc(100dvh-1.5rem-var(--safe-top)-var(--safe-bottom)))] md:max-h-[min(32rem,70dvh)]`,
|
|
32
|
+
} as const;
|
|
33
|
+
|
|
34
|
+
const motion = {
|
|
35
|
+
right:
|
|
36
|
+
"translate-x-(--drawer-swipe-movement-x) data-[starting-style]:translate-x-[calc(100%+1rem+var(--safe-right))] data-[ending-style]:translate-x-[calc(100%+1rem+var(--safe-right))]",
|
|
37
|
+
left: "translate-x-(--drawer-swipe-movement-x) data-[starting-style]:-translate-x-[calc(100%+1rem+var(--safe-left))] data-[ending-style]:-translate-x-[calc(100%+1rem+var(--safe-left))]",
|
|
38
|
+
bottom:
|
|
39
|
+
"translate-y-(--drawer-swipe-movement-y) data-[starting-style]:translate-y-[calc(100%+1rem+var(--safe-bottom))] data-[ending-style]:translate-y-[calc(100%+1rem+var(--safe-bottom))]",
|
|
40
|
+
top: "translate-y-(--drawer-swipe-movement-y) data-[starting-style]:-translate-y-[calc(100%+1rem+var(--safe-top))] data-[ending-style]:-translate-y-[calc(100%+1rem+var(--safe-top))]",
|
|
41
|
+
} as const;
|
|
42
|
+
|
|
43
|
+
const thumb = {
|
|
44
|
+
right: "top-1/2 left-2 h-[18%] min-h-12 w-1.5 -translate-y-1/2",
|
|
45
|
+
left: "top-1/2 right-2 h-[18%] min-h-12 w-1.5 -translate-y-1/2",
|
|
46
|
+
bottom: "top-2 left-1/2 h-1.5 w-[18%] min-w-12 -translate-x-1/2",
|
|
47
|
+
top: "bottom-2 left-1/2 h-1.5 w-[18%] min-w-12 -translate-x-1/2",
|
|
48
|
+
} as const;
|
|
49
|
+
|
|
50
|
+
const room = { right: "pl-4 md:pl-7", left: "pr-4 md:pr-7", bottom: "pt-4 md:pt-7", top: "pb-4 md:pb-7" } as const;
|
|
51
|
+
|
|
52
|
+
const transition =
|
|
53
|
+
"transition-[translate,opacity] duration-[var(--motion-duration)] ease-[var(--motion-ease)] data-[swiping]:duration-0 motion-reduce:transition-none";
|
|
54
|
+
|
|
55
|
+
export function Sheet({ side = "right", ...props }: ComponentProps<typeof BaseDrawer.Root> & { side?: SheetSide }) {
|
|
56
|
+
return (
|
|
57
|
+
<SideContext.Provider value={side}>
|
|
58
|
+
<BaseDrawer.Root swipeDirection={direction[side]} {...props} />
|
|
59
|
+
</SideContext.Provider>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function SheetContent({ className, children, ...props }: ComponentProps<typeof BaseDrawer.Popup>) {
|
|
64
|
+
const side = useContext(SideContext);
|
|
65
|
+
return (
|
|
66
|
+
<BaseDrawer.Portal>
|
|
67
|
+
<BaseDrawer.Backdrop
|
|
68
|
+
className={cn(
|
|
69
|
+
"fixed inset-0 z-[var(--z-overlay)] bg-transparent opacity-[calc(1-var(--drawer-swipe-progress,0))] backdrop-blur-2xl transition-[opacity,backdrop-filter] duration-[calc(var(--motion-duration)*2.2)] ease-[var(--motion-ease)] data-[ending-style]:opacity-0 data-[ending-style]:backdrop-blur-[0px] data-[starting-style]:opacity-0 data-[starting-style]:backdrop-blur-[0px]",
|
|
70
|
+
)}
|
|
71
|
+
/>
|
|
72
|
+
<BaseDrawer.Viewport className="fixed inset-0 z-[var(--z-modal)]">
|
|
73
|
+
<BaseDrawer.Popup
|
|
74
|
+
className={cn(
|
|
75
|
+
"border-border bg-bg-elevated/70 fixed flex touch-auto flex-col overflow-hidden rounded-[var(--radius-lg)] border p-4 shadow-[var(--shadow-lg)] backdrop-blur-xl outline-none data-[swiping]:select-none",
|
|
76
|
+
frame[side],
|
|
77
|
+
room[side],
|
|
78
|
+
motion[side],
|
|
79
|
+
transition,
|
|
80
|
+
className,
|
|
81
|
+
)}
|
|
82
|
+
{...props}
|
|
83
|
+
>
|
|
84
|
+
<span aria-hidden className={cn("bg-border-strong pointer-events-none absolute hidden rounded-full md:block", thumb[side])} />
|
|
85
|
+
<BaseDrawer.Content className="flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto overscroll-contain">
|
|
86
|
+
{children}
|
|
87
|
+
</BaseDrawer.Content>
|
|
88
|
+
<BaseDrawer.Close className="text-fg-subtle hover:bg-bg-subtle hover:text-fg absolute top-3 right-3 z-[1] inline-flex size-7 cursor-pointer items-center justify-center rounded-[var(--radius-sm)]">
|
|
89
|
+
<Icon icon={Cancel01Icon} variant="stroke" size={16} />
|
|
90
|
+
<span className="sr-only">Close</span>
|
|
91
|
+
</BaseDrawer.Close>
|
|
92
|
+
</BaseDrawer.Popup>
|
|
93
|
+
</BaseDrawer.Viewport>
|
|
94
|
+
</BaseDrawer.Portal>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button as BaseButton } from "@base-ui/react/button";
|
|
4
|
+
import { SidebarLeftIcon } from "@hugeicons-pro/core-stroke-rounded";
|
|
5
|
+
import { createContext, useContext, useEffect, useMemo, useState, type ComponentProps, type ReactNode } from "react";
|
|
6
|
+
import { cn } from "../lib/cn";
|
|
7
|
+
import type { AppIcon, IconVariant } from "./icon";
|
|
8
|
+
import { Glide, glideActive } from "./glide";
|
|
9
|
+
import { Icon } from "./icon";
|
|
10
|
+
|
|
11
|
+
const ACTIVE = '[aria-current="page"]';
|
|
12
|
+
import { ScrollArea } from "./scroll-area";
|
|
13
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip";
|
|
14
|
+
|
|
15
|
+
interface SidebarState {
|
|
16
|
+
collapsed: boolean;
|
|
17
|
+
toggle: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const SidebarContext = createContext<SidebarState | null>(null);
|
|
21
|
+
|
|
22
|
+
export const sidebarLabel =
|
|
23
|
+
"min-w-0 truncate transition-opacity duration-[var(--motion-duration)] ease-[var(--motion-ease)]";
|
|
24
|
+
|
|
25
|
+
export function useSidebar() {
|
|
26
|
+
const value = useContext(SidebarContext);
|
|
27
|
+
if (!value) throw new Error("useSidebar must be used within SidebarProvider");
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function SidebarProvider({ children, defaultCollapsed = false }: { children: ReactNode; defaultCollapsed?: boolean }) {
|
|
32
|
+
const [collapsed, setCollapsed] = useState(defaultCollapsed);
|
|
33
|
+
useEffect(() => setCollapsed(defaultCollapsed), [defaultCollapsed]);
|
|
34
|
+
const value = useMemo(() => ({ collapsed, toggle: () => setCollapsed((current) => !current) }), [collapsed]);
|
|
35
|
+
return <SidebarContext.Provider value={value}>{children}</SidebarContext.Provider>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function Sidebar({ className, ...props }: ComponentProps<"aside">) {
|
|
39
|
+
const { collapsed } = useSidebar();
|
|
40
|
+
return (
|
|
41
|
+
<aside
|
|
42
|
+
data-collapsed={collapsed || undefined}
|
|
43
|
+
className={cn(
|
|
44
|
+
"bg-bg-elevated border-border hidden h-full min-h-0 w-[calc(15rem+var(--safe-left))] shrink-0 flex-col overflow-hidden border-r pt-[var(--safe-top)] pb-[var(--safe-bottom)] pl-[var(--safe-left)] transition-[width] duration-[var(--motion-duration)] ease-[var(--motion-ease)] md:flex",
|
|
45
|
+
collapsed && "w-[calc(3.5rem+var(--safe-left))]",
|
|
46
|
+
className,
|
|
47
|
+
)}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function SidebarHeader({ className, ...props }: ComponentProps<"div">) {
|
|
54
|
+
return <div className={cn("flex items-center justify-between gap-2 px-3 py-3", className)} {...props} />;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function SidebarNav({
|
|
58
|
+
className,
|
|
59
|
+
indicatorClassName,
|
|
60
|
+
children,
|
|
61
|
+
...props
|
|
62
|
+
}: ComponentProps<"nav"> & { indicatorClassName?: string }) {
|
|
63
|
+
return (
|
|
64
|
+
<nav className={cn("flex min-h-0 flex-1 flex-col px-2 pb-3", className)} {...props}>
|
|
65
|
+
<ScrollArea hideScrollbar className="h-full min-h-0 flex-1" contentClassName="min-w-0! w-full">
|
|
66
|
+
<div className="relative flex flex-col gap-0.5">
|
|
67
|
+
<Glide selector={ACTIVE} className={cn("bg-accent-muted/60", indicatorClassName)} />
|
|
68
|
+
{children}
|
|
69
|
+
</div>
|
|
70
|
+
</ScrollArea>
|
|
71
|
+
</nav>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function SidebarGroup({ className, ...props }: ComponentProps<"div">) {
|
|
76
|
+
return <div className={cn("flex flex-col gap-0.5", className)} {...props} />;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function SidebarLabel({ className, ...props }: ComponentProps<"p">) {
|
|
80
|
+
const { collapsed } = useSidebar();
|
|
81
|
+
return (
|
|
82
|
+
<p
|
|
83
|
+
className={cn(
|
|
84
|
+
"text-fg-subtle px-3 pt-3 pb-1 text-[11px] font-medium tracking-wide uppercase",
|
|
85
|
+
collapsed && "sr-only",
|
|
86
|
+
className,
|
|
87
|
+
)}
|
|
88
|
+
{...props}
|
|
89
|
+
/>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function SidebarFooter({ className, ...props }: ComponentProps<"div">) {
|
|
94
|
+
return <div className={cn("mt-auto flex flex-col gap-1 p-2", className)} {...props} />;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function SidebarInset({ className, ...props }: ComponentProps<"div">) {
|
|
98
|
+
return <div className={cn("min-w-0 flex-1", className)} {...props} />;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function SidebarTrigger({ className, ...props }: ComponentProps<"button">) {
|
|
102
|
+
const { toggle, collapsed } = useSidebar();
|
|
103
|
+
return (
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
aria-label={collapsed ? "Expand sidebar" : "Collapse sidebar"}
|
|
107
|
+
className={cn(
|
|
108
|
+
"text-fg-muted hover:bg-bg-subtle hover:text-fg inline-flex size-8 items-center justify-center rounded-[var(--radius-sm)]",
|
|
109
|
+
className,
|
|
110
|
+
)}
|
|
111
|
+
onClick={toggle}
|
|
112
|
+
{...props}
|
|
113
|
+
>
|
|
114
|
+
<Icon icon={SidebarLeftIcon} variant="stroke" size={16} />
|
|
115
|
+
</button>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function SidebarItem({
|
|
120
|
+
icon,
|
|
121
|
+
iconVariant = "stroke",
|
|
122
|
+
active,
|
|
123
|
+
className,
|
|
124
|
+
children,
|
|
125
|
+
render,
|
|
126
|
+
...props
|
|
127
|
+
}: ComponentProps<typeof BaseButton> & { icon?: AppIcon; iconVariant?: IconVariant; active?: boolean }) {
|
|
128
|
+
const { collapsed } = useSidebar();
|
|
129
|
+
const item = (
|
|
130
|
+
<BaseButton
|
|
131
|
+
nativeButton={render ? false : true}
|
|
132
|
+
render={render}
|
|
133
|
+
aria-current={active ? "page" : undefined}
|
|
134
|
+
className={cn(
|
|
135
|
+
"text-fg-muted hover:bg-bg-subtle hover:text-fg relative flex h-[calc(var(--row-h)-0.25rem)] w-full cursor-pointer items-center gap-2 overflow-hidden rounded-[var(--radius-sm)] px-3 text-sm whitespace-nowrap outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
136
|
+
active && cn("bg-accent-muted text-accent hover:text-accent hover:bg-transparent", glideActive),
|
|
137
|
+
className,
|
|
138
|
+
)}
|
|
139
|
+
{...props}
|
|
140
|
+
>
|
|
141
|
+
{icon ? <Icon icon={icon} variant={iconVariant} size={16} className="shrink-0" /> : null}
|
|
142
|
+
<span className={cn(sidebarLabel, collapsed && "opacity-0")}>{children}</span>
|
|
143
|
+
</BaseButton>
|
|
144
|
+
);
|
|
145
|
+
return (
|
|
146
|
+
<Tooltip disabled={!collapsed}>
|
|
147
|
+
<TooltipTrigger render={item} />
|
|
148
|
+
<TooltipContent side="right">{children}</TooltipContent>
|
|
149
|
+
</Tooltip>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function BottomBar({ className, children, ...props }: ComponentProps<"nav">) {
|
|
154
|
+
return (
|
|
155
|
+
<nav
|
|
156
|
+
className={cn(
|
|
157
|
+
"bg-bg-elevated/70 border-border fixed right-[calc(0.75rem+var(--safe-right))] bottom-[calc(0.75rem+var(--safe-bottom))] left-[calc(0.75rem+var(--safe-left))] z-[var(--z-sticky)] rounded-[var(--radius-lg)] border p-1 shadow-[var(--shadow-md)] backdrop-blur-xl md:hidden",
|
|
158
|
+
className,
|
|
159
|
+
)}
|
|
160
|
+
{...props}
|
|
161
|
+
>
|
|
162
|
+
<div className="relative flex items-stretch justify-around">
|
|
163
|
+
<Glide selector={ACTIVE} className="bg-accent-muted/60 rounded-[var(--radius-md)]" />
|
|
164
|
+
{children}
|
|
165
|
+
</div>
|
|
166
|
+
</nav>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function BottomBarItem({
|
|
171
|
+
icon,
|
|
172
|
+
iconVariant = "stroke",
|
|
173
|
+
active,
|
|
174
|
+
className,
|
|
175
|
+
children,
|
|
176
|
+
render,
|
|
177
|
+
...props
|
|
178
|
+
}: ComponentProps<typeof BaseButton> & { icon?: AppIcon; iconVariant?: IconVariant; active?: boolean }) {
|
|
179
|
+
return (
|
|
180
|
+
<BaseButton
|
|
181
|
+
nativeButton={render ? false : true}
|
|
182
|
+
render={render}
|
|
183
|
+
aria-current={active ? "page" : undefined}
|
|
184
|
+
className={cn(
|
|
185
|
+
"text-fg-muted hover:bg-bg-subtle relative flex flex-1 cursor-pointer flex-col items-center gap-0.5 rounded-[var(--radius-md)] px-2 py-1.5 text-[11px] transition-colors duration-[var(--motion-duration)]",
|
|
186
|
+
active && cn("bg-accent-muted text-accent hover:bg-transparent", glideActive),
|
|
187
|
+
className,
|
|
188
|
+
)}
|
|
189
|
+
{...props}
|
|
190
|
+
>
|
|
191
|
+
{icon ? <Icon icon={icon} variant={iconVariant} size={18} /> : null}
|
|
192
|
+
{children}
|
|
193
|
+
</BaseButton>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
|
|
4
|
+
export function Skeleton({ className, ...props }: ComponentProps<"div">) {
|
|
5
|
+
return (
|
|
6
|
+
<div
|
|
7
|
+
className={cn(
|
|
8
|
+
"bg-bg-muted relative overflow-hidden rounded-[var(--radius-md)] before:absolute before:inset-0 before:animate-[shimmer_1.4s_linear_infinite] before:bg-[linear-gradient(90deg,transparent,color-mix(in_oklch,var(--fg)_14%,transparent),transparent)] before:bg-[length:200%_100%] before:content-[''] motion-reduce:before:animate-none",
|
|
9
|
+
className,
|
|
10
|
+
)}
|
|
11
|
+
{...props}
|
|
12
|
+
/>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Slider as BaseSlider } from "@base-ui/react/slider";
|
|
4
|
+
import type { ComponentProps } from "react";
|
|
5
|
+
import { cn } from "../lib/cn";
|
|
6
|
+
|
|
7
|
+
type SliderProps = Omit<ComponentProps<typeof BaseSlider.Root>, "value" | "defaultValue" | "onValueChange"> & {
|
|
8
|
+
value?: number;
|
|
9
|
+
defaultValue?: number;
|
|
10
|
+
onValueChange?: (value: number) => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function Slider({ className, onValueChange, ...props }: SliderProps) {
|
|
14
|
+
return (
|
|
15
|
+
<BaseSlider.Root
|
|
16
|
+
className={cn(
|
|
17
|
+
"hogoshi-grab flex w-full touch-none items-center py-2 data-[disabled]:opacity-50",
|
|
18
|
+
className,
|
|
19
|
+
)}
|
|
20
|
+
onValueChange={(next) => onValueChange?.(typeof next === "number" ? next : next[0])}
|
|
21
|
+
{...props}
|
|
22
|
+
>
|
|
23
|
+
<BaseSlider.Control className="flex w-full items-center">
|
|
24
|
+
<BaseSlider.Track className="bg-bg-muted relative h-1 w-full rounded-full">
|
|
25
|
+
<BaseSlider.Indicator className="bg-accent absolute h-full rounded-full" />
|
|
26
|
+
<BaseSlider.Thumb className="hogoshi-grab border-accent bg-bg-elevated size-4 rounded-full border shadow-[var(--shadow-sm)] outline-none focus-visible:ring-2 focus-visible:ring-ring" />
|
|
27
|
+
</BaseSlider.Track>
|
|
28
|
+
</BaseSlider.Control>
|
|
29
|
+
</BaseSlider.Root>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, type ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
const IDLE_MS = 700;
|
|
6
|
+
const EASE = 0.16;
|
|
7
|
+
const LINE = 40;
|
|
8
|
+
const DRIFT = 2;
|
|
9
|
+
const SCROLL_KEYS = ["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " "];
|
|
10
|
+
|
|
11
|
+
interface Motion {
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
writtenX: number;
|
|
15
|
+
writtenY: number;
|
|
16
|
+
frame: number;
|
|
17
|
+
last: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const motions = new WeakMap<Element, Motion>();
|
|
21
|
+
|
|
22
|
+
function markScrolling(element: Element) {
|
|
23
|
+
const node = element as HTMLElement;
|
|
24
|
+
node.setAttribute("data-scrolling", "");
|
|
25
|
+
window.clearTimeout(Number(node.dataset.scrollTimer ?? 0));
|
|
26
|
+
node.dataset.scrollTimer = String(
|
|
27
|
+
window.setTimeout(() => {
|
|
28
|
+
node.removeAttribute("data-scrolling");
|
|
29
|
+
delete node.dataset.scrollTimer;
|
|
30
|
+
}, IDLE_MS),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function motionAllowed() {
|
|
35
|
+
const motion = document.documentElement.getAttribute("data-motion");
|
|
36
|
+
if (motion === "off" || motion === "reduced") return false;
|
|
37
|
+
return !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function stop(element: Element) {
|
|
41
|
+
const motion = motions.get(element);
|
|
42
|
+
if (!motion) return;
|
|
43
|
+
cancelAnimationFrame(motion.frame);
|
|
44
|
+
motions.delete(element);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function stopFrom(target: EventTarget | null) {
|
|
48
|
+
for (let node = target instanceof Element ? target : null; node; node = node.parentElement) stop(node);
|
|
49
|
+
if (document.scrollingElement) stop(document.scrollingElement);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function canScroll(element: Element, dx: number, dy: number) {
|
|
53
|
+
const root = element === document.scrollingElement;
|
|
54
|
+
const style = getComputedStyle(element);
|
|
55
|
+
const scrollY = root
|
|
56
|
+
? style.overflowY !== "hidden" && getComputedStyle(document.body).overflowY !== "hidden"
|
|
57
|
+
: /auto|scroll|overlay/.test(style.overflowY);
|
|
58
|
+
const scrollX = root ? style.overflowX !== "hidden" : /auto|scroll|overlay/.test(style.overflowX);
|
|
59
|
+
const maxY = element.scrollHeight - element.clientHeight;
|
|
60
|
+
const maxX = element.scrollWidth - element.clientWidth;
|
|
61
|
+
const motion = motions.get(element);
|
|
62
|
+
const top = motion?.y ?? element.scrollTop;
|
|
63
|
+
const left = motion?.x ?? element.scrollLeft;
|
|
64
|
+
const vertical = scrollY && maxY > 0 && (dy < 0 ? top > 0 : dy > 0 && top < maxY);
|
|
65
|
+
const horizontal = scrollX && maxX > 0 && (dx < 0 ? left > 0 : dx > 0 && left < maxX);
|
|
66
|
+
return vertical || horizontal;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function findScroller(target: EventTarget | null, dx: number, dy: number) {
|
|
70
|
+
let node = target instanceof Element ? target : null;
|
|
71
|
+
while (node && node !== document.body && node !== document.documentElement) {
|
|
72
|
+
if (node.hasAttribute("data-native-scroll")) return null;
|
|
73
|
+
if (canScroll(node, dx, dy)) return node;
|
|
74
|
+
node = node.parentElement;
|
|
75
|
+
}
|
|
76
|
+
const root = document.scrollingElement;
|
|
77
|
+
return root && canScroll(root, dx, dy) ? root : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isMouseWheel(event: WheelEvent) {
|
|
81
|
+
if (event.deltaMode !== 0) return true;
|
|
82
|
+
return event.deltaX === 0 && Math.abs(event.deltaY) >= 50 && Number.isInteger(event.deltaY);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function step(element: Element, motion: Motion, now: number) {
|
|
86
|
+
const moved =
|
|
87
|
+
Math.abs(element.scrollTop - motion.writtenY) > DRIFT || Math.abs(element.scrollLeft - motion.writtenX) > DRIFT;
|
|
88
|
+
if (moved) {
|
|
89
|
+
motions.delete(element);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const dt = Math.min(64, now - motion.last || 16);
|
|
93
|
+
motion.last = now;
|
|
94
|
+
const k = 1 - Math.pow(1 - EASE, dt / 16.67);
|
|
95
|
+
const nextY = element.scrollTop + (motion.y - element.scrollTop) * k;
|
|
96
|
+
const nextX = element.scrollLeft + (motion.x - element.scrollLeft) * k;
|
|
97
|
+
const done = Math.abs(motion.y - nextY) < 0.5 && Math.abs(motion.x - nextX) < 0.5;
|
|
98
|
+
element.scrollTop = done ? motion.y : nextY;
|
|
99
|
+
element.scrollLeft = done ? motion.x : nextX;
|
|
100
|
+
motion.writtenY = element.scrollTop;
|
|
101
|
+
motion.writtenX = element.scrollLeft;
|
|
102
|
+
if (done) {
|
|
103
|
+
motions.delete(element);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
motion.frame = requestAnimationFrame((time) => step(element, motion, time));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function onWheel(event: WheelEvent) {
|
|
110
|
+
if (event.defaultPrevented || event.ctrlKey) return;
|
|
111
|
+
if (!isMouseWheel(event) || !motionAllowed()) {
|
|
112
|
+
stopFrom(event.target);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const scale = event.deltaMode === 1 ? LINE : event.deltaMode === 2 ? window.innerHeight : 1;
|
|
116
|
+
const sideways = event.shiftKey && !event.deltaX;
|
|
117
|
+
const dx = (sideways ? event.deltaY : event.deltaX) * scale;
|
|
118
|
+
const dy = (sideways ? 0 : event.deltaY) * scale;
|
|
119
|
+
const element = findScroller(event.target, dx, dy);
|
|
120
|
+
if (!element) return;
|
|
121
|
+
event.preventDefault();
|
|
122
|
+
const maxY = element.scrollHeight - element.clientHeight;
|
|
123
|
+
const maxX = element.scrollWidth - element.clientWidth;
|
|
124
|
+
const current = motions.get(element);
|
|
125
|
+
const motion = current ?? {
|
|
126
|
+
x: element.scrollLeft,
|
|
127
|
+
y: element.scrollTop,
|
|
128
|
+
writtenX: element.scrollLeft,
|
|
129
|
+
writtenY: element.scrollTop,
|
|
130
|
+
frame: 0,
|
|
131
|
+
last: performance.now(),
|
|
132
|
+
};
|
|
133
|
+
if (Math.sign(dy) !== Math.sign(motion.y - element.scrollTop) && motion.y !== element.scrollTop) motion.y = element.scrollTop;
|
|
134
|
+
motion.y = Math.max(0, Math.min(maxY, motion.y + dy));
|
|
135
|
+
motion.x = Math.max(0, Math.min(maxX, motion.x + dx));
|
|
136
|
+
if (current) return;
|
|
137
|
+
motions.set(element, motion);
|
|
138
|
+
motion.frame = requestAnimationFrame((time) => step(element, motion, time));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function onScroll(event: Event) {
|
|
142
|
+
const target = event.target === document ? document.scrollingElement : event.target;
|
|
143
|
+
if (target instanceof Element) markScrolling(target);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function onPointerDown(event: PointerEvent) {
|
|
147
|
+
stopFrom(event.target);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function onKeyDown(event: KeyboardEvent) {
|
|
151
|
+
if (SCROLL_KEYS.includes(event.key)) stopFrom(event.target);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function SmoothScroll({ children }: { children: ReactNode }) {
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
window.addEventListener("wheel", onWheel, { passive: false });
|
|
157
|
+
document.addEventListener("scroll", onScroll, true);
|
|
158
|
+
window.addEventListener("pointerdown", onPointerDown, true);
|
|
159
|
+
window.addEventListener("keydown", onKeyDown, true);
|
|
160
|
+
return () => {
|
|
161
|
+
window.removeEventListener("wheel", onWheel);
|
|
162
|
+
document.removeEventListener("scroll", onScroll, true);
|
|
163
|
+
window.removeEventListener("pointerdown", onPointerDown, true);
|
|
164
|
+
window.removeEventListener("keydown", onKeyDown, true);
|
|
165
|
+
};
|
|
166
|
+
}, []);
|
|
167
|
+
|
|
168
|
+
return children;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function scrollToTop(element: Element = document.scrollingElement ?? document.documentElement) {
|
|
172
|
+
stop(element);
|
|
173
|
+
element.scrollTo({ top: 0, behavior: motionAllowed() ? "smooth" : "auto" });
|
|
174
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { LoaderPinwheelIcon } from "@hugeicons-pro/core-stroke-rounded";
|
|
2
|
+
import type { ComponentProps } from "react";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
import { Icon } from "./icon";
|
|
5
|
+
|
|
6
|
+
const sizes = { sm: 14, md: 16, lg: 20 } as const;
|
|
7
|
+
|
|
8
|
+
export function Spinner({
|
|
9
|
+
className,
|
|
10
|
+
size = "md",
|
|
11
|
+
...props
|
|
12
|
+
}: ComponentProps<"span"> & { size?: keyof typeof sizes }) {
|
|
13
|
+
return (
|
|
14
|
+
<span role="status" aria-label="Loading" className={cn("inline-flex", className)} {...props}>
|
|
15
|
+
<Icon icon={LoaderPinwheelIcon} variant="stroke" size={sizes[size]} className="animate-spin" />
|
|
16
|
+
</span>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Switch as BaseSwitch } from "@base-ui/react/switch";
|
|
4
|
+
import type { ComponentProps } from "react";
|
|
5
|
+
import { cn } from "../lib/cn";
|
|
6
|
+
|
|
7
|
+
export function Switch({ className, ...props }: ComponentProps<typeof BaseSwitch.Root>) {
|
|
8
|
+
return (
|
|
9
|
+
<BaseSwitch.Root
|
|
10
|
+
className={cn(
|
|
11
|
+
"bg-bg-muted hover:bg-bg-subtle inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full p-0.5 transition-[background-color,transform] duration-[var(--motion-duration)] outline-none",
|
|
12
|
+
"focus-visible:ring-ring focus-visible:ring-2",
|
|
13
|
+
"data-[checked]:bg-accent data-[checked]:hover:bg-accent-hover",
|
|
14
|
+
"data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 data-[disabled]:hover:bg-bg-muted",
|
|
15
|
+
className,
|
|
16
|
+
)}
|
|
17
|
+
{...props}
|
|
18
|
+
>
|
|
19
|
+
<BaseSwitch.Thumb className="bg-bg-elevated size-4 translate-x-0 rounded-full shadow-[var(--shadow-sm)] transition-transform duration-[var(--motion-duration)] ease-[var(--motion-ease)] data-[checked]:translate-x-4" />
|
|
20
|
+
</BaseSwitch.Root>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ArrowDown01Icon, ArrowUp01Icon, ArrowUpDownIcon } from "@hugeicons-pro/core-stroke-rounded";
|
|
2
|
+
import type { ComponentProps } from "react";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
import { Icon } from "./icon";
|
|
5
|
+
|
|
6
|
+
export type SortDirection = "asc" | "desc";
|
|
7
|
+
|
|
8
|
+
export function Table({ className, ...props }: ComponentProps<"table">) {
|
|
9
|
+
return (
|
|
10
|
+
<div className="w-full overflow-x-auto">
|
|
11
|
+
<table className={cn("w-full border-separate border-spacing-0 text-sm", className)} {...props} />
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function TableHeader({ className, ...props }: ComponentProps<"thead">) {
|
|
17
|
+
return <thead className={cn("text-fg-muted text-left text-xs", className)} {...props} />;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function TableBody({ className, ...props }: ComponentProps<"tbody">) {
|
|
21
|
+
return <tbody className={cn("text-fg", className)} {...props} />;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function TableRow({ className, ...props }: ComponentProps<"tr">) {
|
|
25
|
+
return <tr className={cn("border-border border-b last:border-0", className)} {...props} />;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function TableHead({
|
|
29
|
+
className,
|
|
30
|
+
sort,
|
|
31
|
+
onSort,
|
|
32
|
+
children,
|
|
33
|
+
...props
|
|
34
|
+
}: ComponentProps<"th"> & { sort?: SortDirection | false; onSort?: () => void }) {
|
|
35
|
+
const icon = sort === "asc" ? ArrowUp01Icon : sort === "desc" ? ArrowDown01Icon : ArrowUpDownIcon;
|
|
36
|
+
return (
|
|
37
|
+
<th className={cn("border-border border-b px-3 py-2 font-medium", className)} {...props}>
|
|
38
|
+
{onSort ? (
|
|
39
|
+
<button type="button" className="hover:text-fg inline-flex cursor-pointer items-center gap-1" onClick={onSort}>
|
|
40
|
+
{children}
|
|
41
|
+
<Icon icon={icon} variant="stroke" size={14} className={sort ? "text-fg" : "text-fg-subtle"} />
|
|
42
|
+
</button>
|
|
43
|
+
) : (
|
|
44
|
+
children
|
|
45
|
+
)}
|
|
46
|
+
</th>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function TableCell({ className, ...props }: ComponentProps<"td">) {
|
|
51
|
+
return <td className={cn("border-border border-b px-3 py-2.5 align-middle", className)} {...props} />;
|
|
52
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Tabs as BaseTabs } from "@base-ui/react/tabs";
|
|
4
|
+
import type { ComponentProps } from "react";
|
|
5
|
+
import { cn } from "../lib/cn";
|
|
6
|
+
|
|
7
|
+
export function Tabs({ className, ...props }: ComponentProps<typeof BaseTabs.Root>) {
|
|
8
|
+
return <BaseTabs.Root className={cn("grid", className)} {...props} />;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function TabsList({ className, children, ...props }: ComponentProps<typeof BaseTabs.List>) {
|
|
12
|
+
return (
|
|
13
|
+
<BaseTabs.List
|
|
14
|
+
className={cn(
|
|
15
|
+
"bg-bg-muted relative inline-flex items-center gap-1 rounded-[var(--radius-md)] p-1",
|
|
16
|
+
className,
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
>
|
|
20
|
+
{children}
|
|
21
|
+
<BaseTabs.Indicator className="bg-bg-elevated/70 pointer-events-none absolute top-0 left-0 z-0 h-[var(--active-tab-height)] w-[var(--active-tab-width)] translate-x-[var(--active-tab-left)] translate-y-[var(--active-tab-top)] rounded-[var(--radius-sm)] shadow-[var(--shadow-sm)] backdrop-blur-md transition-[translate,width,height] duration-[var(--motion-duration)] ease-[var(--motion-ease)] motion-reduce:transition-none" />
|
|
22
|
+
</BaseTabs.List>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function TabsTrigger({ className, ...props }: ComponentProps<typeof BaseTabs.Tab>) {
|
|
27
|
+
return (
|
|
28
|
+
<BaseTabs.Tab
|
|
29
|
+
className={cn(
|
|
30
|
+
"text-fg-muted hover:text-fg data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 data-[selected]:text-fg focus-visible:ring-ring relative z-[1] inline-flex h-8 cursor-pointer items-center justify-center rounded-[var(--radius-sm)] px-3 text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
31
|
+
className,
|
|
32
|
+
)}
|
|
33
|
+
{...props}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function TabsContent({ className, ...props }: ComponentProps<typeof BaseTabs.Panel>) {
|
|
39
|
+
return (
|
|
40
|
+
<BaseTabs.Panel
|
|
41
|
+
className={cn(
|
|
42
|
+
"col-start-1 row-start-2 pt-4 outline-none data-[ending-style]:pointer-events-none data-[ending-style]:animate-[tab-out_calc(var(--motion-duration)*3)_var(--motion-ease)_forwards] data-[starting-style]:animate-[tab-in_calc(var(--motion-duration)*3)_var(--motion-ease)]",
|
|
43
|
+
className,
|
|
44
|
+
)}
|
|
45
|
+
{...props}
|
|
46
|
+
/>
|
|
47
|
+
);
|
|
48
|
+
}
|