@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,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
|
+
}
|