@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.
- package/README.md +1 -1
- package/dist/chunk-5HSOBJ4F.mjs +5968 -0
- package/dist/index-BgG8Homu.d.mts +1461 -0
- package/dist/index-BgG8Homu.d.ts +1461 -0
- package/dist/index.d.mts +206 -100
- package/dist/index.d.ts +206 -100
- package/dist/index.js +4275 -1455
- package/dist/index.mjs +689 -735
- 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 +3449 -701
- package/dist/ui.mjs +106 -3
- package/package.json +20 -16
- package/src/app/globals.css +1953 -266
- package/src/components/charts/index.ts +9 -8
- package/src/components/charts/quill/BarChart.tsx +85 -0
- package/src/components/charts/quill/FunnelChart.tsx +49 -0
- package/src/components/charts/quill/InsightShell.tsx +27 -0
- package/src/components/charts/quill/MetricCard.tsx +44 -0
- package/src/components/charts/quill/PieChart.tsx +81 -0
- package/src/components/charts/quill/Sparkline.tsx +57 -0
- package/src/components/charts/quill/TimeSeriesLineChart.tsx +62 -0
- package/src/components/charts/quill/index.ts +9 -0
- package/src/components/icons/IconBase.tsx +20 -12
- package/src/components/icons/createIconBase.tsx +12 -7
- package/src/components/icons/icons.tsx +210 -18
- package/src/components/icons/index.ts +3 -0
- package/src/components/icons/lemon-brand-icons.tsx +16 -0
- package/src/components/icons/lemon-category-icons.tsx +72 -0
- package/src/components/icons/lemon-icons.tsx +57 -0
- 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 +195 -212
- package/src/components/sections/KbdShowcase.tsx +47 -21
- package/src/components/sections/ModalShowcase.tsx +7 -6
- package/src/components/sections/NewComponentsShowcase.tsx +1052 -0
- package/src/components/sections/ProductLayoutsShowcase.tsx +78 -0
- package/src/components/sections/ProgressShowcase.tsx +50 -53
- package/src/components/sections/QuillChartShowcase.tsx +92 -0
- package/src/components/sections/QuillDashboardShowcase.tsx +149 -0
- package/src/components/sections/SidebarShowcase.tsx +151 -0
- package/src/components/sections/SwapShowcase.tsx +13 -5
- package/src/components/sections/TableShowcase.tsx +2 -0
- package/src/components/sections/TooltipShowcase.tsx +19 -25
- package/src/components/ui/Alert.tsx +28 -19
- package/src/components/ui/AlertDialog.tsx +62 -0
- package/src/components/ui/AppLayout.tsx +27 -0
- package/src/components/ui/AuthLayout.tsx +58 -0
- package/src/components/ui/Autocomplete.tsx +106 -0
- package/src/components/ui/Badge.tsx +10 -5
- package/src/components/ui/Banner.tsx +63 -0
- package/src/components/ui/Button.tsx +22 -12
- package/src/components/ui/ButtonChrome.tsx +1 -1
- package/src/components/ui/ButtonGroup.tsx +16 -0
- package/src/components/ui/Calendar.tsx +6 -7
- package/src/components/ui/Card.tsx +21 -11
- package/src/components/ui/Chip.tsx +46 -0
- package/src/components/ui/Collapsible.tsx +46 -0
- package/src/components/ui/CollapsibleSection.tsx +11 -11
- package/src/components/ui/Combobox.tsx +119 -0
- package/src/components/ui/ContextMenu.tsx +81 -0
- package/src/components/ui/DataTable.tsx +133 -0
- package/src/components/ui/DatePicker.tsx +96 -0
- package/src/components/ui/Dialog.tsx +75 -0
- package/src/components/ui/Divider.tsx +39 -0
- package/src/components/ui/Dot.tsx +32 -0
- 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/InputGroup.tsx +16 -0
- package/src/components/ui/Kbd.tsx +88 -13
- package/src/components/ui/Lettermark.tsx +47 -0
- package/src/components/ui/Link.tsx +32 -0
- package/src/components/ui/Loader.tsx +96 -0
- package/src/components/ui/LoadingBar.tsx +25 -0
- package/src/components/ui/Menubar.tsx +66 -0
- package/src/components/ui/Metric.tsx +34 -0
- package/src/components/ui/Modal.tsx +66 -36
- package/src/components/ui/NumberField.tsx +76 -0
- package/src/components/ui/Popover.tsx +73 -0
- package/src/components/ui/ProgressCircle.tsx +63 -0
- package/src/components/ui/Resizable.tsx +71 -0
- package/src/components/ui/Row.tsx +51 -0
- package/src/components/ui/ScrollArea.tsx +54 -0
- package/src/components/ui/SettingsLayout.tsx +94 -0
- package/src/components/ui/ShowcaseWrapper.tsx +4 -16
- package/src/components/ui/Sidebar.tsx +77 -0
- package/src/components/ui/Snack.tsx +48 -0
- package/src/components/ui/Spinner.tsx +30 -0
- package/src/components/ui/Splotch.tsx +33 -0
- package/src/components/ui/Stepper.tsx +5 -6
- package/src/components/ui/Table.tsx +2 -0
- package/src/components/ui/Tag.tsx +65 -0
- package/src/components/ui/Toggle.tsx +62 -0
- package/src/components/ui/Tooltip.test.tsx +58 -0
- package/src/components/ui/Tooltip.tsx +211 -0
- package/src/components/ui/Widget.tsx +31 -0
- package/src/components/ui/first-slice.test.tsx +98 -0
- package/src/components/ui/index.ts +335 -22
- 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/chunk-SHF57J7U.mjs +0 -2910
- package/dist/index-CmS6hcUk.d.mts +0 -753
- package/dist/index-CmS6hcUk.d.ts +0 -753
- package/src/components/charts/ph-insight-charts.tsx +0 -162
- package/src/components/dashboard/DashboardFiltersBar.tsx +0 -19
- package/src/components/dashboard/DashboardGrid.tsx +0 -23
- package/src/components/dashboard/DashboardInsightPlaceholder.tsx +0 -37
- package/src/components/dashboard/DashboardKpiCard.tsx +0 -25
- package/src/components/dashboard/DashboardToolbar.tsx +0 -23
- package/src/components/dashboard/DashboardWell.tsx +0 -10
- package/src/components/dashboard/InsightMiniCard.tsx +0 -26
- package/src/components/dashboard/InsightShell.tsx +0 -37
- package/src/components/dashboard/InsightShellHeader.tsx +0 -22
- package/src/components/dashboard/MiniTrendBars.tsx +0 -51
- package/src/components/dashboard/index.ts +0 -31
- package/src/components/dashboard/types.ts +0 -9
- package/src/components/sections/ChartShowcase.tsx +0 -190
- package/src/components/sections/DashboardShowcase.tsx +0 -191
- 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-
|
|
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,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
|
-
<
|
|
7
|
-
<div className="flex flex-wrap items-center gap-10">
|
|
8
|
-
<
|
|
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
|
-
|
|
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
|
-
<
|
|
16
|
-
Dashed chrome
|
|
17
|
-
</
|
|
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
|
-
</
|
|
16
|
+
</TooltipProvider>`;
|
|
20
17
|
|
|
21
18
|
return (
|
|
22
19
|
<ShowcaseWrapper
|
|
23
20
|
title="Tooltips"
|
|
24
|
-
description="
|
|
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
|
-
<
|
|
29
|
-
<div className="flex flex-wrap items-center gap-10">
|
|
30
|
-
<
|
|
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
|
-
|
|
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
|
-
<
|
|
38
|
-
Dashed chrome
|
|
39
|
-
</
|
|
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
|
-
</
|
|
35
|
+
</TooltipProvider>
|
|
42
36
|
</ShowcaseWrapper>
|
|
43
37
|
);
|
|
44
38
|
}
|
|
@@ -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,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
|
|
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";
|
|
@@ -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";
|