@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.
Files changed (126) hide show
  1. package/README.md +1 -1
  2. package/dist/chunk-FDMD3IIN.mjs +3831 -0
  3. package/dist/index-D1RTT2Ap.d.mts +1044 -0
  4. package/dist/index-D1RTT2Ap.d.ts +1044 -0
  5. package/dist/index.d.mts +11 -758
  6. package/dist/index.d.ts +11 -758
  7. package/dist/index.js +1749 -768
  8. package/dist/index.mjs +312 -2846
  9. package/dist/tailwind-preset.js +7 -0
  10. package/dist/tailwind-preset.mjs +7 -0
  11. package/dist/ui.d.mts +4 -0
  12. package/dist/ui.d.ts +4 -0
  13. package/dist/ui.js +3864 -0
  14. package/dist/ui.mjs +169 -0
  15. package/package.json +28 -6
  16. package/src/app/globals.css +1005 -227
  17. package/src/components/charts/index.ts +8 -0
  18. package/src/components/charts/ph-insight-charts.tsx +162 -0
  19. package/src/components/dashboard/DashboardFiltersBar.tsx +19 -0
  20. package/src/components/dashboard/DashboardGrid.tsx +23 -0
  21. package/src/components/dashboard/DashboardInsightPlaceholder.tsx +37 -0
  22. package/src/components/dashboard/DashboardKpiCard.tsx +25 -0
  23. package/src/components/dashboard/DashboardToolbar.tsx +23 -0
  24. package/src/components/dashboard/DashboardWell.tsx +10 -0
  25. package/src/components/dashboard/InsightMiniCard.tsx +26 -0
  26. package/src/components/dashboard/InsightShell.tsx +37 -0
  27. package/src/components/dashboard/InsightShellHeader.tsx +22 -0
  28. package/src/components/dashboard/MiniTrendBars.tsx +51 -0
  29. package/src/components/dashboard/index.ts +31 -0
  30. package/src/components/dashboard/types.ts +9 -0
  31. package/src/components/icons/IconBase.tsx +47 -0
  32. package/src/components/icons/createIconBase.tsx +32 -0
  33. package/src/components/icons/icons.tsx +557 -0
  34. package/src/components/icons/index.ts +3 -0
  35. package/src/components/sections/AccordionShowcase.tsx +45 -0
  36. package/src/components/sections/AlertShowcase.tsx +40 -0
  37. package/src/components/sections/AvatarShowcase.tsx +80 -0
  38. package/src/components/sections/BadgeShowcase.tsx +65 -0
  39. package/src/components/sections/ButtonShowcase.tsx +308 -0
  40. package/src/components/sections/CalendarShowcase.tsx +35 -0
  41. package/src/components/sections/CardShowcase.tsx +159 -0
  42. package/src/components/sections/ChartShowcase.tsx +190 -0
  43. package/src/components/sections/CodeMockupShowcase.tsx +56 -0
  44. package/src/components/sections/ColorPalette.tsx +117 -0
  45. package/src/components/sections/CommandPaletteShowcase.tsx +48 -0
  46. package/src/components/sections/CountdownShowcase.tsx +43 -0
  47. package/src/components/sections/DashboardShowcase.tsx +191 -0
  48. package/src/components/sections/DiffShowcase.tsx +70 -0
  49. package/src/components/sections/DrawerShowcase.tsx +97 -0
  50. package/src/components/sections/DropdownShowcase.tsx +98 -0
  51. package/src/components/sections/EmptyStateShowcase.tsx +50 -0
  52. package/src/components/sections/FilterChipsShowcase.tsx +98 -0
  53. package/src/components/sections/FormShowcase.tsx +127 -0
  54. package/src/components/sections/HoverCardShowcase.tsx +50 -0
  55. package/src/components/sections/IconShowcase.tsx +127 -0
  56. package/src/components/sections/IndicatorShowcase.tsx +52 -0
  57. package/src/components/sections/KbdShowcase.tsx +57 -0
  58. package/src/components/sections/ModalShowcase.tsx +211 -0
  59. package/src/components/sections/NavigationShowcase.tsx +199 -0
  60. package/src/components/sections/ProductLayoutsShowcase.tsx +138 -0
  61. package/src/components/sections/ProgressShowcase.tsx +82 -0
  62. package/src/components/sections/SegmentedControlShowcase.tsx +49 -0
  63. package/src/components/sections/SetupThemeShowcase.tsx +262 -0
  64. package/src/components/sections/StackShowcase.tsx +33 -0
  65. package/src/components/sections/StatShowcase.tsx +129 -0
  66. package/src/components/sections/StepperShowcase.tsx +37 -0
  67. package/src/components/sections/SwapShowcase.tsx +99 -0
  68. package/src/components/sections/TabShowcase.tsx +84 -0
  69. package/src/components/sections/TableShowcase.tsx +140 -0
  70. package/src/components/sections/TimelineShowcase.tsx +83 -0
  71. package/src/components/sections/ToastShowcase.tsx +108 -0
  72. package/src/components/sections/TooltipShowcase.tsx +44 -0
  73. package/src/components/sections/UtilityShowcase.tsx +81 -0
  74. package/src/components/ui/Accordion.tsx +77 -0
  75. package/src/components/ui/Alert.tsx +77 -0
  76. package/src/components/ui/AuthLayout.tsx +58 -0
  77. package/src/components/ui/Avatar.tsx +97 -0
  78. package/src/components/ui/Badge.tsx +60 -0
  79. package/src/components/ui/Button.tsx +120 -0
  80. package/src/components/ui/ButtonChrome.tsx +20 -0
  81. package/src/components/ui/Calendar.tsx +115 -0
  82. package/src/components/ui/Card.tsx +70 -0
  83. package/src/components/ui/ChatBubble.tsx +71 -0
  84. package/src/components/ui/CodeBlock.tsx +48 -0
  85. package/src/components/ui/CollapsibleSection.tsx +49 -0
  86. package/src/components/ui/CommandPalette.tsx +137 -0
  87. package/src/components/ui/ComponentDocs.tsx +52 -0
  88. package/src/components/ui/Drawer.tsx +71 -0
  89. package/src/components/ui/DropdownMenu.tsx +262 -0
  90. package/src/components/ui/EmptyState.tsx +33 -0
  91. package/src/components/ui/FilterBar.tsx +165 -0
  92. package/src/components/ui/FilterChips.tsx +80 -0
  93. package/src/components/ui/FilterControls.tsx +30 -0
  94. package/src/components/ui/FormField.tsx +69 -0
  95. package/src/components/ui/HoverCard.tsx +36 -0
  96. package/src/components/ui/Indicator.tsx +61 -0
  97. package/src/components/ui/Input.tsx +385 -0
  98. package/src/components/ui/Kbd.tsx +113 -0
  99. package/src/components/ui/Loader.tsx +96 -0
  100. package/src/components/ui/Modal.tsx +105 -0
  101. package/src/components/ui/Navigation.tsx +94 -0
  102. package/src/components/ui/Panel.tsx +18 -0
  103. package/src/components/ui/Progress.tsx +59 -0
  104. package/src/components/ui/Rating.tsx +65 -0
  105. package/src/components/ui/SearchInput.tsx +106 -0
  106. package/src/components/ui/SegmentedControl.tsx +44 -0
  107. package/src/components/ui/SettingsLayout.tsx +94 -0
  108. package/src/components/ui/ShowcaseWrapper.tsx +124 -0
  109. package/src/components/ui/Stat.tsx +39 -0
  110. package/src/components/ui/Stepper.tsx +74 -0
  111. package/src/components/ui/Table.tsx +55 -0
  112. package/src/components/ui/Tabs.tsx +53 -0
  113. package/src/components/ui/Terminal.tsx +54 -0
  114. package/src/components/ui/ThemeDomSync.tsx +17 -0
  115. package/src/components/ui/ThemeManager.tsx +18 -0
  116. package/src/components/ui/ThemeSwitcher.tsx +46 -0
  117. package/src/components/ui/Timeline.tsx +60 -0
  118. package/src/components/ui/Toast.tsx +70 -0
  119. package/src/components/ui/Typography.tsx +129 -0
  120. package/src/components/ui/first-slice.test.tsx +98 -0
  121. package/src/components/ui/index.ts +151 -0
  122. package/src/components/ui/interaction-primitives.test.tsx +77 -0
  123. package/src/components/ui/kbd-icons.test.tsx +90 -0
  124. package/src/components/ui/product-patterns.test.tsx +77 -0
  125. package/src/styles/themes.css +528 -0
  126. package/tailwind-preset.ts +7 -0
