supa-ui 0.0.0-stage → 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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +52 -2
- package/dist/index.d.ts +30 -0
- package/dist/index.js +25 -0
- package/dist/layout/container.d.ts +5 -0
- package/dist/layout/container.js +12 -0
- package/dist/layout/footer.d.ts +13 -0
- package/dist/layout/footer.js +6 -0
- package/dist/layout/header.d.ts +19 -0
- package/dist/layout/header.js +5 -0
- package/dist/layout/layout.d.ts +10 -0
- package/dist/layout/layout.js +5 -0
- package/dist/layout/navbar.d.ts +17 -0
- package/dist/layout/navbar.js +19 -0
- package/dist/layout/sidebar.d.ts +10 -0
- package/dist/layout/sidebar.js +11 -0
- package/dist/lib/cn.d.ts +1 -0
- package/dist/lib/cn.js +4 -0
- package/dist/shared/empty-state.d.ts +11 -0
- package/dist/shared/empty-state.js +14 -0
- package/dist/shared/loading-state.d.ts +5 -0
- package/dist/shared/loading-state.js +6 -0
- package/dist/shared/page-header.d.ts +8 -0
- package/dist/shared/page-header.js +5 -0
- package/dist/styles.css +2 -0
- package/dist/toastly/provider.d.ts +3 -0
- package/dist/toastly/provider.js +111 -0
- package/dist/toastly/toastly.d.ts +28 -0
- package/dist/toastly/toastly.js +92 -0
- package/dist/toastly/types.d.ts +36 -0
- package/dist/toastly/types.js +1 -0
- package/dist/ui/avatar.d.ts +7 -0
- package/dist/ui/avatar.js +12 -0
- package/dist/ui/badge.d.ts +6 -0
- package/dist/ui/badge.js +15 -0
- package/dist/ui/button.d.ts +10 -0
- package/dist/ui/button.js +18 -0
- package/dist/ui/card.d.ts +12 -0
- package/dist/ui/card.js +20 -0
- package/dist/ui/checkbox.d.ts +6 -0
- package/dist/ui/checkbox.js +8 -0
- package/dist/ui/dropdown.d.ts +19 -0
- package/dist/ui/dropdown.js +43 -0
- package/dist/ui/input.d.ts +10 -0
- package/dist/ui/input.js +19 -0
- package/dist/ui/link.d.ts +11 -0
- package/dist/ui/link.js +17 -0
- package/dist/ui/modal.d.ts +12 -0
- package/dist/ui/modal.js +28 -0
- package/dist/ui/select.d.ts +14 -0
- package/dist/ui/select.js +17 -0
- package/dist/ui/spinner.d.ts +5 -0
- package/dist/ui/spinner.js +6 -0
- package/dist/ui/tabs.d.ts +16 -0
- package/dist/ui/tabs.js +39 -0
- package/dist/ui/textarea.d.ts +7 -0
- package/dist/ui/textarea.js +10 -0
- package/package.json +66 -4
- package/src/theme.css +130 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export const TOASTLY_EVENTS = {
|
|
2
|
+
add: "toastly:add-toast",
|
|
3
|
+
dismiss: "toastly:dismiss-alert",
|
|
4
|
+
alert: "toastly:alert",
|
|
5
|
+
confirm: "toastly:confirm"
|
|
6
|
+
};
|
|
7
|
+
let counter = 0;
|
|
8
|
+
const emit = (name, detail) => {
|
|
9
|
+
if (typeof window === "undefined")
|
|
10
|
+
return false;
|
|
11
|
+
window.dispatchEvent(new CustomEvent(name, { detail }));
|
|
12
|
+
return true;
|
|
13
|
+
};
|
|
14
|
+
const normalize = (input) => typeof input === "string" ? { message: input } : input;
|
|
15
|
+
const add = (input) => {
|
|
16
|
+
const toast = normalize(input);
|
|
17
|
+
const id = toast.id ?? `toastly-${Date.now()}-${++counter}`;
|
|
18
|
+
emit(TOASTLY_EVENTS.add, { ...toast, id });
|
|
19
|
+
return id;
|
|
20
|
+
};
|
|
21
|
+
const withVariant = (variant) => (input) => add({ ...normalize(input), variant });
|
|
22
|
+
const dismiss = (id) => {
|
|
23
|
+
emit(TOASTLY_EVENTS.dismiss, { id });
|
|
24
|
+
};
|
|
25
|
+
export const toastly = {
|
|
26
|
+
add,
|
|
27
|
+
info: withVariant("info"),
|
|
28
|
+
success: withVariant("success"),
|
|
29
|
+
warning: withVariant("warning"),
|
|
30
|
+
error: withVariant("error"),
|
|
31
|
+
dismiss,
|
|
32
|
+
promise(promise, messages) {
|
|
33
|
+
const id = add({ message: messages.loading, variant: "info", loading: true, autoDismiss: false });
|
|
34
|
+
return promise.then((value) => {
|
|
35
|
+
dismiss(id);
|
|
36
|
+
const message = typeof messages.success === "function" ? messages.success(value) : messages.success;
|
|
37
|
+
add({ message, variant: "success" });
|
|
38
|
+
return value;
|
|
39
|
+
}, (error) => {
|
|
40
|
+
dismiss(id);
|
|
41
|
+
const message = typeof messages.error === "function" ? messages.error(error) : messages.error;
|
|
42
|
+
add({ message, variant: "error" });
|
|
43
|
+
throw error;
|
|
44
|
+
});
|
|
45
|
+
},
|
|
46
|
+
alert(event) {
|
|
47
|
+
return new Promise((resolve) => {
|
|
48
|
+
const sent = emit(TOASTLY_EVENTS.alert, {
|
|
49
|
+
...event,
|
|
50
|
+
onClose: () => {
|
|
51
|
+
event.onClose?.();
|
|
52
|
+
resolve();
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
if (!sent)
|
|
56
|
+
resolve();
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
confirm(event) {
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
let settled = false;
|
|
62
|
+
const settle = (value) => {
|
|
63
|
+
if (settled)
|
|
64
|
+
return;
|
|
65
|
+
settled = true;
|
|
66
|
+
resolve(value);
|
|
67
|
+
};
|
|
68
|
+
const sent = emit(TOASTLY_EVENTS.confirm, {
|
|
69
|
+
...event,
|
|
70
|
+
onConfirm: () => {
|
|
71
|
+
event.onConfirm?.();
|
|
72
|
+
settle(true);
|
|
73
|
+
},
|
|
74
|
+
onCancel: () => {
|
|
75
|
+
event.onCancel?.();
|
|
76
|
+
settle(false);
|
|
77
|
+
},
|
|
78
|
+
onClose: () => {
|
|
79
|
+
event.onClose?.();
|
|
80
|
+
settle(false);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
if (!sent)
|
|
84
|
+
settle(false);
|
|
85
|
+
});
|
|
86
|
+
},
|
|
87
|
+
to(x, y) {
|
|
88
|
+
return {
|
|
89
|
+
add: (input) => add({ ...normalize(input), position: [x, y] })
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
export type ToastVariant = "info" | "success" | "warning" | "error";
|
|
3
|
+
export type ToastlyPosition = "top-left" | "top-center" | "top-right" | "left-center" | "center" | "right-center" | "bottom-left" | "bottom-center" | "bottom-right" | "tl" | "tc" | "tr" | "lc" | "c" | "rc" | "bl" | "bc" | "br";
|
|
4
|
+
export type ToastEvent = {
|
|
5
|
+
id?: string;
|
|
6
|
+
message: React.ReactNode;
|
|
7
|
+
variant?: ToastVariant;
|
|
8
|
+
allowDismiss?: boolean;
|
|
9
|
+
autoDismiss?: boolean;
|
|
10
|
+
autoDismissIn?: number;
|
|
11
|
+
loading?: boolean;
|
|
12
|
+
position?: [x?: number | `${number}`, y?: number | `${number}`];
|
|
13
|
+
};
|
|
14
|
+
export type AlertEvent = {
|
|
15
|
+
title?: string;
|
|
16
|
+
message?: React.ReactNode;
|
|
17
|
+
okLabel?: string;
|
|
18
|
+
onClose?: () => void;
|
|
19
|
+
};
|
|
20
|
+
export type ConfirmEvent = {
|
|
21
|
+
title?: string;
|
|
22
|
+
message?: React.ReactNode;
|
|
23
|
+
confirmLabel?: string;
|
|
24
|
+
cancelLabel?: string;
|
|
25
|
+
danger?: boolean;
|
|
26
|
+
onConfirm?: () => void;
|
|
27
|
+
onCancel?: () => void;
|
|
28
|
+
onClose?: () => void;
|
|
29
|
+
};
|
|
30
|
+
export type ToastlyProviderProps = {
|
|
31
|
+
children?: React.ReactNode;
|
|
32
|
+
position?: ToastlyPosition;
|
|
33
|
+
duration?: number | `${number}`;
|
|
34
|
+
maxVisible?: number;
|
|
35
|
+
};
|
|
36
|
+
export type ToastInput = string | ToastEvent;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type AvatarProps = {
|
|
3
|
+
src?: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
size?: "sm" | "md" | "lg";
|
|
6
|
+
} & React.HTMLAttributes<HTMLSpanElement>;
|
|
7
|
+
export default function Avatar({ src, name, size, className, ...props }: AvatarProps): React.JSX.Element;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
export default function Avatar({ src, name = "", size = "md", className = "", ...props }) {
|
|
4
|
+
const sizes = { sm: "size-6 text-[10px]", md: "size-8 text-xs", lg: "size-10 text-sm" };
|
|
5
|
+
const initials = name
|
|
6
|
+
.split(" ")
|
|
7
|
+
.filter(Boolean)
|
|
8
|
+
.slice(0, 2)
|
|
9
|
+
.map((part) => part[0]?.toUpperCase())
|
|
10
|
+
.join("");
|
|
11
|
+
return (_jsx("span", { className: cn("inline-flex shrink-0 items-center justify-center overflow-hidden rounded-full border border-border bg-muted font-medium text-foreground-light", sizes[size], className), ...props, children: src ? _jsx("img", { src: src, alt: name, className: "size-full object-cover" }) : initials }));
|
|
12
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type BadgeProps = {
|
|
3
|
+
variant?: "default" | "primary" | "outline" | "danger";
|
|
4
|
+
size?: "sm" | "md";
|
|
5
|
+
} & React.HTMLAttributes<HTMLSpanElement>;
|
|
6
|
+
export default function Badge({ variant, size, className, children, ...props }: BadgeProps): React.JSX.Element;
|
package/dist/ui/badge.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
export default function Badge({ variant = "default", size = "md", className = "", children, ...props }) {
|
|
4
|
+
const variants = {
|
|
5
|
+
default: "border-border bg-muted text-foreground-light",
|
|
6
|
+
primary: "border-primary/30 bg-primary/10 text-primary",
|
|
7
|
+
outline: "border-border-strong bg-transparent text-foreground-light",
|
|
8
|
+
danger: "border-destructive/30 bg-destructive/10 text-destructive"
|
|
9
|
+
};
|
|
10
|
+
const sizes = {
|
|
11
|
+
sm: "px-1.5 py-0.5 text-[10px]",
|
|
12
|
+
md: "px-2 py-0.5 text-xs"
|
|
13
|
+
};
|
|
14
|
+
return (_jsx("span", { className: cn("inline-flex shrink-0 items-center gap-1 rounded-full border font-medium whitespace-nowrap [&_svg]:size-3", variants[variant], sizes[size], className), ...props, children: children }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
3
|
+
variant?: "primary" | "secondary" | "outline" | "ghost" | "danger";
|
|
4
|
+
size?: "tiny" | "sm" | "md" | "lg";
|
|
5
|
+
loading?: boolean;
|
|
6
|
+
leftIcon?: React.ReactNode;
|
|
7
|
+
rightIcon?: React.ReactNode;
|
|
8
|
+
};
|
|
9
|
+
export default function Button({ variant, size, loading, disabled, leftIcon, rightIcon, className, children, type, ...props }: ButtonProps): React.JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
export default function Button({ variant = "primary", size = "md", loading = false, disabled, leftIcon, rightIcon, className = "", children, type = "button", ...props }) {
|
|
4
|
+
const variants = {
|
|
5
|
+
primary: "bg-primary text-primary-foreground shadow-[var(--sui-button-shadow-raised)] hover:bg-primary/90",
|
|
6
|
+
secondary: "bg-secondary text-secondary-foreground border border-border hover:bg-accent",
|
|
7
|
+
outline: "border border-border bg-transparent text-foreground hover:bg-accent",
|
|
8
|
+
ghost: "bg-transparent text-foreground hover:bg-accent",
|
|
9
|
+
danger: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90"
|
|
10
|
+
};
|
|
11
|
+
const sizes = {
|
|
12
|
+
tiny: "h-[26px] rounded-md px-2.5 text-xs [&_svg]:size-3.5",
|
|
13
|
+
sm: "h-8 rounded-md px-3 text-xs [&_svg]:size-4",
|
|
14
|
+
md: "h-9 rounded-md px-3.5 text-sm [&_svg]:size-4",
|
|
15
|
+
lg: "h-10 rounded-lg px-5 text-sm [&_svg]:size-5"
|
|
16
|
+
};
|
|
17
|
+
return (_jsxs("button", { type: type, disabled: disabled || loading, "aria-busy": loading || undefined, className: cn("relative inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap font-medium text-center transition-[background-color,border-color,color,transform] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] motion-safe:active:scale-[0.97] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50", variants[variant], sizes[size], className), ...props, children: [loading ? (_jsx("span", { className: "size-4 animate-spin rounded-full border-2 border-current border-r-transparent" })) : (leftIcon), _jsx("span", { className: "truncate", children: children }), !loading && rightIcon] }));
|
|
18
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
type DivProps = React.HTMLAttributes<HTMLDivElement>;
|
|
3
|
+
export declare function CardHeader({ className, ...props }: DivProps): React.JSX.Element;
|
|
4
|
+
export declare function CardTitle({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>): React.JSX.Element;
|
|
5
|
+
export declare function CardDescription({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>): React.JSX.Element;
|
|
6
|
+
export declare function CardContent({ className, ...props }: DivProps): React.JSX.Element;
|
|
7
|
+
export declare function CardFooter({ className, ...props }: DivProps): React.JSX.Element;
|
|
8
|
+
export type CardProps = {
|
|
9
|
+
interactive?: boolean;
|
|
10
|
+
} & DivProps;
|
|
11
|
+
export default function Card({ interactive, className, ...props }: CardProps): React.JSX.Element;
|
|
12
|
+
export {};
|
package/dist/ui/card.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
export function CardHeader({ className = "", ...props }) {
|
|
4
|
+
return _jsx("div", { className: cn("flex flex-col gap-1 border-b border-border px-5 py-4", className), ...props });
|
|
5
|
+
}
|
|
6
|
+
export function CardTitle({ className = "", ...props }) {
|
|
7
|
+
return _jsx("h3", { className: cn("text-sm font-medium text-foreground", className), ...props });
|
|
8
|
+
}
|
|
9
|
+
export function CardDescription({ className = "", ...props }) {
|
|
10
|
+
return _jsx("p", { className: cn("text-sm text-foreground-muted", className), ...props });
|
|
11
|
+
}
|
|
12
|
+
export function CardContent({ className = "", ...props }) {
|
|
13
|
+
return _jsx("div", { className: cn("px-5 py-4", className), ...props });
|
|
14
|
+
}
|
|
15
|
+
export function CardFooter({ className = "", ...props }) {
|
|
16
|
+
return _jsx("div", { className: cn("flex items-center gap-2 border-t border-border px-5 py-3", className), ...props });
|
|
17
|
+
}
|
|
18
|
+
export default function Card({ interactive = false, className = "", ...props }) {
|
|
19
|
+
return (_jsx("div", { className: cn("overflow-hidden rounded-lg border border-border bg-card text-card-foreground", interactive && "cursor-pointer transition-colors duration-200 hover:border-border-strong", className), ...props }));
|
|
20
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type CheckboxProps = {
|
|
3
|
+
label?: React.ReactNode;
|
|
4
|
+
description?: string;
|
|
5
|
+
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, "type">;
|
|
6
|
+
export default function Checkbox({ label, description, className, id, disabled, ...props }: CheckboxProps): React.JSX.Element;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
export default function Checkbox({ label, description, className = "", id, disabled, ...props }) {
|
|
5
|
+
const generatedId = React.useId();
|
|
6
|
+
const checkboxId = id ?? generatedId;
|
|
7
|
+
return (_jsxs("div", { className: cn("flex items-start gap-2.5", disabled && "opacity-50"), children: [_jsx("input", { id: checkboxId, type: "checkbox", disabled: disabled, className: cn("mt-0.5 size-4 shrink-0 cursor-pointer rounded border-control accent-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed", className), ...props }), (label || description) && (_jsxs("label", { htmlFor: checkboxId, className: "flex cursor-pointer flex-col gap-0.5", children: [label && _jsx("span", { className: "text-sm text-foreground", children: label }), description && _jsx("span", { className: "text-xs text-foreground-muted", children: description })] }))] }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type DropdownEntry = {
|
|
3
|
+
type?: "item";
|
|
4
|
+
label: string;
|
|
5
|
+
icon?: React.ReactNode;
|
|
6
|
+
onSelect?: () => void;
|
|
7
|
+
danger?: boolean;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
} | {
|
|
10
|
+
type: "separator";
|
|
11
|
+
};
|
|
12
|
+
export type DropdownProps = {
|
|
13
|
+
trigger: React.ReactNode;
|
|
14
|
+
items: DropdownEntry[];
|
|
15
|
+
align?: "start" | "end";
|
|
16
|
+
triggerLabel?: string;
|
|
17
|
+
className?: string;
|
|
18
|
+
};
|
|
19
|
+
export default function Dropdown({ trigger, items, align, triggerLabel, className }: DropdownProps): React.JSX.Element;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
export default function Dropdown({ trigger, items, align = "end", triggerLabel = "Menu", className = "" }) {
|
|
6
|
+
const [open, setOpen] = React.useState(false);
|
|
7
|
+
const rootRef = React.useRef(null);
|
|
8
|
+
const menuRef = React.useRef(null);
|
|
9
|
+
React.useEffect(() => {
|
|
10
|
+
if (!open)
|
|
11
|
+
return;
|
|
12
|
+
const onPointer = (event) => {
|
|
13
|
+
if (!rootRef.current?.contains(event.target))
|
|
14
|
+
setOpen(false);
|
|
15
|
+
};
|
|
16
|
+
const onKey = (event) => {
|
|
17
|
+
if (event.key === "Escape")
|
|
18
|
+
setOpen(false);
|
|
19
|
+
};
|
|
20
|
+
document.addEventListener("mousedown", onPointer);
|
|
21
|
+
document.addEventListener("keydown", onKey);
|
|
22
|
+
menuRef.current?.querySelector('[role="menuitem"]:not([disabled])')?.focus();
|
|
23
|
+
return () => {
|
|
24
|
+
document.removeEventListener("mousedown", onPointer);
|
|
25
|
+
document.removeEventListener("keydown", onKey);
|
|
26
|
+
};
|
|
27
|
+
}, [open]);
|
|
28
|
+
const onMenuKeyDown = (event) => {
|
|
29
|
+
const nodes = Array.from(menuRef.current?.querySelectorAll('[role="menuitem"]:not([disabled])') ?? []);
|
|
30
|
+
const index = nodes.indexOf(document.activeElement);
|
|
31
|
+
if (event.key === "ArrowDown")
|
|
32
|
+
nodes[(index + 1) % nodes.length]?.focus();
|
|
33
|
+
else if (event.key === "ArrowUp")
|
|
34
|
+
nodes[(index - 1 + nodes.length) % nodes.length]?.focus();
|
|
35
|
+
else
|
|
36
|
+
return;
|
|
37
|
+
event.preventDefault();
|
|
38
|
+
};
|
|
39
|
+
return (_jsxs("div", { ref: rootRef, className: cn("relative inline-flex", className), children: [_jsx("button", { type: "button", "aria-label": triggerLabel, "aria-haspopup": "menu", "aria-expanded": open, onClick: () => setOpen((v) => !v), className: "inline-flex cursor-pointer items-center rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: trigger }), open && (_jsx("div", { ref: menuRef, role: "menu", onKeyDown: onMenuKeyDown, style: { animation: "sui-pop-in 120ms ease-out" }, className: cn("absolute top-full z-50 mt-1.5 min-w-44 rounded-md border border-border bg-popover p-1 text-sm text-foreground shadow-lg", align === "end" ? "right-0 origin-top-right" : "left-0 origin-top-left"), children: items.map((entry, index) => entry.type === "separator" ? (_jsx("div", { role: "separator", className: "my-1 h-px bg-border" }, index)) : (_jsxs("button", { role: "menuitem", type: "button", disabled: entry.disabled, onClick: () => {
|
|
40
|
+
entry.onSelect?.();
|
|
41
|
+
setOpen(false);
|
|
42
|
+
}, className: cn("flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-left outline-none transition-colors hover:bg-accent focus:bg-accent disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4", entry.danger && "text-destructive"), children: [entry.icon, entry.label] }, `${entry.label}-${index}`))) }))] }));
|
|
43
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type InputProps = {
|
|
3
|
+
label?: string;
|
|
4
|
+
hint?: string;
|
|
5
|
+
error?: string;
|
|
6
|
+
icon?: React.ReactNode;
|
|
7
|
+
size?: "sm" | "md" | "lg";
|
|
8
|
+
variant?: "default" | "filled";
|
|
9
|
+
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, "size">;
|
|
10
|
+
export default function Input({ label, hint, error, icon, size, variant, className, id, disabled, ...props }: InputProps): React.JSX.Element;
|
package/dist/ui/input.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
export default function Input({ label, hint, error, icon, size = "md", variant = "default", className = "", id, disabled, ...props }) {
|
|
5
|
+
const generatedId = React.useId();
|
|
6
|
+
const inputId = id ?? generatedId;
|
|
7
|
+
const descriptionId = `${inputId}-description`;
|
|
8
|
+
const errorId = `${inputId}-error`;
|
|
9
|
+
const sizes = {
|
|
10
|
+
sm: "h-8 px-2.5 text-xs",
|
|
11
|
+
md: "h-[34px] px-3 text-sm",
|
|
12
|
+
lg: "h-10 px-3.5 text-base"
|
|
13
|
+
};
|
|
14
|
+
const variants = {
|
|
15
|
+
default: "bg-field border-control",
|
|
16
|
+
filled: "bg-muted border-transparent"
|
|
17
|
+
};
|
|
18
|
+
return (_jsxs("div", { className: "flex w-full flex-col gap-1.5", children: [label && (_jsx("label", { htmlFor: inputId, className: "text-sm font-medium text-foreground", children: label })), _jsxs("div", { className: "relative flex w-full items-center", children: [icon && (_jsx("span", { className: "pointer-events-none absolute left-3 flex items-center text-foreground-muted [&_svg]:size-4", children: icon })), _jsx("input", { id: inputId, disabled: disabled, "aria-invalid": !!error, "aria-describedby": error ? errorId : hint ? descriptionId : undefined, className: cn("flex w-full min-w-0 rounded-md border text-foreground placeholder:text-foreground-muted file:border-0 file:bg-transparent file:text-sm file:font-medium read-only:border-button read-only:text-foreground-light transition-colors duration-200 hover:border-control-hover focus:border-control-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 disabled:text-foreground-muted aria-invalid:border-destructive aria-invalid:focus-visible:ring-destructive", variants[variant], sizes[size], icon ? "pl-9" : "", className), ...props })] }), error && (_jsx("span", { id: errorId, role: "alert", className: "text-xs text-destructive", children: error })), !error && hint && (_jsx("span", { id: descriptionId, className: "text-xs text-foreground-muted", children: hint }))] }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type LinkProps = {
|
|
3
|
+
url: string;
|
|
4
|
+
text?: string;
|
|
5
|
+
icon?: React.ReactNode;
|
|
6
|
+
active?: boolean;
|
|
7
|
+
as?: React.ElementType;
|
|
8
|
+
variant?: "default" | "primary" | "secondary" | "ghost";
|
|
9
|
+
size?: "sm" | "md" | "lg";
|
|
10
|
+
} & Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href">;
|
|
11
|
+
export default function Link({ url, text, icon, active, as, variant, size, className, children, ...props }: LinkProps): React.JSX.Element;
|
package/dist/ui/link.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
export default function Link({ url, text, icon, active = false, as, variant = "default", size = "md", className = "", children, ...props }) {
|
|
4
|
+
const Component = as ?? "a";
|
|
5
|
+
const variants = {
|
|
6
|
+
default: "text-foreground-lighter hover:bg-accent hover:text-foreground",
|
|
7
|
+
primary: "text-primary hover:bg-primary/10",
|
|
8
|
+
secondary: "text-secondary-foreground hover:bg-secondary",
|
|
9
|
+
ghost: "text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
10
|
+
};
|
|
11
|
+
const sizes = {
|
|
12
|
+
sm: "h-7 px-2 text-xs [&>svg]:size-4",
|
|
13
|
+
md: "h-8 px-2 text-sm [&>svg]:size-5",
|
|
14
|
+
lg: "h-10 px-3 text-base [&>svg]:size-5"
|
|
15
|
+
};
|
|
16
|
+
return (_jsxs(Component, { href: url, "data-active": active, "aria-current": active ? "page" : undefined, className: cn("flex w-full items-center gap-2 overflow-hidden rounded-md text-left outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring active:bg-accent [&>span:last-child]:truncate [&>svg]:shrink-0", variants[variant], sizes[size], active && "bg-accent font-medium text-foreground", className), ...props, children: [icon, text ? _jsx("span", { children: text }) : children] }));
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type ModalProps = {
|
|
3
|
+
open: boolean;
|
|
4
|
+
onClose: () => void;
|
|
5
|
+
title?: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
footer?: React.ReactNode;
|
|
8
|
+
size?: "sm" | "md" | "lg";
|
|
9
|
+
children?: React.ReactNode;
|
|
10
|
+
className?: string;
|
|
11
|
+
};
|
|
12
|
+
export default function Modal({ open, onClose, title, description, footer, size, children, className }: ModalProps): React.JSX.Element | null;
|
package/dist/ui/modal.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
export default function Modal({ open, onClose, title, description, footer, size = "md", children, className = "" }) {
|
|
6
|
+
React.useEffect(() => {
|
|
7
|
+
if (!open)
|
|
8
|
+
return;
|
|
9
|
+
const onKeyDown = (event) => {
|
|
10
|
+
if (event.key === "Escape")
|
|
11
|
+
onClose();
|
|
12
|
+
};
|
|
13
|
+
const previousOverflow = document.body.style.overflow;
|
|
14
|
+
document.body.style.overflow = "hidden";
|
|
15
|
+
document.addEventListener("keydown", onKeyDown);
|
|
16
|
+
return () => {
|
|
17
|
+
document.body.style.overflow = previousOverflow;
|
|
18
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
19
|
+
};
|
|
20
|
+
}, [open, onClose]);
|
|
21
|
+
if (!open)
|
|
22
|
+
return null;
|
|
23
|
+
const sizes = { sm: "max-w-sm", md: "max-w-lg", lg: "max-w-2xl" };
|
|
24
|
+
return (_jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4 backdrop-blur-xs", onMouseDown: (event) => {
|
|
25
|
+
if (event.target === event.currentTarget)
|
|
26
|
+
onClose();
|
|
27
|
+
}, children: _jsxs("div", { role: "dialog", "aria-modal": "true", "aria-label": title, className: cn("flex max-h-[90vh] w-full flex-col overflow-hidden rounded-lg border border-border bg-popover text-foreground shadow-2xl", sizes[size], className), children: [(title || description) && (_jsxs("div", { className: "flex items-start justify-between gap-4 border-b border-border px-5 py-4", children: [_jsxs("div", { className: "flex flex-col gap-1", children: [title && _jsx("h2", { className: "text-sm font-medium", children: title }), description && _jsx("p", { className: "text-sm text-foreground-muted", children: description })] }), _jsx("button", { type: "button", onClick: onClose, "aria-label": "Close", className: "-mr-1.5 inline-flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-md text-foreground-muted transition-colors hover:bg-accent hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: _jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "size-4", children: _jsx("path", { d: "M18 6 6 18M6 6l12 12" }) }) })] })), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto px-5 py-4", children: children }), footer && (_jsx("div", { className: "flex items-center justify-end gap-2 border-t border-border px-5 py-3", children: footer }))] }) }));
|
|
28
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type SelectOption = {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
};
|
|
7
|
+
export type SelectProps = {
|
|
8
|
+
label?: string;
|
|
9
|
+
hint?: string;
|
|
10
|
+
error?: string;
|
|
11
|
+
size?: "sm" | "md" | "lg";
|
|
12
|
+
options?: SelectOption[];
|
|
13
|
+
} & Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "size">;
|
|
14
|
+
export default function Select({ label, hint, error, size, options, className, id, disabled, children, ...props }: SelectProps): React.JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
export default function Select({ label, hint, error, size = "md", options, className = "", id, disabled, children, ...props }) {
|
|
5
|
+
const generatedId = React.useId();
|
|
6
|
+
const selectId = id ?? generatedId;
|
|
7
|
+
const descriptionId = `${selectId}-description`;
|
|
8
|
+
const errorId = `${selectId}-error`;
|
|
9
|
+
const sizes = {
|
|
10
|
+
sm: "h-8 pl-2.5 pr-8 text-xs",
|
|
11
|
+
md: "h-[34px] pl-3 pr-9 text-sm",
|
|
12
|
+
lg: "h-10 pl-3.5 pr-10 text-base"
|
|
13
|
+
};
|
|
14
|
+
return (_jsxs("div", { className: "flex w-full flex-col gap-1.5", children: [label && (_jsx("label", { htmlFor: selectId, className: "text-sm font-medium text-foreground", children: label })), _jsxs("div", { className: "relative flex w-full items-center", children: [_jsx("select", { id: selectId, disabled: disabled, "aria-invalid": !!error, "aria-describedby": error ? errorId : hint ? descriptionId : undefined, className: cn("w-full min-w-0 cursor-pointer appearance-none rounded-md border border-control bg-field text-foreground transition-colors duration-200 hover:border-control-hover focus:border-control-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive", sizes[size], className), ...props, children: options
|
|
15
|
+
? options.map((option) => (_jsx("option", { value: option.value, disabled: option.disabled, children: option.label }, option.value)))
|
|
16
|
+
: children }), _jsx("svg", { "aria-hidden": "true", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", className: "pointer-events-none absolute right-3 size-4 text-foreground-muted", children: _jsx("path", { d: "m6 9 6 6 6-6" }) })] }), error && (_jsx("span", { id: errorId, role: "alert", className: "text-xs text-destructive", children: error })), !error && hint && (_jsx("span", { id: descriptionId, className: "text-xs text-foreground-muted", children: hint }))] }));
|
|
17
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/cn";
|
|
3
|
+
export default function Spinner({ size = "md", className = "" }) {
|
|
4
|
+
const sizes = { sm: "size-3.5", md: "size-5", lg: "size-8" };
|
|
5
|
+
return (_jsx("span", { role: "status", "aria-label": "Loading", className: cn("inline-block animate-spin rounded-full border-2 border-foreground-muted border-r-transparent", sizes[size], className) }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type TabItem = {
|
|
3
|
+
value: string;
|
|
4
|
+
label: string;
|
|
5
|
+
icon?: React.ReactNode;
|
|
6
|
+
content?: React.ReactNode;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
};
|
|
9
|
+
export type TabsProps = {
|
|
10
|
+
items: TabItem[];
|
|
11
|
+
value?: string;
|
|
12
|
+
defaultValue?: string;
|
|
13
|
+
onChange?: (value: string) => void;
|
|
14
|
+
className?: string;
|
|
15
|
+
};
|
|
16
|
+
export default function Tabs({ items, value, defaultValue, onChange, className }: TabsProps): React.JSX.Element;
|
package/dist/ui/tabs.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../lib/cn";
|
|
5
|
+
export default function Tabs({ items, value, defaultValue, onChange, className = "" }) {
|
|
6
|
+
const [inner, setInner] = React.useState(defaultValue ?? items[0]?.value);
|
|
7
|
+
const current = value ?? inner;
|
|
8
|
+
const baseId = React.useId();
|
|
9
|
+
const select = (next) => {
|
|
10
|
+
if (value === undefined)
|
|
11
|
+
setInner(next);
|
|
12
|
+
onChange?.(next);
|
|
13
|
+
};
|
|
14
|
+
const onKeyDown = (event) => {
|
|
15
|
+
const enabled = items.filter((item) => !item.disabled);
|
|
16
|
+
const index = enabled.findIndex((item) => item.value === current);
|
|
17
|
+
let next = index;
|
|
18
|
+
if (event.key === "ArrowRight")
|
|
19
|
+
next = (index + 1) % enabled.length;
|
|
20
|
+
else if (event.key === "ArrowLeft")
|
|
21
|
+
next = (index - 1 + enabled.length) % enabled.length;
|
|
22
|
+
else if (event.key === "Home")
|
|
23
|
+
next = 0;
|
|
24
|
+
else if (event.key === "End")
|
|
25
|
+
next = enabled.length - 1;
|
|
26
|
+
else
|
|
27
|
+
return;
|
|
28
|
+
event.preventDefault();
|
|
29
|
+
select(enabled[next].value);
|
|
30
|
+
document.getElementById(`${baseId}-tab-${enabled[next].value}`)?.focus();
|
|
31
|
+
};
|
|
32
|
+
const active = items.find((item) => item.value === current);
|
|
33
|
+
return (_jsxs("div", { className: cn("flex w-full flex-col gap-4", className), children: [_jsx("div", { role: "tablist", onKeyDown: onKeyDown, className: "flex items-center gap-1 overflow-x-auto border-b border-border", children: items.map((item) => {
|
|
34
|
+
const selected = item.value === current;
|
|
35
|
+
return (_jsxs("button", { id: `${baseId}-tab-${item.value}`, role: "tab", type: "button", "aria-selected": selected, "aria-controls": `${baseId}-panel-${item.value}`, tabIndex: selected ? 0 : -1, disabled: item.disabled, onClick: () => select(item.value), className: cn("-mb-px inline-flex h-9 shrink-0 cursor-pointer items-center gap-2 border-b-2 px-3 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:size-4", selected
|
|
36
|
+
? "border-primary font-medium text-foreground"
|
|
37
|
+
: "border-transparent text-foreground-muted hover:text-foreground"), children: [item.icon, item.label] }, item.value));
|
|
38
|
+
}) }), active?.content !== undefined && (_jsx("div", { role: "tabpanel", id: `${baseId}-panel-${active.value}`, "aria-labelledby": `${baseId}-tab-${active.value}`, children: active.content }))] }));
|
|
39
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type TextareaProps = {
|
|
3
|
+
label?: string;
|
|
4
|
+
hint?: string;
|
|
5
|
+
error?: string;
|
|
6
|
+
} & React.TextareaHTMLAttributes<HTMLTextAreaElement>;
|
|
7
|
+
export default function Textarea({ label, hint, error, className, id, rows, ...props }: TextareaProps): React.JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "../lib/cn";
|
|
4
|
+
export default function Textarea({ label, hint, error, className = "", id, rows = 4, ...props }) {
|
|
5
|
+
const generatedId = React.useId();
|
|
6
|
+
const textareaId = id ?? generatedId;
|
|
7
|
+
const descriptionId = `${textareaId}-description`;
|
|
8
|
+
const errorId = `${textareaId}-error`;
|
|
9
|
+
return (_jsxs("div", { className: "flex w-full flex-col gap-1.5", children: [label && (_jsx("label", { htmlFor: textareaId, className: "text-sm font-medium text-foreground", children: label })), _jsx("textarea", { id: textareaId, rows: rows, "aria-invalid": !!error, "aria-describedby": error ? errorId : hint ? descriptionId : undefined, className: cn("flex w-full min-w-0 resize-y rounded-md border border-control bg-field px-3 py-2 text-sm text-foreground placeholder:text-foreground-muted transition-colors duration-200 hover:border-control-hover focus:border-control-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive", className), ...props }), error && (_jsx("span", { id: errorId, role: "alert", className: "text-xs text-destructive", children: error })), !error && hint && (_jsx("span", { id: descriptionId, className: "text-xs text-foreground-muted", children: hint }))] }));
|
|
10
|
+
}
|