@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,214 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Moon02Icon, Sun01Icon } from "@hugeicons-pro/core-stroke-rounded";
|
|
4
|
+
import { flushSync } from "react-dom";
|
|
5
|
+
import { cn } from "../lib/cn";
|
|
6
|
+
import { Icon } from "./icon";
|
|
7
|
+
import { sidebarLabel } from "./sidebar";
|
|
8
|
+
import { useTheme, type ResolvedMode } from "../theme/theme";
|
|
9
|
+
|
|
10
|
+
const REVEAL_MS = 900;
|
|
11
|
+
let switching = false;
|
|
12
|
+
|
|
13
|
+
function applyMode(next: ResolvedMode) {
|
|
14
|
+
document.documentElement.setAttribute("data-mode", next);
|
|
15
|
+
document.documentElement.style.colorScheme = next;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function paintTheme(media: HTMLCanvasElement, mode: string | null) {
|
|
19
|
+
media.dispatchEvent(new CustomEvent("hogoshi-theme-paint", { detail: mode }));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function freeze(media: HTMLVideoElement | HTMLCanvasElement, copy: Element) {
|
|
23
|
+
const canvas = document.createElement("canvas");
|
|
24
|
+
canvas.className = media.className;
|
|
25
|
+
canvas.style.cssText = media.style.cssText;
|
|
26
|
+
canvas.width = media instanceof HTMLVideoElement ? media.videoWidth || media.clientWidth : media.width;
|
|
27
|
+
canvas.height = media instanceof HTMLVideoElement ? media.videoHeight || media.clientHeight : media.height;
|
|
28
|
+
const next = copy.closest("[data-mode]")?.getAttribute("data-mode") ?? null;
|
|
29
|
+
const current = document.documentElement.getAttribute("data-mode");
|
|
30
|
+
if (media instanceof HTMLCanvasElement && next) paintTheme(media, next);
|
|
31
|
+
try {
|
|
32
|
+
canvas.getContext("2d")?.drawImage(media, 0, 0, canvas.width, canvas.height);
|
|
33
|
+
} catch {
|
|
34
|
+
if (media instanceof HTMLCanvasElement && current) paintTheme(media, current);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (media instanceof HTMLCanvasElement && current) paintTheme(media, current);
|
|
38
|
+
copy.replaceWith(canvas);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function holdPage() {
|
|
42
|
+
const root = document.documentElement;
|
|
43
|
+
root.setAttribute("data-theme-switching", "");
|
|
44
|
+
const media = [...document.querySelectorAll("video, audio")].filter(
|
|
45
|
+
(node): node is HTMLMediaElement => node instanceof HTMLMediaElement && !node.paused,
|
|
46
|
+
);
|
|
47
|
+
media.forEach((node) => node.pause());
|
|
48
|
+
const running = document.getAnimations().filter((animation) => animation.playState === "running");
|
|
49
|
+
running.forEach((animation) => animation.pause());
|
|
50
|
+
const stop = (event: Event) => {
|
|
51
|
+
event.preventDefault();
|
|
52
|
+
event.stopPropagation();
|
|
53
|
+
};
|
|
54
|
+
const onKey = (event: KeyboardEvent) => {
|
|
55
|
+
if (event.key !== "Enter" && event.key !== " " && event.key !== "Escape") return;
|
|
56
|
+
stop(event);
|
|
57
|
+
};
|
|
58
|
+
const url = location.href;
|
|
59
|
+
const onPop = () => {
|
|
60
|
+
if (location.href !== url) history.pushState(null, "", url);
|
|
61
|
+
};
|
|
62
|
+
document.addEventListener("click", stop, true);
|
|
63
|
+
document.addEventListener("auxclick", stop, true);
|
|
64
|
+
document.addEventListener("submit", stop, true);
|
|
65
|
+
document.addEventListener("keydown", onKey, true);
|
|
66
|
+
document.addEventListener("wheel", stop, { capture: true, passive: false });
|
|
67
|
+
document.addEventListener("touchmove", stop, { capture: true, passive: false });
|
|
68
|
+
window.addEventListener("popstate", onPop);
|
|
69
|
+
return () => {
|
|
70
|
+
document.removeEventListener("click", stop, true);
|
|
71
|
+
document.removeEventListener("auxclick", stop, true);
|
|
72
|
+
document.removeEventListener("submit", stop, true);
|
|
73
|
+
document.removeEventListener("keydown", onKey, true);
|
|
74
|
+
document.removeEventListener("wheel", stop, true);
|
|
75
|
+
document.removeEventListener("touchmove", stop, true);
|
|
76
|
+
window.removeEventListener("popstate", onPop);
|
|
77
|
+
root.removeAttribute("data-theme-switching");
|
|
78
|
+
running.forEach((animation) => {
|
|
79
|
+
try {
|
|
80
|
+
animation.play();
|
|
81
|
+
} catch {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
media.forEach((node) => void node.play().catch(() => undefined));
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function snapshot(next: ResolvedMode) {
|
|
90
|
+
const body = document.body;
|
|
91
|
+
const originals = [...body.querySelectorAll("*")];
|
|
92
|
+
const page = document.createElement("div");
|
|
93
|
+
page.className = body.className;
|
|
94
|
+
page.style.cssText = body.style.cssText;
|
|
95
|
+
page.append(...body.cloneNode(true).childNodes);
|
|
96
|
+
|
|
97
|
+
const layer = document.createElement("div");
|
|
98
|
+
layer.className = "hogoshi-theme-layer";
|
|
99
|
+
layer.setAttribute("aria-hidden", "true");
|
|
100
|
+
layer.setAttribute("data-mode", next);
|
|
101
|
+
layer.inert = true;
|
|
102
|
+
layer.append(page);
|
|
103
|
+
body.append(layer);
|
|
104
|
+
|
|
105
|
+
const copies = [...page.querySelectorAll("*")];
|
|
106
|
+
originals.forEach((node, at) => {
|
|
107
|
+
const copy = copies[at];
|
|
108
|
+
if (!copy) return;
|
|
109
|
+
if (node.scrollTop || node.scrollLeft) {
|
|
110
|
+
copy.scrollTop = node.scrollTop;
|
|
111
|
+
copy.scrollLeft = node.scrollLeft;
|
|
112
|
+
}
|
|
113
|
+
if (node instanceof HTMLElement && copy instanceof HTMLElement) {
|
|
114
|
+
const style = getComputedStyle(node);
|
|
115
|
+
if (style.animationName && style.animationName !== "none") {
|
|
116
|
+
copy.style.animation = "none";
|
|
117
|
+
copy.style.opacity = style.opacity;
|
|
118
|
+
copy.style.filter = style.filter;
|
|
119
|
+
copy.style.transform = style.transform;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (node instanceof HTMLVideoElement || node instanceof HTMLCanvasElement) freeze(node, copy);
|
|
123
|
+
});
|
|
124
|
+
layer.scrollTop = window.scrollY;
|
|
125
|
+
layer.scrollLeft = window.scrollX;
|
|
126
|
+
return layer;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function reveal(layer: HTMLElement, inward: boolean) {
|
|
130
|
+
const radius = Math.ceil(Math.hypot(window.innerWidth, window.innerHeight));
|
|
131
|
+
const full = `circle(${radius}px at 50% 50%)`;
|
|
132
|
+
const none = "circle(0px at 50% 50%)";
|
|
133
|
+
if (inward) layer.style.clipPath = full;
|
|
134
|
+
return layer
|
|
135
|
+
.animate(inward ? [{ clipPath: full }, { clipPath: none }] : [{ clipPath: none }, { clipPath: full }], {
|
|
136
|
+
duration: REVEAL_MS,
|
|
137
|
+
easing: "cubic-bezier(0.4, 0, 0.2, 1)",
|
|
138
|
+
fill: "forwards",
|
|
139
|
+
})
|
|
140
|
+
.finished.catch(() => undefined);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function ThemeToggle({
|
|
144
|
+
className,
|
|
145
|
+
label,
|
|
146
|
+
hideLabel = false,
|
|
147
|
+
}: {
|
|
148
|
+
className?: string;
|
|
149
|
+
label?: string;
|
|
150
|
+
hideLabel?: boolean;
|
|
151
|
+
}) {
|
|
152
|
+
const { resolved, setMode } = useTheme();
|
|
153
|
+
|
|
154
|
+
const swap = (next: ResolvedMode) => {
|
|
155
|
+
flushSync(() => setMode(next));
|
|
156
|
+
applyMode(next);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const onClick = () => {
|
|
160
|
+
if (switching) return;
|
|
161
|
+
const root = document.documentElement;
|
|
162
|
+
const next = root.getAttribute("data-mode") === "dark" ? "light" : "dark";
|
|
163
|
+
const reduce =
|
|
164
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").matches || root.getAttribute("data-motion") === "off";
|
|
165
|
+
if (reduce) {
|
|
166
|
+
swap(next);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
switching = true;
|
|
171
|
+
const release = holdPage();
|
|
172
|
+
const current = next === "dark" ? "light" : "dark";
|
|
173
|
+
const inward = next === "dark";
|
|
174
|
+
const layer = snapshot(inward ? current : next);
|
|
175
|
+
const finish = () => {
|
|
176
|
+
layer.remove();
|
|
177
|
+
requestAnimationFrame(() => {
|
|
178
|
+
requestAnimationFrame(() => {
|
|
179
|
+
release();
|
|
180
|
+
switching = false;
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
if (inward) {
|
|
185
|
+
const radius = Math.ceil(Math.hypot(window.innerWidth, window.innerHeight));
|
|
186
|
+
layer.style.clipPath = `circle(${radius}px at 50% 50%)`;
|
|
187
|
+
swap(next);
|
|
188
|
+
void reveal(layer, true).then(finish);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
void reveal(layer, false).then(() => {
|
|
192
|
+
swap(next);
|
|
193
|
+
finish();
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<button
|
|
199
|
+
type="button"
|
|
200
|
+
className={cn(
|
|
201
|
+
"text-fg-muted hover:bg-bg-subtle hover:text-fg inline-flex cursor-pointer items-center gap-2 rounded-[var(--radius-sm)] text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
202
|
+
label
|
|
203
|
+
? "h-[calc(var(--row-h)-0.25rem)] w-full justify-start overflow-hidden px-3 whitespace-nowrap"
|
|
204
|
+
: "size-8 justify-center",
|
|
205
|
+
className,
|
|
206
|
+
)}
|
|
207
|
+
aria-label={label ?? (resolved === "dark" ? "Switch to light" : "Switch to dark")}
|
|
208
|
+
onClick={onClick}
|
|
209
|
+
>
|
|
210
|
+
<Icon icon={resolved === "dark" ? Sun01Icon : Moon02Icon} variant="stroke" size={16} className="shrink-0" />
|
|
211
|
+
{label ? <span className={cn(sidebarLabel, hideLabel && "opacity-0")}>{label}</span> : null}
|
|
212
|
+
</button>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Toaster as Sonner, toast } from "sonner";
|
|
4
|
+
import { useTheme } from "../theme/theme";
|
|
5
|
+
|
|
6
|
+
export { toast };
|
|
7
|
+
|
|
8
|
+
export function Toaster() {
|
|
9
|
+
const { resolved } = useTheme();
|
|
10
|
+
return (
|
|
11
|
+
<Sonner
|
|
12
|
+
theme={resolved}
|
|
13
|
+
position="bottom-right"
|
|
14
|
+
closeButton
|
|
15
|
+
offset={{
|
|
16
|
+
bottom: "calc(24px + var(--safe-bottom))",
|
|
17
|
+
right: "calc(24px + var(--safe-right))",
|
|
18
|
+
}}
|
|
19
|
+
mobileOffset={{
|
|
20
|
+
bottom: "calc(5.75rem + var(--safe-bottom))",
|
|
21
|
+
left: "calc(16px + var(--safe-left))",
|
|
22
|
+
right: "calc(16px + var(--safe-right))",
|
|
23
|
+
}}
|
|
24
|
+
style={{ zIndex: "var(--z-toast)" }}
|
|
25
|
+
toastOptions={{
|
|
26
|
+
classNames: {
|
|
27
|
+
toast:
|
|
28
|
+
"hogoshi-grab! bg-bg-elevated! text-fg! border-border! border! shadow-[var(--shadow-md)]! rounded-[var(--radius-md)]! select-none!",
|
|
29
|
+
title: "pointer-events-none! select-none!",
|
|
30
|
+
description: "text-fg-muted! pointer-events-none! select-none!",
|
|
31
|
+
actionButton: "bg-accent! text-accent-fg! pointer-events-auto!",
|
|
32
|
+
cancelButton: "bg-bg-subtle! text-fg-muted! pointer-events-auto!",
|
|
33
|
+
closeButton: "pointer-events-auto!",
|
|
34
|
+
},
|
|
35
|
+
}}
|
|
36
|
+
/>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Tooltip as BaseTooltip } from "@base-ui/react/tooltip";
|
|
4
|
+
import type { ComponentProps, ReactNode } from "react";
|
|
5
|
+
import { cn } from "../lib/cn";
|
|
6
|
+
import { PopupArrow, popupMotion } from "./popup";
|
|
7
|
+
|
|
8
|
+
export function TooltipProvider({ children }: { children: ReactNode }) {
|
|
9
|
+
return <BaseTooltip.Provider delay={200}>{children}</BaseTooltip.Provider>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const Tooltip = BaseTooltip.Root;
|
|
13
|
+
|
|
14
|
+
export function TooltipTrigger({ className, ...props }: ComponentProps<typeof BaseTooltip.Trigger>) {
|
|
15
|
+
return <BaseTooltip.Trigger className={className} {...props} />;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function TooltipContent({
|
|
19
|
+
className,
|
|
20
|
+
children,
|
|
21
|
+
side = "top",
|
|
22
|
+
align = "center",
|
|
23
|
+
...props
|
|
24
|
+
}: ComponentProps<typeof BaseTooltip.Popup> &
|
|
25
|
+
Pick<ComponentProps<typeof BaseTooltip.Positioner>, "side" | "align">) {
|
|
26
|
+
return (
|
|
27
|
+
<BaseTooltip.Portal>
|
|
28
|
+
<BaseTooltip.Positioner side={side} align={align} sideOffset={8} className="z-[var(--z-popover)]">
|
|
29
|
+
<BaseTooltip.Popup
|
|
30
|
+
className={cn(
|
|
31
|
+
"bg-fg text-bg w-max max-w-64 rounded-[var(--radius-sm)] px-2 py-1 text-xs whitespace-nowrap shadow-[var(--shadow-sm)]",
|
|
32
|
+
popupMotion,
|
|
33
|
+
className,
|
|
34
|
+
)}
|
|
35
|
+
{...props}
|
|
36
|
+
>
|
|
37
|
+
{children}
|
|
38
|
+
<PopupArrow arrow={BaseTooltip.Arrow} inverted />
|
|
39
|
+
</BaseTooltip.Popup>
|
|
40
|
+
</BaseTooltip.Positioner>
|
|
41
|
+
</BaseTooltip.Portal>
|
|
42
|
+
);
|
|
43
|
+
}
|