@xenide-io/the-old-ui-theme 0.2.9 → 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 (61) hide show
  1. package/README.md +1 -1
  2. package/dist/{chunk-SHF57J7U.mjs → chunk-FDMD3IIN.mjs} +1573 -652
  3. package/dist/index-D1RTT2Ap.d.mts +1044 -0
  4. package/dist/index-D1RTT2Ap.d.ts +1044 -0
  5. package/dist/index.d.mts +10 -7
  6. package/dist/index.d.ts +10 -7
  7. package/dist/index.js +1705 -744
  8. package/dist/index.mjs +61 -1
  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 +1584 -666
  14. package/dist/ui.mjs +31 -1
  15. package/package.json +18 -4
  16. package/src/app/globals.css +1005 -227
  17. package/src/components/icons/IconBase.tsx +20 -12
  18. package/src/components/icons/createIconBase.tsx +12 -7
  19. package/src/components/icons/icons.tsx +208 -18
  20. package/src/components/sections/AlertShowcase.tsx +2 -1
  21. package/src/components/sections/BadgeShowcase.tsx +1 -1
  22. package/src/components/sections/ButtonShowcase.tsx +6 -10
  23. package/src/components/sections/CardShowcase.tsx +22 -6
  24. package/src/components/sections/DropdownShowcase.tsx +4 -6
  25. package/src/components/sections/FilterChipsShowcase.tsx +74 -26
  26. package/src/components/sections/FormShowcase.tsx +2 -1
  27. package/src/components/sections/IconShowcase.tsx +86 -206
  28. package/src/components/sections/KbdShowcase.tsx +47 -21
  29. package/src/components/sections/ModalShowcase.tsx +7 -6
  30. package/src/components/sections/ProductLayoutsShowcase.tsx +138 -0
  31. package/src/components/sections/ProgressShowcase.tsx +50 -53
  32. package/src/components/sections/SwapShowcase.tsx +13 -5
  33. package/src/components/ui/Alert.tsx +28 -19
  34. package/src/components/ui/AuthLayout.tsx +58 -0
  35. package/src/components/ui/Badge.tsx +10 -5
  36. package/src/components/ui/Button.tsx +22 -12
  37. package/src/components/ui/ButtonChrome.tsx +1 -1
  38. package/src/components/ui/Calendar.tsx +6 -7
  39. package/src/components/ui/Card.tsx +21 -11
  40. package/src/components/ui/CollapsibleSection.tsx +11 -11
  41. package/src/components/ui/DropdownMenu.tsx +189 -65
  42. package/src/components/ui/FilterBar.tsx +165 -0
  43. package/src/components/ui/FilterChips.tsx +65 -35
  44. package/src/components/ui/FilterControls.tsx +30 -0
  45. package/src/components/ui/FormField.tsx +69 -0
  46. package/src/components/ui/Input.tsx +166 -140
  47. package/src/components/ui/Kbd.tsx +88 -13
  48. package/src/components/ui/Loader.tsx +96 -0
  49. package/src/components/ui/Modal.tsx +66 -36
  50. package/src/components/ui/SettingsLayout.tsx +94 -0
  51. package/src/components/ui/ShowcaseWrapper.tsx +4 -16
  52. package/src/components/ui/Stepper.tsx +5 -6
  53. package/src/components/ui/first-slice.test.tsx +98 -0
  54. package/src/components/ui/index.ts +62 -3
  55. package/src/components/ui/interaction-primitives.test.tsx +77 -0
  56. package/src/components/ui/kbd-icons.test.tsx +90 -0
  57. package/src/components/ui/product-patterns.test.tsx +77 -0
  58. package/src/styles/themes.css +411 -1
  59. package/tailwind-preset.ts +7 -0
  60. package/dist/index-CmS6hcUk.d.mts +0 -753
  61. package/dist/index-CmS6hcUk.d.ts +0 -753
