@xenide-io/the-old-ui-theme 0.2.5 → 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.
Files changed (112) hide show
  1. package/dist/chunk-FWCSY2DS.mjs +37 -0
  2. package/dist/chunk-SHF57J7U.mjs +2910 -0
  3. package/dist/index-CmS6hcUk.d.mts +753 -0
  4. package/dist/index-CmS6hcUk.d.ts +753 -0
  5. package/dist/index.d.mts +5 -755
  6. package/dist/index.d.ts +5 -755
  7. package/dist/index.js +655 -378
  8. package/dist/index.mjs +348 -2695
  9. package/dist/tailwind-preset.mjs +2 -0
  10. package/dist/ui.d.mts +3 -0
  11. package/dist/ui.d.ts +3 -0
  12. package/dist/ui.js +2946 -0
  13. package/dist/ui.mjs +139 -0
  14. package/package.json +13 -5
  15. package/src/components/charts/index.ts +8 -0
  16. package/src/components/charts/ph-insight-charts.tsx +162 -0
  17. package/src/components/dashboard/DashboardFiltersBar.tsx +19 -0
  18. package/src/components/dashboard/DashboardGrid.tsx +23 -0
  19. package/src/components/dashboard/DashboardInsightPlaceholder.tsx +37 -0
  20. package/src/components/dashboard/DashboardKpiCard.tsx +25 -0
  21. package/src/components/dashboard/DashboardToolbar.tsx +23 -0
  22. package/src/components/dashboard/DashboardWell.tsx +10 -0
  23. package/src/components/dashboard/InsightMiniCard.tsx +26 -0
  24. package/src/components/dashboard/InsightShell.tsx +37 -0
  25. package/src/components/dashboard/InsightShellHeader.tsx +22 -0
  26. package/src/components/dashboard/MiniTrendBars.tsx +51 -0
  27. package/src/components/dashboard/index.ts +31 -0
  28. package/src/components/dashboard/types.ts +9 -0
  29. package/src/components/icons/IconBase.tsx +39 -0
  30. package/src/components/icons/createIconBase.tsx +27 -0
  31. package/src/components/icons/icons.tsx +367 -0
  32. package/src/components/icons/index.ts +3 -0
  33. package/src/components/sections/AccordionShowcase.tsx +45 -0
  34. package/src/components/sections/AlertShowcase.tsx +39 -0
  35. package/src/components/sections/AvatarShowcase.tsx +80 -0
  36. package/src/components/sections/BadgeShowcase.tsx +65 -0
  37. package/src/components/sections/ButtonShowcase.tsx +312 -0
  38. package/src/components/sections/CalendarShowcase.tsx +35 -0
  39. package/src/components/sections/CardShowcase.tsx +143 -0
  40. package/src/components/sections/ChartShowcase.tsx +190 -0
  41. package/src/components/sections/CodeMockupShowcase.tsx +56 -0
  42. package/src/components/sections/ColorPalette.tsx +117 -0
  43. package/src/components/sections/CommandPaletteShowcase.tsx +48 -0
  44. package/src/components/sections/CountdownShowcase.tsx +43 -0
  45. package/src/components/sections/DashboardShowcase.tsx +191 -0
  46. package/src/components/sections/DiffShowcase.tsx +70 -0
  47. package/src/components/sections/DrawerShowcase.tsx +97 -0
  48. package/src/components/sections/DropdownShowcase.tsx +100 -0
  49. package/src/components/sections/EmptyStateShowcase.tsx +50 -0
  50. package/src/components/sections/FilterChipsShowcase.tsx +50 -0
  51. package/src/components/sections/FormShowcase.tsx +126 -0
  52. package/src/components/sections/HoverCardShowcase.tsx +50 -0
  53. package/src/components/sections/IconShowcase.tsx +247 -0
  54. package/src/components/sections/IndicatorShowcase.tsx +52 -0
  55. package/src/components/sections/KbdShowcase.tsx +31 -0
  56. package/src/components/sections/ModalShowcase.tsx +210 -0
  57. package/src/components/sections/NavigationShowcase.tsx +199 -0
  58. package/src/components/sections/ProgressShowcase.tsx +85 -0
  59. package/src/components/sections/SegmentedControlShowcase.tsx +49 -0
  60. package/src/components/sections/SetupThemeShowcase.tsx +262 -0
  61. package/src/components/sections/StackShowcase.tsx +33 -0
  62. package/src/components/sections/StatShowcase.tsx +129 -0
  63. package/src/components/sections/StepperShowcase.tsx +37 -0
  64. package/src/components/sections/SwapShowcase.tsx +91 -0
  65. package/src/components/sections/TabShowcase.tsx +84 -0
  66. package/src/components/sections/TableShowcase.tsx +140 -0
  67. package/src/components/sections/TimelineShowcase.tsx +83 -0
  68. package/src/components/sections/ToastShowcase.tsx +108 -0
  69. package/src/components/sections/TooltipShowcase.tsx +44 -0
  70. package/src/components/sections/UtilityShowcase.tsx +81 -0
  71. package/src/components/ui/Accordion.tsx +77 -0
  72. package/src/components/ui/Alert.tsx +68 -0
  73. package/src/components/ui/Avatar.tsx +97 -0
  74. package/src/components/ui/Badge.tsx +55 -0
  75. package/src/components/ui/Button.tsx +110 -0
  76. package/src/components/ui/ButtonChrome.tsx +20 -0
  77. package/src/components/ui/Calendar.tsx +116 -0
  78. package/src/components/ui/Card.tsx +60 -0
  79. package/src/components/ui/ChatBubble.tsx +71 -0
  80. package/src/components/ui/CodeBlock.tsx +48 -0
  81. package/src/components/ui/CollapsibleSection.tsx +49 -0
  82. package/src/components/ui/CommandPalette.tsx +137 -0
  83. package/src/components/ui/ComponentDocs.tsx +52 -0
  84. package/src/components/ui/Drawer.tsx +71 -0
  85. package/src/components/ui/DropdownMenu.tsx +138 -0
  86. package/src/components/ui/EmptyState.tsx +33 -0
  87. package/src/components/ui/FilterChips.tsx +50 -0
  88. package/src/components/ui/HoverCard.tsx +36 -0
  89. package/src/components/ui/Indicator.tsx +61 -0
  90. package/src/components/ui/Input.tsx +359 -0
  91. package/src/components/ui/Kbd.tsx +38 -0
  92. package/src/components/ui/Modal.tsx +75 -0
  93. package/src/components/ui/Navigation.tsx +94 -0
  94. package/src/components/ui/Panel.tsx +18 -0
  95. package/src/components/ui/Progress.tsx +59 -0
  96. package/src/components/ui/Rating.tsx +65 -0
  97. package/src/components/ui/SearchInput.tsx +106 -0
  98. package/src/components/ui/SegmentedControl.tsx +44 -0
  99. package/src/components/ui/ShowcaseWrapper.tsx +136 -0
  100. package/src/components/ui/Stat.tsx +39 -0
  101. package/src/components/ui/Stepper.tsx +75 -0
  102. package/src/components/ui/Table.tsx +55 -0
  103. package/src/components/ui/Tabs.tsx +53 -0
  104. package/src/components/ui/Terminal.tsx +54 -0
  105. package/src/components/ui/ThemeDomSync.tsx +17 -0
  106. package/src/components/ui/ThemeManager.tsx +18 -0
  107. package/src/components/ui/ThemeSwitcher.tsx +46 -0
  108. package/src/components/ui/Timeline.tsx +60 -0
  109. package/src/components/ui/Toast.tsx +70 -0
  110. package/src/components/ui/Typography.tsx +129 -0
  111. package/src/components/ui/index.ts +92 -0
  112. 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
+ }