@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
package/src/select.tsx
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import {
|
|
5
|
+
CheckIcon as Check,
|
|
6
|
+
ChevronDownIcon as ChevronDown,
|
|
7
|
+
} from "@radix-ui/react-icons";
|
|
8
|
+
|
|
9
|
+
import { cn } from "./utils";
|
|
10
|
+
|
|
11
|
+
export interface SelectOption {
|
|
12
|
+
value: string;
|
|
13
|
+
label: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Custom single-select styled to match the app (Input-like trigger + popover
|
|
18
|
+
* list), replacing the native `<select>`. Click-to-open, outside-click/Escape
|
|
19
|
+
* to close, checkmark on the active option.
|
|
20
|
+
*/
|
|
21
|
+
export function Select({
|
|
22
|
+
value,
|
|
23
|
+
onValueChange,
|
|
24
|
+
options,
|
|
25
|
+
id,
|
|
26
|
+
ariaLabel,
|
|
27
|
+
placeholder = "Select…",
|
|
28
|
+
className,
|
|
29
|
+
}: {
|
|
30
|
+
value: string;
|
|
31
|
+
onValueChange: (value: string) => void;
|
|
32
|
+
options: SelectOption[];
|
|
33
|
+
id?: string;
|
|
34
|
+
ariaLabel?: string;
|
|
35
|
+
placeholder?: string;
|
|
36
|
+
/** Applied to the root (e.g. width in a flex row). */
|
|
37
|
+
className?: string;
|
|
38
|
+
}) {
|
|
39
|
+
const [open, setOpen] = React.useState(false);
|
|
40
|
+
const ref = React.useRef<HTMLDivElement>(null);
|
|
41
|
+
|
|
42
|
+
React.useEffect(() => {
|
|
43
|
+
if (!open) return;
|
|
44
|
+
const onDown = (e: MouseEvent) => {
|
|
45
|
+
if (ref.current && !ref.current.contains(e.target as Node)) {
|
|
46
|
+
setOpen(false);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const onKey = (e: KeyboardEvent) => {
|
|
50
|
+
if (e.key === "Escape") setOpen(false);
|
|
51
|
+
};
|
|
52
|
+
document.addEventListener("mousedown", onDown);
|
|
53
|
+
document.addEventListener("keydown", onKey);
|
|
54
|
+
return () => {
|
|
55
|
+
document.removeEventListener("mousedown", onDown);
|
|
56
|
+
document.removeEventListener("keydown", onKey);
|
|
57
|
+
};
|
|
58
|
+
}, [open]);
|
|
59
|
+
|
|
60
|
+
const selected = options.find((o) => o.value === value);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className={cn("relative", className)} ref={ref}>
|
|
64
|
+
<button
|
|
65
|
+
type="button"
|
|
66
|
+
id={id}
|
|
67
|
+
aria-haspopup="listbox"
|
|
68
|
+
aria-expanded={open}
|
|
69
|
+
aria-label={ariaLabel}
|
|
70
|
+
onClick={() => setOpen((v) => !v)}
|
|
71
|
+
className={cn(
|
|
72
|
+
"flex h-8 w-full items-center justify-between gap-2 rounded-md border border-input bg-background px-2.5 transition-colors",
|
|
73
|
+
"hover:bg-accent/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
|
|
74
|
+
)}
|
|
75
|
+
>
|
|
76
|
+
<span className={cn("truncate", !selected && "text-muted-foreground")}>
|
|
77
|
+
{selected ? selected.label : placeholder}
|
|
78
|
+
</span>
|
|
79
|
+
<ChevronDown
|
|
80
|
+
className={cn(
|
|
81
|
+
"size-3.5 shrink-0 text-muted-foreground transition-transform",
|
|
82
|
+
open && "rotate-180",
|
|
83
|
+
)}
|
|
84
|
+
aria-hidden="true"
|
|
85
|
+
/>
|
|
86
|
+
</button>
|
|
87
|
+
{open && (
|
|
88
|
+
<div
|
|
89
|
+
role="listbox"
|
|
90
|
+
aria-label={ariaLabel}
|
|
91
|
+
tabIndex={-1}
|
|
92
|
+
className="vui-pop-in absolute z-50 mt-1 max-h-60 w-full min-w-max overflow-auto rounded-md border border-border bg-popover text-popover-foreground shadow-md"
|
|
93
|
+
>
|
|
94
|
+
{options.map((o) => {
|
|
95
|
+
const active = o.value === value;
|
|
96
|
+
return (
|
|
97
|
+
<button
|
|
98
|
+
key={o.value}
|
|
99
|
+
type="button"
|
|
100
|
+
role="option"
|
|
101
|
+
aria-selected={active}
|
|
102
|
+
onClick={() => {
|
|
103
|
+
onValueChange(o.value);
|
|
104
|
+
setOpen(false);
|
|
105
|
+
}}
|
|
106
|
+
className={cn(
|
|
107
|
+
"flex w-full items-center justify-between gap-2 border-b border-border px-3 py-2 text-left last:border-b-0 hover:bg-accent hover:text-accent-foreground",
|
|
108
|
+
active && "bg-accent/60",
|
|
109
|
+
)}
|
|
110
|
+
>
|
|
111
|
+
<span className="truncate">{o.label}</span>
|
|
112
|
+
{active && (
|
|
113
|
+
<Check className="size-3.5 shrink-0 text-[var(--button-primary)]" />
|
|
114
|
+
)}
|
|
115
|
+
</button>
|
|
116
|
+
);
|
|
117
|
+
})}
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
package/src/table-io.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import/export helpers for RecordView datatables — no external dependencies.
|
|
3
|
+
* CSV/JSON round-trip fully; "Excel" is an HTML table saved as .xls (Excel opens
|
|
4
|
+
* it); PDF is browser print-to-PDF. True .xlsx read/write would need a library.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export type IoColumn = { key: string; label: string };
|
|
8
|
+
type Row = Record<string, unknown>;
|
|
9
|
+
|
|
10
|
+
function csvEscape(value: string): string {
|
|
11
|
+
return /[",\n]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function htmlEscape(value: string): string {
|
|
15
|
+
return value
|
|
16
|
+
.replace(/&/g, "&")
|
|
17
|
+
.replace(/</g, "<")
|
|
18
|
+
.replace(/>/g, ">");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function rowsToCSV(cols: IoColumn[], rows: Row[]): string {
|
|
22
|
+
const head = cols.map((c) => csvEscape(c.label)).join(",");
|
|
23
|
+
const body = rows
|
|
24
|
+
.map((r) => cols.map((c) => csvEscape(String(r[c.key] ?? ""))).join(","))
|
|
25
|
+
.join("\n");
|
|
26
|
+
return `${head}\n${body}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function rowsToTableHTML(cols: IoColumn[], rows: Row[]): string {
|
|
30
|
+
const head = cols.map((c) => `<th>${htmlEscape(c.label)}</th>`).join("");
|
|
31
|
+
const body = rows
|
|
32
|
+
.map(
|
|
33
|
+
(r) =>
|
|
34
|
+
`<tr>${cols
|
|
35
|
+
.map((c) => `<td>${htmlEscape(String(r[c.key] ?? ""))}</td>`)
|
|
36
|
+
.join("")}</tr>`,
|
|
37
|
+
)
|
|
38
|
+
.join("");
|
|
39
|
+
return `<table border="1" cellspacing="0" cellpadding="6"><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table>`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function downloadFile(
|
|
43
|
+
filename: string,
|
|
44
|
+
content: string,
|
|
45
|
+
mime: string,
|
|
46
|
+
): void {
|
|
47
|
+
const blob = new Blob([content], { type: mime });
|
|
48
|
+
const url = URL.createObjectURL(blob);
|
|
49
|
+
const a = document.createElement("a");
|
|
50
|
+
a.href = url;
|
|
51
|
+
a.download = filename;
|
|
52
|
+
document.body.appendChild(a);
|
|
53
|
+
a.click();
|
|
54
|
+
a.remove();
|
|
55
|
+
URL.revokeObjectURL(url);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Open a print window with the table; the user saves as PDF from the dialog. */
|
|
59
|
+
export function printTable(title: string, tableHTML: string): void {
|
|
60
|
+
const w = window.open("", "_blank");
|
|
61
|
+
if (!w) return;
|
|
62
|
+
w.document.write(
|
|
63
|
+
`<!doctype html><html><head><meta charset="utf-8"><title>${htmlEscape(
|
|
64
|
+
title,
|
|
65
|
+
)}</title><style>body{font-family:Inter,system-ui,sans-serif;font-size:12px;padding:24px;color:#101112}h1{font-size:16px;margin:0 0 12px}table{border-collapse:collapse;width:100%}th,td{border:1px solid #e2e0e8;padding:6px 8px;text-align:left}th{background:#f4f4f6}</style></head><body><h1>${htmlEscape(
|
|
66
|
+
title,
|
|
67
|
+
)}</h1>${tableHTML}</body></html>`,
|
|
68
|
+
);
|
|
69
|
+
w.document.close();
|
|
70
|
+
w.focus();
|
|
71
|
+
w.print();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Minimal CSV parser → objects keyed by header row. Handles quoted fields. */
|
|
75
|
+
export function parseCSV(text: string): Record<string, string>[] {
|
|
76
|
+
const rows: string[][] = [];
|
|
77
|
+
let field = "";
|
|
78
|
+
let row: string[] = [];
|
|
79
|
+
let inQuotes = false;
|
|
80
|
+
for (let i = 0; i < text.length; i++) {
|
|
81
|
+
const ch = text[i];
|
|
82
|
+
if (inQuotes) {
|
|
83
|
+
if (ch === '"') {
|
|
84
|
+
if (text[i + 1] === '"') {
|
|
85
|
+
field += '"';
|
|
86
|
+
i++;
|
|
87
|
+
} else inQuotes = false;
|
|
88
|
+
} else field += ch;
|
|
89
|
+
} else if (ch === '"') inQuotes = true;
|
|
90
|
+
else if (ch === ",") {
|
|
91
|
+
row.push(field);
|
|
92
|
+
field = "";
|
|
93
|
+
} else if (ch === "\n" || ch === "\r") {
|
|
94
|
+
if (ch === "\r" && text[i + 1] === "\n") i++;
|
|
95
|
+
row.push(field);
|
|
96
|
+
field = "";
|
|
97
|
+
if (row.some((c) => c !== "")) rows.push(row);
|
|
98
|
+
row = [];
|
|
99
|
+
} else field += ch;
|
|
100
|
+
}
|
|
101
|
+
if (field !== "" || row.length) {
|
|
102
|
+
row.push(field);
|
|
103
|
+
if (row.some((c) => c !== "")) rows.push(row);
|
|
104
|
+
}
|
|
105
|
+
const header = rows[0];
|
|
106
|
+
if (!header) return [];
|
|
107
|
+
return rows.slice(1).map((r) => {
|
|
108
|
+
const obj: Record<string, string> = {};
|
|
109
|
+
header.forEach((h, idx) => {
|
|
110
|
+
obj[h] = r[idx] ?? "";
|
|
111
|
+
});
|
|
112
|
+
return obj;
|
|
113
|
+
});
|
|
114
|
+
}
|
package/src/table.tsx
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "./utils";
|
|
4
|
+
|
|
5
|
+
export const Table = React.forwardRef<
|
|
6
|
+
HTMLTableElement,
|
|
7
|
+
React.HTMLAttributes<HTMLTableElement>
|
|
8
|
+
>(({ className, ...props }, ref) => (
|
|
9
|
+
<table
|
|
10
|
+
ref={ref}
|
|
11
|
+
className={cn("w-full caption-bottom text-sm font-normal", className)}
|
|
12
|
+
{...props}
|
|
13
|
+
/>
|
|
14
|
+
));
|
|
15
|
+
Table.displayName = "Table";
|
|
16
|
+
|
|
17
|
+
export const TableHeader = React.forwardRef<
|
|
18
|
+
HTMLTableSectionElement,
|
|
19
|
+
React.HTMLAttributes<HTMLTableSectionElement>
|
|
20
|
+
>(({ className, ...props }, ref) => (
|
|
21
|
+
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
|
22
|
+
));
|
|
23
|
+
TableHeader.displayName = "TableHeader";
|
|
24
|
+
|
|
25
|
+
export const TableBody = React.forwardRef<
|
|
26
|
+
HTMLTableSectionElement,
|
|
27
|
+
React.HTMLAttributes<HTMLTableSectionElement>
|
|
28
|
+
>(({ className, ...props }, ref) => (
|
|
29
|
+
<tbody ref={ref} className={className} {...props} />
|
|
30
|
+
));
|
|
31
|
+
TableBody.displayName = "TableBody";
|
|
32
|
+
|
|
33
|
+
export const TableFooter = React.forwardRef<
|
|
34
|
+
HTMLTableSectionElement,
|
|
35
|
+
React.HTMLAttributes<HTMLTableSectionElement>
|
|
36
|
+
>(({ className, ...props }, ref) => (
|
|
37
|
+
<tfoot
|
|
38
|
+
ref={ref}
|
|
39
|
+
className={cn("border-t bg-muted/40 font-medium", className)}
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
));
|
|
43
|
+
TableFooter.displayName = "TableFooter";
|
|
44
|
+
|
|
45
|
+
export const TableRow = React.forwardRef<
|
|
46
|
+
HTMLTableRowElement,
|
|
47
|
+
React.HTMLAttributes<HTMLTableRowElement>
|
|
48
|
+
>(({ className, ...props }, ref) => (
|
|
49
|
+
<tr
|
|
50
|
+
ref={ref}
|
|
51
|
+
className={cn(
|
|
52
|
+
"border-b border-border transition-colors hover:bg-muted/50",
|
|
53
|
+
className,
|
|
54
|
+
)}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
));
|
|
58
|
+
TableRow.displayName = "TableRow";
|
|
59
|
+
|
|
60
|
+
export const TableHead = React.forwardRef<
|
|
61
|
+
HTMLTableCellElement,
|
|
62
|
+
React.ThHTMLAttributes<HTMLTableCellElement>
|
|
63
|
+
>(({ className, ...props }, ref) => (
|
|
64
|
+
<th
|
|
65
|
+
ref={ref}
|
|
66
|
+
className={cn(
|
|
67
|
+
"h-8 border-r border-border px-3 text-left align-middle font-semibold text-foreground last:border-r-0 [&:has([role=checkbox])]:pr-0",
|
|
68
|
+
className,
|
|
69
|
+
)}
|
|
70
|
+
{...props}
|
|
71
|
+
/>
|
|
72
|
+
));
|
|
73
|
+
TableHead.displayName = "TableHead";
|
|
74
|
+
|
|
75
|
+
export const TableCell = React.forwardRef<
|
|
76
|
+
HTMLTableCellElement,
|
|
77
|
+
React.TdHTMLAttributes<HTMLTableCellElement>
|
|
78
|
+
>(({ className, ...props }, ref) => (
|
|
79
|
+
<td
|
|
80
|
+
ref={ref}
|
|
81
|
+
className={cn(
|
|
82
|
+
"border-r border-border px-3 py-1.5 align-middle last:border-r-0 [&:has([role=checkbox])]:pr-0",
|
|
83
|
+
className,
|
|
84
|
+
)}
|
|
85
|
+
{...props}
|
|
86
|
+
/>
|
|
87
|
+
));
|
|
88
|
+
TableCell.displayName = "TableCell";
|
|
89
|
+
|
|
90
|
+
export const TableCaption = React.forwardRef<
|
|
91
|
+
HTMLTableCaptionElement,
|
|
92
|
+
React.HTMLAttributes<HTMLTableCaptionElement>
|
|
93
|
+
>(({ className, ...props }, ref) => (
|
|
94
|
+
<caption
|
|
95
|
+
ref={ref}
|
|
96
|
+
className={cn("mt-4 text-muted-foreground", className)}
|
|
97
|
+
{...props}
|
|
98
|
+
/>
|
|
99
|
+
));
|
|
100
|
+
TableCaption.displayName = "TableCaption";
|
package/src/theme.css
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VUI Starter design system — single source of truth, shared across web apps.
|
|
3
|
+
*
|
|
4
|
+
* Neutral shadcn/ui token set (oklch) plus the PULSE brand palette kept for the
|
|
5
|
+
* gradient wordmark. Each app's globals.css just `@import`s this file after
|
|
6
|
+
* Tailwind; the token→utility mapping (`@theme inline`), the base reset, and the
|
|
7
|
+
* component-scan (`@source`) all live here so nothing is duplicated per app.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/* Scan this package's components from any consuming app. */
|
|
11
|
+
@source "../";
|
|
12
|
+
|
|
13
|
+
@custom-variant dark (&:is(.dark *));
|
|
14
|
+
|
|
15
|
+
:root {
|
|
16
|
+
--radius: 0.625rem;
|
|
17
|
+
--background: oklch(1 0 0);
|
|
18
|
+
--foreground: oklch(0.177 0 0); /* #101112 — global text color */
|
|
19
|
+
--card: oklch(1 0 0);
|
|
20
|
+
--card-foreground: oklch(0.145 0 0);
|
|
21
|
+
--popover: oklch(1 0 0);
|
|
22
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
23
|
+
--primary: oklch(0.205 0 0);
|
|
24
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
25
|
+
--secondary: oklch(0.97 0 0);
|
|
26
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
27
|
+
--muted: oklch(0.97 0 0);
|
|
28
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
29
|
+
--accent: oklch(0.97 0 0);
|
|
30
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
31
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
32
|
+
--destructive-foreground: oklch(0.985 0 0);
|
|
33
|
+
--border: oklch(0.922 0 0);
|
|
34
|
+
--input: oklch(0.922 0 0);
|
|
35
|
+
--ring: oklch(0.708 0 0);
|
|
36
|
+
/* Per-page accent (overridden by the app per route) — tints module icons. */
|
|
37
|
+
--page-accent: var(--muted-foreground);
|
|
38
|
+
/* Text highlight (::selection) */
|
|
39
|
+
--selection: #266df0;
|
|
40
|
+
--selection-foreground: #ffffff;
|
|
41
|
+
/* brand primary action button */
|
|
42
|
+
--button-primary: #266df0;
|
|
43
|
+
--button-primary-hover: #215bc4;
|
|
44
|
+
--button-primary-foreground: #ffffff;
|
|
45
|
+
--button-shadow: inset 0px 0px 0px 1px rgba(0, 0, 0, 0.06),
|
|
46
|
+
0px 2px 4px -2px rgba(38, 109, 240, 0.12),
|
|
47
|
+
0px 3px 6px -2px rgba(38, 109, 240, 0.08);
|
|
48
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
49
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
50
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
51
|
+
--chart-4: oklch(0.828 0.189 84.429);
|
|
52
|
+
--chart-5: oklch(0.769 0.188 70.08);
|
|
53
|
+
--sidebar: oklch(0.985 0 0);
|
|
54
|
+
--sidebar-foreground: var(--foreground);
|
|
55
|
+
--sidebar-primary: oklch(0.205 0 0);
|
|
56
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
57
|
+
--sidebar-accent: oklch(0.97 0 0);
|
|
58
|
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
59
|
+
--sidebar-border: oklch(0.922 0 0);
|
|
60
|
+
--sidebar-ring: oklch(0.708 0 0);
|
|
61
|
+
|
|
62
|
+
/* Brand palette — used by the gradient wordmark (from-brand-indigo …). */
|
|
63
|
+
--brand-indigo: #0866ff;
|
|
64
|
+
--brand-violet: #3b82f6;
|
|
65
|
+
--brand-coral: #ff5c40;
|
|
66
|
+
--brand-mint: #4fe3c1;
|
|
67
|
+
--brand-ink: #060914;
|
|
68
|
+
--brand-mist: #eef1fb;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.dark {
|
|
72
|
+
--background: oklch(0.145 0 0);
|
|
73
|
+
--foreground: oklch(0.985 0 0);
|
|
74
|
+
--card: oklch(0.205 0 0);
|
|
75
|
+
--card-foreground: oklch(0.985 0 0);
|
|
76
|
+
--popover: oklch(0.205 0 0);
|
|
77
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
78
|
+
--primary: oklch(0.922 0 0);
|
|
79
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
80
|
+
--secondary: oklch(0.269 0 0);
|
|
81
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
82
|
+
--muted: oklch(0.269 0 0);
|
|
83
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
84
|
+
--accent: oklch(0.269 0 0);
|
|
85
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
86
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
87
|
+
--destructive-foreground: oklch(0.985 0 0);
|
|
88
|
+
--border: oklch(1 0 0 / 10%);
|
|
89
|
+
--input: oklch(1 0 0 / 15%);
|
|
90
|
+
--ring: oklch(0.556 0 0);
|
|
91
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
92
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
93
|
+
--chart-3: oklch(0.769 0.188 70.08);
|
|
94
|
+
--chart-4: oklch(0.627 0.265 303.9);
|
|
95
|
+
--chart-5: oklch(0.645 0.246 16.439);
|
|
96
|
+
--sidebar: oklch(0.205 0 0);
|
|
97
|
+
/* --sidebar-foreground inherits var(--foreground) from :root */
|
|
98
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
99
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
100
|
+
--sidebar-accent: oklch(0.269 0 0);
|
|
101
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
102
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
103
|
+
--sidebar-ring: oklch(0.556 0 0);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@theme inline {
|
|
107
|
+
--font-sans: var(--font-inter), ui-sans-serif, system-ui, -apple-system,
|
|
108
|
+
"Segoe UI", Roboto, sans-serif;
|
|
109
|
+
--font-mono: var(--font-jetbrains-mono), ui-monospace, SFMono-Regular, Menlo,
|
|
110
|
+
monospace;
|
|
111
|
+
|
|
112
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
113
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
114
|
+
--radius-lg: var(--radius);
|
|
115
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
116
|
+
|
|
117
|
+
--color-background: var(--background);
|
|
118
|
+
--color-foreground: var(--foreground);
|
|
119
|
+
--color-card: var(--card);
|
|
120
|
+
--color-card-foreground: var(--card-foreground);
|
|
121
|
+
--color-popover: var(--popover);
|
|
122
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
123
|
+
--color-primary: var(--primary);
|
|
124
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
125
|
+
--color-secondary: var(--secondary);
|
|
126
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
127
|
+
--color-muted: var(--muted);
|
|
128
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
129
|
+
--color-accent: var(--accent);
|
|
130
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
131
|
+
--color-destructive: var(--destructive);
|
|
132
|
+
--color-destructive-foreground: var(--destructive-foreground);
|
|
133
|
+
--color-border: var(--border);
|
|
134
|
+
--color-input: var(--input);
|
|
135
|
+
--color-ring: var(--ring);
|
|
136
|
+
--color-selection: var(--selection);
|
|
137
|
+
--color-selection-foreground: var(--selection-foreground);
|
|
138
|
+
--color-chart-1: var(--chart-1);
|
|
139
|
+
--color-chart-2: var(--chart-2);
|
|
140
|
+
--color-chart-3: var(--chart-3);
|
|
141
|
+
--color-chart-4: var(--chart-4);
|
|
142
|
+
--color-chart-5: var(--chart-5);
|
|
143
|
+
--color-sidebar: var(--sidebar);
|
|
144
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
145
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
146
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
147
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
148
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
149
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
150
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
151
|
+
|
|
152
|
+
--color-brand-indigo: var(--brand-indigo);
|
|
153
|
+
--color-brand-violet: var(--brand-violet);
|
|
154
|
+
--color-brand-coral: var(--brand-coral);
|
|
155
|
+
--color-brand-mint: var(--brand-mint);
|
|
156
|
+
--color-brand-ink: var(--brand-ink);
|
|
157
|
+
--color-brand-mist: var(--brand-mist);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
@layer base {
|
|
161
|
+
* {
|
|
162
|
+
@apply border-border outline-ring/50;
|
|
163
|
+
}
|
|
164
|
+
html {
|
|
165
|
+
overscroll-behavior: none;
|
|
166
|
+
scroll-behavior: smooth;
|
|
167
|
+
}
|
|
168
|
+
body {
|
|
169
|
+
@apply bg-background text-foreground font-sans antialiased;
|
|
170
|
+
min-height: 100vh;
|
|
171
|
+
/* Global type baseline: Inter 14px / medium, with ligatures. */
|
|
172
|
+
font-size: 14px;
|
|
173
|
+
font-weight: 500;
|
|
174
|
+
font-feature-settings: "calt", "liga";
|
|
175
|
+
}
|
|
176
|
+
::selection {
|
|
177
|
+
background-color: var(--selection);
|
|
178
|
+
color: var(--selection-foreground);
|
|
179
|
+
}
|
|
180
|
+
/* Every clickable element shows the hand cursor (global — no inline needed). */
|
|
181
|
+
button:not(:disabled),
|
|
182
|
+
[role="button"],
|
|
183
|
+
[role="menuitem"],
|
|
184
|
+
[role="menuitemcheckbox"],
|
|
185
|
+
[role="tab"],
|
|
186
|
+
summary,
|
|
187
|
+
label[for],
|
|
188
|
+
a[href] {
|
|
189
|
+
cursor: pointer;
|
|
190
|
+
}
|
|
191
|
+
/* Every icon is a bordered chip. Radix icons render `<svg width="15">`, so we
|
|
192
|
+
target that (custom SVGs like the logo use viewBox only and are exempt). The
|
|
193
|
+
Tailwind size utility (e.g. size-4) sets the glyph box, and content-box adds
|
|
194
|
+
the border + padding outside it so the glyph stays crisp. Active/emphasis
|
|
195
|
+
states just flip bg/border/color via utilities (they win in the later layer). */
|
|
196
|
+
svg[width="15"] {
|
|
197
|
+
box-sizing: content-box;
|
|
198
|
+
padding: 2px;
|
|
199
|
+
border: 1px solid var(--border);
|
|
200
|
+
border-radius: calc(var(--radius) - 4px);
|
|
201
|
+
}
|
|
202
|
+
/* Radix icons are thin filled glyphs; add a matching stroke so they read
|
|
203
|
+
bolder/heavier throughout the app. */
|
|
204
|
+
svg[width="15"] path {
|
|
205
|
+
stroke: currentColor;
|
|
206
|
+
stroke-width: 0.55px;
|
|
207
|
+
stroke-linejoin: round;
|
|
208
|
+
stroke-linecap: round;
|
|
209
|
+
}
|
|
210
|
+
/* ponytail: rest of the paste's reset is already covered by Tailwind preflight. */
|
|
211
|
+
@media (prefers-reduced-motion: reduce) {
|
|
212
|
+
* {
|
|
213
|
+
scroll-behavior: auto !important;
|
|
214
|
+
transition-duration: 0.01ms !important;
|
|
215
|
+
animation-duration: 0.01ms !important;
|
|
216
|
+
animation-iteration-count: 1 !important;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* Motion primitives — enterprise-style entrance transitions. Reduced-motion is
|
|
222
|
+
respected via the rule above (animation-duration is clamped for those users). */
|
|
223
|
+
/* Opacity-only (no transform) so a faded page never becomes the containing
|
|
224
|
+
block for the fixed detail-panel overlay — the backdrop must cover the whole
|
|
225
|
+
viewport (sidebar, top bar, footer), not just the content area. */
|
|
226
|
+
@keyframes vui-fade-in {
|
|
227
|
+
from {
|
|
228
|
+
opacity: 0;
|
|
229
|
+
}
|
|
230
|
+
to {
|
|
231
|
+
opacity: 1;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
@keyframes vui-overlay-in {
|
|
235
|
+
from {
|
|
236
|
+
opacity: 0;
|
|
237
|
+
}
|
|
238
|
+
to {
|
|
239
|
+
opacity: 1;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
@keyframes vui-slide-in-right {
|
|
243
|
+
from {
|
|
244
|
+
transform: translateX(100%);
|
|
245
|
+
}
|
|
246
|
+
to {
|
|
247
|
+
transform: translateX(0);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
@keyframes vui-pop-in {
|
|
251
|
+
from {
|
|
252
|
+
opacity: 0;
|
|
253
|
+
transform: scale(0.96) translateY(-4px);
|
|
254
|
+
}
|
|
255
|
+
to {
|
|
256
|
+
opacity: 1;
|
|
257
|
+
transform: scale(1) translateY(0);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.vui-fade-in {
|
|
262
|
+
animation: vui-fade-in 0.28s cubic-bezier(0.22, 1, 0.36, 1) both;
|
|
263
|
+
}
|
|
264
|
+
.vui-overlay-in {
|
|
265
|
+
animation: vui-overlay-in 0.2s ease-out both;
|
|
266
|
+
}
|
|
267
|
+
.vui-panel-in {
|
|
268
|
+
animation: vui-slide-in-right 0.3s cubic-bezier(0.32, 0.72, 0, 1) both;
|
|
269
|
+
}
|
|
270
|
+
.vui-pop-in {
|
|
271
|
+
animation: vui-pop-in 0.14s ease-out both;
|
|
272
|
+
transform-origin: var(--vui-pop-origin, top);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
@keyframes vui-slide-out-right {
|
|
276
|
+
from {
|
|
277
|
+
transform: translateX(0);
|
|
278
|
+
}
|
|
279
|
+
to {
|
|
280
|
+
transform: translateX(100%);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
@keyframes vui-overlay-out {
|
|
284
|
+
from {
|
|
285
|
+
opacity: 1;
|
|
286
|
+
}
|
|
287
|
+
to {
|
|
288
|
+
opacity: 0;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
.vui-panel-out {
|
|
292
|
+
animation: vui-slide-out-right 0.24s cubic-bezier(0.32, 0.72, 0, 1) both;
|
|
293
|
+
}
|
|
294
|
+
.vui-overlay-out {
|
|
295
|
+
animation: vui-overlay-out 0.2s ease-out both;
|
|
296
|
+
}
|
package/src/utils.ts
ADDED