@@ -0,0 +1,138 @@
1
+ import {
2
+ AuthCard,
3
+ AuthDivider,
4
+ AuthLayout,
5
+ Badge,
6
+ Button,
7
+ Checkbox,
8
+ Input,
9
+ LoadingState,
10
+ Panel,
11
+ SettingsLayout,
12
+ SettingsNav,
13
+ ShowcaseWrapper,
14
+ Toggle,
15
+ } from "@/components/ui";
16
+ import { IconDataReel, IconMail, IconPerson, IconTuning } from "@/components/icons";
17
+
18
+ const settingsGroups = [
19
+ {
20
+ label: "Workspace",
21
+ items: [
22
+ { id: "general", label: "General", href: "#settings", icon: <IconTuning className="h-4 w-4" aria-hidden />, active: true },
23
+ { id: "members", label: "Members", href: "#settings", icon: <IconPerson className="h-4 w-4" aria-hidden />, badge: "12" },
24
+ ],
25
+ },
26
+ {
27
+ label: "Data",
28
+ items: [
29
+ { id: "sources", label: "Sources", href: "#settings", icon: <IconDataReel className="h-4 w-4" aria-hidden /> },
30
+ { id: "email", label: "Email reports", href: "#settings", icon: <IconMail className="h-4 w-4" aria-hidden /> },
31
+ ],
32
+ },
33
+ ] as const;
34
+
35
+ export default function ProductLayoutsShowcase() {
36
+ const authCode = `import { AuthCard, AuthDivider, AuthLayout, Button, Input } from "@xenide-io/the-old-ui-theme";
37
+
38
+ <AuthLayout brand="Acme Analytics" footer="Privacy · Terms">
39
+ <AuthCard title="Welcome back" description="Sign in to your workspace">
40
+ <Input label="Work email" type="email" autoComplete="email" />
41
+ <Input label="Password" type="password" autoComplete="current-password" />
42
+ <Button variant="primary" className="w-full">Sign in</Button>
43
+ <AuthDivider />
44
+ <Button variant="secondary" className="w-full">Continue with SSO</Button>
45
+ </AuthCard>
46
+ </AuthLayout>`;
47
+
48
+ const settingsCode = `import { SettingsLayout, SettingsNav } from "@xenide-io/the-old-ui-theme";
49
+
50
+ <SettingsLayout
51
+ title="Workspace settings"
52
+ navigation={<SettingsNav groups={settingsGroups} />}
53
+ >
54
+ {/* Route-specific settings panels */}
55
+ </SettingsLayout>`;
56
+
57
+ return (
58
+ <>
59
+ <ShowcaseWrapper
60
+ id="auth"
61
+ title="Authentication layouts"
62
+ description="Focused sign-in, verification, recovery, and loading shells. Authentication state and provider logic stay application-owned."
63
+ code={authCode}
64
+ filename="SignInPage.tsx"
65
+ >
66
+ <div className="grid gap-5 xl:grid-cols-[1.35fr,0.65fr]">
67
+ <AuthLayout
68
+ className="min-h-[34rem]"
69
+ brand={<span>The Old UI</span>}
70
+ aside={
71
+ <div className="flex h-full flex-col justify-between">
72
+ <strong className="text-xl">Understand every product decision.</strong>
73
+ <p className="max-w-xs text-sm opacity-80">One focused authentication step at a time, with room for product context.</p>
74
+ </div>
75
+ }
76
+ footer={<>Privacy · Terms · Status</>}
77
+ >
78
+ <AuthCard
79
+ title="Welcome back"
80
+ description="Sign in to your workspace"
81
+ footer={<>New to the workspace? <a href="#auth" className="font-medium text-ph-brand">Create an account</a></>}
82
+ >
83
+ <Input label="Work email" type="email" autoComplete="email" placeholder="you@company.com" />
84
+ <Input label="Password" type="password" autoComplete="current-password" />
85
+ <div className="flex items-center justify-between gap-3">
86
+ <Checkbox label="Remember me" />
87
+ <a href="#auth" className="text-xs font-medium text-ph-brand">Forgot password?</a>
88
+ </div>
89
+ <Button variant="primary" className="w-full">Sign in</Button>
90
+ <AuthDivider />
91
+ <Button variant="secondary" className="w-full">Continue with SSO</Button>
92
+ </AuthCard>
93
+ </AuthLayout>
94
+
95
+ <div className="grid gap-5">
96
+ <AuthCard title="Check your inbox" description="We sent a six-digit code to alex@company.com">
97
+ <Input label="Verification code" inputMode="numeric" autoComplete="one-time-code" placeholder="000000" />
98
+ <Button variant="primary" className="w-full">Verify email</Button>
99
+ </AuthCard>
100
+ <AuthCard title="Preparing workspace" description="Authentication completed successfully.">
101
+ <LoadingState
102
+ label="Preparing your workspace"
103
+ title="Loading your workspace"
104
+ description="Connecting data sources and restoring your last view."
105
+ variant="dots"
106
+ />
107
+ </AuthCard>
108
+ </div>
109
+ </div>
110
+ </ShowcaseWrapper>
111
+
112
+ <ShowcaseWrapper
113
+ id="settings"
114
+ title="Settings layout"
115
+ description="Route-friendly grouped navigation, mobile disclosure menu, page actions, and flexible settings content."
116
+ code={settingsCode}
117
+ filename="SettingsPage.tsx"
118
+ >
119
+ <SettingsLayout
120
+ title="Workspace settings"
121
+ description="Manage identity, data collection, and workspace defaults."
122
+ actions={<Button variant="primary" size="sm">Save changes</Button>}
123
+ navigation={<SettingsNav groups={settingsGroups} />}
124
+ >
125
+ <Panel title="Workspace identity" description="Shown in navigation, reports, and invitations." className="space-y-4">
126
+ <Input label="Workspace name" defaultValue="Acme Analytics" />
127
+ <Input label="Workspace URL" defaultValue="app.example.com/acme" />
128
+ </Panel>
129
+ <Panel title="Collection defaults" className="space-y-4">
130
+ <Toggle label="Capture page views automatically" defaultChecked />
131
+ <Toggle label="Mask sensitive text inputs" defaultChecked />
132
+ <div><Badge variant="warning">Restart required</Badge></div>
133
+ </Panel>
134
+ </SettingsLayout>
135
+ </ShowcaseWrapper>
136
+ </>
137
+ );
138
+ }
@@ -1,82 +1,79 @@
1
- import { IconSpinner } from "@/components/icons";
2
- import { ComponentDocs, Progress, ShowcaseWrapper } from "@/components/ui";
1
+ import {
2
+ ComponentDocs,
3
+ Loader,
4
+ LoadingState,
5
+ Progress,
6
+ ShowcaseWrapper,
7
+ Skeleton,
8
+ } from "@/components/ui";
3
9
 
