@xenide-io/the-old-ui-theme 0.2.6 → 0.3.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.
- package/README.md +1 -1
- package/dist/chunk-FDMD3IIN.mjs +3831 -0
- package/dist/index-D1RTT2Ap.d.mts +1044 -0
- package/dist/index-D1RTT2Ap.d.ts +1044 -0
- package/dist/index.d.mts +11 -758
- package/dist/index.d.ts +11 -758
- package/dist/index.js +1749 -768
- package/dist/index.mjs +312 -2846
- package/dist/tailwind-preset.js +7 -0
- package/dist/tailwind-preset.mjs +7 -0
- package/dist/ui.d.mts +4 -0
- package/dist/ui.d.ts +4 -0
- package/dist/ui.js +3864 -0
- package/dist/ui.mjs +169 -0
- package/package.json +28 -6
- package/src/app/globals.css +1005 -227
- 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 +47 -0
- package/src/components/icons/createIconBase.tsx +32 -0
- package/src/components/icons/icons.tsx +557 -0
- package/src/components/icons/index.ts +3 -0
- package/src/components/sections/AccordionShowcase.tsx +45 -0
- package/src/components/sections/AlertShowcase.tsx +40 -0
- package/src/components/sections/AvatarShowcase.tsx +80 -0
- package/src/components/sections/BadgeShowcase.tsx +65 -0
- package/src/components/sections/ButtonShowcase.tsx +308 -0
- package/src/components/sections/CalendarShowcase.tsx +35 -0
- package/src/components/sections/CardShowcase.tsx +159 -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 +98 -0
- package/src/components/sections/EmptyStateShowcase.tsx +50 -0
- package/src/components/sections/FilterChipsShowcase.tsx +98 -0
- package/src/components/sections/FormShowcase.tsx +127 -0
- package/src/components/sections/HoverCardShowcase.tsx +50 -0
- package/src/components/sections/IconShowcase.tsx +127 -0
- package/src/components/sections/IndicatorShowcase.tsx +52 -0
- package/src/components/sections/KbdShowcase.tsx +57 -0
- package/src/components/sections/ModalShowcase.tsx +211 -0
- package/src/components/sections/NavigationShowcase.tsx +199 -0
- package/src/components/sections/ProductLayoutsShowcase.tsx +138 -0
- package/src/components/sections/ProgressShowcase.tsx +82 -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 +99 -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 +77 -0
- package/src/components/ui/AuthLayout.tsx +58 -0
- package/src/components/ui/Avatar.tsx +97 -0
- package/src/components/ui/Badge.tsx +60 -0
- package/src/components/ui/Button.tsx +120 -0
- package/src/components/ui/ButtonChrome.tsx +20 -0
- package/src/components/ui/Calendar.tsx +115 -0
- package/src/components/ui/Card.tsx +70 -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 +262 -0
- package/src/components/ui/EmptyState.tsx +33 -0
- package/src/components/ui/FilterBar.tsx +165 -0
- package/src/components/ui/FilterChips.tsx +80 -0
- package/src/components/ui/FilterControls.tsx +30 -0
- package/src/components/ui/FormField.tsx +69 -0
- package/src/components/ui/HoverCard.tsx +36 -0
- package/src/components/ui/Indicator.tsx +61 -0
- package/src/components/ui/Input.tsx +385 -0
- package/src/components/ui/Kbd.tsx +113 -0
- package/src/components/ui/Loader.tsx +96 -0
- package/src/components/ui/Modal.tsx +105 -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/SettingsLayout.tsx +94 -0
- package/src/components/ui/ShowcaseWrapper.tsx +124 -0
- package/src/components/ui/Stat.tsx +39 -0
- package/src/components/ui/Stepper.tsx +74 -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/first-slice.test.tsx +98 -0
- package/src/components/ui/index.ts +151 -0
- package/src/components/ui/interaction-primitives.test.tsx +77 -0
- package/src/components/ui/kbd-icons.test.tsx +90 -0
- package/src/components/ui/product-patterns.test.tsx +77 -0
- package/src/styles/themes.css +528 -0
- package/tailwind-preset.ts +7 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, type ReactNode } from "react";
|
|
4
|
+
import { cn } from "@/lib/cn";
|
|
5
|
+
import { CodeBlock } from "@/components/ui";
|
|
6
|
+
import { IconCodePreview } from "@/components/icons";
|
|
7
|
+
|
|
8
|
+
interface ShowcaseWrapperProps {
|
|
9
|
+
id?: string;
|
|
10
|
+
title: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
docs?: ReactNode;
|
|
13
|
+
code: string;
|
|
14
|
+
filename?: string;
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ShowcaseWrapper({
|
|
20
|
+
id,
|
|
21
|
+
title,
|
|
22
|
+
description,
|
|
23
|
+
docs,
|
|
24
|
+
code,
|
|
25
|
+
filename = "Example.tsx",
|
|
26
|
+
children,
|
|
27
|
+
className,
|
|
28
|
+
}: ShowcaseWrapperProps) {
|
|
29
|
+
const [showCode, setShowCode] = useState(false);
|
|
30
|
+
const fallbackId = titleIdMap[title] ?? title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<section id={id ?? fallbackId} className={cn("scroll-mt-20", className)}>
|
|
34
|
+
<div className="ph-h2-rule mb-8">
|
|
35
|
+
<div className="flex items-center gap-1.5">
|
|
36
|
+
<h2>{title}</h2>
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
onClick={() => setShowCode(!showCode)}
|
|
40
|
+
className={cn(
|
|
41
|
+
"flex h-10 w-10 items-center justify-center rounded-full border transition-all",
|
|
42
|
+
showCode
|
|
43
|
+
? "border-ph-brand bg-ph-brand text-[var(--ph-on-accent)] shadow-lg shadow-ph-brand/25"
|
|
44
|
+
: "border-ph-border bg-ph-surface text-ph-subtle hover:border-ph-brand hover:text-ph-brand"
|
|
45
|
+
)}
|
|
46
|
+
title={showCode ? "Hide code" : "Show code"}
|
|
47
|
+
aria-label={showCode ? "Hide code" : "Show code"}
|
|
48
|
+
>
|
|
49
|
+
<IconCodePreview className="h-5 w-5" aria-hidden />
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
<hr />
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
{description && (
|
|
56
|
+
<p className="mb-6 text-sm text-ph-subtle">{description}</p>
|
|
57
|
+
)}
|
|
58
|
+
|
|
59
|
+
{docs && <div className="mb-6">{docs}</div>}
|
|
60
|
+
|
|
61
|
+
<div
|
|
62
|
+
className={cn(
|
|
63
|
+
"gap-6",
|
|
64
|
+
showCode ? "lg:grid lg:grid-cols-[1fr,minmax(360px,420px)]" : "block"
|
|
65
|
+
)}
|
|
66
|
+
>
|
|
67
|
+
<div className={cn("min-w-0", showCode && "lg:pr-2")}>{children}</div>
|
|
68
|
+
|
|
69
|
+
<div
|
|
70
|
+
className={cn(
|
|
71
|
+
"transition-all duration-300",
|
|
72
|
+
showCode
|
|
73
|
+
? "block opacity-100"
|
|
74
|
+
: "hidden opacity-0 lg:hidden"
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
77
|
+
<div className="sticky top-20">
|
|
78
|
+
<CodeBlock code={code} filename={filename} />
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</section>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const titleIdMap: Record<string, string> = {
|
|
87
|
+
"Icons": "icons",
|
|
88
|
+
"Colour tokens": "colors",
|
|
89
|
+
"Buttons": "buttons",
|
|
90
|
+
"Badges": "badges",
|
|
91
|
+
"Alerts": "alerts",
|
|
92
|
+
"Toasts": "toasts",
|
|
93
|
+
"Cards": "cards",
|
|
94
|
+
"Forms": "forms",
|
|
95
|
+
"Inputs & Forms": "forms",
|
|
96
|
+
"Tables": "tables",
|
|
97
|
+
"Accordion": "accordion",
|
|
98
|
+
"Calendar": "calendar",
|
|
99
|
+
"Hover Card": "hovercard",
|
|
100
|
+
"Modals": "modals",
|
|
101
|
+
"Drawers": "drawers",
|
|
102
|
+
"Navigation chrome": "navigation",
|
|
103
|
+
"Navigation utilities": "utilities",
|
|
104
|
+
"Stepper": "stepper",
|
|
105
|
+
"Command Palette": "command",
|
|
106
|
+
"Filter Chips": "filters",
|
|
107
|
+
"Empty State": "emptystate",
|
|
108
|
+
"Charts (PostHog-style)": "charts",
|
|
109
|
+
"Dashboard shells": "dashboard",
|
|
110
|
+
"Tabs": "tabs",
|
|
111
|
+
"Dropdowns": "dropdowns",
|
|
112
|
+
"Progress & loading": "progress",
|
|
113
|
+
"Tooltip": "tooltip",
|
|
114
|
+
"Avatars": "avatars",
|
|
115
|
+
"Stats & conversational UI": "stats",
|
|
116
|
+
"Timeline": "timeline",
|
|
117
|
+
"Countdown motif": "countdown",
|
|
118
|
+
"Keyboard glyphs": "kbd",
|
|
119
|
+
"Icon swap": "swap",
|
|
120
|
+
"Stacked surfaces": "stack",
|
|
121
|
+
"Diff slider": "diff",
|
|
122
|
+
"Badge hotspots": "indicator",
|
|
123
|
+
"Terminal capture": "code-mockup",
|
|
124
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export type StatTone = "default" | "brand" | "blue" | "purple" | "warning";
|
|
5
|
+
|
|
6
|
+
export interface StatProps extends ComponentPropsWithoutRef<"article"> {
|
|
7
|
+
icon?: ReactNode;
|
|
8
|
+
label: ReactNode;
|
|
9
|
+
value: ReactNode;
|
|
10
|
+
footer?: ReactNode;
|
|
11
|
+
tone?: StatTone;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const toneMap: Record<StatTone, string> = {
|
|
15
|
+
default: "text-ph-ink",
|
|
16
|
+
brand: "text-ph-brand",
|
|
17
|
+
blue: "text-ph-blue",
|
|
18
|
+
purple: "text-ph-purple",
|
|
19
|
+
warning: "text-amber-900",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const statMap: Record<StatTone, string> = {
|
|
23
|
+
default: "",
|
|
24
|
+
brand: "",
|
|
25
|
+
blue: "",
|
|
26
|
+
purple: "",
|
|
27
|
+
warning: "border-amber-200 bg-amber-50/70",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function Stat({ icon, label, value, footer, tone = "default", className, ...props }: StatProps) {
|
|
31
|
+
return (
|
|
32
|
+
<article className={cn("ph-stat", statMap[tone], className)} {...props}>
|
|
33
|
+
{icon && <div className={cn("mb-2 h-7 w-7", toneMap[tone])}>{icon}</div>}
|
|
34
|
+
<div className={cn("ph-stat-label", tone === "warning" && toneMap[tone])}>{label}</div>
|
|
35
|
+
<div className={cn("ph-stat-val", toneMap[tone])}>{value}</div>
|
|
36
|
+
{footer && <p className="text-xs text-ph-subtle">{footer}</p>}
|
|
37
|
+
</article>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/cn";
|
|
4
|
+
import { IconCheck } from "@/components/icons";
|
|
5
|
+
|
|
6
|
+
interface Step {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
status?: "pending" | "current" | "completed" | "error";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface StepperProps {
|
|
14
|
+
steps: Step[];
|
|
15
|
+
currentStep: number;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function Stepper({ steps, currentStep, className }: StepperProps) {
|
|
20
|
+
return (
|
|
21
|
+
<div className={cn("flex items-start gap-2", className)}>
|
|
22
|
+
{steps.map((step, index) => {
|
|
23
|
+
const isCompleted = index < currentStep;
|
|
24
|
+
const isCurrent = index === currentStep;
|
|
25
|
+
const isPending = index > currentStep;
|
|
26
|
+
const isError = step.status === "error";
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div key={step.id} className="flex flex-1 items-start gap-2">
|
|
30
|
+
<div className="flex flex-1 flex-col items-center">
|
|
31
|
+
<div
|
|
32
|
+
className={cn(
|
|
33
|
+
"flex h-9 w-9 items-center justify-center rounded-full text-sm font-bold transition-colors",
|
|
34
|
+
isCompleted && "bg-ph-success text-[var(--ph-on-success)]",
|
|
35
|
+
isCurrent && !isError && "bg-ph-brand text-[var(--ph-on-accent)] ring-4 ring-ph-brand/20",
|
|
36
|
+
isCurrent && isError && "bg-ph-danger text-[var(--ph-on-danger)]",
|
|
37
|
+
isPending && "bg-ph-muted text-ph-mutedtext"
|
|
38
|
+
)}
|
|
39
|
+
>
|
|
40
|
+
{isCompleted ? (
|
|
41
|
+
<IconCheck className="h-5 w-5" aria-hidden />
|
|
42
|
+
) : (
|
|
43
|
+
index + 1
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
<div className="mt-2 text-center">
|
|
47
|
+
<p
|
|
48
|
+
className={cn(
|
|
49
|
+
"text-sm font-semibold",
|
|
50
|
+
isCurrent && "text-ph-ink",
|
|
51
|
+
!isCurrent && "text-ph-mutedtext"
|
|
52
|
+
)}
|
|
53
|
+
>
|
|
54
|
+
{step.label}
|
|
55
|
+
</p>
|
|
56
|
+
{step.description && (
|
|
57
|
+
<p className="mt-0.5 text-xs text-ph-mutedtext">{step.description}</p>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
{index < steps.length - 1 && (
|
|
62
|
+
<div
|
|
63
|
+
className={cn(
|
|
64
|
+
"mt-4 h-0.5 flex-1 rounded-full transition-colors",
|
|
65
|
+
isCompleted ? "bg-ph-success" : "bg-ph-border"
|
|
66
|
+
)}
|
|
67
|
+
/>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
})}
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface TableColumn<T> {
|
|
5
|
+
key: string;
|
|
6
|
+
header: ReactNode;
|
|
7
|
+
cell: (row: T) => ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TableProps<T> {
|
|
12
|
+
data: T[];
|
|
13
|
+
columns: TableColumn<T>[];
|
|
14
|
+
zebra?: boolean;
|
|
15
|
+
compact?: boolean;
|
|
16
|
+
ribbon?: boolean;
|
|
17
|
+
getRowStyle?: (row: T) => CSSProperties | undefined;
|
|
18
|
+
className?: string;
|
|
19
|
+
caption?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function Table<T>({
|
|
23
|
+
data,
|
|
24
|
+
columns,
|
|
25
|
+
zebra = false,
|
|
26
|
+
compact = false,
|
|
27
|
+
ribbon = false,
|
|
28
|
+
getRowStyle,
|
|
29
|
+
className,
|
|
30
|
+
caption,
|
|
31
|
+
}: TableProps<T>) {
|
|
32
|
+
return (
|
|
33
|
+
<div className={cn("ph-table-wrap", compact && "ph-table-compact", className)}>
|
|
34
|
+
<table className={cn("ph-table", zebra && "ph-table-zebra", ribbon && "ph-table--ribbon")}>
|
|
35
|
+
{caption && <caption className="sr-only">{caption}</caption>}
|
|
36
|
+
<thead>
|
|
37
|
+
<tr>
|
|
38
|
+
{columns.map((col) => (
|
|
39
|
+
<th key={col.key} className={col.className}>{col.header}</th>
|
|
40
|
+
))}
|
|
41
|
+
</tr>
|
|
42
|
+
</thead>
|
|
43
|
+
<tbody>
|
|
44
|
+
{data.map((row, i) => (
|
|
45
|
+
<tr key={i} style={getRowStyle?.(row)}>
|
|
46
|
+
{columns.map((col) => (
|
|
47
|
+
<td key={col.key} className={col.className}>{col.cell(row)}</td>
|
|
48
|
+
))}
|
|
49
|
+
</tr>
|
|
50
|
+
))}
|
|
51
|
+
</tbody>
|
|
52
|
+
</table>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { cn } from "@/lib/cn";
|
|
2
|
+
|
|
3
|
+
export interface TabItem {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type TabsVariant = "pill" | "underline";
|
|
9
|
+
|
|
10
|
+
export interface TabsProps {
|
|
11
|
+
items: TabItem[];
|
|
12
|
+
value: string;
|
|
13
|
+
onChange: (value: string) => void;
|
|
14
|
+
variant?: TabsVariant;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function Tabs({ items, value, onChange, variant = "pill", className }: TabsProps) {
|
|
19
|
+
if (variant === "underline") {
|
|
20
|
+
return (
|
|
21
|
+
<div className={cn("flex gap-6 border-b border-ph-border", className)}>
|
|
22
|
+
{items.map((item) => (
|
|
23
|
+
<button
|
|
24
|
+
key={item.value}
|
|
25
|
+
type="button"
|
|
26
|
+
onClick={() => onChange(item.value)}
|
|
27
|
+
className={cn(
|
|
28
|
+
"-mb-px border-b-2 pb-2 text-sm font-semibold transition",
|
|
29
|
+
value === item.value ? "border-ph-brand text-ph-ink" : "border-transparent text-ph-mutedtext hover:text-ph-ink"
|
|
30
|
+
)}
|
|
31
|
+
>
|
|
32
|
+
{item.label}
|
|
33
|
+
</button>
|
|
34
|
+
))}
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className={cn("ph-tabs-row inline-flex", className)}>
|
|
41
|
+
{items.map((item) => (
|
|
42
|
+
<button
|
|
43
|
+
key={item.value}
|
|
44
|
+
type="button"
|
|
45
|
+
onClick={() => onChange(item.value)}
|
|
46
|
+
className={cn("ph-tab", value === item.value && "ph-tab-active")}
|
|
47
|
+
>
|
|
48
|
+
{item.label}
|
|
49
|
+
</button>
|
|
50
|
+
))}
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface TerminalProps extends ComponentPropsWithoutRef<"div"> {
|
|
5
|
+
/** Window title */
|
|
6
|
+
title?: string;
|
|
7
|
+
/** Terminal lines — array of { text, color? } or plain strings */
|
|
8
|
+
lines?: Array<{ text: string; color?: string } | string>;
|
|
9
|
+
/** Custom content instead of lines */
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function Terminal({
|
|
14
|
+
title = "sandbox",
|
|
15
|
+
lines,
|
|
16
|
+
children,
|
|
17
|
+
className,
|
|
18
|
+
...props
|
|
19
|
+
}: TerminalProps) {
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
className={cn(
|
|
23
|
+
"overflow-hidden rounded-xl border border-ph-border shadow-ph-md",
|
|
24
|
+
className,
|
|
25
|
+
)}
|
|
26
|
+
{...props}
|
|
27
|
+
>
|
|
28
|
+
<header className="flex items-center gap-2 border-b border-ph-border bg-ph-muted px-4 py-3">
|
|
29
|
+
<div className="h-3 w-3 rounded-full bg-red-400" />
|
|
30
|
+
<div className="h-3 w-3 rounded-full bg-amber-400" />
|
|
31
|
+
<div className="h-3 w-3 rounded-full bg-emerald-400" />
|
|
32
|
+
{title && (
|
|
33
|
+
<span className="text-xs font-medium text-ph-mutedtext">{title}</span>
|
|
34
|
+
)}
|
|
35
|
+
</header>
|
|
36
|
+
<div className="ph-code rounded-none border-x-0 border-b-0 text-[13px] leading-relaxed">
|
|
37
|
+
{children || (
|
|
38
|
+
<>
|
|
39
|
+
{lines?.map((line, i) => {
|
|
40
|
+
const text = typeof line === "string" ? line : line.text;
|
|
41
|
+
const color = typeof line === "string" ? undefined : line.color;
|
|
42
|
+
return (
|
|
43
|
+
<span key={i}>
|
|
44
|
+
<span className={color || "text-neutral-600"}>{text}</span>
|
|
45
|
+
{i < lines.length - 1 && <br />}
|
|
46
|
+
</span>
|
|
47
|
+
);
|
|
48
|
+
})}
|
|
49
|
+
</>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useLayoutEffect } from "react";
|
|
4
|
+
import { persistTheme, readStoredTheme } from "@/themes";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Re-applies `html[data-theme]` after React commits. Hydration can clear attributes
|
|
8
|
+
* that were set only by `THEME_INIT_SCRIPT` — without this sync, `:root`-scoped
|
|
9
|
+
* light tokens effectively win visually.
|
|
10
|
+
*/
|
|
11
|
+
export function ThemeDomSync() {
|
|
12
|
+
useLayoutEffect(() => {
|
|
13
|
+
persistTheme(readStoredTheme());
|
|
14
|
+
}, []);
|
|
15
|
+
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import { useLayoutEffect } from "react";
|
|
5
|
+
import { persistTheme, readStoredTheme } from "@/themes";
|
|
6
|
+
|
|
7
|
+
export interface ThemeManagerProps {
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Client wrapper that keeps html[data-theme] synced with localStorage. */
|
|
12
|
+
export function ThemeManager({ children }: ThemeManagerProps) {
|
|
13
|
+
useLayoutEffect(() => {
|
|
14
|
+
persistTheme(readStoredTheme());
|
|
15
|
+
}, []);
|
|
16
|
+
|
|
17
|
+
return <>{children}</>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { persistTheme, readStoredTheme, THEME_GROUPS, THEMES, type ThemeId } from "@/themes";
|
|
5
|
+
import { cn } from "@/lib/cn";
|
|
6
|
+
|
|
7
|
+
export interface ThemeSwitcherProps {
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Colour-only theme picker — persists to localStorage and sets html[data-theme]. */
|
|
12
|
+
export function ThemeSwitcher({ className }: ThemeSwitcherProps) {
|
|
13
|
+
const [themeId, setThemeId] = useState<ThemeId>("hedgehog-light");
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const id = readStoredTheme() as ThemeId;
|
|
17
|
+
persistTheme(id);
|
|
18
|
+
setThemeId(id);
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<label className={cn("flex min-w-0 items-center gap-2", className)}>
|
|
23
|
+
<span className="shrink-0 text-xs font-medium text-ph-mutedtext sm:text-sm">Theme</span>
|
|
24
|
+
<select
|
|
25
|
+
className="ph-select max-w-[11rem] !min-h-[2rem] py-1 pl-2.5 pr-8 text-xs sm:max-w-[14rem] sm:text-sm"
|
|
26
|
+
value={themeId}
|
|
27
|
+
aria-label="Colour theme"
|
|
28
|
+
onChange={(event) => {
|
|
29
|
+
const next = event.target.value as ThemeId;
|
|
30
|
+
persistTheme(next);
|
|
31
|
+
setThemeId(next);
|
|
32
|
+
}}
|
|
33
|
+
>
|
|
34
|
+
{THEME_GROUPS.map((group) => (
|
|
35
|
+
<optgroup key={group} label={group}>
|
|
36
|
+
{THEMES.filter((theme) => theme.group === group).map((theme) => (
|
|
37
|
+
<option key={theme.id} value={theme.id}>
|
|
38
|
+
{theme.name}
|
|
39
|
+
</option>
|
|
40
|
+
))}
|
|
41
|
+
</optgroup>
|
|
42
|
+
))}
|
|
43
|
+
</select>
|
|
44
|
+
</label>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface TimelineEvent {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
time?: string;
|
|
9
|
+
icon?: ReactNode;
|
|
10
|
+
color?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TimelineProps extends ComponentPropsWithoutRef<"ol"> {
|
|
14
|
+
events: TimelineEvent[];
|
|
15
|
+
/** Alternate left/right layout on large screens */
|
|
16
|
+
alternate?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function Timeline({
|
|
20
|
+
events,
|
|
21
|
+
alternate = false,
|
|
22
|
+
className,
|
|
23
|
+
...props
|
|
24
|
+
}: TimelineProps) {
|
|
25
|
+
return (
|
|
26
|
+
<ol
|
|
27
|
+
className={cn("relative space-y-8", className)}
|
|
28
|
+
{...props}
|
|
29
|
+
>
|
|
30
|
+
{events.map((event, idx) => {
|
|
31
|
+
const align = alternate && idx % 2 !== 0 ? "lg:flex-row-reverse" : "";
|
|
32
|
+
return (
|
|
33
|
+
<li key={event.id} className={cn("relative flex gap-6", align)}>
|
|
34
|
+
<div
|
|
35
|
+
className={cn(
|
|
36
|
+
"relative z-10 flex h-12 w-12 shrink-0 items-center justify-center rounded-full ring-8 ring-ph-muted shadow-md",
|
|
37
|
+
event.color || "bg-ph-brand text-white",
|
|
38
|
+
)}
|
|
39
|
+
>
|
|
40
|
+
{event.icon}
|
|
41
|
+
</div>
|
|
42
|
+
<article className="flex-1 rounded-xl border border-ph-border bg-ph-surface p-4 shadow-ph">
|
|
43
|
+
<div className="flex flex-wrap items-baseline gap-3">
|
|
44
|
+
<h4 className="text-base font-semibold text-ph-ink">{event.title}</h4>
|
|
45
|
+
{event.time && (
|
|
46
|
+
<time className="text-[11px] font-semibold uppercase tracking-wide text-ph-mutedtext">
|
|
47
|
+
{event.time}
|
|
48
|
+
</time>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
{event.description && (
|
|
52
|
+
<p className="mt-2 text-sm text-ph-subtle">{event.description}</p>
|
|
53
|
+
)}
|
|
54
|
+
</article>
|
|
55
|
+
</li>
|
|
56
|
+
);
|
|
57
|
+
})}
|
|
58
|
+
</ol>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import {
|
|
3
|
+
IconCancel,
|
|
4
|
+
IconCheckCircle,
|
|
5
|
+
IconClose,
|
|
6
|
+
IconErrorOutline,
|
|
7
|
+
IconExclamation,
|
|
8
|
+
} from "@/components/icons";
|
|
9
|
+
import { cn } from "@/lib/cn";
|
|
10
|
+
|
|
11
|
+
export type ToastStatus = "info" | "success" | "warning" | "danger";
|
|
12
|
+
|
|
13
|
+
export interface ToastProps {
|
|
14
|
+
status?: ToastStatus;
|
|
15
|
+
title: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
onDismiss?: () => void;
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const statusConfig: Record<ToastStatus, { icon: typeof IconErrorOutline; color: string; bg: string } > = {
|
|
22
|
+
info: { icon: IconErrorOutline, color: "text-ph-info", bg: "ph-toast-info" },
|
|
23
|
+
success: { icon: IconCheckCircle, color: "text-ph-success", bg: "ph-toast-success" },
|
|
24
|
+
warning: { icon: IconExclamation, color: "text-ph-warning", bg: "ph-toast-warning" },
|
|
25
|
+
danger: { icon: IconCancel, color: "text-ph-danger", bg: "ph-toast-danger" },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function Toast({
|
|
29
|
+
status = "info",
|
|
30
|
+
title,
|
|
31
|
+
description,
|
|
32
|
+
onDismiss,
|
|
33
|
+
className,
|
|
34
|
+
}: ToastProps) {
|
|
35
|
+
const config = statusConfig[status];
|
|
36
|
+
const Icon = config.icon;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className={cn("ph-toast", config.bg, className)}>
|
|
40
|
+
<Icon className={cn("mt-0.5 h-5 w-5 shrink-0", config.color)} aria-hidden />
|
|
41
|
+
<div className="pr-8">
|
|
42
|
+
<div className="font-semibold">{title}</div>
|
|
43
|
+
{description && <p className="mt-1 text-xs text-ph-subtle">{description}</p>}
|
|
44
|
+
</div>
|
|
45
|
+
{onDismiss && (
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
aria-label={`Dismiss ${title}`}
|
|
49
|
+
className="absolute right-2 top-2 ph-btn ph-btn-ghost ph-btn-xs ph-btn-circle"
|
|
50
|
+
onClick={onDismiss}
|
|
51
|
+
>
|
|
52
|
+
<IconClose className="h-4 w-4" />
|
|
53
|
+
</button>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ToastStackProps {
|
|
60
|
+
children?: ReactNode;
|
|
61
|
+
className?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function ToastStack({ children, className }: ToastStackProps) {
|
|
65
|
+
return (
|
|
66
|
+
<div className={cn("ph-toast-stack", className)}>
|
|
67
|
+
{children}
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|