@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.
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,233 @@
1
+ "use client";
2
+
3
+ import {
4
+ AlarmClockIcon,
5
+ ArrowLeft01Icon,
6
+ ArrowRight01Icon,
7
+ AspectRatioIcon,
8
+ DashboardSpeed01Icon,
9
+ HdIcon,
10
+ KeyboardIcon,
11
+ RepeatIcon,
12
+ Tick02Icon,
13
+ } from "@hugeicons-pro/core-stroke-rounded";
14
+ import { useLayoutEffect, useRef, useState, type ReactNode } from "react";
15
+ import { cn } from "../lib/cn";
16
+ import { Icon, type AppIcon } from "./icon";
17
+ import { Kbd } from "./kbd";
18
+ import { Switch } from "./switch";
19
+
20
+ export type VideoFit = "contain" | "cover";
21
+ export type SleepTimer = number | "end" | null;
22
+
23
+ export const SPEEDS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2];
24
+
25
+ const SLEEP: { value: SleepTimer; label: string }[] = [
26
+ { value: null, label: "Off" },
27
+ { value: 15, label: "15 minutes" },
28
+ { value: 30, label: "30 minutes" },
29
+ { value: 60, label: "1 hour" },
30
+ { value: "end", label: "End of video" },
31
+ ];
32
+
33
+ const FITS: { value: VideoFit; label: string }[] = [
34
+ { value: "contain", label: "Fit" },
35
+ { value: "cover", label: "Fill" },
36
+ ];
37
+
38
+ const KEYS: [string, string[]][] = [
39
+ ["Play or pause", ["Space", "K"]],
40
+ ["Back or forward 5s", ["←", "→"]],
41
+ ["Back or forward 10s", ["J", "L"]],
42
+ ["Volume", ["↑", "↓"]],
43
+ ["Mute", ["M"]],
44
+ ["Fullscreen", ["F"]],
45
+ ["Mini player", ["I"]],
46
+ ["Jump to 0–90%", ["0", "9"]],
47
+ ["Previous or next frame", [",", "."]],
48
+ ["Speed", ["<", ">"]],
49
+ ];
50
+
51
+ type View = "main" | "speed" | "quality" | "sleep" | "fit" | "keys";
52
+
53
+ const rowClass =
54
+ "hover:bg-fg/10 flex h-9 w-full cursor-pointer items-center gap-3 rounded-[var(--radius-sm)] px-2.5 text-left text-sm outline-none transition-colors duration-[var(--motion-duration)] focus-visible:bg-fg/10";
55
+
56
+ const speedLabel = (rate: number) => (rate === 1 ? "Normal" : `${rate}×`);
57
+
58
+ export interface VideoSettingsProps {
59
+ rate: number;
60
+ onRate: (rate: number) => void;
61
+ qualities: string[];
62
+ quality: string;
63
+ onQuality: (quality: string) => void;
64
+ sleep: SleepTimer;
65
+ onSleep: (sleep: SleepTimer) => void;
66
+ fit: VideoFit;
67
+ onFit: (fit: VideoFit) => void;
68
+ loop: boolean;
69
+ onLoop: (loop: boolean) => void;
70
+ }
71
+
72
+ function Row({ icon, label, value, onClick }: { icon: AppIcon; label: string; value: string; onClick: () => void }) {
73
+ return (
74
+ <button type="button" className={rowClass} onClick={onClick}>
75
+ <Icon icon={icon} variant="stroke" size={18} className="text-fg-muted shrink-0" />
76
+ <span className="flex-1">{label}</span>
77
+ <span className="text-fg-muted text-xs">{value}</span>
78
+ <Icon icon={ArrowRight01Icon} variant="stroke" size={16} className="text-fg-subtle shrink-0" />
79
+ </button>
80
+ );
81
+ }
82
+
83
+ function Header({ title, onBack }: { title: string; onBack: () => void }) {
84
+ return (
85
+ <button type="button" className={cn(rowClass, "border-border mb-1 rounded-b-none border-b font-medium")} onClick={onBack}>
86
+ <Icon icon={ArrowLeft01Icon} variant="stroke" size={18} className="shrink-0" />
87
+ {title}
88
+ </button>
89
+ );
90
+ }
91
+
92
+ function Options<T>({
93
+ options,
94
+ value,
95
+ onPick,
96
+ }: {
97
+ options: { value: T; label: string }[];
98
+ value: T;
99
+ onPick: (value: T) => void;
100
+ }) {
101
+ return options.map((option) => (
102
+ <button
103
+ key={option.label}
104
+ type="button"
105
+ role="menuitemradio"
106
+ aria-checked={option.value === value}
107
+ className={rowClass}
108
+ onClick={() => onPick(option.value)}
109
+ >
110
+ <span className="flex size-[18px] shrink-0 items-center justify-center">
111
+ {option.value === value ? <Icon icon={Tick02Icon} variant="stroke" size={16} /> : null}
112
+ </span>
113
+ {option.label}
114
+ </button>
115
+ ));
116
+ }
117
+
118
+ export function VideoSettings(props: VideoSettingsProps) {
119
+ const [view, setView] = useState<View>("main");
120
+ const [motion, setMotion] = useState<"forward" | "back" | null>(null);
121
+ const [height, setHeight] = useState<number>();
122
+ const inner = useRef<HTMLDivElement>(null);
123
+
124
+ useLayoutEffect(() => {
125
+ const node = inner.current;
126
+ if (!node) return;
127
+ const observer = new ResizeObserver(() => setHeight(node.offsetHeight));
128
+ observer.observe(node);
129
+ return () => observer.disconnect();
130
+ }, [view]);
131
+
132
+ const go = (next: View) => {
133
+ setMotion(next === "main" ? "back" : "forward");
134
+ setView(next);
135
+ };
136
+
137
+ const pick =
138
+ <T,>(apply: (value: T) => void) =>
139
+ (value: T) => {
140
+ apply(value);
141
+ go("main");
142
+ };
143
+
144
+ const sleepLabel = SLEEP.find((option) => option.value === props.sleep)?.label ?? "Off";
145
+ const fitLabel = FITS.find((option) => option.value === props.fit)?.label ?? "Fit";
146
+
147
+ const views: Record<View, ReactNode> = {
148
+ main: (
149
+ <>
150
+ <Row icon={DashboardSpeed01Icon} label="Speed" value={speedLabel(props.rate)} onClick={() => go("speed")} />
151
+ {props.qualities.length > 1 ? (
152
+ <Row icon={HdIcon} label="Quality" value={props.quality} onClick={() => go("quality")} />
153
+ ) : null}
154
+ <Row icon={AlarmClockIcon} label="Sleep timer" value={sleepLabel} onClick={() => go("sleep")} />
155
+ <Row icon={AspectRatioIcon} label="Video fit" value={fitLabel} onClick={() => go("fit")} />
156
+ <label className={rowClass}>
157
+ <Icon icon={RepeatIcon} variant="stroke" size={18} className="text-fg-muted shrink-0" />
158
+ <span className="flex-1">Loop</span>
159
+ <Switch checked={props.loop} onCheckedChange={props.onLoop} />
160
+ </label>
161
+ <Row icon={KeyboardIcon} label="Hotkeys" value="" onClick={() => go("keys")} />
162
+ </>
163
+ ),
164
+ speed: (
165
+ <>
166
+ <Header title="Speed" onBack={() => go("main")} />
167
+ <Options
168
+ options={SPEEDS.map((rate) => ({ value: rate, label: speedLabel(rate) }))}
169
+ value={props.rate}
170
+ onPick={pick(props.onRate)}
171
+ />
172
+ </>
173
+ ),
174
+ quality: (
175
+ <>
176
+ <Header title="Quality" onBack={() => go("main")} />
177
+ <Options
178
+ options={props.qualities.map((quality) => ({ value: quality, label: quality }))}
179
+ value={props.quality}
180
+ onPick={pick(props.onQuality)}
181
+ />
182
+ </>
183
+ ),
184
+ sleep: (
185
+ <>
186
+ <Header title="Sleep timer" onBack={() => go("main")} />
187
+ <Options options={SLEEP} value={props.sleep} onPick={pick(props.onSleep)} />
188
+ </>
189
+ ),
190
+ fit: (
191
+ <>
192
+ <Header title="Video fit" onBack={() => go("main")} />
193
+ <Options options={FITS} value={props.fit} onPick={pick(props.onFit)} />
194
+ </>
195
+ ),
196
+ keys: (
197
+ <>
198
+ <Header title="Hotkeys" onBack={() => go("main")} />
199
+ <div className="grid gap-1.5 px-2.5 pt-1 pb-2">
200
+ {KEYS.map(([label, keys]) => (
201
+ <div key={label} className="flex items-center justify-between gap-3 text-xs">
202
+ <span className="text-fg-muted">{label}</span>
203
+ <span className="flex gap-1">
204
+ {keys.map((key) => (
205
+ <Kbd key={key}>{key}</Kbd>
206
+ ))}
207
+ </span>
208
+ </div>
209
+ ))}
210
+ </div>
211
+ </>
212
+ ),
213
+ };
214
+
215
+ return (
216
+ <div
217
+ className="overflow-hidden transition-[height] duration-[var(--motion-duration)] ease-[var(--motion-ease)]"
218
+ style={{ height }}
219
+ >
220
+ <div
221
+ ref={inner}
222
+ key={view}
223
+ className={cn(
224
+ "flex flex-col",
225
+ motion === "forward" && "animate-[panel-from-right_var(--motion-duration)_var(--motion-ease)]",
226
+ motion === "back" && "animate-[panel-from-left_var(--motion-duration)_var(--motion-ease)]",
227
+ )}
228
+ >
229
+ {views[view]}
230
+ </div>
231
+ </div>
232
+ );
233
+ }
package/src/index.ts ADDED
@@ -0,0 +1,102 @@
1
+ export { Avatar, AvatarFallback, AvatarImage } from "./components/avatar";
2
+ export { Badge } from "./components/badge";
3
+ export type { BadgeProps } from "./components/badge";
4
+ export { Button, buttonVariants } from "./components/button";
5
+ export type { ButtonProps } from "./components/button";
6
+ export {
7
+ BottomBar,
8
+ BottomBarItem,
9
+ Sidebar,
10
+ SidebarFooter,
11
+ SidebarGroup,
12
+ SidebarHeader,
13
+ SidebarInset,
14
+ SidebarItem,
15
+ SidebarLabel,
16
+ SidebarNav,
17
+ SidebarProvider,
18
+ SidebarTrigger,
19
+ sidebarLabel,
20
+ useSidebar,
21
+ } from "./components/sidebar";
22
+ export { Card, CardDescription, CardHeader, CardTitle } from "./components/card";
23
+ export { BootScreen, PageLoader } from "./components/page-loader";
24
+ export { Breadcrumb } from "./components/breadcrumb";
25
+ export type { BreadcrumbItem } from "./components/breadcrumb";
26
+ export { Result } from "./components/result";
27
+ export { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./components/table";
28
+ export type { SortDirection } from "./components/table";
29
+ export { Progress } from "./components/progress";
30
+ export { Logo } from "./components/logo";
31
+ export { LOGO_PATH, LOGO_VIEWBOX, logoSvg } from "./components/logo-mark";
32
+ export { Navbar, NavbarItem, NavbarMenu, NavbarMenuLink, NavbarMenus } from "./components/navbar";
33
+ export { Glide, glideActive } from "./components/glide";
34
+ export type { GlideProps } from "./components/glide";
35
+ export { Calendar } from "./components/calendar";
36
+ export { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "./components/collapsible";
37
+ export type { CalendarRange } from "./components/calendar";
38
+ export { CodeBlock } from "./components/code-block";
39
+ export { Counter } from "./components/counter";
40
+ export { AreaChart, BarChart, ChartContainer, ChartLegend, DonutChart, LineChart, Sparkline } from "./components/chart";
41
+ export type { CartesianChartProps, ChartConfig } from "./components/chart";
42
+ export { Checkbox } from "./components/checkbox";
43
+ export {
44
+ Command,
45
+ CommandDialog,
46
+ CommandEmpty,
47
+ CommandGroup,
48
+ CommandInput,
49
+ CommandItem,
50
+ CommandList,
51
+ CommandShortcut,
52
+ } from "./components/command";
53
+ export {
54
+ ContextMenu,
55
+ ContextMenuContent,
56
+ ContextMenuItem,
57
+ ContextMenuRoot,
58
+ ContextMenuTrigger,
59
+ ContextMenuViewport,
60
+ } from "./components/context-menu";
61
+ export type { ContextMenuEntry } from "./components/context-menu";
62
+ export { Dialog, DialogClose, DialogContent, DialogDescription, DialogTitle, DialogTrigger } from "./components/dialog";
63
+ export {
64
+ DropdownMenu,
65
+ DropdownMenuContent,
66
+ DropdownMenuItem,
67
+ DropdownMenuSub,
68
+ DropdownMenuSubContent,
69
+ DropdownMenuSubTrigger,
70
+ DropdownMenuTrigger,
71
+ } from "./components/dropdown-menu";
72
+ export { FileList, FileUpload, FileUploadDropzone, formatBytes } from "./components/file-upload";
73
+ export type { FileRejection, FileUploadProps } from "./components/file-upload";
74
+ export { Icon } from "./components/icon";
75
+ export { Image, ImageGallery, ImageViewer, downloadImage } from "./components/image-viewer";
76
+ export type { ViewerImage } from "./components/image-viewer";
77
+ export { Input, Label, Textarea } from "./components/input";
78
+ export { Kbd } from "./components/kbd";
79
+ export type { AppIcon, IconProps, IconVariant } from "./components/icon";
80
+ export { Otp } from "./components/otp";
81
+ export { Popover, PopoverContent, PopoverTrigger } from "./components/popover";
82
+ export { Pophover, PophoverContent, PophoverTrigger } from "./components/pophover";
83
+ export { ScrollArea } from "./components/scroll-area";
84
+ export { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./components/select";
85
+ export { Separator } from "./components/separator";
86
+ export { Sheet, SheetClose, SheetContent, SheetDescription, SheetTitle, SheetTrigger } from "./components/sheet";
87
+ export type { SheetSide } from "./components/sheet";
88
+ export { Slider } from "./components/slider";
89
+ export { Spinner } from "./components/spinner";
90
+ export { Skeleton } from "./components/skeleton";
91
+ export { Switch } from "./components/switch";
92
+ export { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/tabs";
93
+ export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./components/tooltip";
94
+ export { SmoothScroll, scrollToTop } from "./components/smooth-scroll";
95
+ export { Toaster, toast } from "./components/toaster";
96
+ export { ThemeToggle } from "./components/theme-toggle";
97
+ export { VideoPlayer, formatTime } from "./components/video-player";
98
+ export type { VideoPlayerProps, VideoSource } from "./components/video-player";
99
+ export type { SleepTimer, VideoFit } from "./components/video-settings";
100
+ export { ThemeProvider, useTheme } from "./theme/theme";
101
+ export type { ColorMode, ResolvedMode } from "./theme/theme";
102
+ export { THEME_INIT_SCRIPT } from "./theme/theme-script";
package/src/lib/cn.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,128 @@
1
+ function escapeHtml(value: string) {
2
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
3
+ }
4
+
5
+ function span(className: string, text: string) {
6
+ return `<span class="${className}">${escapeHtml(text)}</span>`;
7
+ }
8
+
9
+ function isTagStart(source: string, index: number) {
10
+ if (source[index] !== "<") return false;
11
+ const next = source[index + 1];
12
+ if (next === "/") return /[A-Za-z]/.test(source[index + 2] ?? "");
13
+ return /[A-Za-z]/.test(next ?? "");
14
+ }
15
+
16
+ export function highlightJsx(source: string) {
17
+ let index = 0;
18
+ let html = "";
19
+
20
+ const readString = (quote: string) => {
21
+ let end = index + 1;
22
+ while (end < source.length) {
23
+ if (source[end] === "\\") {
24
+ end += 2;
25
+ continue;
26
+ }
27
+ if (source[end] === quote) {
28
+ end += 1;
29
+ break;
30
+ }
31
+ end += 1;
32
+ }
33
+ const text = source.slice(index, end);
34
+ index = end;
35
+ return span("hljs-string", text);
36
+ };
37
+
38
+ const readTag = () => {
39
+ let chunk = "";
40
+ const closing = source[index + 1] === "/";
41
+ chunk += span("hljs-punctuation", closing ? "</" : "<");
42
+ index += closing ? 2 : 1;
43
+ const nameStart = index;
44
+ while (index < source.length && /[A-Za-z0-9_$.]/.test(source[index])) index += 1;
45
+ chunk += span("hljs-name", source.slice(nameStart, index));
46
+ if (closing) {
47
+ while (index < source.length && source[index] !== ">") {
48
+ chunk += span("hljs-punctuation", source[index]);
49
+ index += 1;
50
+ }
51
+ if (source[index] === ">") {
52
+ chunk += span("hljs-punctuation", ">");
53
+ index += 1;
54
+ }
55
+ return chunk;
56
+ }
57
+ while (index < source.length) {
58
+ const char = source[index];
59
+ if (char === "/" && source[index + 1] === ">") {
60
+ chunk += span("hljs-punctuation", "/>");
61
+ index += 2;
62
+ break;
63
+ }
64
+ if (char === ">") {
65
+ chunk += span("hljs-punctuation", ">");
66
+ index += 1;
67
+ break;
68
+ }
69
+ if (char === " " || char === "\n" || char === "\t" || char === "\r") {
70
+ chunk += char;
71
+ index += 1;
72
+ continue;
73
+ }
74
+ if (char === '"' || char === "'") {
75
+ chunk += readString(char);
76
+ continue;
77
+ }
78
+ if (char === "=" || char === "{" || char === "}" || char === "?" || char === "|" || char === "(" || char === ")" || char === ",") {
79
+ chunk += span("hljs-punctuation", char);
80
+ index += 1;
81
+ continue;
82
+ }
83
+ if (/[0-9]/.test(char)) {
84
+ const start = index;
85
+ while (index < source.length && /[0-9.]/.test(source[index])) index += 1;
86
+ chunk += span("hljs-number", source.slice(start, index));
87
+ continue;
88
+ }
89
+ if (/[A-Za-z_$]/.test(char)) {
90
+ const start = index;
91
+ while (index < source.length && /[A-Za-z0-9_$.]/.test(source[index])) index += 1;
92
+ const word = source.slice(start, index);
93
+ let look = index;
94
+ while (look < source.length && (source[look] === " " || source[look] === "\n" || source[look] === "\t")) look += 1;
95
+ chunk += span(source[look] === "=" ? "hljs-attr" : "hljs-boolean", word);
96
+ continue;
97
+ }
98
+ chunk += span("hljs-punctuation", char);
99
+ index += 1;
100
+ }
101
+ return chunk;
102
+ };
103
+
104
+ while (index < source.length) {
105
+ if (isTagStart(source, index)) {
106
+ html += readTag();
107
+ continue;
108
+ }
109
+ if (source[index] === '"' || source[index] === "'" || source[index] === "`") {
110
+ html += readString(source[index]);
111
+ continue;
112
+ }
113
+ let end = index + 1;
114
+ while (
115
+ end < source.length &&
116
+ !isTagStart(source, end) &&
117
+ source[end] !== '"' &&
118
+ source[end] !== "'" &&
119
+ source[end] !== "`"
120
+ ) {
121
+ end += 1;
122
+ }
123
+ html += escapeHtml(source.slice(index, end));
124
+ index = end;
125
+ }
126
+
127
+ return html;
128
+ }