@viliha/vui-ui 1.0.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/LICENSE +21 -0
- package/package.json +73 -0
- package/src/avatar.tsx +33 -0
- package/src/badge.tsx +42 -0
- package/src/button.tsx +70 -0
- package/src/card.tsx +77 -0
- package/src/chart.tsx +144 -0
- package/src/checkbox.tsx +20 -0
- package/src/code.tsx +11 -0
- package/src/confirm-dialog.tsx +59 -0
- package/src/dialog.tsx +137 -0
- package/src/dropdown-menu.tsx +123 -0
- package/src/input.tsx +23 -0
- package/src/menu.tsx +80 -0
- package/src/record-view.tsx +1506 -0
- package/src/select.tsx +122 -0
- package/src/table-io.ts +114 -0
- package/src/table.tsx +100 -0
- package/src/theme.css +296 -0
- package/src/utils.ts +7 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "./utils";
|
|
6
|
+
import { Checkbox } from "./checkbox";
|
|
7
|
+
|
|
8
|
+
interface DropdownProps {
|
|
9
|
+
label: string;
|
|
10
|
+
icon?: React.ReactNode;
|
|
11
|
+
align?: "start" | "end";
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
/** Accessible name for icon-only triggers (when label is empty). */
|
|
14
|
+
ariaLabel?: string;
|
|
15
|
+
/** Render the trigger as a compact toolbar button (default) or a plain one. */
|
|
16
|
+
active?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Minimal click-to-open menu with outside-click + Escape to close. */
|
|
20
|
+
export function Dropdown({
|
|
21
|
+
label,
|
|
22
|
+
icon,
|
|
23
|
+
align = "start",
|
|
24
|
+
children,
|
|
25
|
+
ariaLabel,
|
|
26
|
+
active,
|
|
27
|
+
}: DropdownProps) {
|
|
28
|
+
const [open, setOpen] = React.useState(false);
|
|
29
|
+
const ref = React.useRef<HTMLDivElement>(null);
|
|
30
|
+
|
|
31
|
+
React.useEffect(() => {
|
|
32
|
+
if (!open) return;
|
|
33
|
+
function onDocMouseDown(event: MouseEvent) {
|
|
34
|
+
if (ref.current && !ref.current.contains(event.target as Node)) {
|
|
35
|
+
setOpen(false);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function onKey(event: KeyboardEvent) {
|
|
39
|
+
if (event.key === "Escape") setOpen(false);
|
|
40
|
+
}
|
|
41
|
+
document.addEventListener("mousedown", onDocMouseDown);
|
|
42
|
+
document.addEventListener("keydown", onKey);
|
|
43
|
+
return () => {
|
|
44
|
+
document.removeEventListener("mousedown", onDocMouseDown);
|
|
45
|
+
document.removeEventListener("keydown", onKey);
|
|
46
|
+
};
|
|
47
|
+
}, [open]);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="relative" ref={ref}>
|
|
51
|
+
<button
|
|
52
|
+
type="button"
|
|
53
|
+
onClick={() => setOpen((value) => !value)}
|
|
54
|
+
aria-haspopup="menu"
|
|
55
|
+
aria-expanded={open}
|
|
56
|
+
aria-label={ariaLabel ?? label}
|
|
57
|
+
className={cn(
|
|
58
|
+
"inline-flex h-7 cursor-pointer items-center gap-1.5 rounded-md px-2 font-medium transition-colors",
|
|
59
|
+
active
|
|
60
|
+
? "bg-accent text-accent-foreground"
|
|
61
|
+
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground",
|
|
62
|
+
)}
|
|
63
|
+
>
|
|
64
|
+
{icon}
|
|
65
|
+
{label}
|
|
66
|
+
</button>
|
|
67
|
+
{open && (
|
|
68
|
+
<div
|
|
69
|
+
className={cn(
|
|
70
|
+
"vui-pop-in absolute z-40 mt-1 min-w-52 overflow-hidden rounded-md border border-border bg-popover text-left text-sm font-normal text-popover-foreground shadow-md",
|
|
71
|
+
align === "end" ? "right-0" : "left-0",
|
|
72
|
+
)}
|
|
73
|
+
>
|
|
74
|
+
{children}
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
interface DropdownItemProps {
|
|
82
|
+
children: React.ReactNode;
|
|
83
|
+
onSelect?: () => void;
|
|
84
|
+
checked?: boolean;
|
|
85
|
+
icon?: React.ReactNode;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function DropdownItem({
|
|
89
|
+
children,
|
|
90
|
+
onSelect,
|
|
91
|
+
checked,
|
|
92
|
+
icon,
|
|
93
|
+
}: DropdownItemProps) {
|
|
94
|
+
return (
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
role="menuitemcheckbox"
|
|
98
|
+
aria-checked={checked}
|
|
99
|
+
onClick={onSelect}
|
|
100
|
+
className="flex w-full cursor-pointer items-center gap-2 border-b border-border px-3 py-2 text-left last:border-b-0 hover:bg-accent hover:text-accent-foreground"
|
|
101
|
+
>
|
|
102
|
+
<span className="flex-1 truncate">{children}</span>
|
|
103
|
+
{icon}
|
|
104
|
+
{checked !== undefined && (
|
|
105
|
+
<Checkbox
|
|
106
|
+
checked={checked}
|
|
107
|
+
readOnly
|
|
108
|
+
tabIndex={-1}
|
|
109
|
+
aria-hidden="true"
|
|
110
|
+
className="pointer-events-none"
|
|
111
|
+
/>
|
|
112
|
+
)}
|
|
113
|
+
</button>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function DropdownLabel({ children }: { children: React.ReactNode }) {
|
|
118
|
+
return (
|
|
119
|
+
<p className="border-b border-border px-3 py-2 text-left font-medium text-muted-foreground">
|
|
120
|
+
{children}
|
|
121
|
+
</p>
|
|
122
|
+
);
|
|
123
|
+
}
|
package/src/input.tsx
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
|
|
5
|
+
export const Input = React.forwardRef<
|
|
6
|
+
HTMLInputElement,
|
|
7
|
+
React.ComponentProps<"input">
|
|
8
|
+
>(({ className, type, ...props }, ref) => (
|
|
9
|
+
<input
|
|
10
|
+
type={type}
|
|
11
|
+
ref={ref}
|
|
12
|
+
className={cn(
|
|
13
|
+
"flex h-8 w-full rounded-md border border-input bg-background px-2.5 py-1 transition-colors",
|
|
14
|
+
"placeholder:text-muted-foreground",
|
|
15
|
+
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
|
|
16
|
+
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
17
|
+
"file:border-0 file:bg-transparent file:file:font-medium",
|
|
18
|
+
className,
|
|
19
|
+
)}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
));
|
|
23
|
+
Input.displayName = "Input";
|
package/src/menu.tsx
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "./utils";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Bordered list primitives — the theme standard for menus and record lists.
|
|
9
|
+
* Each row draws its own bottom divider (the last one is omitted), so any list
|
|
10
|
+
* built from these can't miss the border. Use `Menu` as the rounded, clipped
|
|
11
|
+
* container and `MenuItem` for each row; `MenuLabel` for section headers.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Shared row styling — exported for the rare case you must style a bespoke
|
|
15
|
+
* element (e.g. a framework `<Link>`) yet still match the standard. */
|
|
16
|
+
export const menuItemClass =
|
|
17
|
+
"flex w-full items-center gap-2.5 border-b border-border px-3 py-2 text-left transition-colors last:border-b-0 hover:bg-accent";
|
|
18
|
+
|
|
19
|
+
export function Menu({
|
|
20
|
+
className,
|
|
21
|
+
children,
|
|
22
|
+
...props
|
|
23
|
+
}: React.HTMLAttributes<HTMLDivElement>) {
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
role="menu"
|
|
27
|
+
className={cn(
|
|
28
|
+
"overflow-hidden rounded-lg border border-border bg-popover text-popover-foreground",
|
|
29
|
+
className,
|
|
30
|
+
)}
|
|
31
|
+
{...props}
|
|
32
|
+
>
|
|
33
|
+
{children}
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface MenuItemProps
|
|
39
|
+
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
40
|
+
/** Render as a different element (e.g. a framework `<Link>`); default button. */
|
|
41
|
+
as?: React.ElementType;
|
|
42
|
+
href?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A bordered list row. Renders a `<button>` by default; pass `as={Link}` +
|
|
46
|
+
* `href` to render a navigation link. Divider + hover come baked in. */
|
|
47
|
+
export const MenuItem = React.forwardRef<HTMLButtonElement, MenuItemProps>(
|
|
48
|
+
function MenuItem({ as: Comp = "button", className, children, ...props }, ref) {
|
|
49
|
+
return (
|
|
50
|
+
<Comp
|
|
51
|
+
ref={ref}
|
|
52
|
+
role="menuitem"
|
|
53
|
+
className={cn(menuItemClass, className)}
|
|
54
|
+
{...(Comp === "button" ? { type: "button" } : {})}
|
|
55
|
+
{...props}
|
|
56
|
+
>
|
|
57
|
+
{children}
|
|
58
|
+
</Comp>
|
|
59
|
+
);
|
|
60
|
+
},
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
export function MenuLabel({
|
|
64
|
+
className,
|
|
65
|
+
children,
|
|
66
|
+
}: {
|
|
67
|
+
className?: string;
|
|
68
|
+
children: React.ReactNode;
|
|
69
|
+
}) {
|
|
70
|
+
return (
|
|
71
|
+
<p
|
|
72
|
+
className={cn(
|
|
73
|
+
"border-b border-border px-3 py-2 font-medium text-muted-foreground",
|
|
74
|
+
className,
|
|
75
|
+
)}
|
|
76
|
+
>
|
|
77
|
+
{children}
|
|
78
|
+
</p>
|
|
79
|
+
);
|
|
80
|
+
}
|