@xenide-io/the-old-ui-theme 0.5.4 → 0.6.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/dist/{Chip-C5hRFAhe.d.mts → Chip-DzC5xtK9.d.mts} +8 -3
- package/dist/{Chip-C5hRFAhe.d.ts → Chip-DzC5xtK9.d.ts} +8 -3
- package/dist/chunk-3G43JEI3.mjs +2564 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +7 -2
- package/dist/index.mjs +3 -1
- package/dist/tailwind-preset.js +1 -0
- package/dist/tailwind-preset.mjs +1 -0
- package/dist/ui.d.mts +4 -531
- package/dist/ui.d.ts +4 -531
- package/dist/ui.js +407 -2504
- package/dist/ui.mjs +5 -91
- package/package.json +1 -1
- package/src/app/globals.css +18 -0
- package/src/components/ui/Typography.tsx +12 -4
- package/src/components/ui/index.ts +7 -184
- package/src/components/ui/typography.test.tsx +27 -0
- package/src/styles/suite-skin.css +21 -1
- package/src/styles/typography.css +72 -3
- package/tailwind-preset.ts +1 -0
- package/dist/chunk-SBBKXV35.mjs +0 -4620
- package/src/components/ui/AlertDialog.tsx +0 -62
- package/src/components/ui/AppLayout.tsx +0 -27
- package/src/components/ui/Autocomplete.tsx +0 -106
- package/src/components/ui/Banner.tsx +0 -63
- package/src/components/ui/ButtonGroup.tsx +0 -16
- package/src/components/ui/ChatBubble.tsx +0 -71
- package/src/components/ui/Collapsible.tsx +0 -46
- package/src/components/ui/Combobox.tsx +0 -119
- package/src/components/ui/ContextMenu.tsx +0 -81
- package/src/components/ui/DataTable.tsx +0 -133
- package/src/components/ui/DatePicker.tsx +0 -96
- package/src/components/ui/Dialog.tsx +0 -75
- package/src/components/ui/Divider.tsx +0 -39
- package/src/components/ui/Drawer.tsx +0 -71
- package/src/components/ui/HoverCard.tsx +0 -36
- package/src/components/ui/Indicator.tsx +0 -61
- package/src/components/ui/InputGroup.tsx +0 -16
- package/src/components/ui/Lettermark.tsx +0 -47
- package/src/components/ui/LoadingBar.tsx +0 -25
- package/src/components/ui/Menubar.tsx +0 -66
- package/src/components/ui/Metric.tsx +0 -34
- package/src/components/ui/Navigation.tsx +0 -94
- package/src/components/ui/NumberField.tsx +0 -76
- package/src/components/ui/Popover.tsx +0 -73
- package/src/components/ui/ProgressCircle.tsx +0 -63
- package/src/components/ui/Rating.tsx +0 -65
- package/src/components/ui/Resizable.tsx +0 -71
- package/src/components/ui/Row.tsx +0 -51
- package/src/components/ui/ScrollArea.tsx +0 -54
- package/src/components/ui/Sidebar.tsx +0 -79
- package/src/components/ui/Snack.tsx +0 -48
- package/src/components/ui/Splotch.tsx +0 -33
- package/src/components/ui/Stepper.tsx +0 -74
- package/src/components/ui/Tabs.tsx +0 -53
- package/src/components/ui/Tag.tsx +0 -65
- package/src/components/ui/Terminal.tsx +0 -54
- package/src/components/ui/ThemeManager.tsx +0 -18
- package/src/components/ui/Timeline.tsx +0 -60
- package/src/components/ui/Toast.tsx +0 -70
- package/src/components/ui/Toggle.tsx +0 -62
- package/src/components/ui/Widget.tsx +0 -31
|
@@ -1,62 +0,0 @@
|
|
|
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";
|
|
@@ -1,27 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,106 +0,0 @@
|
|
|
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,63 +0,0 @@
|
|
|
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";
|
|
@@ -1,16 +0,0 @@
|
|
|
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,71 +0,0 @@
|
|
|
1
|
-
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
|
-
import { cn } from "@/lib/cn";
|
|
3
|
-
|
|
4
|
-
export type ChatBubbleVariant = "from" | "self";
|
|
5
|
-
|
|
6
|
-
export interface ChatBubbleProps extends ComponentPropsWithoutRef<"div"> {
|
|
7
|
-
/** Message content */
|
|
8
|
-
children?: ReactNode;
|
|
9
|
-
/** Avatar or icon */
|
|
10
|
-
avatar?: ReactNode;
|
|
11
|
-
/** Sender name */
|
|
12
|
-
name?: string;
|
|
13
|
-
/** Timestamp */
|
|
14
|
-
time?: string;
|
|
15
|
-
/** "from" = left/grey, "self" = right/brand */
|
|
16
|
-
variant?: ChatBubbleVariant;
|
|
17
|
-
/** Footer text (e.g. "Synced with...") */
|
|
18
|
-
footer?: ReactNode;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function ChatBubble({
|
|
22
|
-
children,
|
|
23
|
-
avatar,
|
|
24
|
-
name,
|
|
25
|
-
time,
|
|
26
|
-
variant = "from",
|
|
27
|
-
footer,
|
|
28
|
-
className,
|
|
29
|
-
...props
|
|
30
|
-
}: ChatBubbleProps) {
|
|
31
|
-
const isSelf = variant === "self";
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<div
|
|
35
|
-
className={cn(
|
|
36
|
-
"flex gap-3",
|
|
37
|
-
isSelf && "flex-row-reverse",
|
|
38
|
-
className,
|
|
39
|
-
)}
|
|
40
|
-
{...props}
|
|
41
|
-
>
|
|
42
|
-
{avatar && (
|
|
43
|
-
<div className="shrink-0">{avatar}</div>
|
|
44
|
-
)}
|
|
45
|
-
<div className={cn("min-w-0 flex-1", isSelf && "text-right")}>
|
|
46
|
-
{(name || time) && (
|
|
47
|
-
<header
|
|
48
|
-
className={cn(
|
|
49
|
-
"flex gap-2 text-xs text-ph-mutedtext",
|
|
50
|
-
isSelf && "flex-row-reverse justify-end",
|
|
51
|
-
)}
|
|
52
|
-
>
|
|
53
|
-
{name && <span className="font-semibold text-ph-ink">{name}</span>}
|
|
54
|
-
{time && <span>{time}</span>}
|
|
55
|
-
</header>
|
|
56
|
-
)}
|
|
57
|
-
<div
|
|
58
|
-
className={cn(
|
|
59
|
-
"mt-2 inline-block text-left shadow-ph",
|
|
60
|
-
isSelf ? "ph-chat-self" : "ph-chat-from",
|
|
61
|
-
)}
|
|
62
|
-
>
|
|
63
|
-
{children}
|
|
64
|
-
</div>
|
|
65
|
-
{footer && (
|
|
66
|
-
<footer className="mt-2 text-[11px] text-ph-mutedtext">{footer}</footer>
|
|
67
|
-
)}
|
|
68
|
-
</div>
|
|
69
|
-
</div>
|
|
70
|
-
);
|
|
71
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
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,119 +0,0 @@
|
|
|
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";
|
|
@@ -1,81 +0,0 @@
|
|
|
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";
|