@hogoshi/ui 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/package.json +63 -4
  2. package/src/components/avatar.tsx +30 -0
  3. package/src/components/badge.tsx +26 -0
  4. package/src/components/breadcrumb.tsx +37 -0
  5. package/src/components/button.tsx +58 -0
  6. package/src/components/calendar.tsx +244 -0
  7. package/src/components/card.tsx +18 -0
  8. package/src/components/chart.tsx +387 -0
  9. package/src/components/checkbox.tsx +26 -0
  10. package/src/components/code-block.tsx +118 -0
  11. package/src/components/collapsible.tsx +31 -0
  12. package/src/components/command.tsx +111 -0
  13. package/src/components/context-menu.tsx +150 -0
  14. package/src/components/counter.tsx +56 -0
  15. package/src/components/dialog.tsx +36 -0
  16. package/src/components/dropdown-menu.tsx +54 -0
  17. package/src/components/file-upload.tsx +381 -0
  18. package/src/components/glide.tsx +82 -0
  19. package/src/components/icon.tsx +28 -0
  20. package/src/components/image-viewer.tsx +370 -0
  21. package/src/components/input.tsx +187 -0
  22. package/src/components/kbd.tsx +14 -0
  23. package/src/components/logo-mark.ts +8 -0
  24. package/src/components/logo.tsx +36 -0
  25. package/src/components/navbar.tsx +144 -0
  26. package/src/components/otp.tsx +93 -0
  27. package/src/components/page-loader.tsx +37 -0
  28. package/src/components/pophover.tsx +28 -0
  29. package/src/components/popover.tsx +36 -0
  30. package/src/components/popup.tsx +75 -0
  31. package/src/components/progress.tsx +26 -0
  32. package/src/components/result.tsx +22 -0
  33. package/src/components/scroll-area.tsx +38 -0
  34. package/src/components/select.tsx +52 -0
  35. package/src/components/separator.tsx +14 -0
  36. package/src/components/sheet.tsx +96 -0
  37. package/src/components/sidebar.tsx +195 -0
  38. package/src/components/skeleton.tsx +14 -0
  39. package/src/components/slider.tsx +31 -0
  40. package/src/components/smooth-scroll.tsx +174 -0
  41. package/src/components/spinner.tsx +18 -0
  42. package/src/components/switch.tsx +22 -0
  43. package/src/components/table.tsx +52 -0
  44. package/src/components/tabs.tsx +48 -0
  45. package/src/components/theme-toggle.tsx +214 -0
  46. package/src/components/toaster.tsx +38 -0
  47. package/src/components/tooltip.tsx +43 -0
  48. package/src/components/video-player.tsx +559 -0
  49. package/src/components/video-settings.tsx +233 -0
  50. package/src/index.ts +102 -0
  51. package/src/lib/cn.ts +6 -0
  52. package/src/lib/jsx-highlight.ts +128 -0
  53. package/src/styles.css +443 -0
  54. package/src/theme/theme-script.ts +13 -0
  55. package/src/theme/theme.tsx +77 -0
  56. package/styles.css +1 -0
  57. 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
+ }