@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,94 @@
|
|
|
1
|
+
import { IconChevronLeft, IconChevronRight } from "@/components/icons";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface BreadcrumbItem {
|
|
5
|
+
label: string;
|
|
6
|
+
href?: string;
|
|
7
|
+
current?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface BreadcrumbsProps {
|
|
11
|
+
items: BreadcrumbItem[];
|
|
12
|
+
label?: string;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function Breadcrumbs({ items, label = "Breadcrumb", className }: BreadcrumbsProps) {
|
|
17
|
+
return (
|
|
18
|
+
<nav aria-label={label} className={cn("ph-breadcrumbs", className)}>
|
|
19
|
+
{items.map((item, index) => (
|
|
20
|
+
<span key={`${item.label}-${index}`} className="contents">
|
|
21
|
+
{index > 0 ? <span className="ph-breadcrumb-sep" aria-hidden="true">/</span> : null}
|
|
22
|
+
{item.current || !item.href ? (
|
|
23
|
+
<span className="ph-breadcrumb-current" aria-current={item.current ? "page" : undefined}>
|
|
24
|
+
{item.label}
|
|
25
|
+
</span>
|
|
26
|
+
) : (
|
|
27
|
+
<a href={item.href} className="ph-breadcrumb-link">
|
|
28
|
+
{item.label}
|
|
29
|
+
</a>
|
|
30
|
+
)}
|
|
31
|
+
</span>
|
|
32
|
+
))}
|
|
33
|
+
</nav>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface PaginationProps {
|
|
38
|
+
page: number;
|
|
39
|
+
totalPages: number;
|
|
40
|
+
onPageChange?: (page: number) => void;
|
|
41
|
+
label?: string;
|
|
42
|
+
compact?: boolean;
|
|
43
|
+
className?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function Pagination({
|
|
47
|
+
page,
|
|
48
|
+
totalPages,
|
|
49
|
+
onPageChange,
|
|
50
|
+
label = "Pagination",
|
|
51
|
+
compact = false,
|
|
52
|
+
className,
|
|
53
|
+
}: PaginationProps) {
|
|
54
|
+
const pages = compact ? [Math.max(1, page - 1), page, Math.min(totalPages, page + 1)] : [1, 2, 3, totalPages];
|
|
55
|
+
const uniquePages = Array.from(new Set(pages)).filter((n) => n >= 1 && n <= totalPages);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<nav aria-label={label} className={cn("ph-pagination", className)}>
|
|
59
|
+
<button
|
|
60
|
+
type="button"
|
|
61
|
+
className="ph-pagination-btn"
|
|
62
|
+
disabled={page <= 1}
|
|
63
|
+
aria-label="Previous page"
|
|
64
|
+
onClick={() => onPageChange?.(page - 1)}
|
|
65
|
+
>
|
|
66
|
+
<IconChevronLeft className="mx-auto h-4 w-4" aria-hidden />
|
|
67
|
+
</button>
|
|
68
|
+
{uniquePages.map((pageNumber, index) => (
|
|
69
|
+
<span key={pageNumber} className="contents">
|
|
70
|
+
{!compact && index === uniquePages.length - 1 && pageNumber > 4 ? (
|
|
71
|
+
<span className="ph-pagination-ellipsis" aria-hidden="true">...</span>
|
|
72
|
+
) : null}
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
className={cn("ph-pagination-btn", pageNumber === page && "ph-pagination-btn-active")}
|
|
76
|
+
aria-current={pageNumber === page ? "page" : undefined}
|
|
77
|
+
onClick={() => onPageChange?.(pageNumber)}
|
|
78
|
+
>
|
|
79
|
+
{pageNumber}
|
|
80
|
+
</button>
|
|
81
|
+
</span>
|
|
82
|
+
))}
|
|
83
|
+
<button
|
|
84
|
+
type="button"
|
|
85
|
+
className="ph-pagination-btn"
|
|
86
|
+
disabled={page >= totalPages}
|
|
87
|
+
aria-label="Next page"
|
|
88
|
+
onClick={() => onPageChange?.(page + 1)}
|
|
89
|
+
>
|
|
90
|
+
<IconChevronRight className="mx-auto h-4 w-4" aria-hidden />
|
|
91
|
+
</button>
|
|
92
|
+
</nav>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface PanelProps extends Omit<ComponentPropsWithoutRef<"div">, "title"> {
|
|
5
|
+
title?: ReactNode;
|
|
6
|
+
description?: ReactNode;
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Panel({ title, description, children, className, ...props }: PanelProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div className={cn("ph-panel", className)} {...props}>
|
|
13
|
+
{title && <h3 className="text-sm font-semibold uppercase tracking-wider text-ph-mutedtext">{title}</h3>}
|
|
14
|
+
{description && <p className="text-sm text-ph-subtle">{description}</p>}
|
|
15
|
+
{children}
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef } from "react";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface ProgressProps extends ComponentPropsWithoutRef<"div"> {
|
|
5
|
+
/** Current value (0-100) */
|
|
6
|
+
value: number;
|
|
7
|
+
/** Maximum value (default 100) */
|
|
8
|
+
max?: number;
|
|
9
|
+
/** Color class for the bar */
|
|
10
|
+
color?: string;
|
|
11
|
+
/** Optional label shown above */
|
|
12
|
+
label?: string;
|
|
13
|
+
/** Show percentage text */
|
|
14
|
+
showPercentage?: boolean;
|
|
15
|
+
/** Size variant */
|
|
16
|
+
size?: "sm" | "md" | "lg";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const sizeMap = {
|
|
20
|
+
sm: "h-1",
|
|
21
|
+
md: "h-2",
|
|
22
|
+
lg: "h-3",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function Progress({
|
|
26
|
+
value,
|
|
27
|
+
max = 100,
|
|
28
|
+
color = "bg-ph-brand",
|
|
29
|
+
label,
|
|
30
|
+
showPercentage = false,
|
|
31
|
+
size = "md",
|
|
32
|
+
className,
|
|
33
|
+
...props
|
|
34
|
+
}: ProgressProps) {
|
|
35
|
+
const percentage = Math.min(100, Math.max(0, (value / max) * 100));
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className={cn("w-full", className)} {...props}>
|
|
39
|
+
{(label || showPercentage) && (
|
|
40
|
+
<div className="mb-1.5 flex justify-between text-sm text-ph-subtle">
|
|
41
|
+
{label && <span>{label}</span>}
|
|
42
|
+
{showPercentage && (
|
|
43
|
+
<span className="tabular-nums">{Math.round(percentage)}%</span>
|
|
44
|
+
)}
|
|
45
|
+
</div>
|
|
46
|
+
)}
|
|
47
|
+
<div className={cn("ph-progress", sizeMap[size])}>
|
|
48
|
+
<div
|
|
49
|
+
className={cn("ph-progress-bar", color)}
|
|
50
|
+
style={{ width: `${percentage}%` }}
|
|
51
|
+
role="progressbar"
|
|
52
|
+
aria-valuenow={value}
|
|
53
|
+
aria-valuemin={0}
|
|
54
|
+
aria-valuemax={max}
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef } from "react";
|
|
2
|
+
import { IconStar } from "@/components/icons";
|
|
3
|
+
import { cn } from "@/lib/cn";
|
|
4
|
+
|
|
5
|
+
export interface RatingProps extends Omit<ComponentPropsWithoutRef<"div">, "onChange"> {
|
|
6
|
+
label?: string;
|
|
7
|
+
value?: number;
|
|
8
|
+
max?: number;
|
|
9
|
+
readOnly?: boolean;
|
|
10
|
+
onChange?: (value: number) => void;
|
|
11
|
+
wrapperClassName?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function Rating({
|
|
15
|
+
label,
|
|
16
|
+
value = 0,
|
|
17
|
+
max = 5,
|
|
18
|
+
readOnly = true,
|
|
19
|
+
onChange,
|
|
20
|
+
className,
|
|
21
|
+
wrapperClassName,
|
|
22
|
+
...props
|
|
23
|
+
}: RatingProps) {
|
|
24
|
+
const stars = Array.from({ length: max }, (_, index) => index + 1);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className={cn("ph-field", wrapperClassName)}>
|
|
28
|
+
{label && <span className="ph-label">{label}</span>}
|
|
29
|
+
<div
|
|
30
|
+
className={cn("ph-rating flex", className)}
|
|
31
|
+
role={readOnly ? "img" : "radiogroup"}
|
|
32
|
+
aria-label={label ?? `Rating: ${value} out of ${max}`}
|
|
33
|
+
{...props}
|
|
34
|
+
>
|
|
35
|
+
{stars.map((star) => {
|
|
36
|
+
const active = star <= value;
|
|
37
|
+
|
|
38
|
+
if (readOnly) {
|
|
39
|
+
return (
|
|
40
|
+
<IconStar
|
|
41
|
+
key={star}
|
|
42
|
+
className={cn("h-6 w-6", active ? "text-amber-500" : "text-ph-border")}
|
|
43
|
+
aria-hidden
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<button
|
|
50
|
+
key={star}
|
|
51
|
+
type="button"
|
|
52
|
+
className="rounded p-0.5 transition hover:scale-105 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ph-brand"
|
|
53
|
+
role="radio"
|
|
54
|
+
aria-checked={active}
|
|
55
|
+
aria-label={`${star} out of ${max}`}
|
|
56
|
+
onClick={() => onChange?.(star)}
|
|
57
|
+
>
|
|
58
|
+
<IconStar className={cn("h-6 w-6", active ? "text-amber-500" : "text-ph-border")} />
|
|
59
|
+
</button>
|
|
60
|
+
);
|
|
61
|
+
})}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef } from "react";
|
|
2
|
+
import { useId } from "react";
|
|
3
|
+
import { IconSearch } from "@/components/icons";
|
|
4
|
+
import { cn } from "@/lib/cn";
|
|
5
|
+
import { Button } from "@/components/ui/Button";
|
|
6
|
+
|
|
7
|
+
export interface SearchInputProps extends Omit<ComponentPropsWithoutRef<"input">, "type"> {
|
|
8
|
+
/** Associated label text — always rendered (visually hidden when `hideLabel`) */
|
|
9
|
+
label?: string;
|
|
10
|
+
hideLabel?: boolean;
|
|
11
|
+
/** Optional shortcut hint shown inside the field (e.g. "⌘K") */
|
|
12
|
+
shortcut?: string;
|
|
13
|
+
density?: "default" | "compact";
|
|
14
|
+
wrapperClassName?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Labelled search field — use in nav bars and toolbars. */
|
|
18
|
+
export function SearchInput({
|
|
19
|
+
id: idProp,
|
|
20
|
+
label = "Search",
|
|
21
|
+
hideLabel = false,
|
|
22
|
+
shortcut,
|
|
23
|
+
density = "default",
|
|
24
|
+
className,
|
|
25
|
+
wrapperClassName,
|
|
26
|
+
placeholder,
|
|
27
|
+
...props
|
|
28
|
+
}: SearchInputProps) {
|
|
29
|
+
const autoId = useId();
|
|
30
|
+
const id = idProp ?? autoId;
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className={cn("relative", wrapperClassName)}>
|
|
34
|
+
<label htmlFor={id} className={hideLabel ? "sr-only" : "ph-label mb-1.5 block"}>
|
|
35
|
+
{label}
|
|
36
|
+
</label>
|
|
37
|
+
<div className="relative">
|
|
38
|
+
{shortcut != null ? (
|
|
39
|
+
<span
|
|
40
|
+
className="ph-shortcut pointer-events-none absolute left-3 top-1/2 z-10 -translate-y-1/2 text-xs"
|
|
41
|
+
aria-hidden
|
|
42
|
+
>
|
|
43
|
+
{shortcut}
|
|
44
|
+
</span>
|
|
45
|
+
) : null}
|
|
46
|
+
<input
|
|
47
|
+
id={id}
|
|
48
|
+
type="search"
|
|
49
|
+
placeholder={placeholder}
|
|
50
|
+
aria-label={hideLabel ? label : undefined}
|
|
51
|
+
className={cn(
|
|
52
|
+
"ph-input w-full",
|
|
53
|
+
density === "compact" && "max-w-xs !min-h-[2rem] py-1.5 text-sm",
|
|
54
|
+
shortcut != null && "pl-[3.25rem]",
|
|
55
|
+
className,
|
|
56
|
+
)}
|
|
57
|
+
{...props}
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface SearchGroupProps extends Omit<ComponentPropsWithoutRef<"input">, "type"> {
|
|
65
|
+
label?: string;
|
|
66
|
+
submitLabel?: string;
|
|
67
|
+
wrapperClassName?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Input + primary search — one control shell, 1px vertical seam, matched height to `ph-input`. */
|
|
71
|
+
export function SearchGroup({
|
|
72
|
+
id: idProp,
|
|
73
|
+
label = "Search records",
|
|
74
|
+
submitLabel = "Search",
|
|
75
|
+
placeholder = "Persons, actions, flags…",
|
|
76
|
+
wrapperClassName,
|
|
77
|
+
className,
|
|
78
|
+
...props
|
|
79
|
+
}: SearchGroupProps) {
|
|
80
|
+
const autoId = useId();
|
|
81
|
+
const id = idProp ?? autoId;
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className={cn("ph-field", wrapperClassName)}>
|
|
85
|
+
<label htmlFor={id} className="ph-label">
|
|
86
|
+
{label}
|
|
87
|
+
</label>
|
|
88
|
+
<div className="ph-search-group flex w-full max-w-lg min-h-[2.3125rem] items-stretch divide-x divide-ph-border">
|
|
89
|
+
<input
|
|
90
|
+
id={id}
|
|
91
|
+
type="search"
|
|
92
|
+
placeholder={placeholder}
|
|
93
|
+
className={cn("ph-input ph-input-group-field min-w-0 flex-1", className)}
|
|
94
|
+
{...props}
|
|
95
|
+
/>
|
|
96
|
+
<Button
|
|
97
|
+
type="submit"
|
|
98
|
+
variant="primary"
|
|
99
|
+
icon={<IconSearch className="h-4 w-4" aria-hidden />}
|
|
100
|
+
className="ph-search-group__submit"
|
|
101
|
+
aria-label={submitLabel}
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/cn";
|
|
4
|
+
|
|
5
|
+
interface SegmentedControlOption {
|
|
6
|
+
value: string;
|
|
7
|
+
label: string;
|
|
8
|
+
icon?: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface SegmentedControlProps {
|
|
12
|
+
options: SegmentedControlOption[];
|
|
13
|
+
value: string;
|
|
14
|
+
onChange: (value: string) => void;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function SegmentedControl({ options, value, onChange, className }: SegmentedControlProps) {
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
className={cn(
|
|
22
|
+
"inline-flex rounded-lg bg-ph-muted p-1",
|
|
23
|
+
className
|
|
24
|
+
)}
|
|
25
|
+
>
|
|
26
|
+
{options.map((option) => (
|
|
27
|
+
<button
|
|
28
|
+
key={option.value}
|
|
29
|
+
type="button"
|
|
30
|
+
onClick={() => onChange(option.value)}
|
|
31
|
+
className={cn(
|
|
32
|
+
"relative flex items-center gap-1.5 rounded-md px-3.5 py-1.5 text-sm font-medium transition-all",
|
|
33
|
+
value === option.value
|
|
34
|
+
? "bg-ph-surface text-ph-ink shadow-ph"
|
|
35
|
+
: "text-ph-subtle hover:text-ph-ink"
|
|
36
|
+
)}
|
|
37
|
+
>
|
|
38
|
+
{option.icon && <span className="h-4 w-4">{option.icon}</span>}
|
|
39
|
+
{option.label}
|
|
40
|
+
</button>
|
|
41
|
+
))}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, type ReactNode } from "react";
|
|
4
|
+
import { cn } from "@/lib/cn";
|
|
5
|
+
import { CodeBlock } from "@/components/ui";
|
|
6
|
+
|
|
7
|
+
interface ShowcaseWrapperProps {
|
|
8
|
+
id?: string;
|
|
9
|
+
title: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
docs?: ReactNode;
|
|
12
|
+
code: string;
|
|
13
|
+
filename?: string;
|
|
14
|
+
children: ReactNode;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function ShowcaseWrapper({
|
|
19
|
+
id,
|
|
20
|
+
title,
|
|
21
|
+
description,
|
|
22
|
+
docs,
|
|
23
|
+
code,
|
|
24
|
+
filename = "Example.tsx",
|
|
25
|
+
children,
|
|
26
|
+
className,
|
|
27
|
+
}: ShowcaseWrapperProps) {
|
|
28
|
+
const [showCode, setShowCode] = useState(false);
|
|
29
|
+
const fallbackId = titleIdMap[title] ?? title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<section id={id ?? fallbackId} className={cn("scroll-mt-20", className)}>
|
|
33
|
+
<div className="ph-h2-rule mb-8">
|
|
34
|
+
<div className="flex items-center gap-1.5">
|
|
35
|
+
<h2>{title}</h2>
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
onClick={() => setShowCode(!showCode)}
|
|
39
|
+
className={cn(
|
|
40
|
+
"flex h-10 w-10 items-center justify-center rounded-full border transition-all",
|
|
41
|
+
showCode
|
|
42
|
+
? "border-ph-brand bg-ph-brand text-white shadow-lg shadow-ph-brand/25"
|
|
43
|
+
: "border-ph-border bg-ph-surface text-ph-subtle hover:border-ph-brand hover:text-ph-brand"
|
|
44
|
+
)}
|
|
45
|
+
title={showCode ? "Hide code" : "Show code"}
|
|
46
|
+
>
|
|
47
|
+
<svg
|
|
48
|
+
className="h-5 w-5"
|
|
49
|
+
viewBox="0 0 24 24"
|
|
50
|
+
fill="none"
|
|
51
|
+
stroke="currentColor"
|
|
52
|
+
strokeWidth="2"
|
|
53
|
+
strokeLinecap="round"
|
|
54
|
+
strokeLinejoin="round"
|
|
55
|
+
>
|
|
56
|
+
<rect x="2" y="3" width="20" height="14" rx="2" ry="2" />
|
|
57
|
+
<line x1="8" y1="21" x2="16" y2="21" />
|
|
58
|
+
<line x1="12" y1="17" x2="12" y2="21" />
|
|
59
|
+
<line x1="6" y1="8" x2="6" y2="8" />
|
|
60
|
+
<line x1="10" y1="8" x2="10" y2="8" />
|
|
61
|
+
</svg>
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
<hr />
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
{description && (
|
|
68
|
+
<p className="mb-6 text-sm text-ph-subtle">{description}</p>
|
|
69
|
+
)}
|
|
70
|
+
|
|
71
|
+
{docs && <div className="mb-6">{docs}</div>}
|
|
72
|
+
|
|
73
|
+
<div
|
|
74
|
+
className={cn(
|
|
75
|
+
"gap-6",
|
|
76
|
+
showCode ? "lg:grid lg:grid-cols-[1fr,minmax(360px,420px)]" : "block"
|
|
77
|
+
)}
|
|
78
|
+
>
|
|
79
|
+
<div className={cn("min-w-0", showCode && "lg:pr-2")}>{children}</div>
|
|
80
|
+
|
|
81
|
+
<div
|
|
82
|
+
className={cn(
|
|
83
|
+
"transition-all duration-300",
|
|
84
|
+
showCode
|
|
85
|
+
? "block opacity-100"
|
|
86
|
+
: "hidden opacity-0 lg:hidden"
|
|
87
|
+
)}
|
|
88
|
+
>
|
|
89
|
+
<div className="sticky top-20">
|
|
90
|
+
<CodeBlock code={code} filename={filename} />
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
</section>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const titleIdMap: Record<string, string> = {
|
|
99
|
+
"Icons": "icons",
|
|
100
|
+
"Colour tokens": "colors",
|
|
101
|
+
"Buttons": "buttons",
|
|
102
|
+
"Badges": "badges",
|
|
103
|
+
"Alerts": "alerts",
|
|
104
|
+
"Toasts": "toasts",
|
|
105
|
+
"Cards": "cards",
|
|
106
|
+
"Forms": "forms",
|
|
107
|
+
"Inputs & Forms": "forms",
|
|
108
|
+
"Tables": "tables",
|
|
109
|
+
"Accordion": "accordion",
|
|
110
|
+
"Calendar": "calendar",
|
|
111
|
+
"Hover Card": "hovercard",
|
|
112
|
+
"Modals": "modals",
|
|
113
|
+
"Drawers": "drawers",
|
|
114
|
+
"Navigation chrome": "navigation",
|
|
115
|
+
"Navigation utilities": "utilities",
|
|
116
|
+
"Stepper": "stepper",
|
|
117
|
+
"Command Palette": "command",
|
|
118
|
+
"Filter Chips": "filters",
|
|
119
|
+
"Empty State": "emptystate",
|
|
120
|
+
"Charts (PostHog-style)": "charts",
|
|
121
|
+
"Dashboard shells": "dashboard",
|
|
122
|
+
"Tabs": "tabs",
|
|
123
|
+
"Dropdowns": "dropdowns",
|
|
124
|
+
"Progress & loading": "progress",
|
|
125
|
+
"Tooltip": "tooltip",
|
|
126
|
+
"Avatars": "avatars",
|
|
127
|
+
"Stats & conversational UI": "stats",
|
|
128
|
+
"Timeline": "timeline",
|
|
129
|
+
"Countdown motif": "countdown",
|
|
130
|
+
"Keyboard glyphs": "kbd",
|
|
131
|
+
"Icon swap": "swap",
|
|
132
|
+
"Stacked surfaces": "stack",
|
|
133
|
+
"Diff slider": "diff",
|
|
134
|
+
"Badge hotspots": "indicator",
|
|
135
|
+
"Terminal capture": "code-mockup",
|
|
136
|
+
};
|
|
@@ -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,75 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@/lib/cn";
|
|
4
|
+
|
|
5
|
+
interface Step {
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
status?: "pending" | "current" | "completed" | "error";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface StepperProps {
|
|
13
|
+
steps: Step[];
|
|
14
|
+
currentStep: number;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function Stepper({ steps, currentStep, className }: StepperProps) {
|
|
19
|
+
return (
|
|
20
|
+
<div className={cn("flex items-start gap-2", className)}>
|
|
21
|
+
{steps.map((step, index) => {
|
|
22
|
+
const isCompleted = index < currentStep;
|
|
23
|
+
const isCurrent = index === currentStep;
|
|
24
|
+
const isPending = index > currentStep;
|
|
25
|
+
const isError = step.status === "error";
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div key={step.id} className="flex flex-1 items-start gap-2">
|
|
29
|
+
<div className="flex flex-1 flex-col items-center">
|
|
30
|
+
<div
|
|
31
|
+
className={cn(
|
|
32
|
+
"flex h-9 w-9 items-center justify-center rounded-full text-sm font-bold transition-colors",
|
|
33
|
+
isCompleted && "bg-ph-success text-white",
|
|
34
|
+
isCurrent && !isError && "bg-ph-brand text-white ring-4 ring-ph-brand/20",
|
|
35
|
+
isCurrent && isError && "bg-ph-danger text-white",
|
|
36
|
+
isPending && "bg-ph-muted text-ph-mutedtext"
|
|
37
|
+
)}
|
|
38
|
+
>
|
|
39
|
+
{isCompleted ? (
|
|
40
|
+
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3">
|
|
41
|
+
<path d="M5 13l4 4L19 7" />
|
|
42
|
+
</svg>
|
|
43
|
+
) : (
|
|
44
|
+
index + 1
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
<div className="mt-2 text-center">
|
|
48
|
+
<p
|
|
49
|
+
className={cn(
|
|
50
|
+
"text-sm font-semibold",
|
|
51
|
+
isCurrent && "text-ph-ink",
|
|
52
|
+
!isCurrent && "text-ph-mutedtext"
|
|
53
|
+
)}
|
|
54
|
+
>
|
|
55
|
+
{step.label}
|
|
56
|
+
</p>
|
|
57
|
+
{step.description && (
|
|
58
|
+
<p className="mt-0.5 text-xs text-ph-mutedtext">{step.description}</p>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
{index < steps.length - 1 && (
|
|
63
|
+
<div
|
|
64
|
+
className={cn(
|
|
65
|
+
"mt-4 h-0.5 flex-1 rounded-full transition-colors",
|
|
66
|
+
isCompleted ? "bg-ph-success" : "bg-ph-border"
|
|
67
|
+
)}
|
|
68
|
+
/>
|
|
69
|
+
)}
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
})}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -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
|
+
}
|