@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.
- package/README.md +1 -1
- package/dist/{chunk-SHF57J7U.mjs → chunk-FDMD3IIN.mjs} +1573 -652
- package/dist/index-D1RTT2Ap.d.mts +1044 -0
- package/dist/index-D1RTT2Ap.d.ts +1044 -0
- package/dist/index.d.mts +10 -7
- package/dist/index.d.ts +10 -7
- package/dist/index.js +1705 -744
- package/dist/index.mjs +61 -1
- package/dist/tailwind-preset.js +7 -0
- package/dist/tailwind-preset.mjs +7 -0
- package/dist/ui.d.mts +3 -2
- package/dist/ui.d.ts +3 -2
- package/dist/ui.js +1584 -666
- package/dist/ui.mjs +31 -1
- package/package.json +18 -4
- package/src/app/globals.css +1005 -227
- package/src/components/icons/IconBase.tsx +20 -12
- package/src/components/icons/createIconBase.tsx +12 -7
- package/src/components/icons/icons.tsx +208 -18
- package/src/components/sections/AlertShowcase.tsx +2 -1
- package/src/components/sections/BadgeShowcase.tsx +1 -1
- package/src/components/sections/ButtonShowcase.tsx +6 -10
- package/src/components/sections/CardShowcase.tsx +22 -6
- package/src/components/sections/DropdownShowcase.tsx +4 -6
- package/src/components/sections/FilterChipsShowcase.tsx +74 -26
- package/src/components/sections/FormShowcase.tsx +2 -1
- package/src/components/sections/IconShowcase.tsx +86 -206
- package/src/components/sections/KbdShowcase.tsx +47 -21
- package/src/components/sections/ModalShowcase.tsx +7 -6
- package/src/components/sections/ProductLayoutsShowcase.tsx +138 -0
- package/src/components/sections/ProgressShowcase.tsx +50 -53
- package/src/components/sections/SwapShowcase.tsx +13 -5
- package/src/components/ui/Alert.tsx +28 -19
- package/src/components/ui/AuthLayout.tsx +58 -0
- package/src/components/ui/Badge.tsx +10 -5
- package/src/components/ui/Button.tsx +22 -12
- package/src/components/ui/ButtonChrome.tsx +1 -1
- package/src/components/ui/Calendar.tsx +6 -7
- package/src/components/ui/Card.tsx +21 -11
- package/src/components/ui/CollapsibleSection.tsx +11 -11
- package/src/components/ui/DropdownMenu.tsx +189 -65
- package/src/components/ui/FilterBar.tsx +165 -0
- package/src/components/ui/FilterChips.tsx +65 -35
- package/src/components/ui/FilterControls.tsx +30 -0
- package/src/components/ui/FormField.tsx +69 -0
- package/src/components/ui/Input.tsx +166 -140
- package/src/components/ui/Kbd.tsx +88 -13
- package/src/components/ui/Loader.tsx +96 -0
- package/src/components/ui/Modal.tsx +66 -36
- package/src/components/ui/SettingsLayout.tsx +94 -0
- package/src/components/ui/ShowcaseWrapper.tsx +4 -16
- package/src/components/ui/Stepper.tsx +5 -6
- package/src/components/ui/first-slice.test.tsx +98 -0
- package/src/components/ui/index.ts +62 -3
- 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 +411 -1
- package/tailwind-preset.ts +7 -0
- package/dist/index-CmS6hcUk.d.mts +0 -753
- 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 {
|
|
2
|
-
|
|
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
|
-
<
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
<
|
|
15
|
-
|
|
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="
|
|
27
|
+
description="Determinate progress, compact action loaders, full-region loading states, and structure-matching cold-load skeletons."
|
|
34
28
|
code={code}
|
|
35
|
-
filename="
|
|
29
|
+
filename="LoadingExample.tsx"
|
|
36
30
|
docs={
|
|
37
31
|
<ComponentDocs
|
|
38
32
|
rows={[
|
|
39
|
-
{ name: "
|
|
40
|
-
{ name: "
|
|
41
|
-
{ name: "
|
|
42
|
-
{ name: "
|
|
43
|
-
{ name: "
|
|
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-
|
|
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">
|
|
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="
|
|
58
|
-
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
<
|
|
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
|
-
|
|
56
|
+
))}
|
|
65
57
|
</div>
|
|
66
58
|
|
|
67
|
-
<div className="
|
|
68
|
-
<div className="
|
|
69
|
-
<
|
|
70
|
-
|
|
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="
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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-
|
|
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-
|
|
94
|
+
onClassName="text-ph-warning"
|
|
87
95
|
/>
|
|
88
96
|
</div>
|
|
89
97
|
</ShowcaseWrapper>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { IconCancel, IconCheckCircle,
|
|
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
|
|
18
|
-
info: { icon:
|
|
19
|
-
success: { icon: IconCheckCircle
|
|
20
|
-
warning: { icon: IconExclamation
|
|
21
|
-
danger: { icon: IconCancel
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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="
|
|
59
|
-
aria-label=
|
|
67
|
+
className="ph-alert-dismiss"
|
|
68
|
+
aria-label={dismissLabel}
|
|
60
69
|
>
|
|
61
|
-
<
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
}
|
|
80
|
-
const usesLemonChrome =
|
|
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={
|
|
87
|
+
<ButtonChrome label={label} icon={leadingIcon} sideIcon={sideIcon} split={split} />
|
|
84
88
|
) : (
|
|
85
89
|
<>
|
|
86
|
-
{
|
|
87
|
-
{
|
|
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
|
-
/**
|
|
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">
|