@xenide-io/the-old-ui-theme 0.2.9 → 0.3.1

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 (133) hide show
  1. package/README.md +1 -1
  2. package/dist/chunk-5HSOBJ4F.mjs +5968 -0
  3. package/dist/index-BgG8Homu.d.mts +1461 -0
  4. package/dist/index-BgG8Homu.d.ts +1461 -0
  5. package/dist/index.d.mts +206 -100
  6. package/dist/index.d.ts +206 -100
  7. package/dist/index.js +4275 -1455
  8. package/dist/index.mjs +689 -735
  9. package/dist/tailwind-preset.js +7 -0
  10. package/dist/tailwind-preset.mjs +7 -0
  11. package/dist/ui.d.mts +3 -2
  12. package/dist/ui.d.ts +3 -2
  13. package/dist/ui.js +3449 -701
  14. package/dist/ui.mjs +106 -3
  15. package/package.json +20 -16
  16. package/src/app/globals.css +1953 -266
  17. package/src/components/charts/index.ts +9 -8
  18. package/src/components/charts/quill/BarChart.tsx +85 -0
  19. package/src/components/charts/quill/FunnelChart.tsx +49 -0
  20. package/src/components/charts/quill/InsightShell.tsx +27 -0
  21. package/src/components/charts/quill/MetricCard.tsx +44 -0
  22. package/src/components/charts/quill/PieChart.tsx +81 -0
  23. package/src/components/charts/quill/Sparkline.tsx +57 -0
  24. package/src/components/charts/quill/TimeSeriesLineChart.tsx +62 -0
  25. package/src/components/charts/quill/index.ts +9 -0
  26. package/src/components/icons/IconBase.tsx +20 -12
  27. package/src/components/icons/createIconBase.tsx +12 -7
  28. package/src/components/icons/icons.tsx +210 -18
  29. package/src/components/icons/index.ts +3 -0
  30. package/src/components/icons/lemon-brand-icons.tsx +16 -0
  31. package/src/components/icons/lemon-category-icons.tsx +72 -0
  32. package/src/components/icons/lemon-icons.tsx +57 -0
  33. package/src/components/sections/AlertShowcase.tsx +2 -1
  34. package/src/components/sections/BadgeShowcase.tsx +1 -1
  35. package/src/components/sections/ButtonShowcase.tsx +6 -10
  36. package/src/components/sections/CardShowcase.tsx +22 -6
  37. package/src/components/sections/DropdownShowcase.tsx +4 -6
  38. package/src/components/sections/FilterChipsShowcase.tsx +74 -26
  39. package/src/components/sections/FormShowcase.tsx +2 -1
  40. package/src/components/sections/IconShowcase.tsx +195 -212
  41. package/src/components/sections/KbdShowcase.tsx +47 -21
  42. package/src/components/sections/ModalShowcase.tsx +7 -6
  43. package/src/components/sections/NewComponentsShowcase.tsx +1052 -0
  44. package/src/components/sections/ProductLayoutsShowcase.tsx +78 -0
  45. package/src/components/sections/ProgressShowcase.tsx +50 -53
  46. package/src/components/sections/QuillChartShowcase.tsx +92 -0
  47. package/src/components/sections/QuillDashboardShowcase.tsx +149 -0
  48. package/src/components/sections/SidebarShowcase.tsx +151 -0
  49. package/src/components/sections/SwapShowcase.tsx +13 -5
  50. package/src/components/sections/TableShowcase.tsx +2 -0
  51. package/src/components/sections/TooltipShowcase.tsx +19 -25
  52. package/src/components/ui/Alert.tsx +28 -19
  53. package/src/components/ui/AlertDialog.tsx +62 -0
  54. package/src/components/ui/AppLayout.tsx +27 -0
  55. package/src/components/ui/AuthLayout.tsx +58 -0
  56. package/src/components/ui/Autocomplete.tsx +106 -0
  57. package/src/components/ui/Badge.tsx +10 -5
  58. package/src/components/ui/Banner.tsx +63 -0
  59. package/src/components/ui/Button.tsx +22 -12
  60. package/src/components/ui/ButtonChrome.tsx +1 -1
  61. package/src/components/ui/ButtonGroup.tsx +16 -0
  62. package/src/components/ui/Calendar.tsx +6 -7
  63. package/src/components/ui/Card.tsx +21 -11
  64. package/src/components/ui/Chip.tsx +46 -0
  65. package/src/components/ui/Collapsible.tsx +46 -0
  66. package/src/components/ui/CollapsibleSection.tsx +11 -11
  67. package/src/components/ui/Combobox.tsx +119 -0
  68. package/src/components/ui/ContextMenu.tsx +81 -0
  69. package/src/components/ui/DataTable.tsx +133 -0
  70. package/src/components/ui/DatePicker.tsx +96 -0
  71. package/src/components/ui/Dialog.tsx +75 -0
  72. package/src/components/ui/Divider.tsx +39 -0
  73. package/src/components/ui/Dot.tsx +32 -0
  74. package/src/components/ui/DropdownMenu.tsx +189 -65
  75. package/src/components/ui/FilterBar.tsx +165 -0
  76. package/src/components/ui/FilterChips.tsx +65 -35
  77. package/src/components/ui/FilterControls.tsx +30 -0
  78. package/src/components/ui/FormField.tsx +69 -0
  79. package/src/components/ui/Input.tsx +166 -140
  80. package/src/components/ui/InputGroup.tsx +16 -0
  81. package/src/components/ui/Kbd.tsx +88 -13
  82. package/src/components/ui/Lettermark.tsx +47 -0
  83. package/src/components/ui/Link.tsx +32 -0
  84. package/src/components/ui/Loader.tsx +96 -0
  85. package/src/components/ui/LoadingBar.tsx +25 -0
  86. package/src/components/ui/Menubar.tsx +66 -0
  87. package/src/components/ui/Metric.tsx +34 -0
  88. package/src/components/ui/Modal.tsx +66 -36
  89. package/src/components/ui/NumberField.tsx +76 -0
  90. package/src/components/ui/Popover.tsx +73 -0
  91. package/src/components/ui/ProgressCircle.tsx +63 -0
  92. package/src/components/ui/Resizable.tsx +71 -0
  93. package/src/components/ui/Row.tsx +51 -0
  94. package/src/components/ui/ScrollArea.tsx +54 -0
  95. package/src/components/ui/SettingsLayout.tsx +94 -0
  96. package/src/components/ui/ShowcaseWrapper.tsx +4 -16
  97. package/src/components/ui/Sidebar.tsx +77 -0
  98. package/src/components/ui/Snack.tsx +48 -0
  99. package/src/components/ui/Spinner.tsx +30 -0
  100. package/src/components/ui/Splotch.tsx +33 -0
  101. package/src/components/ui/Stepper.tsx +5 -6
  102. package/src/components/ui/Table.tsx +2 -0
  103. package/src/components/ui/Tag.tsx +65 -0
  104. package/src/components/ui/Toggle.tsx +62 -0
  105. package/src/components/ui/Tooltip.test.tsx +58 -0
  106. package/src/components/ui/Tooltip.tsx +211 -0
  107. package/src/components/ui/Widget.tsx +31 -0
  108. package/src/components/ui/first-slice.test.tsx +98 -0
  109. package/src/components/ui/index.ts +335 -22
  110. package/src/components/ui/interaction-primitives.test.tsx +77 -0
  111. package/src/components/ui/kbd-icons.test.tsx +90 -0
  112. package/src/components/ui/product-patterns.test.tsx +77 -0
  113. package/src/styles/themes.css +411 -1
  114. package/tailwind-preset.ts +7 -0
  115. package/dist/chunk-SHF57J7U.mjs +0 -2910
  116. package/dist/index-CmS6hcUk.d.mts +0 -753
  117. package/dist/index-CmS6hcUk.d.ts +0 -753
  118. package/src/components/charts/ph-insight-charts.tsx +0 -162
  119. package/src/components/dashboard/DashboardFiltersBar.tsx +0 -19
  120. package/src/components/dashboard/DashboardGrid.tsx +0 -23
  121. package/src/components/dashboard/DashboardInsightPlaceholder.tsx +0 -37
  122. package/src/components/dashboard/DashboardKpiCard.tsx +0 -25
  123. package/src/components/dashboard/DashboardToolbar.tsx +0 -23
  124. package/src/components/dashboard/DashboardWell.tsx +0 -10
  125. package/src/components/dashboard/InsightMiniCard.tsx +0 -26
  126. package/src/components/dashboard/InsightShell.tsx +0 -37
  127. package/src/components/dashboard/InsightShellHeader.tsx +0 -22
  128. package/src/components/dashboard/MiniTrendBars.tsx +0 -51
  129. package/src/components/dashboard/index.ts +0 -31
  130. package/src/components/dashboard/types.ts +0 -9
  131. package/src/components/sections/ChartShowcase.tsx +0 -190
  132. package/src/components/sections/DashboardShowcase.tsx +0 -191
  133. package/src/components/sections/StatShowcase.tsx +0 -129
