@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,108 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
ComponentDocs,
|
|
6
|
+
Button,
|
|
7
|
+
ShowcaseWrapper,
|
|
8
|
+
Toast,
|
|
9
|
+
ToastStack,
|
|
10
|
+
} from "@/components/ui";
|
|
11
|
+
|
|
12
|
+
const toasts = [
|
|
13
|
+
{
|
|
14
|
+
id: 1,
|
|
15
|
+
status: "info" as const,
|
|
16
|
+
title: "New deploy",
|
|
17
|
+
text: "Warehouse sync node rotated without downtime.",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 2,
|
|
21
|
+
status: "success" as const,
|
|
22
|
+
title: "Exports healthy",
|
|
23
|
+
text: "Snowflake incremental finished in 41s.",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 3,
|
|
27
|
+
status: "warning" as const,
|
|
28
|
+
title: "Approaching quota",
|
|
29
|
+
text: "Session replay capture at 92% monthly allocation.",
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 4,
|
|
33
|
+
status: "danger" as const,
|
|
34
|
+
title: "Batch failed",
|
|
35
|
+
text: "Stripe connector returned HTTP 524 — backing off.",
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export default function ToastShowcase() {
|
|
40
|
+
const [visible, setVisible] = useState<number[]>(toasts.map((t) => t.id));
|
|
41
|
+
|
|
42
|
+
const code = `import { Button, Toast, ToastStack } from "the-old-ui";
|
|
43
|
+
|
|
44
|
+
<div className="ph-panel">
|
|
45
|
+
<div className="mb-4 flex items-center justify-between">
|
|
46
|
+
<h3 className="text-sm font-semibold uppercase tracking-wider text-ph-mutedtext">Snack stack demo</h3>
|
|
47
|
+
<Button variant="ghost" size="sm" onClick={() => setVisible(toasts.map((t) => t.id))}>
|
|
48
|
+
Restore all
|
|
49
|
+
</Button>
|
|
50
|
+
</div>
|
|
51
|
+
<ToastStack className="max-w-xl">
|
|
52
|
+
{toasts.map((toast) => {
|
|
53
|
+
if (!visible.includes(toast.id)) return null;
|
|
54
|
+
return (
|
|
55
|
+
<Toast
|
|
56
|
+
key={toast.id}
|
|
57
|
+
status={toast.status}
|
|
58
|
+
title={toast.title}
|
|
59
|
+
description={toast.text}
|
|
60
|
+
onDismiss={() => setVisible((v) => v.filter((x) => x !== toast.id))}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
})}
|
|
64
|
+
</ToastStack>
|
|
65
|
+
</div>`;
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<ShowcaseWrapper
|
|
69
|
+
title="Toasts"
|
|
70
|
+
description="Snack stack notifications with semantic colour variants and dismiss actions."
|
|
71
|
+
code={code}
|
|
72
|
+
filename="ToastExample.tsx"
|
|
73
|
+
docs={
|
|
74
|
+
<ComponentDocs
|
|
75
|
+
rows={[
|
|
76
|
+
{ name: "status", type: "info | success | warning | danger", defaultValue: "info", description: "Controls icon and colour treatment." },
|
|
77
|
+
{ name: "title", type: "string", description: "Required toast heading." },
|
|
78
|
+
{ name: "description", type: "string", description: "Optional supporting copy." },
|
|
79
|
+
{ name: "onDismiss", type: "() => void", description: "Shows a close button when provided." },
|
|
80
|
+
]}
|
|
81
|
+
/>
|
|
82
|
+
}
|
|
83
|
+
>
|
|
84
|
+
<div className="ph-panel">
|
|
85
|
+
<div className="mb-4 flex items-center justify-between">
|
|
86
|
+
<h3 className="text-sm font-semibold uppercase tracking-wider text-ph-mutedtext">Snack stack demo</h3>
|
|
87
|
+
<Button variant="ghost" size="sm" onClick={() => setVisible(toasts.map((t) => t.id))}>
|
|
88
|
+
Restore all
|
|
89
|
+
</Button>
|
|
90
|
+
</div>
|
|
91
|
+
<ToastStack className="max-w-xl">
|
|
92
|
+
{toasts.map((toast) => {
|
|
93
|
+
if (!visible.includes(toast.id)) return null;
|
|
94
|
+
return (
|
|
95
|
+
<Toast
|
|
96
|
+
key={toast.id}
|
|
97
|
+
status={toast.status}
|
|
98
|
+
title={toast.title}
|
|
99
|
+
description={toast.text}
|
|
100
|
+
onDismiss={() => setVisible((v) => v.filter((x) => x !== toast.id))}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
})}
|
|
104
|
+
</ToastStack>
|
|
105
|
+
</div>
|
|
106
|
+
</ShowcaseWrapper>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -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,77 @@
|
|
|
1
|
+
import { forwardRef, type AriaAttributes, type HTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
import { IconCancel, IconCheckCircle, IconClose, IconExclamation, IconInfo } from "@/components/icons";
|
|
3
|
+
import { cn } from "@/lib/cn";
|
|
4
|
+
|
|
5
|
+
export type AlertStatus = "info" | "success" | "warning" | "danger";
|
|
6
|
+
|
|
7
|
+
export interface AlertProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
|
|
8
|
+
status?: AlertStatus;
|
|
9
|
+
title?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
icon?: ReactNode;
|
|
12
|
+
children?: ReactNode;
|
|
13
|
+
className?: string;
|
|
14
|
+
onDismiss?: () => void;
|
|
15
|
+
live?: AriaAttributes["aria-live"];
|
|
16
|
+
dismissLabel?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const statusConfig: Record<AlertStatus, { icon: typeof IconInfo } > = {
|
|
20
|
+
info: { icon: IconInfo },
|
|
21
|
+
success: { icon: IconCheckCircle },
|
|
22
|
+
warning: { icon: IconExclamation },
|
|
23
|
+
danger: { icon: IconCancel },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const statusClassMap: Record<AlertStatus, string > = {
|
|
27
|
+
info: "ph-alert-info",
|
|
28
|
+
success: "ph-alert-success",
|
|
29
|
+
warning: "ph-alert-warning",
|
|
30
|
+
danger: "ph-alert-danger",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const Alert = forwardRef<HTMLDivElement, AlertProps>(function Alert({
|
|
34
|
+
status = "info",
|
|
35
|
+
title,
|
|
36
|
+
description,
|
|
37
|
+
icon,
|
|
38
|
+
children,
|
|
39
|
+
className,
|
|
40
|
+
onDismiss,
|
|
41
|
+
live,
|
|
42
|
+
dismissLabel = "Dismiss notification",
|
|
43
|
+
role,
|
|
44
|
+
...props
|
|
45
|
+
}, ref) {
|
|
46
|
+
const config = statusConfig[status];
|
|
47
|
+
const Icon = config.icon;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
ref={ref}
|
|
52
|
+
role={role ?? (live === "assertive" ? "alert" : live === "polite" ? "status" : undefined)}
|
|
53
|
+
aria-live={live}
|
|
54
|
+
className={cn("ph-alert", statusClassMap[status], className)}
|
|
55
|
+
{...props}
|
|
56
|
+
>
|
|
57
|
+
{icon ?? <Icon className="ph-alert-icon mt-0.5 h-5 w-5 shrink-0" aria-hidden />}
|
|
58
|
+
<div className="min-w-0 flex-1">
|
|
59
|
+
{title && <p className="font-semibold">{title}</p>}
|
|
60
|
+
{description && <p className="mt-1 text-sm text-ph-subtle">{description}</p>}
|
|
61
|
+
{children}
|
|
62
|
+
</div>
|
|
63
|
+
{onDismiss && (
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
onClick={onDismiss}
|
|
67
|
+
className="ph-alert-dismiss"
|
|
68
|
+
aria-label={dismissLabel}
|
|
69
|
+
>
|
|
70
|
+
<IconClose className="h-4 w-4" aria-hidden />
|
|
71
|
+
</button>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
Alert.displayName = "Alert";
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useId, type ComponentPropsWithoutRef, type ReactNode } from "react";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface AuthLayoutProps extends ComponentPropsWithoutRef<"div"> {
|
|
5
|
+
brand?: ReactNode;
|
|
6
|
+
footer?: ReactNode;
|
|
7
|
+
aside?: ReactNode;
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AuthCardProps extends Omit<ComponentPropsWithoutRef<"section">, "title"> {
|
|
12
|
+
title: ReactNode;
|
|
13
|
+
description?: ReactNode;
|
|
14
|
+
footer?: ReactNode;
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AuthDividerProps extends ComponentPropsWithoutRef<"div"> {
|
|
19
|
+
children?: ReactNode;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function AuthLayout({ brand, footer, aside, children, className, ...props }: AuthLayoutProps) {
|
|
23
|
+
return (
|
|
24
|
+
<div className={cn("ph-auth-layout", aside != null && "ph-auth-layout--split", className)} {...props}>
|
|
25
|
+
{aside ? <aside className="ph-auth-layout__aside">{aside}</aside> : null}
|
|
26
|
+
<div className="ph-auth-layout__main">
|
|
27
|
+
<div className="ph-auth-layout__column">
|
|
28
|
+
{brand ? <div className="ph-auth-layout__brand">{brand}</div> : null}
|
|
29
|
+
{children}
|
|
30
|
+
{footer ? <footer className="ph-auth-layout__footer">{footer}</footer> : null}
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function AuthCard({ title, description, footer, children, className, ...props }: AuthCardProps) {
|
|
38
|
+
const titleId = useId();
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<section className={cn("ph-auth-card", className)} aria-labelledby={titleId} {...props}>
|
|
42
|
+
<header className="ph-auth-card__header">
|
|
43
|
+
<h1 id={titleId}>{title}</h1>
|
|
44
|
+
{description ? <p>{description}</p> : null}
|
|
45
|
+
</header>
|
|
46
|
+
<div className="ph-auth-card__body">{children}</div>
|
|
47
|
+
{footer ? <footer className="ph-auth-card__footer">{footer}</footer> : null}
|
|
48
|
+
</section>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function AuthDivider({ children = "or continue with", className, ...props }: AuthDividerProps) {
|
|
53
|
+
return (
|
|
54
|
+
<div className={cn("ph-auth-divider", className)} {...props}>
|
|
55
|
+
<span>{children}</span>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -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,60 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes, 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 extends HTMLAttributes<HTMLSpanElement> {
|
|
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 const Badge = forwardRef<HTMLSpanElement, BadgeProps>(function Badge({
|
|
38
|
+
variant = "neutral",
|
|
39
|
+
size = "md",
|
|
40
|
+
children,
|
|
41
|
+
className,
|
|
42
|
+
...props
|
|
43
|
+
}, ref) {
|
|
44
|
+
return (
|
|
45
|
+
<span
|
|
46
|
+
ref={ref}
|
|
47
|
+
className={cn(
|
|
48
|
+
"ph-badge",
|
|
49
|
+
variantMap[variant],
|
|
50
|
+
sizeMap[size],
|
|
51
|
+
className
|
|
52
|
+
)}
|
|
53
|
+
{...props}
|
|
54
|
+
>
|
|
55
|
+
{children}
|
|
56
|
+
</span>
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
Badge.displayName = "Badge";
|