@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,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";