4
10
  export default function ProgressShowcase() {
5
- const code = `import { Progress } from "the-old-ui";
11
+ const code = `import { Loader, LoadingState, Progress, Skeleton } from "@xenide-io/the-old-ui-theme";
6
12
 
7
- <div className="space-y-8">
8
- <div className="ph-panel space-y-6">
9
- <Progress label="Ingest backlog" value={72} color="bg-ph-brand" showPercentage />
10
- <Progress label="Exports" value={45} color="bg-ph-blue" showPercentage />
11
- <Progress label="LLM evaluations" value={28} color="bg-ph-purple" showPercentage />
12
- </div>
13
-
14
- <div className="ph-panel flex flex-wrap items-center gap-10">
15
- <Progress value={72} color="bg-ph-brand" size="lg" className="w-48" />
16
- <Progress value={45} color="bg-ph-blue" size="lg" className="w-48" />
17
- </div>
18
-
19
- <div className="ph-panel flex flex-wrap items-center gap-8">
20
- <span className="ph-loading" role="status" aria-label="Loading" />
21
- <IconSpinner className="h-6 w-6 text-ph-brand" role="status" aria-label="Loading" />
22
- <div className="ph-loading-dots" role="status" aria-label="Loading">
23
- <span />
24
- <span />
25
- <span />
26
- </div>
27
- </div>
13
+ <Progress label="Ingest backlog" value={72} showPercentage />
14
+ <Loader variant="spinner" label="Refreshing insight" />
15
+ <Loader variant="dots" label="Loading events" />
16
+ <Loader variant="pulse" label="Connecting" />
17
+ <LoadingState title="Loading dashboard" description="Restoring the latest results." />
18
+ <div role="status" aria-label="Loading account">
19
+ <Skeleton shape="title" />
20
+ <Skeleton shape="text" />
21
+ <Skeleton shape="short-text" />
28
22
  </div>`;
29
23
 
30
24
  return (
31
25
  <ShowcaseWrapper
32
26
  title="Progress & loading"
33
- description="Linear progress bars, spinners, and skeleton placeholders with semantic colour support."
27
+ description="Determinate progress, compact action loaders, full-region loading states, and structure-matching cold-load skeletons."
34
28
  code={code}
35
- filename="ProgressExample.tsx"
29
+ filename="LoadingExample.tsx"
36
30
  docs={
37
31
  <ComponentDocs
38
32
  rows={[
39
- { name: "value", type: "number", description: "Current value." },
40
- { name: "max", type: "number", defaultValue: "100", description: "Maximum value." },
41
- { name: "color", type: "string", defaultValue: "bg-ph-brand", description: "Tailwind colour class for the bar." },
42
- { name: "label", type: "string", description: "Label shown above the bar." },
43
- { name: "showPercentage", type: "boolean", defaultValue: "false", description: "Show percentage text." },
44
- { name: "size", type: "sm | md | lg", defaultValue: "md", description: "Bar height." },
33
+ { name: "Loader.variant", type: "spinner | dots | pulse", defaultValue: "spinner", description: "Indeterminate treatment for compact asynchronous work." },
34
+ { name: "Loader.size", type: "sm | md | lg", defaultValue: "md", description: "Visual loader size." },
35
+ { name: "LoadingState", type: "LoadingStateProps", description: "Centered region state with one accessible status message." },
36
+ { name: "Skeleton.shape", type: "title | text | short-text | avatar | button | block", defaultValue: "text", description: "Decorative skeleton matching expected content geometry." },
37
+ { name: "Progress.value", type: "number", description: "Real determinate progress value." },
45
38
  ]}
46
39
  />
47
40
  }
48
41
  >
49
- <div className="space-y-8">
42
+ <div className="space-y-6">
50
43
  <div className="ph-panel space-y-6">
51
- <h3 className="text-sm font-semibold uppercase tracking-wider text-ph-mutedtext">Linear bars</h3>
44
+ <h3 className="text-sm font-semibold uppercase tracking-wider text-ph-mutedtext">Determinate progress</h3>
52
45
  <Progress label="Ingest backlog" value={72} color="bg-ph-brand" showPercentage />
53
46
  <Progress label="Exports" value={45} color="bg-ph-blue" showPercentage />
54
47
  <Progress label="LLM evaluations" value={28} color="bg-ph-purple" showPercentage />
55
48
  </div>
56
49
 
57
- <div className="ph-panel flex flex-wrap items-center gap-10">
58
- <div className="w-full">
59
- <h3 className="mb-4 text-sm font-semibold uppercase tracking-wider text-ph-mutedtext">Large bars</h3>
60
- <div className="flex gap-10">
61
- <Progress value={72} color="bg-ph-brand" size="lg" className="w-48" />
62
- <Progress value={45} color="bg-ph-blue" size="lg" className="w-48" />
50
+ <div className="grid gap-4 md:grid-cols-3">
51
+ {(["spinner", "dots", "pulse"] as const).map((variant) => (
52
+ <div key={variant} className="ph-panel flex min-h-28 flex-col items-center justify-center gap-3">
53
+ <Loader variant={variant} size="lg" label={`${variant} loader`} />
54
+ <code className="ph-kbd">{variant}</code>
63
55
  </div>
64
- </div>
56
+ ))}
65
57
  </div>
66
58
 
67
- <div className="ph-panel flex flex-wrap items-center gap-8">
68
- <div className="flex items-center gap-3 text-ph-subtle">
69
- <span className="ph-loading" role="status" aria-label="Loading" />
70
- <code className="ph-kbd">ph-loading</code> — CSS spinner
59
+ <div className="grid gap-4 lg:grid-cols-2">
60
+ <div className="ph-panel p-0">
61
+ <LoadingState
62
+ label="Loading dashboard"
63
+ title="Loading dashboard"
64
+ description="Restoring the latest results without pretending progress is measurable."
65
+ />
71
66
  </div>
72
- <div className="flex items-center gap-3 text-ph-subtle">
73
- <IconSpinner className="h-6 w-6 text-ph-brand" role="status" aria-label="Loading" />
74
- <code className="ph-kbd">IconSpinner</code> — Animated icon
75
- </div>
76
- <div className="ph-loading-dots" role="status" aria-label="Loading">
77
- <span />
78
- <span />
79
- <span />
67
+ <div className="ph-panel" role="status" aria-label="Loading account summary">
68
+ <div className="mb-5 flex items-center gap-3">
69
+ <Skeleton shape="avatar" />
70
+ <div className="min-w-0 flex-1">
71
+ <Skeleton shape="title" className="mb-2 w-1/2" />
72
+ <Skeleton shape="short-text" className="w-2/3" />
73
+ </div>
74
+ </div>
75
+ <Skeleton shape="block" className="mb-4" />
76
+ <div className="flex justify-end"><Skeleton shape="button" /></div>
80
77
  </div>
81
78
  </div>
82
79
  </div>
@@ -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,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,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
+ }
@@ -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";
@@ -1,4 +1,4 @@
1
- import type { ButtonHTMLAttributes, ReactNode } from "react";
1
+ import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react";
2
2
  import { ButtonChrome } from "@/components/ui/ButtonChrome";
3
3
  import { cn } from "@/lib/cn";
4
4
 
@@ -29,6 +29,8 @@ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
29
29
  icon?: ReactNode;
30
30
  sideIcon?: ReactNode;
31
31
  children?: ReactNode;
32
+ loading?: boolean;
33
+ loadingLabel?: ReactNode;
32
34
  }
33
35
 
34
36
  const variantClass: Record<ButtonVariant, string> = {
@@ -60,11 +62,8 @@ const shapeClass: Record<ButtonShape, string> = {
60
62
  wide: "ph-btn-wide",
61
63
  };
62
64
 
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({
65
+ /** Filled variants keep a stationary depth plate while the face and content move together. */
66
+ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button({
68
67
  variant = "secondary",
69
68
  size = "md",
70
69
  shape = "default",
@@ -72,19 +71,24 @@ export function Button({
72
71
  split = false,
73
72
  icon,
74
73
  sideIcon,
74
+ loading = false,
75
+ loadingLabel,
75
76
  className,
76
77
  children,
77
78
  type = "button",
79
+ disabled,
78
80
  ...props
79
- }: ButtonProps) {
80
- const usesLemonChrome = variant === "primary" || variant === "secondary";
81
+ }, ref) {
82
+ const usesLemonChrome = !["tertiary", "ghost", "link"].includes(variant);
83
+ const label = loadingLabel ?? children;
84
+ const leadingIcon = loading ? <span className="ph-btn-spinner" aria-hidden /> : icon;
81
85
 
82
86
  const content = usesLemonChrome ? (
83
- <ButtonChrome label={children} icon={icon} sideIcon={sideIcon} split={split} />
87
+ <ButtonChrome label={label} icon={leadingIcon} sideIcon={sideIcon} split={split} />
84
88
  ) : (
85
89
  <>
86
- {icon != null ? <span className="ph-btn-icon">{icon}</span> : null}
87
- {children != null ? <span className="ph-btn-label">{children}</span> : null}
90
+ {leadingIcon != null ? <span className="ph-btn-icon">{leadingIcon}</span> : null}
91
+ {label != null ? <span className="ph-btn-label">{label}</span> : null}
88
92
  {split ? <span className="ph-btn-split-divider" aria-hidden /> : null}
89
93
  {sideIcon != null ? <span className="ph-btn-side-icon">{sideIcon}</span> : null}
90
94
  </>
@@ -92,9 +96,13 @@ export function Button({
92
96
 
93
97
  return (
94
98
  <button
99
+ ref={ref}
95
100
  type={type}
101
+ disabled={disabled || loading}
102
+ aria-busy={loading || undefined}
96
103
  className={cn(
97
104
  "ph-btn",
105
+ usesLemonChrome && "ph-btn-raised",
98
106
  outline && "ph-btn-outline",
99
107
  variantClass[variant],
100
108
  sizeClass[size],
@@ -107,4 +115,6 @@ export function Button({
107
115
  {content}
108
116
  </button>
109
117
  );
110
- }
118
+ });
119
+
120
+ Button.displayName = "Button";
@@ -7,7 +7,7 @@ export interface ButtonChromeProps {
7
7
  split?: boolean;
8
8
  }
9
9
 
10
- /** Inner face for Lemon primary/secondary keeps labels above ::after ridge. */
10
+ /** Content layer for tactile variants; CSS moves it with the independently rendered face. */
11
11
  export function ButtonChrome({ label, icon, sideIcon, split }: ButtonChromeProps) {
12
12
  return (
13
13
  <span className="ph-btn-chrome">