@xenide-io/the-old-ui-theme 0.2.6 → 0.2.9
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/dist/chunk-SHF57J7U.mjs +2910 -0
- package/dist/index-CmS6hcUk.d.mts +753 -0
- package/dist/index-CmS6hcUk.d.ts +753 -0
- package/dist/index.d.mts +5 -755
- package/dist/index.d.ts +5 -755
- package/dist/index.js +43 -23
- package/dist/index.mjs +252 -2846
- package/dist/ui.d.mts +3 -0
- package/dist/ui.d.ts +3 -0
- package/dist/ui.js +2946 -0
- package/dist/ui.mjs +139 -0
- package/package.json +11 -3
- package/src/components/charts/index.ts +8 -0
- package/src/components/charts/ph-insight-charts.tsx +162 -0
- package/src/components/dashboard/DashboardFiltersBar.tsx +19 -0
- package/src/components/dashboard/DashboardGrid.tsx +23 -0
- package/src/components/dashboard/DashboardInsightPlaceholder.tsx +37 -0
- package/src/components/dashboard/DashboardKpiCard.tsx +25 -0
- package/src/components/dashboard/DashboardToolbar.tsx +23 -0
- package/src/components/dashboard/DashboardWell.tsx +10 -0
- package/src/components/dashboard/InsightMiniCard.tsx +26 -0
- package/src/components/dashboard/InsightShell.tsx +37 -0
- package/src/components/dashboard/InsightShellHeader.tsx +22 -0
- package/src/components/dashboard/MiniTrendBars.tsx +51 -0
- package/src/components/dashboard/index.ts +31 -0
- package/src/components/dashboard/types.ts +9 -0
- package/src/components/icons/IconBase.tsx +39 -0
- package/src/components/icons/createIconBase.tsx +27 -0
- package/src/components/icons/icons.tsx +367 -0
- package/src/components/icons/index.ts +3 -0
- package/src/components/sections/AccordionShowcase.tsx +45 -0
- package/src/components/sections/AlertShowcase.tsx +39 -0
- package/src/components/sections/AvatarShowcase.tsx +80 -0
- package/src/components/sections/BadgeShowcase.tsx +65 -0
- package/src/components/sections/ButtonShowcase.tsx +312 -0
- package/src/components/sections/CalendarShowcase.tsx +35 -0
- package/src/components/sections/CardShowcase.tsx +143 -0
- package/src/components/sections/ChartShowcase.tsx +190 -0
- package/src/components/sections/CodeMockupShowcase.tsx +56 -0
- package/src/components/sections/ColorPalette.tsx +117 -0
- package/src/components/sections/CommandPaletteShowcase.tsx +48 -0
- package/src/components/sections/CountdownShowcase.tsx +43 -0
- package/src/components/sections/DashboardShowcase.tsx +191 -0
- package/src/components/sections/DiffShowcase.tsx +70 -0
- package/src/components/sections/DrawerShowcase.tsx +97 -0
- package/src/components/sections/DropdownShowcase.tsx +100 -0
- package/src/components/sections/EmptyStateShowcase.tsx +50 -0
- package/src/components/sections/FilterChipsShowcase.tsx +50 -0
- package/src/components/sections/FormShowcase.tsx +126 -0
- package/src/components/sections/HoverCardShowcase.tsx +50 -0
- package/src/components/sections/IconShowcase.tsx +247 -0
- package/src/components/sections/IndicatorShowcase.tsx +52 -0
- package/src/components/sections/KbdShowcase.tsx +31 -0
- package/src/components/sections/ModalShowcase.tsx +210 -0
- package/src/components/sections/NavigationShowcase.tsx +199 -0
- package/src/components/sections/ProgressShowcase.tsx +85 -0
- package/src/components/sections/SegmentedControlShowcase.tsx +49 -0
- package/src/components/sections/SetupThemeShowcase.tsx +262 -0
- package/src/components/sections/StackShowcase.tsx +33 -0
- package/src/components/sections/StatShowcase.tsx +129 -0
- package/src/components/sections/StepperShowcase.tsx +37 -0
- package/src/components/sections/SwapShowcase.tsx +91 -0
- package/src/components/sections/TabShowcase.tsx +84 -0
- package/src/components/sections/TableShowcase.tsx +140 -0
- package/src/components/sections/TimelineShowcase.tsx +83 -0
- package/src/components/sections/ToastShowcase.tsx +108 -0
- package/src/components/sections/TooltipShowcase.tsx +44 -0
- package/src/components/sections/UtilityShowcase.tsx +81 -0
- package/src/components/ui/Accordion.tsx +77 -0
- package/src/components/ui/Alert.tsx +68 -0
- package/src/components/ui/Avatar.tsx +97 -0
- package/src/components/ui/Badge.tsx +55 -0
- package/src/components/ui/Button.tsx +110 -0
- package/src/components/ui/ButtonChrome.tsx +20 -0
- package/src/components/ui/Calendar.tsx +116 -0
- package/src/components/ui/Card.tsx +60 -0
- package/src/components/ui/ChatBubble.tsx +71 -0
- package/src/components/ui/CodeBlock.tsx +48 -0
- package/src/components/ui/CollapsibleSection.tsx +49 -0
- package/src/components/ui/CommandPalette.tsx +137 -0
- package/src/components/ui/ComponentDocs.tsx +52 -0
- package/src/components/ui/Drawer.tsx +71 -0
- package/src/components/ui/DropdownMenu.tsx +138 -0
- package/src/components/ui/EmptyState.tsx +33 -0
- package/src/components/ui/FilterChips.tsx +50 -0
- package/src/components/ui/HoverCard.tsx +36 -0
- package/src/components/ui/Indicator.tsx +61 -0
- package/src/components/ui/Input.tsx +359 -0
- package/src/components/ui/Kbd.tsx +38 -0
- package/src/components/ui/Modal.tsx +75 -0
- package/src/components/ui/Navigation.tsx +94 -0
- package/src/components/ui/Panel.tsx +18 -0
- package/src/components/ui/Progress.tsx +59 -0
- package/src/components/ui/Rating.tsx +65 -0
- package/src/components/ui/SearchInput.tsx +106 -0
- package/src/components/ui/SegmentedControl.tsx +44 -0
- package/src/components/ui/ShowcaseWrapper.tsx +136 -0
- package/src/components/ui/Stat.tsx +39 -0
- package/src/components/ui/Stepper.tsx +75 -0
- package/src/components/ui/Table.tsx +55 -0
- package/src/components/ui/Tabs.tsx +53 -0
- package/src/components/ui/Terminal.tsx +54 -0
- package/src/components/ui/ThemeDomSync.tsx +17 -0
- package/src/components/ui/ThemeManager.tsx +18 -0
- package/src/components/ui/ThemeSwitcher.tsx +46 -0
- package/src/components/ui/Timeline.tsx +60 -0
- package/src/components/ui/Toast.tsx +70 -0
- package/src/components/ui/Typography.tsx +129 -0
- package/src/components/ui/index.ts +92 -0
- package/src/styles/themes.css +118 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export type CardVariant = "default" | "outlined" | "elevated" | "highlighted";
|
|
5
|
+
|
|
6
|
+
export interface CardProps {
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
title?: ReactNode;
|
|
9
|
+
description?: ReactNode;
|
|
10
|
+
actions?: ReactNode;
|
|
11
|
+
footer?: ReactNode;
|
|
12
|
+
media?: ReactNode;
|
|
13
|
+
variant?: CardVariant;
|
|
14
|
+
className?: string;
|
|
15
|
+
bodyClassName?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const variantMap: Record<CardVariant, string> = {
|
|
19
|
+
default: "bg-ph-surface",
|
|
20
|
+
outlined: "border-2 border-ph-border bg-ph-surface",
|
|
21
|
+
elevated: "bg-ph-surface shadow-ph-md",
|
|
22
|
+
highlighted: "border border-ph-brand/35 bg-orange-50/40 shadow-ph",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function Card({
|
|
26
|
+
children,
|
|
27
|
+
title,
|
|
28
|
+
description,
|
|
29
|
+
actions,
|
|
30
|
+
footer,
|
|
31
|
+
media,
|
|
32
|
+
variant = "default",
|
|
33
|
+
className,
|
|
34
|
+
bodyClassName,
|
|
35
|
+
}: CardProps) {
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
className={cn(
|
|
39
|
+
"ph-card",
|
|
40
|
+
variantMap[variant],
|
|
41
|
+
className
|
|
42
|
+
)}
|
|
43
|
+
>
|
|
44
|
+
{media && <div className="-mx-6 -mt-6 mb-4">{media}</div>}
|
|
45
|
+
<div className={cn("ph-card-body", bodyClassName)}>
|
|
46
|
+
{title && (
|
|
47
|
+
<h3 className="ph-card-title">{title}</h3>
|
|
48
|
+
)}
|
|
49
|
+
{description && (
|
|
50
|
+
<p className="text-sm text-ph-subtle">{description}</p>
|
|
51
|
+
)}
|
|
52
|
+
{children}
|
|
53
|
+
{actions && <div className="ph-card-actions">{actions}</div>}
|
|
54
|
+
</div>
|
|
55
|
+
{footer && (
|
|
56
|
+
<div className="border-t border-ph-border px-6 py-4 text-xs text-ph-subtle">{footer}</div>
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export type ChatBubbleVariant = "from" | "self";
|
|
5
|
+
|
|
6
|
+
export interface ChatBubbleProps extends ComponentPropsWithoutRef<"div"> {
|
|
7
|
+
/** Message content */
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
/** Avatar or icon */
|
|
10
|
+
avatar?: ReactNode;
|
|
11
|
+
/** Sender name */
|
|
12
|
+
name?: string;
|
|
13
|
+
/** Timestamp */
|
|
14
|
+
time?: string;
|
|
15
|
+
/** "from" = left/grey, "self" = right/brand */
|
|
16
|
+
variant?: ChatBubbleVariant;
|
|
17
|
+
/** Footer text (e.g. "Synced with...") */
|
|
18
|
+
footer?: ReactNode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ChatBubble({
|
|
22
|
+
children,
|
|
23
|
+
avatar,
|
|
24
|
+
name,
|
|
25
|
+
time,
|
|
26
|
+
variant = "from",
|
|
27
|
+
footer,
|
|
28
|
+
className,
|
|
29
|
+
...props
|
|
30
|
+
}: ChatBubbleProps) {
|
|
31
|
+
const isSelf = variant === "self";
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div
|
|
35
|
+
className={cn(
|
|
36
|
+
"flex gap-3",
|
|
37
|
+
isSelf && "flex-row-reverse",
|
|
38
|
+
className,
|
|
39
|
+
)}
|
|
40
|
+
{...props}
|
|
41
|
+
>
|
|
42
|
+
{avatar && (
|
|
43
|
+
<div className="shrink-0">{avatar}</div>
|
|
44
|
+
)}
|
|
45
|
+
<div className={cn("min-w-0 flex-1", isSelf && "text-right")}>
|
|
46
|
+
{(name || time) && (
|
|
47
|
+
<header
|
|
48
|
+
className={cn(
|
|
49
|
+
"flex gap-2 text-xs text-ph-mutedtext",
|
|
50
|
+
isSelf && "flex-row-reverse justify-end",
|
|
51
|
+
)}
|
|
52
|
+
>
|
|
53
|
+
{name && <span className="font-semibold text-ph-ink">{name}</span>}
|
|
54
|
+
{time && <span>{time}</span>}
|
|
55
|
+
</header>
|
|
56
|
+
)}
|
|
57
|
+
<div
|
|
58
|
+
className={cn(
|
|
59
|
+
"mt-2 inline-block text-left shadow-ph",
|
|
60
|
+
isSelf ? "ph-chat-self" : "ph-chat-from",
|
|
61
|
+
)}
|
|
62
|
+
>
|
|
63
|
+
{children}
|
|
64
|
+
</div>
|
|
65
|
+
{footer && (
|
|
66
|
+
<footer className="mt-2 text-[11px] text-ph-mutedtext">{footer}</footer>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { cn } from "@/lib/cn";
|
|
5
|
+
|
|
6
|
+
interface CodeBlockProps {
|
|
7
|
+
code: string;
|
|
8
|
+
language?: string;
|
|
9
|
+
filename?: string;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function CodeBlock({ code, language = "tsx", filename, className }: CodeBlockProps) {
|
|
14
|
+
const [copied, setCopied] = useState(false);
|
|
15
|
+
|
|
16
|
+
const handleCopy = async () => {
|
|
17
|
+
await navigator.clipboard.writeText(code);
|
|
18
|
+
setCopied(true);
|
|
19
|
+
setTimeout(() => setCopied(false), 2000);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className={cn("overflow-hidden rounded-lg border border-ph-border bg-[#1a1a2e]", className)}>
|
|
24
|
+
{(filename || language) && (
|
|
25
|
+
<div className="flex items-center justify-between border-b border-white/10 px-4 py-2">
|
|
26
|
+
<div className="flex items-center gap-2">
|
|
27
|
+
<div className="flex gap-1.5">
|
|
28
|
+
<div className="h-3 w-3 rounded-full bg-red-400" />
|
|
29
|
+
<div className="h-3 w-3 rounded-full bg-amber-400" />
|
|
30
|
+
<div className="h-3 w-3 rounded-full bg-emerald-400" />
|
|
31
|
+
</div>
|
|
32
|
+
{filename && <span className="ml-2 text-xs text-white/60">{filename}</span>}
|
|
33
|
+
</div>
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
onClick={handleCopy}
|
|
37
|
+
className="text-xs text-white/60 transition hover:text-white"
|
|
38
|
+
>
|
|
39
|
+
{copied ? "Copied!" : "Copy"}
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
)}
|
|
43
|
+
<pre className="overflow-x-auto p-4 text-sm leading-relaxed">
|
|
44
|
+
<code className="font-mono text-[#e4e4e7]">{code}</code>
|
|
45
|
+
</pre>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { cn } from "@/lib/cn";
|
|
5
|
+
|
|
6
|
+
interface CollapsibleSectionProps {
|
|
7
|
+
title: string;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
defaultOpen?: boolean;
|
|
10
|
+
id?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function CollapsibleSection({ title, children, defaultOpen = true, id }: CollapsibleSectionProps) {
|
|
14
|
+
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<section id={id} className="scroll-mt-20">
|
|
18
|
+
<button
|
|
19
|
+
type="button"
|
|
20
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
21
|
+
className="mb-6 flex w-full items-center justify-between rounded-lg border border-ph-border bg-ph-muted px-5 py-4 text-left transition hover:bg-ph-muted/80"
|
|
22
|
+
>
|
|
23
|
+
<span className="text-lg font-bold text-ph-ink">{title}</span>
|
|
24
|
+
<svg
|
|
25
|
+
className={cn(
|
|
26
|
+
"h-5 w-5 text-ph-subtle transition-transform duration-200",
|
|
27
|
+
isOpen && "rotate-180"
|
|
28
|
+
)}
|
|
29
|
+
viewBox="0 0 24 24"
|
|
30
|
+
fill="none"
|
|
31
|
+
stroke="currentColor"
|
|
32
|
+
strokeWidth="2"
|
|
33
|
+
strokeLinecap="round"
|
|
34
|
+
strokeLinejoin="round"
|
|
35
|
+
>
|
|
36
|
+
<path d="M6 9l6 6 6-6" />
|
|
37
|
+
</svg>
|
|
38
|
+
</button>
|
|
39
|
+
<div
|
|
40
|
+
className={cn(
|
|
41
|
+
"overflow-hidden transition-all duration-300",
|
|
42
|
+
isOpen ? "max-h-[50000px] opacity-100" : "max-h-0 opacity-0"
|
|
43
|
+
)}
|
|
44
|
+
>
|
|
45
|
+
{children}
|
|
46
|
+
</div>
|
|
47
|
+
</section>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useRef } from "react";
|
|
4
|
+
import { cn } from "@/lib/cn";
|
|
5
|
+
import { IconSearch } from "@/components/icons";
|
|
6
|
+
|
|
7
|
+
interface CommandItem {
|
|
8
|
+
id: string;
|
|
9
|
+
label: string;
|
|
10
|
+
shortcut?: string;
|
|
11
|
+
icon?: React.ReactNode;
|
|
12
|
+
onSelect: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface CommandPaletteProps {
|
|
16
|
+
items: CommandItem[];
|
|
17
|
+
isOpen: boolean;
|
|
18
|
+
onClose: () => void;
|
|
19
|
+
placeholder?: string;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function CommandPalette({
|
|
24
|
+
items,
|
|
25
|
+
isOpen,
|
|
26
|
+
onClose,
|
|
27
|
+
placeholder = "Type a command or search...",
|
|
28
|
+
className,
|
|
29
|
+
}: CommandPaletteProps) {
|
|
30
|
+
const [query, setQuery] = useState("");
|
|
31
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
32
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
33
|
+
|
|
34
|
+
const filtered = items.filter((item) =>
|
|
35
|
+
item.label.toLowerCase().includes(query.toLowerCase())
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (isOpen) {
|
|
40
|
+
setQuery("");
|
|
41
|
+
setSelectedIndex(0);
|
|
42
|
+
inputRef.current?.focus();
|
|
43
|
+
}
|
|
44
|
+
}, [isOpen]);
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
48
|
+
if (!isOpen) return;
|
|
49
|
+
|
|
50
|
+
switch (e.key) {
|
|
51
|
+
case "ArrowDown":
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
setSelectedIndex((i) => (i + 1) % filtered.length);
|
|
54
|
+
break;
|
|
55
|
+
case "ArrowUp":
|
|
56
|
+
e.preventDefault();
|
|
57
|
+
setSelectedIndex((i) => (i - 1 + filtered.length) % filtered.length);
|
|
58
|
+
break;
|
|
59
|
+
case "Enter":
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
filtered[selectedIndex]?.onSelect();
|
|
62
|
+
onClose();
|
|
63
|
+
break;
|
|
64
|
+
case "Escape":
|
|
65
|
+
onClose();
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
71
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
72
|
+
}, [isOpen, filtered, selectedIndex, onClose]);
|
|
73
|
+
|
|
74
|
+
if (!isOpen) return null;
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div className="fixed inset-0 z-50 flex items-start justify-center bg-black/40 pt-[20vh] backdrop-blur-sm">
|
|
78
|
+
<div
|
|
79
|
+
className={cn(
|
|
80
|
+
"w-full max-w-xl overflow-hidden rounded-xl border border-ph-border bg-ph-surface shadow-ph-md",
|
|
81
|
+
className
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
84
|
+
<div className="flex items-center gap-3 border-b border-ph-border px-4 py-3">
|
|
85
|
+
<IconSearch className="h-5 w-5 text-ph-mutedtext" />
|
|
86
|
+
<input
|
|
87
|
+
ref={inputRef}
|
|
88
|
+
type="text"
|
|
89
|
+
value={query}
|
|
90
|
+
onChange={(e) => {
|
|
91
|
+
setQuery(e.target.value);
|
|
92
|
+
setSelectedIndex(0);
|
|
93
|
+
}}
|
|
94
|
+
placeholder={placeholder}
|
|
95
|
+
className="flex-1 bg-transparent text-ph-ink outline-none placeholder:text-ph-mutedtext"
|
|
96
|
+
/>
|
|
97
|
+
<kbd className="rounded border border-ph-border bg-ph-muted px-1.5 py-0.5 text-xs text-ph-mutedtext">
|
|
98
|
+
ESC
|
|
99
|
+
</kbd>
|
|
100
|
+
</div>
|
|
101
|
+
<div className="max-h-[50vh] overflow-y-auto py-2">
|
|
102
|
+
{filtered.length === 0 ? (
|
|
103
|
+
<div className="px-4 py-8 text-center text-sm text-ph-mutedtext">
|
|
104
|
+
No results found.
|
|
105
|
+
</div>
|
|
106
|
+
) : (
|
|
107
|
+
filtered.map((item, index) => (
|
|
108
|
+
<button
|
|
109
|
+
key={item.id}
|
|
110
|
+
type="button"
|
|
111
|
+
onClick={() => {
|
|
112
|
+
item.onSelect();
|
|
113
|
+
onClose();
|
|
114
|
+
}}
|
|
115
|
+
onMouseEnter={() => setSelectedIndex(index)}
|
|
116
|
+
className={cn(
|
|
117
|
+
"flex w-full items-center gap-3 px-4 py-2.5 text-left text-sm transition-colors",
|
|
118
|
+
index === selectedIndex
|
|
119
|
+
? "bg-ph-muted text-ph-ink"
|
|
120
|
+
: "text-ph-subtle"
|
|
121
|
+
)}
|
|
122
|
+
>
|
|
123
|
+
{item.icon && <span className="h-5 w-5">{item.icon}</span>}
|
|
124
|
+
<span className="flex-1">{item.label}</span>
|
|
125
|
+
{item.shortcut && (
|
|
126
|
+
<kbd className="rounded border border-ph-border bg-ph-surface px-1.5 py-0.5 text-xs text-ph-mutedtext">
|
|
127
|
+
{item.shortcut}
|
|
128
|
+
</kbd>
|
|
129
|
+
)}
|
|
130
|
+
</button>
|
|
131
|
+
))
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface ComponentPropRow {
|
|
5
|
+
name: string;
|
|
6
|
+
type: string;
|
|
7
|
+
defaultValue?: string;
|
|
8
|
+
description: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ComponentDocsProps {
|
|
12
|
+
title?: string;
|
|
13
|
+
rows: ComponentPropRow[];
|
|
14
|
+
defaultOpen?: boolean;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ComponentDocs({ title = "Props & variants", rows, defaultOpen = false, className }: ComponentDocsProps) {
|
|
19
|
+
return (
|
|
20
|
+
<details className={cn("ph-panel group overflow-hidden p-0", className)} open={defaultOpen}>
|
|
21
|
+
<summary className="flex cursor-pointer list-none items-center justify-between gap-4 border-b border-ph-border px-4 py-3 transition hover:bg-ph-muted/60 [&::-webkit-details-marker]:hidden">
|
|
22
|
+
<h3 className="text-xs font-semibold uppercase tracking-wider text-ph-mutedtext">{title}</h3>
|
|
23
|
+
<span className="rounded-full border border-ph-border bg-ph-surface px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-ph-mutedtext transition group-open:bg-ph-brand group-open:text-white">
|
|
24
|
+
<span className="group-open:hidden">Show</span>
|
|
25
|
+
<span className="hidden group-open:inline">Hide</span>
|
|
26
|
+
</span>
|
|
27
|
+
</summary>
|
|
28
|
+
<div className="overflow-x-auto border-t border-ph-border">
|
|
29
|
+
<table className="w-full text-left text-sm">
|
|
30
|
+
<thead className="bg-ph-muted text-xs uppercase tracking-wider text-ph-mutedtext">
|
|
31
|
+
<tr>
|
|
32
|
+
<th className="px-4 py-2 font-semibold">Prop</th>
|
|
33
|
+
<th className="px-4 py-2 font-semibold">Type</th>
|
|
34
|
+
<th className="px-4 py-2 font-semibold">Default</th>
|
|
35
|
+
<th className="px-4 py-2 font-semibold">Use</th>
|
|
36
|
+
</tr>
|
|
37
|
+
</thead>
|
|
38
|
+
<tbody className="divide-y divide-ph-border">
|
|
39
|
+
{rows.map((row) => (
|
|
40
|
+
<tr key={row.name}>
|
|
41
|
+
<td className="px-4 py-3 align-top font-mono text-xs text-ph-ink">{row.name}</td>
|
|
42
|
+
<td className="px-4 py-3 align-top font-mono text-xs text-ph-subtle">{row.type}</td>
|
|
43
|
+
<td className="px-4 py-3 align-top font-mono text-xs text-ph-subtle">{row.defaultValue ?? "-"}</td>
|
|
44
|
+
<td className="px-4 py-3 align-top text-ph-subtle">{row.description}</td>
|
|
45
|
+
</tr>
|
|
46
|
+
))}
|
|
47
|
+
</tbody>
|
|
48
|
+
</table>
|
|
49
|
+
</div>
|
|
50
|
+
</details>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { IconClose } from "@/components/icons";
|
|
3
|
+
import { cn } from "@/lib/cn";
|
|
4
|
+
import { Button } from "@/components/ui/Button";
|
|
5
|
+
|
|
6
|
+
export type DrawerSide = "left" | "right";
|
|
7
|
+
export type DrawerSize = "sm" | "md" | "lg";
|
|
8
|
+
|
|
9
|
+
export interface DrawerProps {
|
|
10
|
+
open?: boolean;
|
|
11
|
+
onClose?: () => void;
|
|
12
|
+
title?: ReactNode;
|
|
13
|
+
children?: ReactNode;
|
|
14
|
+
footer?: ReactNode;
|
|
15
|
+
side?: DrawerSide;
|
|
16
|
+
size?: DrawerSize;
|
|
17
|
+
className?: string;
|
|
18
|
+
showCloseButton?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const sideMap: Record<DrawerSide, string> = {
|
|
22
|
+
left: "left-0 border-r",
|
|
23
|
+
right: "right-0 border-l",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const sizeMap: Record<DrawerSize, string> = {
|
|
27
|
+
sm: "w-[min(100%,18rem)]",
|
|
28
|
+
md: "w-[min(100%,22rem)]",
|
|
29
|
+
lg: "w-[min(100%,30rem)]",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function Drawer({
|
|
33
|
+
open = false,
|
|
34
|
+
onClose,
|
|
35
|
+
title,
|
|
36
|
+
children,
|
|
37
|
+
footer,
|
|
38
|
+
side = "right",
|
|
39
|
+
size = "md",
|
|
40
|
+
className,
|
|
41
|
+
showCloseButton = true,
|
|
42
|
+
}: DrawerProps) {
|
|
43
|
+
if (!open) return null;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<>
|
|
47
|
+
<div className="fixed inset-0 z-[120] bg-black/45 backdrop-blur-[2px]" role="presentation" onClick={onClose} />
|
|
48
|
+
<aside
|
|
49
|
+
className={cn(
|
|
50
|
+
"fixed inset-y-0 z-[130] flex flex-col overflow-y-auto border-ph-border bg-ph-surface shadow-ph-md",
|
|
51
|
+
sideMap[side],
|
|
52
|
+
sizeMap[size],
|
|
53
|
+
className
|
|
54
|
+
)}
|
|
55
|
+
>
|
|
56
|
+
{(title || showCloseButton) && (
|
|
57
|
+
<header className="flex items-center justify-between border-b border-ph-border px-4 py-3">
|
|
58
|
+
<span className="text-sm font-bold text-ph-ink">{title}</span>
|
|
59
|
+
{showCloseButton && onClose && (
|
|
60
|
+
<Button variant="ghost" className="ph-btn-circle" onClick={onClose} aria-label="Close drawer">
|
|
61
|
+
<IconClose className="h-5 w-5" />
|
|
62
|
+
</Button>
|
|
63
|
+
)}
|
|
64
|
+
</header>
|
|
65
|
+
)}
|
|
66
|
+
<div className="flex-1">{children}</div>
|
|
67
|
+
{footer && <footer className="border-t border-ph-border px-4 py-3">{footer}</footer>}
|
|
68
|
+
</aside>
|
|
69
|
+
</>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { ButtonHTMLAttributes, HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
import { IconChevronDown } from "@/components/icons";
|
|
3
|
+
import { cn } from "@/lib/cn";
|
|
4
|
+
import { ButtonChrome } from "@/components/ui/ButtonChrome";
|
|
5
|
+
import type { ButtonSize } from "@/components/ui/Button";
|
|
6
|
+
|
|
7
|
+
export type DropdownAlign = "start" | "end";
|
|
8
|
+
|
|
9
|
+
export interface DropdownMenuProps extends HTMLAttributes<HTMLDetailsElement> {
|
|
10
|
+
/** Custom trigger node (avatar, icon chip, etc.) */
|
|
11
|
+
trigger: ReactNode;
|
|
12
|
+
/** Accessible name when trigger has no visible text */
|
|
13
|
+
"aria-label": string;
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
align?: DropdownAlign;
|
|
16
|
+
panelClassName?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DropdownButtonProps extends Omit<HTMLAttributes<HTMLDetailsElement>, "children"> {
|
|
20
|
+
/** Visible button label */
|
|
21
|
+
label: string;
|
|
22
|
+
variant?: "primary" | "secondary" | "accent";
|
|
23
|
+
outline?: boolean;
|
|
24
|
+
size?: ButtonSize;
|
|
25
|
+
/** Split control (Run ▾) — divider before trailing chevron */
|
|
26
|
+
split?: boolean;
|
|
27
|
+
icon?: ReactNode;
|
|
28
|
+
/** Defaults to chevron when omitted */
|
|
29
|
+
sideIcon?: ReactNode;
|
|
30
|
+
children: ReactNode;
|
|
31
|
+
align?: DropdownAlign;
|
|
32
|
+
panelClassName?: string;
|
|
33
|
+
/** Overrides accessible name; defaults to `label` */
|
|
34
|
+
"aria-label"?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const sizeClass: Record<ButtonSize, string> = {
|
|
38
|
+
xs: "ph-btn-xs",
|
|
39
|
+
sm: "ph-btn-sm",
|
|
40
|
+
md: "",
|
|
41
|
+
lg: "ph-btn-lg",
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const variantClass = {
|
|
45
|
+
primary: "ph-btn-primary",
|
|
46
|
+
secondary: "ph-btn-secondary",
|
|
47
|
+
accent: "ph-btn-accent",
|
|
48
|
+
} as const;
|
|
49
|
+
|
|
50
|
+
function dropdownPanelClass(align: DropdownAlign, panelClassName?: string) {
|
|
51
|
+
return cn(
|
|
52
|
+
"ph-dropdown-panel absolute top-full z-50 mt-0",
|
|
53
|
+
align === "end" ? "right-0" : "left-0",
|
|
54
|
+
panelClassName,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Generic `<details>` menu — pass any trigger (avatar, icon chip, etc.). */
|
|
59
|
+
export function DropdownMenu({
|
|
60
|
+
trigger,
|
|
61
|
+
children,
|
|
62
|
+
align = "start",
|
|
63
|
+
className,
|
|
64
|
+
panelClassName,
|
|
65
|
+
"aria-label": ariaLabel,
|
|
66
|
+
...props
|
|
67
|
+
}: DropdownMenuProps) {
|
|
68
|
+
return (
|
|
69
|
+
<details className={cn("relative", className)} {...props}>
|
|
70
|
+
<summary
|
|
71
|
+
className="cursor-pointer list-none [&::-webkit-details-marker]:hidden"
|
|
72
|
+
aria-label={ariaLabel}
|
|
73
|
+
>
|
|
74
|
+
{trigger}
|
|
75
|
+
</summary>
|
|
76
|
+
<div className={dropdownPanelClass(align, panelClassName)}>{children}</div>
|
|
77
|
+
</details>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Lemon-styled dropdown trigger (primary / secondary / accent) with labelled chrome. */
|
|
82
|
+
export function DropdownButton({
|
|
83
|
+
label,
|
|
84
|
+
variant = "secondary",
|
|
85
|
+
outline = false,
|
|
86
|
+
size = "md",
|
|
87
|
+
split = false,
|
|
88
|
+
icon,
|
|
89
|
+
sideIcon,
|
|
90
|
+
children,
|
|
91
|
+
align = "start",
|
|
92
|
+
className,
|
|
93
|
+
panelClassName,
|
|
94
|
+
"aria-label": ariaLabel,
|
|
95
|
+
...props
|
|
96
|
+
}: DropdownButtonProps) {
|
|
97
|
+
const usesLemonChrome = variant === "primary" || variant === "secondary";
|
|
98
|
+
const chevron = <IconChevronDown className="h-4 w-4" aria-hidden />;
|
|
99
|
+
const trailing = sideIcon ?? chevron;
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<details className={cn("relative", className)} {...props}>
|
|
103
|
+
<summary
|
|
104
|
+
className={cn(
|
|
105
|
+
"ph-btn cursor-pointer list-none [&::-webkit-details-marker]:hidden",
|
|
106
|
+
variantClass[variant],
|
|
107
|
+
outline && "ph-btn-outline",
|
|
108
|
+
sizeClass[size],
|
|
109
|
+
split && "ph-btn-split",
|
|
110
|
+
)}
|
|
111
|
+
aria-label={ariaLabel ?? label}
|
|
112
|
+
>
|
|
113
|
+
{usesLemonChrome ? (
|
|
114
|
+
<ButtonChrome label={label} icon={icon} sideIcon={trailing} split={split} />
|
|
115
|
+
) : (
|
|
116
|
+
<>
|
|
117
|
+
{icon != null ? <span className="ph-btn-icon">{icon}</span> : null}
|
|
118
|
+
<span className="ph-btn-label">{label}</span>
|
|
119
|
+
<span className="ph-btn-side-icon">{trailing}</span>
|
|
120
|
+
</>
|
|
121
|
+
)}
|
|
122
|
+
</summary>
|
|
123
|
+
<div className={dropdownPanelClass(align, panelClassName)}>{children}</div>
|
|
124
|
+
</details>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function DropdownItem({
|
|
129
|
+
className,
|
|
130
|
+
children,
|
|
131
|
+
...props
|
|
132
|
+
}: ButtonHTMLAttributes<HTMLButtonElement>) {
|
|
133
|
+
return (
|
|
134
|
+
<button type="button" className={cn("ph-dropdown-item w-full text-left", className)} {...props}>
|
|
135
|
+
{children}
|
|
136
|
+
</button>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/cn";
|
|
4
|
+
|
|
5
|
+
interface EmptyStateProps {
|
|
6
|
+
icon?: React.ReactNode;
|
|
7
|
+
title: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
action?: React.ReactNode;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function EmptyState({ icon, title, description, action, className }: EmptyStateProps) {
|
|
14
|
+
return (
|
|
15
|
+
<div
|
|
16
|
+
className={cn(
|
|
17
|
+
"flex flex-col items-center justify-center rounded-xl border border-dashed border-ph-border bg-ph-muted/50 px-8 py-16 text-center",
|
|
18
|
+
className
|
|
19
|
+
)}
|
|
20
|
+
>
|
|
21
|
+
{icon && (
|
|
22
|
+
<div className="mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-ph-muted text-ph-mutedtext">
|
|
23
|
+
{icon}
|
|
24
|
+
</div>
|
|
25
|
+
)}
|
|
26
|
+
<h3 className="text-base font-semibold text-ph-ink">{title}</h3>
|
|
27
|
+
{description && (
|
|
28
|
+
<p className="mt-1 max-w-sm text-sm text-ph-subtle">{description}</p>
|
|
29
|
+
)}
|
|
30
|
+
{action && <div className="mt-4">{action}</div>}
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|