@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
|
@@ -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">
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ReactNode, type HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface ButtonGroupProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function ButtonGroup({ children, className, ...props }: ButtonGroupProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div className={cn("ph-btn-group", className)} {...props}>
|
|
11
|
+
{children}
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
ButtonGroup.displayName = "ButtonGroup";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useState } from "react";
|
|
4
|
+
import { IconChevronLeft, IconChevronRight } from "@/components/icons";
|
|
4
5
|
import { cn } from "@/lib/cn";
|
|
5
6
|
|
|
6
7
|
interface CalendarProps {
|
|
@@ -64,11 +65,10 @@ export function Calendar({ value, onChange, className }: CalendarProps) {
|
|
|
64
65
|
<button
|
|
65
66
|
type="button"
|
|
66
67
|
onClick={prevMonth}
|
|
68
|
+
aria-label="Previous month"
|
|
67
69
|
className="rounded-lg p-1 text-ph-subtle transition hover:bg-ph-muted hover:text-ph-ink"
|
|
68
70
|
>
|
|
69
|
-
<
|
|
70
|
-
<path d="M15 18l-6-6 6-6" />
|
|
71
|
-
</svg>
|
|
71
|
+
<IconChevronLeft className="h-5 w-5" aria-hidden />
|
|
72
72
|
</button>
|
|
73
73
|
<span className="text-sm font-semibold text-ph-ink">
|
|
74
74
|
{monthNames[month]} {year}
|
|
@@ -76,11 +76,10 @@ export function Calendar({ value, onChange, className }: CalendarProps) {
|
|
|
76
76
|
<button
|
|
77
77
|
type="button"
|
|
78
78
|
onClick={nextMonth}
|
|
79
|
+
aria-label="Next month"
|
|
79
80
|
className="rounded-lg p-1 text-ph-subtle transition hover:bg-ph-muted hover:text-ph-ink"
|
|
80
81
|
>
|
|
81
|
-
<
|
|
82
|
-
<path d="M9 18l6-6-6-6" />
|
|
83
|
-
</svg>
|
|
82
|
+
<IconChevronRight className="h-5 w-5" aria-hidden />
|
|
84
83
|
</button>
|
|
85
84
|
</div>
|
|
86
85
|
|
|
@@ -99,7 +98,7 @@ export function Calendar({ value, onChange, className }: CalendarProps) {
|
|
|
99
98
|
className={cn(
|
|
100
99
|
"flex h-full w-full items-center justify-center rounded-lg text-sm transition-colors",
|
|
101
100
|
isSelected(day)
|
|
102
|
-
? "bg-ph-brand font-semibold text-
|
|
101
|
+
? "bg-ph-brand font-semibold text-[var(--ph-on-accent)] shadow-ph"
|
|
103
102
|
: isToday(day)
|
|
104
103
|
? "bg-ph-muted font-semibold text-ph-brand ring-1 ring-ph-brand/30"
|
|
105
104
|
: "text-ph-ink hover:bg-ph-muted"
|
|
@@ -1,9 +1,9 @@
|
|
|
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 CardVariant = "default" | "outlined" | "elevated" | "highlighted";
|
|
5
5
|
|
|
6
|
-
export interface CardProps {
|
|
6
|
+
export interface CardProps extends Omit<HTMLAttributes<HTMLDivElement>, "title"> {
|
|
7
7
|
children?: ReactNode;
|
|
8
8
|
title?: ReactNode;
|
|
9
9
|
description?: ReactNode;
|
|
@@ -13,16 +13,18 @@ export interface CardProps {
|
|
|
13
13
|
variant?: CardVariant;
|
|
14
14
|
className?: string;
|
|
15
15
|
bodyClassName?: string;
|
|
16
|
+
interactive?: boolean;
|
|
17
|
+
titleAs?: "h2" | "h3" | "h4";
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
const variantMap: Record<CardVariant, string> = {
|
|
19
|
-
default: "
|
|
20
|
-
outlined: "border-2 border-ph-border
|
|
21
|
-
elevated: "
|
|
22
|
-
highlighted: "
|
|
21
|
+
default: "",
|
|
22
|
+
outlined: "border-2 border-ph-border",
|
|
23
|
+
elevated: "ph-card-elevated",
|
|
24
|
+
highlighted: "ph-card-highlighted",
|
|
23
25
|
};
|
|
24
26
|
|
|
25
|
-
export function Card({
|
|
27
|
+
export const Card = forwardRef<HTMLDivElement, CardProps>(function Card({
|
|
26
28
|
children,
|
|
27
29
|
title,
|
|
28
30
|
description,
|
|
@@ -32,19 +34,25 @@ export function Card({
|
|
|
32
34
|
variant = "default",
|
|
33
35
|
className,
|
|
34
36
|
bodyClassName,
|
|
35
|
-
|
|
37
|
+
interactive = false,
|
|
38
|
+
titleAs: Title = "h3",
|
|
39
|
+
...props
|
|
40
|
+
}, ref) {
|
|
36
41
|
return (
|
|
37
42
|
<div
|
|
43
|
+
ref={ref}
|
|
38
44
|
className={cn(
|
|
39
45
|
"ph-card",
|
|
40
46
|
variantMap[variant],
|
|
47
|
+
interactive && "ph-card-interactive",
|
|
41
48
|
className
|
|
42
49
|
)}
|
|
50
|
+
{...props}
|
|
43
51
|
>
|
|
44
|
-
{media && <div className="-
|
|
52
|
+
{media && <div className="ph-card-media">{media}</div>}
|
|
45
53
|
<div className={cn("ph-card-body", bodyClassName)}>
|
|
46
54
|
{title && (
|
|
47
|
-
<
|
|
55
|
+
<Title className="ph-card-title">{title}</Title>
|
|
48
56
|
)}
|
|
49
57
|
{description && (
|
|
50
58
|
<p className="text-sm text-ph-subtle">{description}</p>
|
|
@@ -57,4 +65,6 @@ export function Card({
|
|
|
57
65
|
)}
|
|
58
66
|
</div>
|
|
59
67
|
);
|
|
60
|
-
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
Card.displayName = "Card";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { forwardRef, type ButtonHTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface ChipProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
5
|
+
selected?: boolean;
|
|
6
|
+
onRemove?: () => void;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const Chip = forwardRef<HTMLButtonElement, ChipProps>(function Chip({
|
|
11
|
+
selected = false,
|
|
12
|
+
onRemove,
|
|
13
|
+
children,
|
|
14
|
+
className,
|
|
15
|
+
...props
|
|
16
|
+
}, ref) {
|
|
17
|
+
return (
|
|
18
|
+
<button
|
|
19
|
+
ref={ref}
|
|
20
|
+
type="button"
|
|
21
|
+
className={cn(
|
|
22
|
+
"ph-chip",
|
|
23
|
+
selected && "ph-chip--selected",
|
|
24
|
+
className
|
|
25
|
+
)}
|
|
26
|
+
{...props}
|
|
27
|
+
>
|
|
28
|
+
<span className="ph-chip__text">{children}</span>
|
|
29
|
+
{onRemove && (
|
|
30
|
+
<span
|
|
31
|
+
role="button"
|
|
32
|
+
tabIndex={-1}
|
|
33
|
+
onClick={(e) => { e.stopPropagation(); onRemove(); }}
|
|
34
|
+
className="ph-chip__remove"
|
|
35
|
+
aria-label="Remove"
|
|
36
|
+
>
|
|
37
|
+
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
38
|
+
<path d="M2 2l6 6M8 2l-6 6" />
|
|
39
|
+
</svg>
|
|
40
|
+
</span>
|
|
41
|
+
)}
|
|
42
|
+
</button>
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
Chip.displayName = "Chip";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, type ReactNode, type HTMLAttributes } from "react";
|
|
4
|
+
import { cn } from "@/lib/cn";
|
|
5
|
+
|
|
6
|
+
export interface CollapsibleProps extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
trigger: ReactNode;
|
|
8
|
+
open?: boolean;
|
|
9
|
+
onOpenChange?: (open: boolean) => void;
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function Collapsible({
|
|
14
|
+
trigger,
|
|
15
|
+
open: controlledOpen,
|
|
16
|
+
onOpenChange,
|
|
17
|
+
children,
|
|
18
|
+
className,
|
|
19
|
+
...props
|
|
20
|
+
}: CollapsibleProps) {
|
|
21
|
+
const [internalOpen, setInternalOpen] = useState(false);
|
|
22
|
+
const isOpen = controlledOpen ?? internalOpen;
|
|
23
|
+
const setOpen = onOpenChange ?? setInternalOpen;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className={cn("ph-collapsible", className)} {...props}>
|
|
27
|
+
<button
|
|
28
|
+
type="button"
|
|
29
|
+
onClick={() => setOpen(!isOpen)}
|
|
30
|
+
className="ph-collapsible__trigger"
|
|
31
|
+
aria-expanded={isOpen}
|
|
32
|
+
>
|
|
33
|
+
<svg
|
|
34
|
+
className={cn("ph-collapsible__chevron", isOpen && "ph-collapsible__chevron--open")}
|
|
35
|
+
width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"
|
|
36
|
+
>
|
|
37
|
+
<path d="M4 5l3 3 3-3" />
|
|
38
|
+
</svg>
|
|
39
|
+
{trigger}
|
|
40
|
+
</button>
|
|
41
|
+
{isOpen && <div className="ph-collapsible__content">{children}</div>}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
Collapsible.displayName = "Collapsible";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState } from "react";
|
|
3
|
+
import { useId, useState } from "react";
|
|
4
|
+
import { IconChevronDown } from "@/components/icons";
|
|
4
5
|
import { cn } from "@/lib/cn";
|
|
5
6
|
|
|
6
7
|
interface CollapsibleSectionProps {
|
|
@@ -12,31 +13,30 @@ interface CollapsibleSectionProps {
|
|
|
12
13
|
|
|
13
14
|
export function CollapsibleSection({ title, children, defaultOpen = true, id }: CollapsibleSectionProps) {
|
|
14
15
|
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
16
|
+
const generatedId = useId();
|
|
17
|
+
const contentId = `${id ?? generatedId}-content`;
|
|
15
18
|
|
|
16
19
|
return (
|
|
17
20
|
<section id={id} className="scroll-mt-20">
|
|
18
21
|
<button
|
|
19
22
|
type="button"
|
|
20
23
|
onClick={() => setIsOpen(!isOpen)}
|
|
24
|
+
aria-expanded={isOpen}
|
|
25
|
+
aria-controls={contentId}
|
|
21
26
|
className="mb-6 flex w-full items-center justify-between rounded-lg border border-ph-border bg-ph-muted px-5 py-4 text-left transition hover:bg-ph-muted/80"
|
|
22
27
|
>
|
|
23
28
|
<span className="text-lg font-bold text-ph-ink">{title}</span>
|
|
24
|
-
<
|
|
29
|
+
<IconChevronDown
|
|
25
30
|
className={cn(
|
|
26
31
|
"h-5 w-5 text-ph-subtle transition-transform duration-200",
|
|
27
32
|
isOpen && "rotate-180"
|
|
28
33
|
)}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
stroke="currentColor"
|
|
32
|
-
strokeWidth="2"
|
|
33
|
-
strokeLinecap="round"
|
|
34
|
-
strokeLinejoin="round"
|
|
35
|
-
>
|
|
36
|
-
<path d="M6 9l6 6 6-6" />
|
|
37
|
-
</svg>
|
|
34
|
+
aria-hidden
|
|
35
|
+
/>
|
|
38
36
|
</button>
|
|
39
37
|
<div
|
|
38
|
+
id={contentId}
|
|
39
|
+
aria-hidden={!isOpen}
|
|
40
40
|
className={cn(
|
|
41
41
|
"overflow-hidden transition-all duration-300",
|
|
42
42
|
isOpen ? "max-h-[50000px] opacity-100" : "max-h-0 opacity-0"
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect, type ReactNode } from "react";
|
|
4
|
+
import { cn } from "@/lib/cn";
|
|
5
|
+
|
|
6
|
+
export interface ComboboxOption {
|
|
7
|
+
value: string;
|
|
8
|
+
label: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ComboboxProps {
|
|
12
|
+
options: ComboboxOption[];
|
|
13
|
+
value?: string[];
|
|
14
|
+
onChange?: (value: string[]) => void;
|
|
15
|
+
placeholder?: string;
|
|
16
|
+
className?: string;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function Combobox({
|
|
21
|
+
options,
|
|
22
|
+
value = [],
|
|
23
|
+
onChange,
|
|
24
|
+
placeholder = "Search...",
|
|
25
|
+
className,
|
|
26
|
+
disabled,
|
|
27
|
+
}: ComboboxProps) {
|
|
28
|
+
const [open, setOpen] = useState(false);
|
|
29
|
+
const [search, setSearch] = useState("");
|
|
30
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
31
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
32
|
+
|
|
33
|
+
const filtered = options.filter((o) =>
|
|
34
|
+
o.label.toLowerCase().includes(search.toLowerCase())
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const selectedSet = new Set(value);
|
|
38
|
+
|
|
39
|
+
const toggle = (val: string) => {
|
|
40
|
+
const next = selectedSet.has(val)
|
|
41
|
+
? value.filter((v) => v !== val)
|
|
42
|
+
: [...value, val];
|
|
43
|
+
onChange?.(next);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (!open) return;
|
|
48
|
+
const handle = (e: MouseEvent) => {
|
|
49
|
+
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
50
|
+
setOpen(false);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
document.addEventListener("mousedown", handle);
|
|
54
|
+
return () => document.removeEventListener("mousedown", handle);
|
|
55
|
+
}, [open]);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div ref={containerRef} className={cn("ph-combobox relative", className)}>
|
|
59
|
+
<div className="ph-combobox__input-wrap">
|
|
60
|
+
{value.length > 0 && (
|
|
61
|
+
<div className="ph-combobox__chips">
|
|
62
|
+
{value.map((v) => {
|
|
63
|
+
const opt = options.find((o) => o.value === v);
|
|
64
|
+
return (
|
|
65
|
+
<span key={v} className="ph-combobox__chip">
|
|
66
|
+
{opt?.label ?? v}
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
onClick={() => toggle(v)}
|
|
70
|
+
className="ph-combobox__chip-remove"
|
|
71
|
+
aria-label={`Remove ${opt?.label ?? v}`}
|
|
72
|
+
>
|
|
73
|
+
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
74
|
+
<path d="M2 2l6 6M8 2l-6 6" />
|
|
75
|
+
</svg>
|
|
76
|
+
</button>
|
|
77
|
+
</span>
|
|
78
|
+
);
|
|
79
|
+
})}
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
<input
|
|
83
|
+
ref={inputRef}
|
|
84
|
+
type="text"
|
|
85
|
+
value={search}
|
|
86
|
+
onChange={(e) => { setSearch(e.target.value); setOpen(true); }}
|
|
87
|
+
onFocus={() => setOpen(true)}
|
|
88
|
+
placeholder={value.length === 0 ? placeholder : ""}
|
|
89
|
+
disabled={disabled}
|
|
90
|
+
className="ph-combobox__input"
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
{open && filtered.length > 0 && (
|
|
94
|
+
<div className="ph-combobox__dropdown">
|
|
95
|
+
{filtered.map((opt) => (
|
|
96
|
+
<button
|
|
97
|
+
key={opt.value}
|
|
98
|
+
type="button"
|
|
99
|
+
onClick={() => toggle(opt.value)}
|
|
100
|
+
className={cn(
|
|
101
|
+
"ph-combobox__option",
|
|
102
|
+
selectedSet.has(opt.value) && "ph-combobox__option--selected"
|
|
103
|
+
)}
|
|
104
|
+
>
|
|
105
|
+
{selectedSet.has(opt.value) && (
|
|
106
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
|
|
107
|
+
<path d="M2.5 6l2.5 2.5 4.5-5" />
|
|
108
|
+
</svg>
|
|
109
|
+
)}
|
|
110
|
+
<span>{opt.label}</span>
|
|
111
|
+
</button>
|
|
112
|
+
))}
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
Combobox.displayName = "Combobox";
|
|
@@ -0,0 +1,81 @@
|
|
|
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 ContextMenuItem {
|
|
7
|
+
label: string;
|
|
8
|
+
onClick?: () => void;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
separator?: boolean;
|
|
11
|
+
icon?: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface ContextMenuProps extends HTMLAttributes<HTMLDivElement> {
|
|
15
|
+
items: ContextMenuItem[];
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ContextMenu({ items, children, className, ...props }: ContextMenuProps) {
|
|
20
|
+
const [open, setOpen] = useState(false);
|
|
21
|
+
const [pos, setPos] = useState({ x: 0, y: 0 });
|
|
22
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!open) return;
|
|
26
|
+
const handle = (e: MouseEvent) => {
|
|
27
|
+
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
|
28
|
+
setOpen(false);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const handleKey = (e: KeyboardEvent) => {
|
|
32
|
+
if (e.key === "Escape") setOpen(false);
|
|
33
|
+
};
|
|
34
|
+
document.addEventListener("mousedown", handle);
|
|
35
|
+
document.addEventListener("keydown", handleKey);
|
|
36
|
+
return () => {
|
|
37
|
+
document.removeEventListener("mousedown", handle);
|
|
38
|
+
document.removeEventListener("keydown", handleKey);
|
|
39
|
+
};
|
|
40
|
+
}, [open]);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
className={cn("ph-context-menu", className)}
|
|
45
|
+
onContextMenu={(e) => {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
setPos({ x: e.clientX, y: e.clientY });
|
|
48
|
+
setOpen(true);
|
|
49
|
+
}}
|
|
50
|
+
{...props}
|
|
51
|
+
>
|
|
52
|
+
{children}
|
|
53
|
+
{open && (
|
|
54
|
+
<div
|
|
55
|
+
ref={menuRef}
|
|
56
|
+
className="ph-context-menu__panel"
|
|
57
|
+
style={{ left: pos.x, top: pos.y }}
|
|
58
|
+
>
|
|
59
|
+
{items.map((item, i) =>
|
|
60
|
+
item.separator ? (
|
|
61
|
+
<div key={i} className="ph-context-menu__separator" />
|
|
62
|
+
) : (
|
|
63
|
+
<button
|
|
64
|
+
key={i}
|
|
65
|
+
type="button"
|
|
66
|
+
disabled={item.disabled}
|
|
67
|
+
onClick={() => { item.onClick?.(); setOpen(false); }}
|
|
68
|
+
className="ph-context-menu__item"
|
|
69
|
+
>
|
|
70
|
+
{item.icon && <span className="ph-context-menu__icon">{item.icon}</span>}
|
|
71
|
+
<span>{item.label}</span>
|
|
72
|
+
</button>
|
|
73
|
+
)
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
ContextMenu.displayName = "ContextMenu";
|