@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
|
@@ -2,37 +2,112 @@ import type { HTMLAttributes, ReactNode } from "react";
|
|
|
2
2
|
import { cn } from "@/lib/cn";
|
|
3
3
|
|
|
4
4
|
export type KbdVariant = "shortcut" | "key" | "token";
|
|
5
|
+
export type KbdPlatform = "text" | "mac" | "windows";
|
|
5
6
|
|
|
6
7
|
export interface KbdProps extends HTMLAttributes<HTMLElement> {
|
|
7
8
|
variant?: KbdVariant;
|
|
8
|
-
keys?: ReactNode[];
|
|
9
|
+
keys?: readonly ReactNode[];
|
|
10
|
+
platform?: KbdPlatform;
|
|
11
|
+
separator?: ReactNode;
|
|
9
12
|
children?: ReactNode;
|
|
10
13
|
}
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
const platformLabels: Record<KbdPlatform, Record<string, string>> = {
|
|
16
|
+
text: {
|
|
17
|
+
command: "Command",
|
|
18
|
+
cmd: "Command",
|
|
19
|
+
option: "Option",
|
|
20
|
+
alt: "Alt",
|
|
21
|
+
control: "Control",
|
|
22
|
+
ctrl: "Control",
|
|
23
|
+
shift: "Shift",
|
|
24
|
+
},
|
|
25
|
+
mac: {
|
|
26
|
+
command: "⌘",
|
|
27
|
+
cmd: "⌘",
|
|
28
|
+
option: "⌥",
|
|
29
|
+
alt: "⌥",
|
|
30
|
+
control: "⌃",
|
|
31
|
+
ctrl: "⌃",
|
|
32
|
+
shift: "⇧",
|
|
33
|
+
},
|
|
34
|
+
windows: {
|
|
35
|
+
command: "Win",
|
|
36
|
+
cmd: "Win",
|
|
37
|
+
option: "Alt",
|
|
38
|
+
alt: "Alt",
|
|
39
|
+
control: "Ctrl",
|
|
40
|
+
ctrl: "Ctrl",
|
|
41
|
+
shift: "Shift",
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const spokenLabels: Record<string, string> = {
|
|
46
|
+
"⌘": "Command",
|
|
47
|
+
"⌥": "Option",
|
|
48
|
+
"⌃": "Control",
|
|
49
|
+
"⇧": "Shift",
|
|
50
|
+
"↵": "Enter",
|
|
51
|
+
"⏎": "Enter",
|
|
52
|
+
"⌫": "Backspace",
|
|
53
|
+
"⎋": "Escape",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function Kbd({
|
|
57
|
+
variant = "shortcut",
|
|
58
|
+
keys,
|
|
59
|
+
platform = "text",
|
|
60
|
+
separator,
|
|
61
|
+
children,
|
|
62
|
+
className,
|
|
63
|
+
"aria-label": ariaLabel,
|
|
64
|
+
...props
|
|
65
|
+
}: KbdProps) {
|
|
13
66
|
if (variant === "token") {
|
|
14
67
|
return (
|
|
15
|
-
<code className={cn("ph-
|
|
68
|
+
<code className={cn("ph-code-token ph-kbd", className)} aria-label={ariaLabel} {...props}>
|
|
16
69
|
{children}
|
|
17
70
|
</code>
|
|
18
71
|
);
|
|
19
72
|
}
|
|
20
73
|
|
|
21
|
-
const
|
|
74
|
+
const sourceKeys = keys?.length ? keys : children != null ? [children] : [];
|
|
75
|
+
const displayKeys = sourceKeys.map((key) => formatKey(key, platform));
|
|
76
|
+
const accessibleLabel = ariaLabel ?? getShortcutLabel(displayKeys);
|
|
77
|
+
|
|
78
|
+
if (variant === "key") {
|
|
79
|
+
return (
|
|
80
|
+
<kbd className={cn("ph-keycap", className)} aria-label={accessibleLabel} {...props}>
|
|
81
|
+
{displayKeys[0]}
|
|
82
|
+
</kbd>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
22
85
|
|
|
23
86
|
return (
|
|
24
|
-
<kbd className={cn("ph-shortcut", className)} {...props}>
|
|
25
|
-
{
|
|
87
|
+
<kbd className={cn("ph-shortcut-group ph-keycap", className)} aria-label={accessibleLabel} {...props}>
|
|
88
|
+
{displayKeys.map((key, index) => (
|
|
89
|
+
<span key={index} className="contents">
|
|
90
|
+
{index > 0 && separator != null ? <span className="ph-shortcut-separator" aria-hidden="true">{separator}</span> : null}
|
|
91
|
+
<span className="ph-shortcut-key" aria-hidden={accessibleLabel ? true : undefined}>{key}</span>
|
|
92
|
+
</span>
|
|
93
|
+
))}
|
|
26
94
|
</kbd>
|
|
27
95
|
);
|
|
28
96
|
}
|
|
29
97
|
|
|
30
|
-
function
|
|
31
|
-
if (typeof
|
|
98
|
+
function formatKey(key: ReactNode, platform: KbdPlatform) {
|
|
99
|
+
if (typeof key !== "string") return key;
|
|
100
|
+
return platformLabels[platform][key.toLowerCase()] ?? key;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function getShortcutLabel(keys: ReactNode[]) {
|
|
104
|
+
const labels = keys.map((key) => {
|
|
105
|
+
if (typeof key !== "string" && typeof key !== "number") return null;
|
|
106
|
+
const value = String(key);
|
|
107
|
+
return spokenLabels[value] ?? value;
|
|
108
|
+
});
|
|
32
109
|
|
|
33
|
-
return
|
|
34
|
-
.
|
|
35
|
-
|
|
36
|
-
.replace(/\bShift\b/g, "⇧")
|
|
37
|
-
.replace(/\bControl\b|\bCtrl\b/g, "⌃");
|
|
110
|
+
return labels.every((label): label is string => label != null)
|
|
111
|
+
? labels.join(" + ")
|
|
112
|
+
: undefined;
|
|
38
113
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface LettermarkProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
name?: string | number | null;
|
|
6
|
+
index?: number;
|
|
7
|
+
rounded?: boolean;
|
|
8
|
+
outlined?: boolean;
|
|
9
|
+
size?: "xs" | "sm" | "md" | "lg";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const Lettermark = forwardRef<HTMLDivElement, LettermarkProps>(function Lettermark({
|
|
13
|
+
name,
|
|
14
|
+
index,
|
|
15
|
+
outlined = false,
|
|
16
|
+
rounded = false,
|
|
17
|
+
size = "md",
|
|
18
|
+
className,
|
|
19
|
+
...props
|
|
20
|
+
}, ref) {
|
|
21
|
+
const letter = name
|
|
22
|
+
? typeof name === "number"
|
|
23
|
+
? String(Math.floor(name))
|
|
24
|
+
: String.fromCodePoint(name.codePointAt(0)!).toLocaleUpperCase()
|
|
25
|
+
: "?";
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
ref={ref}
|
|
30
|
+
className={cn(
|
|
31
|
+
"ph-lettermark",
|
|
32
|
+
`ph-lettermark--${size}`,
|
|
33
|
+
typeof index === "number" && `ph-lettermark--v-${(index % 8) + 1}`,
|
|
34
|
+
outlined && "ph-lettermark--outlined",
|
|
35
|
+
rounded && "ph-lettermark--rounded",
|
|
36
|
+
letter === "?" && "ph-lettermark--unknown",
|
|
37
|
+
className
|
|
38
|
+
)}
|
|
39
|
+
aria-hidden="true"
|
|
40
|
+
{...props}
|
|
41
|
+
>
|
|
42
|
+
{letter}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
Lettermark.displayName = "Lettermark";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { forwardRef, type AnchorHTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link({
|
|
10
|
+
className,
|
|
11
|
+
disabled = false,
|
|
12
|
+
children,
|
|
13
|
+
...props
|
|
14
|
+
}, ref) {
|
|
15
|
+
return (
|
|
16
|
+
<a
|
|
17
|
+
ref={ref}
|
|
18
|
+
className={cn(
|
|
19
|
+
"ph-link",
|
|
20
|
+
disabled && "ph-link--disabled pointer-events-none opacity-50",
|
|
21
|
+
className
|
|
22
|
+
)}
|
|
23
|
+
tabIndex={disabled ? -1 : undefined}
|
|
24
|
+
aria-disabled={disabled || undefined}
|
|
25
|
+
{...props}
|
|
26
|
+
>
|
|
27
|
+
{children}
|
|
28
|
+
</a>
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
Link.displayName = "Link";
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export type LoaderVariant = "spinner" | "dots" | "pulse";
|
|
5
|
+
export type LoaderSize = "sm" | "md" | "lg";
|
|
6
|
+
export type SkeletonShape = "title" | "text" | "short-text" | "avatar" | "button" | "block";
|
|
7
|
+
|
|
8
|
+
export interface LoaderProps extends Omit<ComponentPropsWithoutRef<"span">, "children"> {
|
|
9
|
+
variant?: LoaderVariant;
|
|
10
|
+
size?: LoaderSize;
|
|
11
|
+
label?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SkeletonProps extends ComponentPropsWithoutRef<"span"> {
|
|
15
|
+
shape?: SkeletonShape;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface LoadingStateProps extends Omit<ComponentPropsWithoutRef<"div">, "title"> {
|
|
19
|
+
label?: string;
|
|
20
|
+
title?: ReactNode;
|
|
21
|
+
description?: ReactNode;
|
|
22
|
+
variant?: LoaderVariant;
|
|
23
|
+
size?: LoaderSize;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const loaderSizeClass: Record<LoaderSize, string> = {
|
|
27
|
+
sm: "ph-loader-sm",
|
|
28
|
+
md: "ph-loader-md",
|
|
29
|
+
lg: "ph-loader-lg",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const skeletonShapeClass: Record<SkeletonShape, string> = {
|
|
33
|
+
title: "ph-skeleton-title",
|
|
34
|
+
text: "ph-skeleton-text",
|
|
35
|
+
"short-text": "ph-skeleton-text-short",
|
|
36
|
+
avatar: "ph-skeleton-avatar",
|
|
37
|
+
button: "ph-skeleton-button",
|
|
38
|
+
block: "ph-skeleton-block",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function Loader({
|
|
42
|
+
variant = "spinner",
|
|
43
|
+
size = "md",
|
|
44
|
+
label = "Loading",
|
|
45
|
+
className,
|
|
46
|
+
...props
|
|
47
|
+
}: LoaderProps) {
|
|
48
|
+
return (
|
|
49
|
+
<span
|
|
50
|
+
role="status"
|
|
51
|
+
aria-label={label}
|
|
52
|
+
className={cn("ph-loader", loaderSizeClass[size], className)}
|
|
53
|
+
{...props}
|
|
54
|
+
>
|
|
55
|
+
{variant === "spinner" ? <span className="ph-loading" aria-hidden /> : null}
|
|
56
|
+
{variant === "dots" ? (
|
|
57
|
+
<span className="ph-loading-dots" aria-hidden>
|
|
58
|
+
<span />
|
|
59
|
+
<span />
|
|
60
|
+
<span />
|
|
61
|
+
</span>
|
|
62
|
+
) : null}
|
|
63
|
+
{variant === "pulse" ? <span className="ph-loading-pulse" aria-hidden /> : null}
|
|
64
|
+
</span>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function Skeleton({ shape = "text", className, ...props }: SkeletonProps) {
|
|
69
|
+
return (
|
|
70
|
+
<span
|
|
71
|
+
className={cn("ph-skeleton", skeletonShapeClass[shape], className)}
|
|
72
|
+
aria-hidden="true"
|
|
73
|
+
{...props}
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function LoadingState({
|
|
79
|
+
label = "Loading",
|
|
80
|
+
title = "Loading",
|
|
81
|
+
description,
|
|
82
|
+
variant = "spinner",
|
|
83
|
+
size = "lg",
|
|
84
|
+
className,
|
|
85
|
+
...props
|
|
86
|
+
}: LoadingStateProps) {
|
|
87
|
+
return (
|
|
88
|
+
<div className={cn("ph-loading-state", className)} role="status" aria-label={label} {...props}>
|
|
89
|
+
<Loader variant={variant} size={size} label={label} aria-hidden="true" />
|
|
90
|
+
<div className="ph-loading-state__copy">
|
|
91
|
+
<strong>{title}</strong>
|
|
92
|
+
{description ? <span>{description}</span> : null}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface LoadingBarProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
active?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function LoadingBar({ active = true, className, ...props }: LoadingBarProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
className={cn(
|
|
12
|
+
"ph-loading-bar",
|
|
13
|
+
active && "ph-loading-bar--active",
|
|
14
|
+
className
|
|
15
|
+
)}
|
|
16
|
+
role="progressbar"
|
|
17
|
+
aria-label="Loading"
|
|
18
|
+
{...props}
|
|
19
|
+
>
|
|
20
|
+
<div className="ph-loading-bar__fill" />
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
LoadingBar.displayName = "LoadingBar";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect, type ReactNode, type HTMLAttributes } from "react";
|
|
4
|
+
import { cn } from "@/lib/cn";
|
|
5
|
+
|
|
6
|
+
export interface MenubarItem {
|
|
7
|
+
label: string;
|
|
8
|
+
items?: { label: string; onClick?: () => void; disabled?: boolean; separator?: boolean }[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface MenubarProps extends HTMLAttributes<HTMLDivElement> {
|
|
12
|
+
items: MenubarItem[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Menubar({ items, className, ...props }: MenubarProps) {
|
|
16
|
+
const [openIndex, setOpenIndex] = useState(-1);
|
|
17
|
+
const barRef = useRef<HTMLDivElement>(null);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (openIndex < 0) return;
|
|
21
|
+
const handle = (e: MouseEvent) => {
|
|
22
|
+
if (barRef.current && !barRef.current.contains(e.target as Node)) {
|
|
23
|
+
setOpenIndex(-1);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
document.addEventListener("mousedown", handle);
|
|
27
|
+
return () => document.removeEventListener("mousedown", handle);
|
|
28
|
+
}, [openIndex]);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div ref={barRef} className={cn("ph-menubar", className)} {...props}>
|
|
32
|
+
{items.map((item, i) => (
|
|
33
|
+
<div key={i} className="ph-menubar__item">
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
onClick={() => setOpenIndex(openIndex === i ? -1 : i)}
|
|
37
|
+
className={cn("ph-menubar__trigger", openIndex === i && "ph-menubar__trigger--open")}
|
|
38
|
+
>
|
|
39
|
+
{item.label}
|
|
40
|
+
</button>
|
|
41
|
+
{openIndex === i && item.items && (
|
|
42
|
+
<div className="ph-menubar__dropdown">
|
|
43
|
+
{item.items.map((sub, j) =>
|
|
44
|
+
sub.separator ? (
|
|
45
|
+
<div key={j} className="ph-menubar__separator" />
|
|
46
|
+
) : (
|
|
47
|
+
<button
|
|
48
|
+
key={j}
|
|
49
|
+
type="button"
|
|
50
|
+
disabled={sub.disabled}
|
|
51
|
+
onClick={() => { sub.onClick?.(); setOpenIndex(-1); }}
|
|
52
|
+
className="ph-menubar__sub-item"
|
|
53
|
+
>
|
|
54
|
+
{sub.label}
|
|
55
|
+
</button>
|
|
56
|
+
)
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
))}
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
Menubar.displayName = "Menubar";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type ReactNode, type HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface MetricProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
value: string | number;
|
|
6
|
+
label: string;
|
|
7
|
+
badge?: ReactNode;
|
|
8
|
+
trend?: ReactNode;
|
|
9
|
+
icon?: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Metric({
|
|
13
|
+
value,
|
|
14
|
+
label,
|
|
15
|
+
badge,
|
|
16
|
+
trend,
|
|
17
|
+
icon,
|
|
18
|
+
className,
|
|
19
|
+
...props
|
|
20
|
+
}: MetricProps) {
|
|
21
|
+
return (
|
|
22
|
+
<div className={cn("ph-metric", className)} {...props}>
|
|
23
|
+
<div className="ph-metric__header">
|
|
24
|
+
{icon && <span className="ph-metric__icon">{icon}</span>}
|
|
25
|
+
<span className="ph-metric__label">{label}</span>
|
|
26
|
+
{badge && <span className="ph-metric__badge">{badge}</span>}
|
|
27
|
+
</div>
|
|
28
|
+
<div className="ph-metric__value">{value}</div>
|
|
29
|
+
{trend && <div className="ph-metric__trend">{trend}</div>}
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
Metric.displayName = "Metric";
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as Dialog from "@radix-ui/react-dialog";
|
|
4
|
+
import { useRef, type ReactNode } from "react";
|
|
2
5
|
import { IconClose } from "@/components/icons";
|
|
3
6
|
import { cn } from "@/lib/cn";
|
|
4
7
|
|
|
@@ -14,6 +17,9 @@ export interface ModalProps {
|
|
|
14
17
|
size?: ModalSize;
|
|
15
18
|
className?: string;
|
|
16
19
|
showCloseButton?: boolean;
|
|
20
|
+
closeOnOutsideClick?: boolean;
|
|
21
|
+
closeOnEscape?: boolean;
|
|
22
|
+
"aria-label"?: string;
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
const sizeMap: Record<ModalSize, string> = {
|
|
@@ -21,7 +27,7 @@ const sizeMap: Record<ModalSize, string> = {
|
|
|
21
27
|
md: "max-w-lg",
|
|
22
28
|
lg: "max-w-2xl",
|
|
23
29
|
xl: "max-w-4xl",
|
|
24
|
-
full: "max-w-
|
|
30
|
+
full: "max-w-none",
|
|
25
31
|
};
|
|
26
32
|
|
|
27
33
|
export function Modal({
|
|
@@ -34,42 +40,66 @@ export function Modal({
|
|
|
34
40
|
size = "md",
|
|
35
41
|
className,
|
|
36
42
|
showCloseButton = true,
|
|
43
|
+
closeOnOutsideClick = true,
|
|
44
|
+
closeOnEscape = true,
|
|
45
|
+
"aria-label": ariaLabel,
|
|
37
46
|
}: ModalProps) {
|
|
38
|
-
|
|
47
|
+
const canDismiss = Boolean(onClose);
|
|
48
|
+
const restoreFocusRef = useRef<HTMLElement | null>(null);
|
|
39
49
|
|
|
40
50
|
return (
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
51
|
+
<Dialog.Root
|
|
52
|
+
open={open}
|
|
53
|
+
onOpenChange={(nextOpen) => {
|
|
54
|
+
if (!nextOpen) onClose?.();
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
<Dialog.Portal>
|
|
58
|
+
<Dialog.Overlay className="ph-modal-overlay" />
|
|
59
|
+
<Dialog.Content
|
|
60
|
+
className={cn("ph-modal-panel", sizeMap[size], className)}
|
|
61
|
+
aria-label={ariaLabel}
|
|
62
|
+
{...(description ? {} : { "aria-describedby": undefined })}
|
|
63
|
+
onOpenAutoFocus={() => {
|
|
64
|
+
restoreFocusRef.current = document.activeElement instanceof HTMLElement
|
|
65
|
+
? document.activeElement
|
|
66
|
+
: null;
|
|
67
|
+
}}
|
|
68
|
+
onCloseAutoFocus={(event) => {
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
restoreFocusRef.current?.focus();
|
|
71
|
+
}}
|
|
72
|
+
onEscapeKeyDown={(event) => {
|
|
73
|
+
if (!canDismiss || !closeOnEscape) event.preventDefault();
|
|
74
|
+
}}
|
|
75
|
+
onPointerDownOutside={(event) => {
|
|
76
|
+
if (!canDismiss || !closeOnOutsideClick) event.preventDefault();
|
|
77
|
+
}}
|
|
78
|
+
>
|
|
79
|
+
{title ? null : <Dialog.Title className="sr-only">{ariaLabel ?? "Dialog"}</Dialog.Title>}
|
|
80
|
+
{(title || description || (showCloseButton && canDismiss)) ? (
|
|
81
|
+
<header className="ph-modal-header">
|
|
82
|
+
<div className="min-w-0">
|
|
83
|
+
{title ? <Dialog.Title className="text-lg font-bold text-ph-ink">{title}</Dialog.Title> : null}
|
|
84
|
+
{description ? (
|
|
85
|
+
<Dialog.Description className="mt-1 text-sm text-ph-subtle">
|
|
86
|
+
{description}
|
|
87
|
+
</Dialog.Description>
|
|
88
|
+
) : null}
|
|
89
|
+
</div>
|
|
90
|
+
{showCloseButton && canDismiss ? (
|
|
91
|
+
<Dialog.Close asChild>
|
|
92
|
+
<button type="button" className="ph-modal-close" aria-label="Close dialog">
|
|
93
|
+
<IconClose className="h-5 w-5" aria-hidden />
|
|
94
|
+
</button>
|
|
95
|
+
</Dialog.Close>
|
|
96
|
+
) : null}
|
|
97
|
+
</header>
|
|
98
|
+
) : null}
|
|
99
|
+
<div className="ph-modal-body">{children}</div>
|
|
100
|
+
{footer ? <div className="ph-modal-footer">{footer}</div> : null}
|
|
101
|
+
</Dialog.Content>
|
|
102
|
+
</Dialog.Portal>
|
|
103
|
+
</Dialog.Root>
|
|
74
104
|
);
|
|
75
105
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, type InputHTMLAttributes } from "react";
|
|
4
|
+
import { cn } from "@/lib/cn";
|
|
5
|
+
|
|
6
|
+
export interface NumberFieldProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "onChange"> {
|
|
7
|
+
value?: number;
|
|
8
|
+
onChange?: (value: number) => void;
|
|
9
|
+
min?: number;
|
|
10
|
+
max?: number;
|
|
11
|
+
step?: number;
|
|
12
|
+
label?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function NumberField({
|
|
16
|
+
value: controlledValue,
|
|
17
|
+
onChange,
|
|
18
|
+
min,
|
|
19
|
+
max,
|
|
20
|
+
step = 1,
|
|
21
|
+
label,
|
|
22
|
+
className,
|
|
23
|
+
disabled,
|
|
24
|
+
defaultValue,
|
|
25
|
+
...inputProps
|
|
26
|
+
}: NumberFieldProps) {
|
|
27
|
+
const [internal, setInternal] = useState(controlledValue ?? 0);
|
|
28
|
+
const value = controlledValue ?? internal;
|
|
29
|
+
|
|
30
|
+
const set = (v: number) => {
|
|
31
|
+
const clamped = Math.max(min ?? -Infinity, Math.min(max ?? Infinity, v));
|
|
32
|
+
setInternal(clamped);
|
|
33
|
+
onChange?.(clamped);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className={cn("ph-number-field", className)}>
|
|
38
|
+
{label && <label className="ph-label">{label}</label>}
|
|
39
|
+
<div className="ph-number-field__control">
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
disabled={disabled || (min !== undefined && value <= min)}
|
|
43
|
+
onClick={() => set(value - step)}
|
|
44
|
+
className="ph-number-field__btn"
|
|
45
|
+
aria-label="Decrease"
|
|
46
|
+
>
|
|
47
|
+
<svg width="12" height="2" viewBox="0 0 12 2" fill="currentColor"><rect y="0" width="12" height="2" rx="1" /></svg>
|
|
48
|
+
</button>
|
|
49
|
+
<input
|
|
50
|
+
type="number"
|
|
51
|
+
value={value}
|
|
52
|
+
onChange={(e) => set(Number(e.target.value))}
|
|
53
|
+
disabled={disabled}
|
|
54
|
+
min={min}
|
|
55
|
+
max={max}
|
|
56
|
+
step={step}
|
|
57
|
+
className="ph-number-field__input"
|
|
58
|
+
{...inputProps}
|
|
59
|
+
/>
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
disabled={disabled || (max !== undefined && value >= max)}
|
|
63
|
+
onClick={() => set(value + step)}
|
|
64
|
+
className="ph-number-field__btn"
|
|
65
|
+
aria-label="Increase"
|
|
66
|
+
>
|
|
67
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
68
|
+
<path d="M6 2v8M2 6h8" />
|
|
69
|
+
</svg>
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
NumberField.displayName = "NumberField";
|