@@ -9,15 +9,17 @@ function IconSwapToggle({
9
9
  onIcon: OnIcon,
10
10
  offClassName,
11
11
  onClassName,
12
+ label,
12
13
  }: {
13
14
  offIcon: typeof IconBellOff;
14
15
  onIcon: typeof IconBell;
15
16
  offClassName?: string;
16
17
  onClassName?: string;
18
+ label: string;
17
19
  }) {
18
20
  return (
19
- <label className="group relative inline-flex h-14 w-14 cursor-pointer items-center justify-center rounded-full bg-ph-surface shadow-ph">
20
- <input type="checkbox" className="sr-only" />
21
+ <label className="group relative inline-flex h-14 w-14 cursor-pointer items-center justify-center rounded-full bg-ph-surface shadow-ph focus-within:ring-2 focus-within:ring-ph-focus focus-within:ring-offset-2">
22
+ <input type="checkbox" className="sr-only" aria-label={label} />
21
23
  <span className="ph-icon-swap h-7 w-7" aria-hidden>
22
24
  <OffIcon
23
25
  className={cn(
@@ -37,10 +39,11 @@ function IconSwapToggle({
37
39
  }
38
40
 
39
41
  export default function SwapShowcase() {
40
- const code = `import { IconBell, IconBellOff, IconMoon, IconSun, IconVolume, IconVolumeOff } from "the-old-ui";
42
+ const code = `import { IconBell, IconBellOff, IconMoon, IconSun, IconVolume, IconVolumeOff } from "@xenide-io/the-old-ui-theme";
41
43
 
42
44
  <div className="flex flex-wrap gap-10 rounded-xl border border-ph-border bg-ph-muted p-8">
43
45
  <IconSwapToggle
46
+ label="Mute audio"
44
47
  offIcon={IconVolume}
45
48
  onIcon={IconVolumeOff}
46
49
  offClassName="text-ph-brand"
@@ -48,6 +51,7 @@ export default function SwapShowcase() {
48
51
  />
49
52
 
50
53
  <IconSwapToggle
54
+ label="Mute notifications"
51
55
  offIcon={IconBell}
52
56
  onIcon={IconBellOff}
53
57
  offClassName="rotate-[-8deg] text-ph-mutedtext"
@@ -55,10 +59,11 @@ export default function SwapShowcase() {
55
59
  />
56
60
 
57
61
  <IconSwapToggle
62
+ label="Use light appearance"
58
63
  offIcon={IconMoon}
59
64
  onIcon={IconSun}
60
65
  offClassName="text-ph-mutedtext"
61
- onClassName="text-amber-500"
66
+ onClassName="text-ph-warning"
62
67
  />
63
68
  </div>`;
64
69
 
@@ -66,6 +71,7 @@ export default function SwapShowcase() {
66
71
  <ShowcaseWrapper title="Icon swap" code={code} filename="SwapExample.tsx">
67
72
  <div className="flex flex-wrap gap-10 rounded-xl border border-ph-border bg-ph-muted p-8">
68
73
  <IconSwapToggle
74
+ label="Mute audio"
69
75
  offIcon={IconVolume}
70
76
  onIcon={IconVolumeOff}
71
77
  offClassName="text-ph-brand"
@@ -73,6 +79,7 @@ export default function SwapShowcase() {
73
79
  />
74
80
 
75
81
  <IconSwapToggle
82
+ label="Mute notifications"
76
83
  offIcon={IconBell}
77
84
  onIcon={IconBellOff}
78
85
  offClassName="rotate-[-8deg] text-ph-mutedtext"
@@ -80,10 +87,11 @@ export default function SwapShowcase() {
80
87
  />
81
88
 
82
89
  <IconSwapToggle
90
+ label="Use light appearance"
83
91
  offIcon={IconMoon}
84
92
  onIcon={IconSun}
85
93
  offClassName="text-ph-mutedtext"
86
- onClassName="text-amber-500"
94
+ onClassName="text-ph-warning"
87
95
  />
88
96
  </div>
89
97
  </ShowcaseWrapper>
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import type { CSSProperties } from "react";
2
4
  import { Badge, ComponentDocs, ShowcaseWrapper, Table, type TableColumn } from "@/components/ui";
3
5
 
@@ -1,44 +1,38 @@
1
- import { Button, ShowcaseWrapper } from "@/components/ui";
1
+ import { Button, ShowcaseWrapper, Tooltip, TooltipProvider } from "@/components/ui";
2
2
 
3
3
  export default function TooltipShowcase() {
4
- const code = `import { Button } from "the-old-ui";
4
+ const code = `import { Button, Tooltip, TooltipProvider } from "the-old-ui";
5
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">
6
+ <TooltipProvider delayDuration={300}>
7
+ <div className="ph-panel flex flex-wrap items-center gap-10">
8
+ <Tooltip content="Run a HogQL query (Command + /)" side="top">
9
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>
10
+ </Tooltip>
14
11
 
15
- <span className="ph-tip cursor-help" title="Native title tooltip for zero-JS parity">
16
- Dashed chrome (title attribute)
17
- </span>
12
+ <Tooltip content="Available on hover and keyboard focus" side="bottom">
13
+ <button type="button" className="ph-tip cursor-help">Dashed chrome</button>
14
+ </Tooltip>
18
15
  </div>
19
- </div>`;
16
+ </TooltipProvider>`;
20
17
 
21
18
  return (
22
19
  <ShowcaseWrapper
23
20
  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."
21
+ description="Accessible supporting text on an adaptive inverse surface. Tooltips open on hover or focus, avoid viewport collisions, and dismiss with Escape."
25
22
  code={code}
26
23
  filename="TooltipExample.tsx"
27
24
  >
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">
25
+ <TooltipProvider delayDuration={300}>
26
+ <div className="ph-panel flex flex-wrap items-center gap-10">
27
+ <Tooltip content="Run a HogQL query (Command + /)" side="top">
31
28
  <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>
29
+ </Tooltip>
36
30
 
37
- <span className="ph-tip cursor-help" title="Native title tooltip for zero-JS parity">
38
- Dashed chrome (title attribute)
39
- </span>
31
+ <Tooltip content="Available on hover and keyboard focus" side="bottom">
32
+ <button type="button" className="ph-tip cursor-help">Dashed chrome</button>
33
+ </Tooltip>
40
34
  </div>
41
- </div>
35
+ </TooltipProvider>
42
36
  </ShowcaseWrapper>
43
37
  );
44
38
  }
@@ -1,10 +1,10 @@
1
- import type { ReactNode } from "react";
2
- import { IconCancel, IconCheckCircle, IconErrorOutline, IconExclamation } from "@/components/icons";
1
+ import { forwardRef, type AriaAttributes, type HTMLAttributes, type ReactNode } from "react";
2
+ import { IconCancel, IconCheckCircle, IconClose, IconExclamation, IconInfo } from "@/components/icons";
3
3
  import { cn } from "@/lib/cn";
4
4
 
5
5
  export type AlertStatus = "info" | "success" | "warning" | "danger";
6
6
 
7
- export interface AlertProps {
7
+ export interface AlertProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
8
8
  status?: AlertStatus;
9
9
  title?: string;
10
10
  description?: string;
@@ -12,13 +12,15 @@ export interface AlertProps {
12
12
  children?: ReactNode;
13
13
  className?: string;
14
14
  onDismiss?: () => void;
15
+ live?: AriaAttributes["aria-live"];
16
+ dismissLabel?: string;
15
17
  }
16
18
 
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" },
19
+ const statusConfig: Record<AlertStatus, { icon: typeof IconInfo } > = {
20
+ info: { icon: IconInfo },
21
+ success: { icon: IconCheckCircle },
22
+ warning: { icon: IconExclamation },
23
+ danger: { icon: IconCancel },
22
24
  };
23
25
 
24
26
  const statusClassMap: Record<AlertStatus, string > = {
@@ -28,7 +30,7 @@ const statusClassMap: Record<AlertStatus, string > = {
28
30
  danger: "ph-alert-danger",
29
31
  };
30
32
 
31
- export function Alert({
33
+ export const Alert = forwardRef<HTMLDivElement, AlertProps>(function Alert({
32
34
  status = "info",
33
35
  title,
34
36
  description,
@@ -36,17 +38,24 @@ export function Alert({
36
38
  children,
37
39
  className,
38
40
  onDismiss,
39
- }: AlertProps) {
41
+ live,
42
+ dismissLabel = "Dismiss notification",
43
+ role,
44
+ ...props
45
+ }, ref) {
40
46
  const config = statusConfig[status];
41
47
  const Icon = config.icon;
42
48
 
43
49
  return (
44
50
  <div
45
- role="alert"
51
+ ref={ref}
52
+ role={role ?? (live === "assertive" ? "alert" : live === "polite" ? "status" : undefined)}
53
+ aria-live={live}
46
54
  className={cn("ph-alert", statusClassMap[status], className)}
55
+ {...props}
47
56
  >
48
- {icon ?? <Icon className={cn("mt-0.5 h-5 w-5 shrink-0", config.color)} />}
49
- <div className="flex-1">
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">
50
59
  {title && <p className="font-semibold">{title}</p>}
51
60
  {description && <p className="mt-1 text-sm text-ph-subtle">{description}</p>}
52
61
  {children}
@@ -55,14 +64,14 @@ export function Alert({
55
64
  <button
56
65
  type="button"
57
66
  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"
67
+ className="ph-alert-dismiss"
68
+ aria-label={dismissLabel}
60
69
  >
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>
70
+ <IconClose className="h-4 w-4" aria-hidden />
64
71
  </button>
65
72
  )}
66
73
  </div>
67
74
  );
68
- }
75
+ });
76
+
77
+ Alert.displayName = "Alert";
@@ -0,0 +1,62 @@
1
+ "use client";
2
+
3
+ import { useState, type ReactNode } from "react";
4
+ import { Modal, type ModalProps } from "@/components/ui/Modal";
5
+ import { Button, type ButtonProps } from "@/components/ui/Button";
6
+
7
+ export interface AlertDialogProps extends Omit<ModalProps, "onClose"> {
8
+ isOpen?: boolean;
9
+ onClose?: () => void;
10
+ onConfirm?: () => void | Promise<void>;
11
+ confirmText?: string;
12
+ cancelText?: string;
13
+ confirmProps?: Partial<ButtonProps>;
14
+ cancelProps?: Partial<ButtonProps>;
15
+ children?: ReactNode;
16
+ }
17
+
18
+ export function AlertDialog({
19
+ isOpen: openProp,
20
+ onClose,
21
+ onConfirm,
22
+ confirmText = "Confirm",
23
+ cancelText = "Cancel",
24
+ confirmProps,
25
+ cancelProps,
26
+ children,
27
+ title,
28
+ ...modalProps
29
+ }: AlertDialogProps) {
30
+ const [loading, setLoading] = useState(false);
31
+ const isOpen = openProp ?? true;
32
+
33
+ const handleConfirm = async () => {
34
+ if (!onConfirm) return;
35
+ setLoading(true);
36
+ try { await onConfirm(); } finally { setLoading(false); }
37
+ onClose?.();
38
+ };
39
+
40
+ return (
41
+ <Modal
42
+ open={isOpen}
43
+ onClose={onClose}
44
+ title={title}
45
+ footer={
46
+ <div className="flex justify-end gap-2">
47
+ <Button variant="secondary" {...cancelProps} onClick={onClose}>{cancelText}</Button>
48
+ {onConfirm && (
49
+ <Button variant="primary" loading={loading} {...confirmProps} onClick={handleConfirm}>
50
+ {confirmText}
51
+ </Button>
52
+ )}
53
+ </div>
54
+ }
55
+ {...modalProps}
56
+ >
57
+ {children}
58
+ </Modal>
59
+ );
60
+ }
61
+
62
+ AlertDialog.displayName = "AlertDialog";
@@ -0,0 +1,27 @@
1
+ "use client";
2
+
3
+ import { type ReactNode, useState } from "react";
4
+ import { cn } from "@/lib/cn";
5
+
6
+ export interface AppLayoutProps {
7
+ sidebar: ReactNode;
8
+ children: ReactNode;
9
+ sidebarWidth?: number;
10
+ className?: string;
11
+ }
12
+
13
+ export function AppLayout({ sidebar, children, sidebarWidth = 240, className }: AppLayoutProps) {
14
+ return (
15
+ <div
16
+ className={cn("ph-app-layout grid min-h-screen", className)}
17
+ style={{ gridTemplateColumns: `${sidebarWidth}px minmax(0, 1fr)` }}
18
+ >
19
+ <aside className="ph-app-layout__sidebar border-r border-ph-border bg-ph-surface overflow-y-auto">
20
+ {sidebar}
21
+ </aside>
22
+ <main className="ph-app-layout__content min-w-0 overflow-y-auto bg-ph-canvas">
23
+ {children}
24
+ </main>
25
+ </div>
26
+ );
27
+ }
@@ -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,106 @@
1
+ "use client";
2
+
3
+ import { useState, useRef, useEffect, type InputHTMLAttributes } from "react";
4
+ import { cn } from "@/lib/cn";
5
+
6
+ export interface AutocompleteOption {
7
+ value: string;
8
+ label: string;
9
+ }
10
+
11
+ export interface AutocompleteProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "onSelect"> {
12
+ options: AutocompleteOption[];
13
+ onSelect?: (option: AutocompleteOption) => void;
14
+ }
15
+
16
+ export function Autocomplete({
17
+ options,
18
+ onSelect,
19
+ className,
20
+ value: controlledValue,
21
+ onChange,
22
+ placeholder = "Type to search...",
23
+ ...inputProps
24
+ }: AutocompleteProps) {
25
+ const [open, setOpen] = useState(false);
26
+ const [search, setSearch] = useState("");
27
+ const [activeIndex, setActiveIndex] = useState(-1);
28
+ const containerRef = useRef<HTMLDivElement>(null);
29
+ const inputRef = useRef<HTMLInputElement>(null);
30
+ const listRef = useRef<HTMLDivElement>(null);
31
+
32
+ const filtered = options.filter((o) =>
33
+ o.label.toLowerCase().includes(search.toLowerCase())
34
+ );
35
+
36
+ const handleSelect = (opt: AutocompleteOption) => {
37
+ setSearch(opt.label);
38
+ setOpen(false);
39
+ onSelect?.(opt);
40
+ };
41
+
42
+ useEffect(() => {
43
+ if (!open) { setActiveIndex(-1); return; }
44
+ const handle = (e: MouseEvent) => {
45
+ if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
46
+ setOpen(false);
47
+ }
48
+ };
49
+ const handleKey = (e: KeyboardEvent) => {
50
+ if (e.key === "ArrowDown") { setActiveIndex((i) => Math.min(i + 1, filtered.length - 1)); e.preventDefault(); }
51
+ if (e.key === "ArrowUp") { setActiveIndex((i) => Math.max(i - 1, 0)); e.preventDefault(); }
52
+ if (e.key === "Enter" && activeIndex >= 0) { handleSelect(filtered[activeIndex]); e.preventDefault(); }
53
+ if (e.key === "Escape") { setOpen(false); }
54
+ };
55
+ document.addEventListener("mousedown", handle);
56
+ document.addEventListener("keydown", handleKey);
57
+ return () => {
58
+ document.removeEventListener("mousedown", handle);
59
+ document.removeEventListener("keydown", handleKey);
60
+ };
61
+ }, [open, filtered, activeIndex]);
62
+
63
+ useEffect(() => {
64
+ if (activeIndex >= 0 && listRef.current) {
65
+ const el = listRef.current.children[activeIndex] as HTMLElement;
66
+ el?.scrollIntoView({ block: "nearest" });
67
+ }
68
+ }, [activeIndex]);
69
+
70
+ return (
71
+ <div ref={containerRef} className={cn("ph-autocomplete relative", className)}>
72
+ <input
73
+ ref={inputRef}
74
+ type="text"
75
+ value={search}
76
+ onChange={(e) => { setSearch(e.target.value); setOpen(true); onChange?.(e); }}
77
+ onFocus={() => setOpen(true)}
78
+ placeholder={placeholder}
79
+ className="ph-input w-full"
80
+ {...inputProps}
81
+ />
82
+ {open && filtered.length > 0 && (
83
+ <div ref={listRef} className="ph-autocomplete__dropdown" role="listbox">
84
+ {filtered.map((opt, i) => (
85
+ <button
86
+ key={opt.value}
87
+ type="button"
88
+ role="option"
89
+ aria-selected={i === activeIndex}
90
+ onMouseDown={() => handleSelect(opt)}
91
+ onMouseEnter={() => setActiveIndex(i)}
92
+ className={cn(
93
+ "ph-autocomplete__option",
94
+ i === activeIndex && "ph-autocomplete__option--active"
95
+ )}
96
+ >
97
+ {opt.label}
98
+ </button>
99
+ ))}
100
+ </div>
101
+ )}
102
+ </div>
103
+ );
104
+ }
105
+
106
+ Autocomplete.displayName = "Autocomplete";
@@ -1,4 +1,4 @@
1
- import type { ReactNode } from "react";
1
+ import { forwardRef, type HTMLAttributes, type ReactNode } from "react";
2
2
  import { cn } from "@/lib/cn";
3
3
 
4
4
  export type BadgeVariant =
@@ -12,7 +12,7 @@ export type BadgeVariant =
12
12
 
13
13
  export type BadgeSize = "sm" | "md";
14
14
 
15
- export interface BadgeProps {
15
+ export interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
16
16
  variant?: BadgeVariant;
17
17
  size?: BadgeSize;
18
18
  children?: ReactNode;
@@ -34,22 +34,27 @@ const sizeMap: Record<BadgeSize, string> = {
34
34
  md: "",
35
35
  };
36
36
 
37
- export function Badge({
37
+ export const Badge = forwardRef<HTMLSpanElement, BadgeProps>(function Badge({
38
38
  variant = "neutral",
39
39
  size = "md",
40
40
  children,
41
41
  className,
42
- }: BadgeProps) {
42
+ ...props
43
+ }, ref) {
43
44
  return (
44
45
  <span
46
+ ref={ref}
45
47
  className={cn(
46
48
  "ph-badge",
47
49
  variantMap[variant],
48
50
  sizeMap[size],
49
51
  className
50
52
  )}
53
+ {...props}
51
54
  >
52
55
  {children}
53
56
  </span>
54
57
  );
55
- }
58
+ });
59
+
60
+ Badge.displayName = "Badge";
@@ -0,0 +1,63 @@
1
+ import { forwardRef, type HTMLAttributes, type ReactNode } from "react";
2
+ import { cn } from "@/lib/cn";
3
+
4
+ export type BannerType = "info" | "success" | "warning" | "danger" | "ai";
5
+
6
+ export interface BannerProps extends HTMLAttributes<HTMLDivElement> {
7
+ type?: BannerType;
8
+ onClose?: () => void;
9
+ action?: ReactNode;
10
+ hideIcon?: boolean;
11
+ square?: boolean;
12
+ icon?: ReactNode;
13
+ children: ReactNode;
14
+ }
15
+
16
+ const typeClass: Record<BannerType, string> = {
17
+ info: "ph-banner-info",
18
+ success: "ph-banner-success",
19
+ warning: "ph-banner-warning",
20
+ danger: "ph-banner-danger",
21
+ ai: "ph-banner-ai",
22
+ };
23
+
24
+ export const Banner = forwardRef<HTMLDivElement, BannerProps>(function Banner({
25
+ type = "info",
26
+ onClose,
27
+ action,
28
+ hideIcon = false,
29
+ square = false,
30
+ icon,
31
+ className,
32
+ children,
33
+ ...props
34
+ }, ref) {
35
+ return (
36
+ <div
37
+ ref={ref}
38
+ role="alert"
39
+ className={cn(
40
+ "ph-banner",
41
+ typeClass[type],
42
+ square && "ph-banner--square",
43
+ className
44
+ )}
45
+ {...props}
46
+ >
47
+ <div className="ph-banner__body">
48
+ {icon}
49
+ <div className="ph-banner__content">{children}</div>
50
+ {action && <div className="ph-banner__action">{action}</div>}
51
+ {onClose && (
52
+ <button type="button" onClick={onClose} className="ph-banner__close" aria-label="Dismiss">
53
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
54
+ <path d="M4 4l8 8M12 4l-8 8" />
55
+ </svg>
56
+ </button>
57
+ )}
58
+ </div>
59
+ </div>
60
+ );
61
+ });
62
+
63
+ Banner.displayName = "Banner";