@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,44 @@
|
|
|
1
|
+
import { Button, ShowcaseWrapper } from "@/components/ui";
|
|
2
|
+
|
|
3
|
+
export default function TooltipShowcase() {
|
|
4
|
+
const code = `import { Button } from "the-old-ui";
|
|
5
|
+
|
|
6
|
+
<div className="ph-panel space-y-8">
|
|
7
|
+
<div className="flex flex-wrap items-center gap-10">
|
|
8
|
+
<div className="group relative inline-block">
|
|
9
|
+
<Button variant="secondary">Hover me</Button>
|
|
10
|
+
<span className="pointer-events-none absolute bottom-full left-1/2 z-10 mb-2 -translate-x-1/2 whitespace-nowrap rounded-md bg-neutral-900 px-2 py-1 text-[11px] font-medium text-white opacity-0 shadow-lg transition-opacity group-hover:opacity-100">
|
|
11
|
+
HogQL <span className="ph-shortcut border-neutral-700 bg-neutral-800 text-neutral-100">⌘ /</span>
|
|
12
|
+
</span>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<span className="ph-tip cursor-help" title="Native title tooltip for zero-JS parity">
|
|
16
|
+
Dashed chrome (title attribute)
|
|
17
|
+
</span>
|
|
18
|
+
</div>
|
|
19
|
+
</div>`;
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<ShowcaseWrapper
|
|
23
|
+
title="Tooltips"
|
|
24
|
+
description="PostHog tooltips lean on muted inverse surfaces. Here we fake the vibe with charcoal chips + blur for contrast on light canvas."
|
|
25
|
+
code={code}
|
|
26
|
+
filename="TooltipExample.tsx"
|
|
27
|
+
>
|
|
28
|
+
<div className="ph-panel space-y-8">
|
|
29
|
+
<div className="flex flex-wrap items-center gap-10">
|
|
30
|
+
<div className="group relative inline-block">
|
|
31
|
+
<Button variant="secondary">Hover me</Button>
|
|
32
|
+
<span className="pointer-events-none absolute bottom-full left-1/2 z-10 mb-2 -translate-x-1/2 whitespace-nowrap rounded-md bg-neutral-900 px-2 py-1 text-[11px] font-medium text-white opacity-0 shadow-lg transition-opacity group-hover:opacity-100">
|
|
33
|
+
HogQL <span className="ph-shortcut border-neutral-700 bg-neutral-800 text-neutral-100">⌘ /</span>
|
|
34
|
+
</span>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<span className="ph-tip cursor-help" title="Native title tooltip for zero-JS parity">
|
|
38
|
+
Dashed chrome (title attribute)
|
|
39
|
+
</span>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</ShowcaseWrapper>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Breadcrumbs, ComponentDocs, Pagination, ShowcaseWrapper } from "@/components/ui";
|
|
4
|
+
|
|
5
|
+
const breadcrumbTrail = [
|
|
6
|
+
{ label: "Project", href: "#" },
|
|
7
|
+
{ label: "Product analytics", href: "#" },
|
|
8
|
+
{ label: "Trends", current: true },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const longBreadcrumbTrail = [
|
|
12
|
+
{ label: "Organisation", href: "#" },
|
|
13
|
+
{ label: "Data pipeline", href: "#" },
|
|
14
|
+
{ label: "Warehouse", href: "#" },
|
|
15
|
+
{ label: "Sources", href: "#" },
|
|
16
|
+
{ label: "Stripe sync", current: true },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export default function UtilityShowcase() {
|
|
20
|
+
const code = `import { Breadcrumbs, Pagination } from "the-old-ui";
|
|
21
|
+
|
|
22
|
+
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
|
|
23
|
+
<div className="ph-panel space-y-6">
|
|
24
|
+
<h3 className="text-sm font-semibold uppercase tracking-wider text-ph-mutedtext">Breadcrumbs</h3>
|
|
25
|
+
|
|
26
|
+
<Breadcrumbs items={breadcrumbTrail} />
|
|
27
|
+
<Breadcrumbs items={longBreadcrumbTrail} label="Long breadcrumb" />
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div className="ph-panel space-y-6">
|
|
31
|
+
<h3 className="text-sm font-semibold uppercase tracking-wider text-ph-mutedtext">Pagination</h3>
|
|
32
|
+
|
|
33
|
+
<Pagination page={1} totalPages={12} label="Table pagination" />
|
|
34
|
+
<Pagination page={5} totalPages={8} compact label="Compact pagination" />
|
|
35
|
+
|
|
36
|
+
<p className="text-xs text-ph-mutedtext">
|
|
37
|
+
Pair with table footers. Active page state and disabled arrows are handled by props.
|
|
38
|
+
</p>
|
|
39
|
+
</div>
|
|
40
|
+
</div>`;
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<ShowcaseWrapper
|
|
44
|
+
title="Navigation utilities"
|
|
45
|
+
description="Breadcrumb and pagination primitives with accessibility and default styling built in."
|
|
46
|
+
code={code}
|
|
47
|
+
filename="UtilityExample.tsx"
|
|
48
|
+
docs={
|
|
49
|
+
<ComponentDocs
|
|
50
|
+
rows={[
|
|
51
|
+
{ name: "Breadcrumbs.items", type: "{ label; href?; current? }[]", description: "Trail entries rendered with separators and current page state." },
|
|
52
|
+
{ name: "Pagination.page", type: "number", description: "Current page." },
|
|
53
|
+
{ name: "Pagination.totalPages", type: "number", description: "Total page count." },
|
|
54
|
+
{ name: "Pagination.compact", type: "boolean", defaultValue: "false", description: "Uses a narrower visible page window." },
|
|
55
|
+
{ name: "Pagination.onPageChange", type: "(page: number) => void", description: "Called by page and arrow buttons." },
|
|
56
|
+
]}
|
|
57
|
+
/>
|
|
58
|
+
}
|
|
59
|
+
>
|
|
60
|
+
<div className="grid grid-cols-1 gap-8 lg:grid-cols-2">
|
|
61
|
+
<div className="ph-panel space-y-6">
|
|
62
|
+
<h3 className="text-sm font-semibold uppercase tracking-wider text-ph-mutedtext">Breadcrumbs</h3>
|
|
63
|
+
|
|
64
|
+
<Breadcrumbs items={breadcrumbTrail} />
|
|
65
|
+
<Breadcrumbs items={longBreadcrumbTrail} label="Long breadcrumb" />
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<div className="ph-panel space-y-6">
|
|
69
|
+
<h3 className="text-sm font-semibold uppercase tracking-wider text-ph-mutedtext">Pagination</h3>
|
|
70
|
+
|
|
71
|
+
<Pagination page={1} totalPages={12} label="Table pagination" />
|
|
72
|
+
<Pagination page={5} totalPages={8} compact label="Compact pagination" />
|
|
73
|
+
|
|
74
|
+
<p className="text-xs text-ph-mutedtext">
|
|
75
|
+
Pair with table footers. Active page state and disabled arrows are handled by props.
|
|
76
|
+
</p>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
</ShowcaseWrapper>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { cn } from "@/lib/cn";
|
|
5
|
+
import { IconChevronDown } from "@/components/icons";
|
|
6
|
+
|
|
7
|
+
interface AccordionItem {
|
|
8
|
+
id: string;
|
|
9
|
+
title: string;
|
|
10
|
+
content: React.ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface AccordionProps {
|
|
14
|
+
items: AccordionItem[];
|
|
15
|
+
allowMultiple?: boolean;
|
|
16
|
+
defaultOpen?: string[];
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function Accordion({
|
|
21
|
+
items,
|
|
22
|
+
allowMultiple = false,
|
|
23
|
+
defaultOpen = [],
|
|
24
|
+
className,
|
|
25
|
+
}: AccordionProps) {
|
|
26
|
+
const [openItems, setOpenItems] = useState<Set<string>>(new Set(defaultOpen));
|
|
27
|
+
|
|
28
|
+
const toggleItem = (id: string) => {
|
|
29
|
+
setOpenItems((prev) => {
|
|
30
|
+
const next = new Set(prev);
|
|
31
|
+
if (next.has(id)) {
|
|
32
|
+
next.delete(id);
|
|
33
|
+
} else {
|
|
34
|
+
if (!allowMultiple) {
|
|
35
|
+
next.clear();
|
|
36
|
+
}
|
|
37
|
+
next.add(id);
|
|
38
|
+
}
|
|
39
|
+
return next;
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className={cn("divide-y divide-ph-border rounded-xl border border-ph-border bg-ph-surface", className)}>
|
|
45
|
+
{items.map((item) => {
|
|
46
|
+
const isOpen = openItems.has(item.id);
|
|
47
|
+
return (
|
|
48
|
+
<div key={item.id} className="first:rounded-t-xl last:rounded-b-xl">
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
onClick={() => toggleItem(item.id)}
|
|
52
|
+
className="flex w-full items-center justify-between px-5 py-4 text-left transition hover:bg-ph-muted"
|
|
53
|
+
>
|
|
54
|
+
<span className="font-semibold text-ph-ink">{item.title}</span>
|
|
55
|
+
<IconChevronDown
|
|
56
|
+
className={cn(
|
|
57
|
+
"h-5 w-5 text-ph-mutedtext transition-transform duration-200",
|
|
58
|
+
isOpen && "rotate-180"
|
|
59
|
+
)}
|
|
60
|
+
/>
|
|
61
|
+
</button>
|
|
62
|
+
<div
|
|
63
|
+
className={cn(
|
|
64
|
+
"grid transition-all duration-200",
|
|
65
|
+
isOpen ? "grid-rows-[1fr]" : "grid-rows-[0fr]"
|
|
66
|
+
)}
|
|
67
|
+
>
|
|
68
|
+
<div className="overflow-hidden">
|
|
69
|
+
<div className="px-5 pb-4 text-ph-subtle">{item.content}</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
})}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { IconCancel, IconCheckCircle, IconErrorOutline, IconExclamation } from "@/components/icons";
|
|
3
|
+
import { cn } from "@/lib/cn";
|
|
4
|
+
|
|
5
|
+
export type AlertStatus = "info" | "success" | "warning" | "danger";
|
|
6
|
+
|
|
7
|
+
export interface AlertProps {
|
|
8
|
+
status?: AlertStatus;
|
|
9
|
+
title?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
icon?: ReactNode;
|
|
12
|
+
children?: ReactNode;
|
|
13
|
+
className?: string;
|
|
14
|
+
onDismiss?: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const statusConfig: Record<AlertStatus, { icon: typeof IconErrorOutline; color: string } > = {
|
|
18
|
+
info: { icon: IconErrorOutline, color: "text-ph-info" },
|
|
19
|
+
success: { icon: IconCheckCircle, color: "text-ph-success" },
|
|
20
|
+
warning: { icon: IconExclamation, color: "text-ph-warning" },
|
|
21
|
+
danger: { icon: IconCancel, color: "text-ph-danger" },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const statusClassMap: Record<AlertStatus, string > = {
|
|
25
|
+
info: "ph-alert-info",
|
|
26
|
+
success: "ph-alert-success",
|
|
27
|
+
warning: "ph-alert-warning",
|
|
28
|
+
danger: "ph-alert-danger",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function Alert({
|
|
32
|
+
status = "info",
|
|
33
|
+
title,
|
|
34
|
+
description,
|
|
35
|
+
icon,
|
|
36
|
+
children,
|
|
37
|
+
className,
|
|
38
|
+
onDismiss,
|
|
39
|
+
}: AlertProps) {
|
|
40
|
+
const config = statusConfig[status];
|
|
41
|
+
const Icon = config.icon;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div
|
|
45
|
+
role="alert"
|
|
46
|
+
className={cn("ph-alert", statusClassMap[status], className)}
|
|
47
|
+
>
|
|
48
|
+
{icon ?? <Icon className={cn("mt-0.5 h-5 w-5 shrink-0", config.color)} />}
|
|
49
|
+
<div className="flex-1">
|
|
50
|
+
{title && <p className="font-semibold">{title}</p>}
|
|
51
|
+
{description && <p className="mt-1 text-sm text-ph-subtle">{description}</p>}
|
|
52
|
+
{children}
|
|
53
|
+
</div>
|
|
54
|
+
{onDismiss && (
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
onClick={onDismiss}
|
|
58
|
+
className="shrink-0 rounded-md p-1 text-ph-subtle hover:bg-black/5 hover:text-ph-ink"
|
|
59
|
+
aria-label="Dismiss"
|
|
60
|
+
>
|
|
61
|
+
<svg className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
62
|
+
<path d="M18 6L6 18M6 6l12 12" />
|
|
63
|
+
</svg>
|
|
64
|
+
</button>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export type AvatarSize = "xs" | "sm" | "md" | "lg" | "xl";
|
|
5
|
+
export type AvatarStatus = "online" | "offline" | "away" | "busy";
|
|
6
|
+
|
|
7
|
+
export interface AvatarProps extends ComponentPropsWithoutRef<"div"> {
|
|
8
|
+
/** Text initials or label (e.g. "JD") */
|
|
9
|
+
label?: string;
|
|
10
|
+
/** Optional icon or image */
|
|
11
|
+
icon?: ReactNode;
|
|
12
|
+
/** Size preset */
|
|
13
|
+
size?: AvatarSize;
|
|
14
|
+
/** Background colour class */
|
|
15
|
+
color?: string;
|
|
16
|
+
/** Status indicator dot */
|
|
17
|
+
status?: AvatarStatus;
|
|
18
|
+
/** Badge count or text */
|
|
19
|
+
badge?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const sizeMap: Record<AvatarSize, string> = {
|
|
23
|
+
xs: "h-8 w-8 text-[10px]",
|
|
24
|
+
sm: "h-10 w-10 text-xs",
|
|
25
|
+
md: "h-12 w-12 text-base",
|
|
26
|
+
lg: "h-16 w-16 text-xl",
|
|
27
|
+
xl: "h-24 w-24 text-3xl",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const statusMap: Record<AvatarStatus, string> = {
|
|
31
|
+
online: "bg-emerald-500",
|
|
32
|
+
offline: "bg-ph-mutedtext",
|
|
33
|
+
away: "bg-amber-400",
|
|
34
|
+
busy: "bg-ph-danger",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function Avatar({
|
|
38
|
+
label,
|
|
39
|
+
icon,
|
|
40
|
+
size = "md",
|
|
41
|
+
color = "bg-ph-brand text-white",
|
|
42
|
+
status,
|
|
43
|
+
badge,
|
|
44
|
+
className,
|
|
45
|
+
...props
|
|
46
|
+
}: AvatarProps) {
|
|
47
|
+
return (
|
|
48
|
+
<div className={cn("relative inline-flex shrink-0", className)} {...props}>
|
|
49
|
+
<div
|
|
50
|
+
className={cn(
|
|
51
|
+
"flex items-center justify-center rounded-full border border-ph-border font-bold shadow-ph",
|
|
52
|
+
sizeMap[size],
|
|
53
|
+
color,
|
|
54
|
+
)}
|
|
55
|
+
>
|
|
56
|
+
{icon || label}
|
|
57
|
+
</div>
|
|
58
|
+
{status && (
|
|
59
|
+
<span
|
|
60
|
+
className={cn(
|
|
61
|
+
"absolute bottom-0 right-1 h-2.5 w-2.5 rounded-full border-2 border-white",
|
|
62
|
+
statusMap[status],
|
|
63
|
+
)}
|
|
64
|
+
/>
|
|
65
|
+
)}
|
|
66
|
+
{badge && (
|
|
67
|
+
<span className="absolute -bottom-1 -right-1 flex min-w-[22px] items-center justify-center rounded-full bg-ph-brand px-1.5 py-px text-[10px] font-bold text-white">
|
|
68
|
+
{badge}
|
|
69
|
+
</span>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface AvatarGroupProps extends ComponentPropsWithoutRef<"div"> {
|
|
76
|
+
children?: ReactNode;
|
|
77
|
+
max?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function AvatarGroup({ children, max, className, ...props }: AvatarGroupProps) {
|
|
81
|
+
const items = Array.isArray(children) ? children : [children];
|
|
82
|
+
const visible = max ? items.slice(0, max) : items;
|
|
83
|
+
const remaining = max ? items.length - max : 0;
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div className={cn("-space-x-3 flex rtl:space-x-reverse", className)} {...props}>
|
|
87
|
+
{visible}
|
|
88
|
+
{remaining > 0 && (
|
|
89
|
+
<Avatar
|
|
90
|
+
label={`+${remaining}`}
|
|
91
|
+
size="md"
|
|
92
|
+
color="bg-ph-toolbar text-ph-subtle"
|
|
93
|
+
/>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export type BadgeVariant =
|
|
5
|
+
| "brand"
|
|
6
|
+
| "neutral"
|
|
7
|
+
| "success"
|
|
8
|
+
| "warning"
|
|
9
|
+
| "danger"
|
|
10
|
+
| "info"
|
|
11
|
+
| "outline";
|
|
12
|
+
|
|
13
|
+
export type BadgeSize = "sm" | "md";
|
|
14
|
+
|
|
15
|
+
export interface BadgeProps {
|
|
16
|
+
variant?: BadgeVariant;
|
|
17
|
+
size?: BadgeSize;
|
|
18
|
+
children?: ReactNode;
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const variantMap: Record<BadgeVariant, string> = {
|
|
23
|
+
brand: "ph-badge-brand",
|
|
24
|
+
neutral: "ph-badge-neutral",
|
|
25
|
+
success: "ph-badge-success",
|
|
26
|
+
warning: "ph-badge-warning",
|
|
27
|
+
danger: "ph-badge-danger",
|
|
28
|
+
info: "ph-badge-info",
|
|
29
|
+
outline: "ph-badge-outline",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const sizeMap: Record<BadgeSize, string> = {
|
|
33
|
+
sm: "text-[10px] px-1.5 py-0.5",
|
|
34
|
+
md: "",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function Badge({
|
|
38
|
+
variant = "neutral",
|
|
39
|
+
size = "md",
|
|
40
|
+
children,
|
|
41
|
+
className,
|
|
42
|
+
}: BadgeProps) {
|
|
43
|
+
return (
|
|
44
|
+
<span
|
|
45
|
+
className={cn(
|
|
46
|
+
"ph-badge",
|
|
47
|
+
variantMap[variant],
|
|
48
|
+
sizeMap[size],
|
|
49
|
+
className
|
|
50
|
+
)}
|
|
51
|
+
>
|
|
52
|
+
{children}
|
|
53
|
+
</span>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { ButtonHTMLAttributes, 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
|
+
}
|
|
33
|
+
|
|
34
|
+
const variantClass: Record<ButtonVariant, string> = {
|
|
35
|
+
primary: "ph-btn-primary",
|
|
36
|
+
secondary: "ph-btn-secondary",
|
|
37
|
+
tertiary: "ph-btn-tertiary",
|
|
38
|
+
signal: "ph-btn-signal",
|
|
39
|
+
accent: "ph-btn-accent",
|
|
40
|
+
neutral: "ph-btn-neutral",
|
|
41
|
+
info: "ph-btn-info",
|
|
42
|
+
success: "ph-btn-success",
|
|
43
|
+
warning: "ph-btn-warning",
|
|
44
|
+
danger: "ph-btn-danger",
|
|
45
|
+
ghost: "ph-btn-ghost",
|
|
46
|
+
link: "ph-btn-link",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const sizeClass: Record<ButtonSize, string> = {
|
|
50
|
+
xs: "ph-btn-xs",
|
|
51
|
+
sm: "ph-btn-sm",
|
|
52
|
+
md: "",
|
|
53
|
+
lg: "ph-btn-lg",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const shapeClass: Record<ButtonShape, string> = {
|
|
57
|
+
default: "",
|
|
58
|
+
circle: "ph-btn-circle",
|
|
59
|
+
square: "ph-btn-square",
|
|
60
|
+
wide: "ph-btn-wide",
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* PostHog LemonButton parity — primary/secondary use ::before border ring + ::after face/ridge
|
|
65
|
+
* (see PostHog `LemonButton.scss`). Prefer this over raw `<button className="ph-btn …">`.
|
|
66
|
+
*/
|
|
67
|
+
export function Button({
|
|
68
|
+
variant = "secondary",
|
|
69
|
+
size = "md",
|
|
70
|
+
shape = "default",
|
|
71
|
+
outline = false,
|
|
72
|
+
split = false,
|
|
73
|
+
icon,
|
|
74
|
+
sideIcon,
|
|
75
|
+
className,
|
|
76
|
+
children,
|
|
77
|
+
type = "button",
|
|
78
|
+
...props
|
|
79
|
+
}: ButtonProps) {
|
|
80
|
+
const usesLemonChrome = variant === "primary" || variant === "secondary";
|
|
81
|
+
|
|
82
|
+
const content = usesLemonChrome ? (
|
|
83
|
+
<ButtonChrome label={children} icon={icon} sideIcon={sideIcon} split={split} />
|
|
84
|
+
) : (
|
|
85
|
+
<>
|
|
86
|
+
{icon != null ? <span className="ph-btn-icon">{icon}</span> : null}
|
|
87
|
+
{children != null ? <span className="ph-btn-label">{children}</span> : null}
|
|
88
|
+
{split ? <span className="ph-btn-split-divider" aria-hidden /> : null}
|
|
89
|
+
{sideIcon != null ? <span className="ph-btn-side-icon">{sideIcon}</span> : null}
|
|
90
|
+
</>
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<button
|
|
95
|
+
type={type}
|
|
96
|
+
className={cn(
|
|
97
|
+
"ph-btn",
|
|
98
|
+
outline && "ph-btn-outline",
|
|
99
|
+
variantClass[variant],
|
|
100
|
+
sizeClass[size],
|
|
101
|
+
shapeClass[shape],
|
|
102
|
+
split && "ph-btn-split",
|
|
103
|
+
className,
|
|
104
|
+
)}
|
|
105
|
+
{...props}
|
|
106
|
+
>
|
|
107
|
+
{content}
|
|
108
|
+
</button>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -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
|
+
/** Inner face for Lemon primary/secondary — keeps labels above ::after ridge. */
|
|
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,116 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { cn } from "@/lib/cn";
|
|
5
|
+
|
|
6
|
+
interface CalendarProps {
|
|
7
|
+
value?: Date;
|
|
8
|
+
onChange?: (date: Date) => void;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Calendar({ value, onChange, className }: CalendarProps) {
|
|
13
|
+
const [currentMonth, setCurrentMonth] = useState(value || new Date());
|
|
14
|
+
const today = new Date();
|
|
15
|
+
|
|
16
|
+
const year = currentMonth.getFullYear();
|
|
17
|
+
const month = currentMonth.getMonth();
|
|
18
|
+
|
|
19
|
+
const firstDay = new Date(year, month, 1).getDay();
|
|
20
|
+
const daysInMonth = new Date(year, month + 1, 0).getDate();
|
|
21
|
+
|
|
22
|
+
const prevMonth = () => setCurrentMonth(new Date(year, month - 1, 1));
|
|
23
|
+
const nextMonth = () => setCurrentMonth(new Date(year, month + 1, 1));
|
|
24
|
+
|
|
25
|
+
const monthNames = [
|
|
26
|
+
"January", "February", "March", "April", "May", "June",
|
|
27
|
+
"July", "August", "September", "October", "November", "December",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const weekDays = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
|
|
31
|
+
|
|
32
|
+
const isSelected = (day: number) => {
|
|
33
|
+
if (!value) return false;
|
|
34
|
+
return (
|
|
35
|
+
value.getDate() === day &&
|
|
36
|
+
value.getMonth() === month &&
|
|
37
|
+
value.getFullYear() === year
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const isToday = (day: number) => {
|
|
42
|
+
return (
|
|
43
|
+
today.getDate() === day &&
|
|
44
|
+
today.getMonth() === month &&
|
|
45
|
+
today.getFullYear() === year
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const handleSelect = (day: number) => {
|
|
50
|
+
onChange?.(new Date(year, month, day));
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const days = [];
|
|
54
|
+
for (let i = 0; i < firstDay; i++) {
|
|
55
|
+
days.push(null);
|
|
56
|
+
}
|
|
57
|
+
for (let i = 1; i <= daysInMonth; i++) {
|
|
58
|
+
days.push(i);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<div className={cn("w-[280px] rounded-xl border border-ph-border bg-ph-surface p-4 shadow-ph", className)}>
|
|
63
|
+
<div className="mb-4 flex items-center justify-between">
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
onClick={prevMonth}
|
|
67
|
+
className="rounded-lg p-1 text-ph-subtle transition hover:bg-ph-muted hover:text-ph-ink"
|
|
68
|
+
>
|
|
69
|
+
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
70
|
+
<path d="M15 18l-6-6 6-6" />
|
|
71
|
+
</svg>
|
|
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
|
+
className="rounded-lg p-1 text-ph-subtle transition hover:bg-ph-muted hover:text-ph-ink"
|
|
80
|
+
>
|
|
81
|
+
<svg className="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
82
|
+
<path d="M9 18l6-6-6-6" />
|
|
83
|
+
</svg>
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div className="grid grid-cols-7 gap-1">
|
|
88
|
+
{weekDays.map((day) => (
|
|
89
|
+
<div key={day} className="py-1 text-center text-xs font-medium text-ph-mutedtext">
|
|
90
|
+
{day}
|
|
91
|
+
</div>
|
|
92
|
+
))}
|
|
93
|
+
{days.map((day, index) => (
|
|
94
|
+
<div key={index} className="aspect-square">
|
|
95
|
+
{day && (
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
onClick={() => handleSelect(day)}
|
|
99
|
+
className={cn(
|
|
100
|
+
"flex h-full w-full items-center justify-center rounded-lg text-sm transition-colors",
|
|
101
|
+
isSelected(day)
|
|
102
|
+
? "bg-ph-brand font-semibold text-white shadow-ph"
|
|
103
|
+
: isToday(day)
|
|
104
|
+
? "bg-ph-muted font-semibold text-ph-brand ring-1 ring-ph-brand/30"
|
|
105
|
+
: "text-ph-ink hover:bg-ph-muted"
|
|
106
|
+
)}
|
|
107
|
+
>
|
|
108
|
+
{day}
|
|
109
|
+
</button>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
))}
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|