@@ -0,0 +1,120 @@
1
+ import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react";
2
+ import { ButtonChrome } from "@/components/ui/ButtonChrome";
3
+ import { cn } from "@/lib/cn";
4
+
5
+ export type ButtonVariant =
6
+ | "primary"
7
+ | "secondary"
8
+ | "tertiary"
9
+ | "signal"
10
+ | "accent"
11
+ | "neutral"
12
+ | "info"
13
+ | "success"
14
+ | "warning"
15
+ | "danger"
16
+ | "ghost"
17
+ | "link";
18
+
19
+ export type ButtonSize = "xs" | "sm" | "md" | "lg";
20
+ export type ButtonShape = "default" | "circle" | "square" | "wide";
21
+
22
+ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
23
+ variant?: ButtonVariant;
24
+ size?: ButtonSize;
25
+ shape?: ButtonShape;
26
+ outline?: boolean;
27
+ /** Toolbar split control (Run ▾) — adds divider before trailing icon slot */
28
+ split?: boolean;
29
+ icon?: ReactNode;
30
+ sideIcon?: ReactNode;
31
+ children?: ReactNode;
32
+ loading?: boolean;
33
+ loadingLabel?: ReactNode;
34
+ }
35
+
36
+ const variantClass: Record<ButtonVariant, string> = {
37
+ primary: "ph-btn-primary",
38
+ secondary: "ph-btn-secondary",
39
+ tertiary: "ph-btn-tertiary",
40
+ signal: "ph-btn-signal",
41
+ accent: "ph-btn-accent",
42
+ neutral: "ph-btn-neutral",
43
+ info: "ph-btn-info",
44
+ success: "ph-btn-success",
45
+ warning: "ph-btn-warning",
46
+ danger: "ph-btn-danger",
47
+ ghost: "ph-btn-ghost",
48
+ link: "ph-btn-link",
49
+ };
50
+
51
+ const sizeClass: Record<ButtonSize, string> = {
52
+ xs: "ph-btn-xs",
53
+ sm: "ph-btn-sm",
54
+ md: "",
55
+ lg: "ph-btn-lg",
56
+ };
57
+
58
+ const shapeClass: Record<ButtonShape, string> = {
59
+ default: "",
60
+ circle: "ph-btn-circle",
61
+ square: "ph-btn-square",
62
+ wide: "ph-btn-wide",
63
+ };
64
+
65
+ /** Filled variants keep a stationary depth plate while the face and content move together. */
66
+ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button({
67
+ variant = "secondary",
68
+ size = "md",
69
+ shape = "default",
70
+ outline = false,
71
+ split = false,
72
+ icon,
73
+ sideIcon,
74
+ loading = false,
75
+ loadingLabel,
76
+ className,
77
+ children,
78
+ type = "button",
79
+ disabled,
80
+ ...props
81
+ }, ref) {
82
+ const usesLemonChrome = !["tertiary", "ghost", "link"].includes(variant);
83
+ const label = loadingLabel ?? children;
84
+ const leadingIcon = loading ? <span className="ph-btn-spinner" aria-hidden /> : icon;
85
+
86
+ const content = usesLemonChrome ? (
87
+ <ButtonChrome label={label} icon={leadingIcon} sideIcon={sideIcon} split={split} />
88
+ ) : (
89
+ <>
90
+ {leadingIcon != null ? <span className="ph-btn-icon">{leadingIcon}</span> : null}
91
+ {label != null ? <span className="ph-btn-label">{label}</span> : null}
92
+ {split ? <span className="ph-btn-split-divider" aria-hidden /> : null}
93
+ {sideIcon != null ? <span className="ph-btn-side-icon">{sideIcon}</span> : null}
94
+ </>
95
+ );
96
+
97
+ return (
98
+ <button
99
+ ref={ref}
100
+ type={type}
101
+ disabled={disabled || loading}
102
+ aria-busy={loading || undefined}
103
+ className={cn(
104
+ "ph-btn",
105
+ usesLemonChrome && "ph-btn-raised",
106
+ outline && "ph-btn-outline",
107
+ variantClass[variant],
108
+ sizeClass[size],
109
+ shapeClass[shape],
110
+ split && "ph-btn-split",
111
+ className,
112
+ )}
113
+ {...props}
114
+ >
115
+ {content}
116
+ </button>
117
+ );
118
+ });
119
+
120
+ Button.displayName = "Button";
@@ -0,0 +1,20 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ export interface ButtonChromeProps {
4
+ label?: ReactNode;
5
+ icon?: ReactNode;
6
+ sideIcon?: ReactNode;
7
+ split?: boolean;
8
+ }
9
+
10
+ /** Content layer for tactile variants; CSS moves it with the independently rendered face. */
11
+ export function ButtonChrome({ label, icon, sideIcon, split }: ButtonChromeProps) {
12
+ return (
13
+ <span className="ph-btn-chrome">
14
+ {icon != null ? <span className="ph-btn-icon">{icon}</span> : null}
15
+ {label != null ? <span className="ph-btn-label">{label}</span> : null}
16
+ {split ? <span className="ph-btn-split-divider" aria-hidden /> : null}
17
+ {sideIcon != null ? <span className="ph-btn-side-icon">{sideIcon}</span> : null}
18
+ </span>
19
+ );
20
+ }
@@ -0,0 +1,115 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { IconChevronLeft, IconChevronRight } from "@/components/icons";
5
+ import { cn } from "@/lib/cn";
6
+
7
+ interface CalendarProps {
8
+ value?: Date;
9
+ onChange?: (date: Date) => void;
10
+ className?: string;
11
+ }
12
+
13
+ export function Calendar({ value, onChange, className }: CalendarProps) {
14
+ const [currentMonth, setCurrentMonth] = useState(value || new Date());
15
+ const today = new Date();
16
+
17
+ const year = currentMonth.getFullYear();
18
+ const month = currentMonth.getMonth();
19
+
20
+ const firstDay = new Date(year, month, 1).getDay();
21
+ const daysInMonth = new Date(year, month + 1, 0).getDate();
22
+
23
+ const prevMonth = () => setCurrentMonth(new Date(year, month - 1, 1));
24
+ const nextMonth = () => setCurrentMonth(new Date(year, month + 1, 1));
25
+
26
+ const monthNames = [
27
+ "January", "February", "March", "April", "May", "June",
28
+ "July", "August", "September", "October", "November", "December",
29
+ ];
30
+
31
+ const weekDays = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
32
+
33
+ const isSelected = (day: number) => {
34
+ if (!value) return false;
35
+ return (
36
+ value.getDate() === day &&
37
+ value.getMonth() === month &&
38
+ value.getFullYear() === year
39
+ );
40
+ };
41
+
42
+ const isToday = (day: number) => {
43
+ return (
44
+ today.getDate() === day &&
45
+ today.getMonth() === month &&
46
+ today.getFullYear() === year
47
+ );
48
+ };
49
+
50
+ const handleSelect = (day: number) => {
51
+ onChange?.(new Date(year, month, day));
52
+ };
53
+
54
+ const days = [];
55
+ for (let i = 0; i < firstDay; i++) {
56
+ days.push(null);
57
+ }
58
+ for (let i = 1; i <= daysInMonth; i++) {
59
+ days.push(i);
60
+ }
61
+
62
+ return (
63
+ <div className={cn("w-[280px] rounded-xl border border-ph-border bg-ph-surface p-4 shadow-ph", className)}>
64
+ <div className="mb-4 flex items-center justify-between">
65
+ <button
66
+ type="button"
67
+ onClick={prevMonth}
68
+ aria-label="Previous month"
69
+ className="rounded-lg p-1 text-ph-subtle transition hover:bg-ph-muted hover:text-ph-ink"
70
+ >
71
+ <IconChevronLeft className="h-5 w-5" aria-hidden />
72
+ </button>
73
+ <span className="text-sm font-semibold text-ph-ink">
74
+ {monthNames[month]} {year}
75
+ </span>
76
+ <button
77
+ type="button"
78
+ onClick={nextMonth}
79
+ aria-label="Next month"
80
+ className="rounded-lg p-1 text-ph-subtle transition hover:bg-ph-muted hover:text-ph-ink"
81
+ >
82
+ <IconChevronRight className="h-5 w-5" aria-hidden />
83
+ </button>
84
+ </div>
85
+
86
+ <div className="grid grid-cols-7 gap-1">
87
+ {weekDays.map((day) => (
88
+ <div key={day} className="py-1 text-center text-xs font-medium text-ph-mutedtext">
89
+ {day}
90
+ </div>
91
+ ))}
92
+ {days.map((day, index) => (
93
+ <div key={index} className="aspect-square">
94
+ {day && (
95
+ <button
96
+ type="button"
97
+ onClick={() => handleSelect(day)}
98
+ className={cn(
99
+ "flex h-full w-full items-center justify-center rounded-lg text-sm transition-colors",
100
+ isSelected(day)
101
+ ? "bg-ph-brand font-semibold text-[var(--ph-on-accent)] shadow-ph"
102
+ : isToday(day)
103
+ ? "bg-ph-muted font-semibold text-ph-brand ring-1 ring-ph-brand/30"
104
+ : "text-ph-ink hover:bg-ph-muted"
105
+ )}
106
+ >
107
+ {day}
108
+ </button>
109
+ )}
110
+ </div>
111
+ ))}
112
+ </div>
113
+ </div>
114
+ );
115
+ }
@@ -0,0 +1,70 @@
1
+ import { forwardRef, type HTMLAttributes, type ReactNode } from "react";
2
+ import { cn } from "@/lib/cn";
3
+
4
+ export type CardVariant = "default" | "outlined" | "elevated" | "highlighted";
5
+
6
+ export interface CardProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
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
+ interactive?: boolean;
17
+ titleAs?: "h2" | "h3" | "h4";
18
+ }
19
+
20
+ const variantMap: Record<CardVariant, string> = {
21
+ default: "",
22
+ outlined: "border-2 border-ph-border",
23
+ elevated: "ph-card-elevated",
24
+ highlighted: "ph-card-highlighted",
25
+ };
26
+
27
+ export const Card = forwardRef<HTMLDivElement, CardProps>(function Card({
28
+ children,
29
+ title,
30
+ description,
31
+ actions,
32
+ footer,
33
+ media,
34
+ variant = "default",
35
+ className,
36
+ bodyClassName,
37
+ interactive = false,
38
+ titleAs: Title = "h3",
39
+ ...props
40
+ }, ref) {
41
+ return (
42
+ <div
43
+ ref={ref}
44
+ className={cn(
45
+ "ph-card",
46
+ variantMap[variant],
47
+ interactive && "ph-card-interactive",
48
+ className
49
+ )}
50
+ {...props}
51
+ >
52
+ {media && <div className="ph-card-media">{media}</div>}
53
+ <div className={cn("ph-card-body", bodyClassName)}>
54
+ {title && (
55
+ <Title className="ph-card-title">{title}</Title>
56
+ )}
57
+ {description && (
58
+ <p className="text-sm text-ph-subtle">{description}</p>
59
+ )}
60
+ {children}
61
+ {actions && <div className="ph-card-actions">{actions}</div>}
62
+ </div>
63
+ {footer && (
64
+ <div className="border-t border-ph-border px-6 py-4 text-xs text-ph-subtle">{footer}</div>
65
+ )}
66
+ </div>
67
+ );
68
+ });
69
+
70
+ Card.displayName = "Card";
@@ -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 { useId, useState } from "react";
4
+ import { IconChevronDown } from "@/components/icons";
5
+ import { cn } from "@/lib/cn";
6
+
7
+ interface CollapsibleSectionProps {
8
+ title: string;
9
+ children: React.ReactNode;
10
+ defaultOpen?: boolean;
11
+ id?: string;
12
+ }
13
+
14
+ export function CollapsibleSection({ title, children, defaultOpen = true, id }: CollapsibleSectionProps) {
15
+ const [isOpen, setIsOpen] = useState(defaultOpen);
16
+ const generatedId = useId();
17
+ const contentId = `${id ?? generatedId}-content`;
18
+
19
+ return (
20
+ <section id={id} className="scroll-mt-20">
21
+ <button
22
+ type="button"
23
+ onClick={() => setIsOpen(!isOpen)}
24
+ aria-expanded={isOpen}
25
+ aria-controls={contentId}
26
+ 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"
27
+ >
28
+ <span className="text-lg font-bold text-ph-ink">{title}</span>
29
+ <IconChevronDown
30
+ className={cn(
31
+ "h-5 w-5 text-ph-subtle transition-transform duration-200",
32
+ isOpen && "rotate-180"
33
+ )}
34
+ aria-hidden
35
+ />
36
+ </button>
37
+ <div
38
+ id={contentId}
39
+ aria-hidden={!isOpen}
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
+ }