@xenide-io/the-old-ui-theme 0.3.0 → 0.3.2
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/chunk-5HSOBJ4F.mjs +5968 -0
- package/dist/{index-D1RTT2Ap.d.mts → index-BgG8Homu.d.mts} +419 -2
- package/dist/{index-D1RTT2Ap.d.ts → index-BgG8Homu.d.ts} +419 -2
- package/dist/index.d.mts +198 -95
- package/dist/index.d.ts +198 -95
- package/dist/index.js +2995 -1136
- package/dist/index.mjs +629 -735
- package/dist/ui.d.mts +1 -1
- package/dist/ui.d.ts +1 -1
- package/dist/ui.js +1986 -156
- package/dist/ui.mjs +76 -3
- package/package.json +4 -14
- package/src/app/globals.css +976 -2
- package/src/components/charts/index.ts +9 -8
- package/src/components/charts/quill/BarChart.tsx +85 -0
- package/src/components/charts/quill/FunnelChart.tsx +49 -0
- package/src/components/charts/quill/InsightShell.tsx +27 -0
- package/src/components/charts/quill/MetricCard.tsx +44 -0
- package/src/components/charts/quill/PieChart.tsx +81 -0
- package/src/components/charts/quill/Sparkline.tsx +57 -0
- package/src/components/charts/quill/TimeSeriesLineChart.tsx +62 -0
- package/src/components/charts/quill/index.ts +9 -0
- package/src/components/icons/icons.tsx +2 -0
- package/src/components/icons/index.ts +3 -0
- package/src/components/icons/lemon-brand-icons.tsx +16 -0
- package/src/components/icons/lemon-category-icons.tsx +72 -0
- package/src/components/icons/lemon-icons.tsx +57 -0
- package/src/components/sections/IconShowcase.tsx +110 -7
- package/src/components/sections/NewComponentsShowcase.tsx +1052 -0
- package/src/components/sections/ProductLayoutsShowcase.tsx +57 -117
- package/src/components/sections/QuillChartShowcase.tsx +92 -0
- package/src/components/sections/QuillDashboardShowcase.tsx +149 -0
- package/src/components/sections/SidebarShowcase.tsx +152 -0
- package/src/components/sections/TableShowcase.tsx +2 -0
- package/src/components/sections/TooltipShowcase.tsx +19 -25
- package/src/components/ui/AlertDialog.tsx +62 -0
- package/src/components/ui/AppLayout.tsx +27 -0
- package/src/components/ui/Autocomplete.tsx +106 -0
- package/src/components/ui/Banner.tsx +63 -0
- package/src/components/ui/ButtonGroup.tsx +16 -0
- package/src/components/ui/Chip.tsx +46 -0
- package/src/components/ui/Collapsible.tsx +46 -0
- package/src/components/ui/Combobox.tsx +119 -0
- package/src/components/ui/ContextMenu.tsx +81 -0
- package/src/components/ui/DataTable.tsx +133 -0
- package/src/components/ui/DatePicker.tsx +96 -0
- package/src/components/ui/Dialog.tsx +75 -0
- package/src/components/ui/Divider.tsx +39 -0
- package/src/components/ui/Dot.tsx +32 -0
- package/src/components/ui/InputGroup.tsx +16 -0
- package/src/components/ui/Lettermark.tsx +47 -0
- package/src/components/ui/Link.tsx +32 -0
- package/src/components/ui/LoadingBar.tsx +25 -0
- package/src/components/ui/Menubar.tsx +66 -0
- package/src/components/ui/Metric.tsx +34 -0
- package/src/components/ui/NumberField.tsx +76 -0
- package/src/components/ui/Popover.tsx +73 -0
- package/src/components/ui/ProgressCircle.tsx +63 -0
- package/src/components/ui/Resizable.tsx +71 -0
- package/src/components/ui/Row.tsx +51 -0
- package/src/components/ui/ScrollArea.tsx +54 -0
- package/src/components/ui/Sidebar.tsx +77 -0
- package/src/components/ui/Snack.tsx +48 -0
- package/src/components/ui/Spinner.tsx +30 -0
- package/src/components/ui/Splotch.tsx +33 -0
- package/src/components/ui/Table.tsx +2 -0
- package/src/components/ui/Tag.tsx +65 -0
- package/src/components/ui/Toggle.tsx +62 -0
- package/src/components/ui/Tooltip.test.tsx +58 -0
- package/src/components/ui/Tooltip.tsx +211 -0
- package/src/components/ui/Widget.tsx +31 -0
- package/src/components/ui/index.ts +275 -21
- package/dist/chunk-FDMD3IIN.mjs +0 -3831
- package/src/components/charts/ph-insight-charts.tsx +0 -162
- package/src/components/dashboard/DashboardFiltersBar.tsx +0 -19
- package/src/components/dashboard/DashboardGrid.tsx +0 -23
- package/src/components/dashboard/DashboardInsightPlaceholder.tsx +0 -37
- package/src/components/dashboard/DashboardKpiCard.tsx +0 -25
- package/src/components/dashboard/DashboardToolbar.tsx +0 -23
- package/src/components/dashboard/DashboardWell.tsx +0 -10
- package/src/components/dashboard/InsightMiniCard.tsx +0 -26
- package/src/components/dashboard/InsightShell.tsx +0 -37
- package/src/components/dashboard/InsightShellHeader.tsx +0 -22
- package/src/components/dashboard/MiniTrendBars.tsx +0 -51
- package/src/components/dashboard/index.ts +0 -31
- package/src/components/dashboard/types.ts +0 -9
- package/src/components/sections/ChartShowcase.tsx +0 -190
- package/src/components/sections/DashboardShowcase.tsx +0 -191
- package/src/components/sections/StatShowcase.tsx +0 -129
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo, type ReactNode } from "react";
|
|
4
|
+
import { cn } from "@/lib/cn";
|
|
5
|
+
|
|
6
|
+
export interface DataTableColumn<T> {
|
|
7
|
+
key: string;
|
|
8
|
+
label: string;
|
|
9
|
+
render?: (item: T) => ReactNode;
|
|
10
|
+
sortable?: boolean;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface DataTableProps<T> {
|
|
15
|
+
columns: DataTableColumn<T>[];
|
|
16
|
+
data: T[];
|
|
17
|
+
pageSize?: number;
|
|
18
|
+
searchable?: boolean;
|
|
19
|
+
searchPlaceholder?: string;
|
|
20
|
+
className?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function DataTable<T extends Record<string, any>>({
|
|
24
|
+
columns,
|
|
25
|
+
data,
|
|
26
|
+
pageSize = 10,
|
|
27
|
+
searchable = false,
|
|
28
|
+
searchPlaceholder = "Search...",
|
|
29
|
+
className,
|
|
30
|
+
}: DataTableProps<T>) {
|
|
31
|
+
const [search, setSearch] = useState("");
|
|
32
|
+
const [sortKey, setSortKey] = useState<string | null>(null);
|
|
33
|
+
const [sortDir, setSortDir] = useState<"asc" | "desc">("asc");
|
|
34
|
+
const [page, setPage] = useState(0);
|
|
35
|
+
|
|
36
|
+
const filtered = useMemo(() => {
|
|
37
|
+
let items = data;
|
|
38
|
+
if (search) {
|
|
39
|
+
const q = search.toLowerCase();
|
|
40
|
+
items = items.filter((item) =>
|
|
41
|
+
Object.values(item).some((v) => String(v).toLowerCase().includes(q))
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (sortKey) {
|
|
45
|
+
items = [...items].sort((a, b) => {
|
|
46
|
+
const av = a[sortKey];
|
|
47
|
+
const bv = b[sortKey];
|
|
48
|
+
const cmp = av < bv ? -1 : av > bv ? 1 : 0;
|
|
49
|
+
return sortDir === "asc" ? cmp : -cmp;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return items;
|
|
53
|
+
}, [data, search, sortKey, sortDir]);
|
|
54
|
+
|
|
55
|
+
const totalPages = Math.ceil(filtered.length / pageSize);
|
|
56
|
+
const paged = filtered.slice(page * pageSize, (page + 1) * pageSize);
|
|
57
|
+
|
|
58
|
+
const handleSort = (key: string) => {
|
|
59
|
+
if (sortKey === key) {
|
|
60
|
+
setSortDir((d) => (d === "asc" ? "desc" : "asc"));
|
|
61
|
+
} else {
|
|
62
|
+
setSortKey(key);
|
|
63
|
+
setSortDir("asc");
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className={cn("ph-data-table", className)}>
|
|
69
|
+
{searchable && (
|
|
70
|
+
<div className="ph-data-table__toolbar">
|
|
71
|
+
<input
|
|
72
|
+
type="text"
|
|
73
|
+
value={search}
|
|
74
|
+
onChange={(e) => { setSearch(e.target.value); setPage(0); }}
|
|
75
|
+
placeholder={searchPlaceholder}
|
|
76
|
+
className="ph-input ph-input-sm"
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
<div className="ph-table-wrap">
|
|
81
|
+
<table className="ph-table">
|
|
82
|
+
<thead>
|
|
83
|
+
<tr>
|
|
84
|
+
{columns.map((col) => (
|
|
85
|
+
<th
|
|
86
|
+
key={col.key}
|
|
87
|
+
className={cn(col.sortable && "cursor-pointer select-none", col.className)}
|
|
88
|
+
onClick={() => col.sortable && handleSort(col.key)}
|
|
89
|
+
>
|
|
90
|
+
{col.label}
|
|
91
|
+
{sortKey === col.key && (
|
|
92
|
+
<span className="ml-1 inline-block">{sortDir === "asc" ? "\u2191" : "\u2193"}</span>
|
|
93
|
+
)}
|
|
94
|
+
</th>
|
|
95
|
+
))}
|
|
96
|
+
</tr>
|
|
97
|
+
</thead>
|
|
98
|
+
<tbody>
|
|
99
|
+
{paged.map((item, i) => (
|
|
100
|
+
<tr key={i}>
|
|
101
|
+
{columns.map((col) => (
|
|
102
|
+
<td key={col.key} className={col.className}>
|
|
103
|
+
{col.render ? col.render(item) : item[col.key]}
|
|
104
|
+
</td>
|
|
105
|
+
))}
|
|
106
|
+
</tr>
|
|
107
|
+
))}
|
|
108
|
+
{paged.length === 0 && (
|
|
109
|
+
<tr>
|
|
110
|
+
<td colSpan={columns.length} className="text-center text-ph-mutedtext py-8">No results</td>
|
|
111
|
+
</tr>
|
|
112
|
+
)}
|
|
113
|
+
</tbody>
|
|
114
|
+
</table>
|
|
115
|
+
</div>
|
|
116
|
+
{totalPages > 1 && (
|
|
117
|
+
<div className="ph-data-table__pagination">
|
|
118
|
+
<button disabled={page === 0} onClick={() => setPage((p) => p - 1)} className="ph-data-table__page-btn">
|
|
119
|
+
Previous
|
|
120
|
+
</button>
|
|
121
|
+
<span className="text-xs text-ph-mutedtext">
|
|
122
|
+
Page {page + 1} of {totalPages}
|
|
123
|
+
</span>
|
|
124
|
+
<button disabled={page >= totalPages - 1} onClick={() => setPage((p) => p + 1)} className="ph-data-table__page-btn">
|
|
125
|
+
Next
|
|
126
|
+
</button>
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
DataTable.displayName = "DataTable";
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo, type InputHTMLAttributes } from "react";
|
|
4
|
+
import { cn } from "@/lib/cn";
|
|
5
|
+
|
|
6
|
+
const MONTHS = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
|
|
7
|
+
const DAYS = ["Su","Mo","Tu","We","Th","Fr","Sa"];
|
|
8
|
+
|
|
9
|
+
export interface DatePickerProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "value" | "onChange" | "type"> {
|
|
10
|
+
value?: Date;
|
|
11
|
+
onChange?: (date: Date) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function DatePicker({ value, onChange, className, ...props }: DatePickerProps) {
|
|
15
|
+
const [open, setOpen] = useState(false);
|
|
16
|
+
const [viewDate, setViewDate] = useState(value ?? new Date());
|
|
17
|
+
const selected = value;
|
|
18
|
+
|
|
19
|
+
const days = useMemo(() => {
|
|
20
|
+
const year = viewDate.getFullYear();
|
|
21
|
+
const month = viewDate.getMonth();
|
|
22
|
+
const first = new Date(year, month, 1).getDay();
|
|
23
|
+
const last = new Date(year, month + 1, 0).getDate();
|
|
24
|
+
const prevLast = new Date(year, month, 0).getDate();
|
|
25
|
+
const cells: { day: number; month: "prev" | "current" | "next" }[] = [];
|
|
26
|
+
for (let i = first - 1; i >= 0; i--) cells.push({ day: prevLast - i, month: "prev" });
|
|
27
|
+
for (let d = 1; d <= last; d++) cells.push({ day: d, month: "current" });
|
|
28
|
+
const remaining = 42 - cells.length;
|
|
29
|
+
for (let d = 1; d <= remaining; d++) cells.push({ day: d, month: "next" });
|
|
30
|
+
return cells;
|
|
31
|
+
}, [viewDate]);
|
|
32
|
+
|
|
33
|
+
const isSelected = (d: number, m: "prev" | "current" | "next") => {
|
|
34
|
+
if (!selected || m !== "current") return false;
|
|
35
|
+
const date = new Date(viewDate.getFullYear(), viewDate.getMonth(), d);
|
|
36
|
+
return date.toDateString() === selected.toDateString();
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const selectDay = (d: number, m: string) => {
|
|
40
|
+
const month = m === "prev" ? viewDate.getMonth() - 1 : m === "next" ? viewDate.getMonth() + 1 : viewDate.getMonth();
|
|
41
|
+
const year = month < 0 ? viewDate.getFullYear() - 1 : month > 11 ? viewDate.getFullYear() + 1 : viewDate.getFullYear();
|
|
42
|
+
const adjustedMonth = ((month % 12) + 12) % 12;
|
|
43
|
+
const date = new Date(year, adjustedMonth, d);
|
|
44
|
+
onChange?.(date);
|
|
45
|
+
setOpen(false);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const nav = (delta: number) => setViewDate(new Date(viewDate.getFullYear(), viewDate.getMonth() + delta, 1));
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div className={cn("ph-datepicker relative", className)}>
|
|
52
|
+
<input
|
|
53
|
+
type="text"
|
|
54
|
+
readOnly
|
|
55
|
+
value={selected ? `${MONTHS[selected.getMonth()]} ${selected.getDate()}, ${selected.getFullYear()}` : ""}
|
|
56
|
+
onFocus={() => setOpen(true)}
|
|
57
|
+
placeholder="Pick a date"
|
|
58
|
+
className="ph-input w-full cursor-pointer"
|
|
59
|
+
{...props}
|
|
60
|
+
/>
|
|
61
|
+
{open && (
|
|
62
|
+
<div className="ph-datepicker__dropdown">
|
|
63
|
+
<div className="ph-datepicker__nav">
|
|
64
|
+
<button type="button" onClick={() => nav(-1)} className="ph-datepicker__nav-btn" aria-label="Previous month">
|
|
65
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M7 3L4 6l3 3"/></svg>
|
|
66
|
+
</button>
|
|
67
|
+
<span className="ph-datepicker__nav-label">{MONTHS[viewDate.getMonth()]} {viewDate.getFullYear()}</span>
|
|
68
|
+
<button type="button" onClick={() => nav(1)} className="ph-datepicker__nav-btn" aria-label="Next month">
|
|
69
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M5 3l3 3-3 3"/></svg>
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
<div className="ph-datepicker__grid">
|
|
73
|
+
{DAYS.map((d) => <div key={d} className="ph-datepicker__day-header">{d}</div>)}
|
|
74
|
+
{days.map((d, i) => (
|
|
75
|
+
<button
|
|
76
|
+
key={i}
|
|
77
|
+
type="button"
|
|
78
|
+
disabled={d.month !== "current"}
|
|
79
|
+
onClick={() => selectDay(d.day, d.month)}
|
|
80
|
+
className={cn(
|
|
81
|
+
"ph-datepicker__day",
|
|
82
|
+
d.month !== "current" && "ph-datepicker__day--other",
|
|
83
|
+
isSelected(d.day, d.month) && "ph-datepicker__day--selected"
|
|
84
|
+
)}
|
|
85
|
+
>
|
|
86
|
+
{d.day}
|
|
87
|
+
</button>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
DatePicker.displayName = "DatePicker";
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useState, type ReactNode } from "react";
|
|
2
|
+
import { Modal, type ModalProps } from "@/components/ui/Modal";
|
|
3
|
+
import { Button, type ButtonProps } from "@/components/ui/Button";
|
|
4
|
+
|
|
5
|
+
export interface DialogProps extends Omit<ModalProps, "onClose"> {
|
|
6
|
+
isOpen?: boolean;
|
|
7
|
+
onClose?: () => void;
|
|
8
|
+
onConfirm?: () => void | Promise<void>;
|
|
9
|
+
confirmText?: string;
|
|
10
|
+
cancelText?: string;
|
|
11
|
+
confirmProps?: Partial<ButtonProps>;
|
|
12
|
+
cancelProps?: Partial<ButtonProps>;
|
|
13
|
+
children?: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function Dialog({
|
|
17
|
+
isOpen: openProp,
|
|
18
|
+
onClose,
|
|
19
|
+
onConfirm,
|
|
20
|
+
confirmText = "Confirm",
|
|
21
|
+
cancelText = "Cancel",
|
|
22
|
+
confirmProps,
|
|
23
|
+
cancelProps,
|
|
24
|
+
children,
|
|
25
|
+
title,
|
|
26
|
+
...modalProps
|
|
27
|
+
}: DialogProps) {
|
|
28
|
+
const [loading, setLoading] = useState(false);
|
|
29
|
+
const isOpen = openProp ?? true;
|
|
30
|
+
|
|
31
|
+
const handleConfirm = async () => {
|
|
32
|
+
if (!onConfirm) return;
|
|
33
|
+
setLoading(true);
|
|
34
|
+
try {
|
|
35
|
+
await onConfirm();
|
|
36
|
+
} finally {
|
|
37
|
+
setLoading(false);
|
|
38
|
+
}
|
|
39
|
+
onClose?.();
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Modal
|
|
44
|
+
open={isOpen}
|
|
45
|
+
onClose={onClose}
|
|
46
|
+
title={title}
|
|
47
|
+
footer={
|
|
48
|
+
<div className="flex justify-end gap-2">
|
|
49
|
+
<Button variant="secondary" {...cancelProps} onClick={onClose}>
|
|
50
|
+
{cancelText}
|
|
51
|
+
</Button>
|
|
52
|
+
{onConfirm && (
|
|
53
|
+
<Button
|
|
54
|
+
variant="primary"
|
|
55
|
+
loading={loading}
|
|
56
|
+
{...confirmProps}
|
|
57
|
+
onClick={handleConfirm}
|
|
58
|
+
>
|
|
59
|
+
{confirmText}
|
|
60
|
+
</Button>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
}
|
|
64
|
+
{...modalProps}
|
|
65
|
+
>
|
|
66
|
+
{children}
|
|
67
|
+
</Modal>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
Dialog.displayName = "Dialog";
|
|
72
|
+
|
|
73
|
+
export function confirmDialog(props: DialogProps) {
|
|
74
|
+
return <Dialog {...props} />;
|
|
75
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface DividerProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
vertical?: boolean;
|
|
6
|
+
dashed?: boolean;
|
|
7
|
+
thick?: boolean;
|
|
8
|
+
label?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Divider({
|
|
12
|
+
vertical = false,
|
|
13
|
+
dashed = false,
|
|
14
|
+
thick = false,
|
|
15
|
+
label,
|
|
16
|
+
className,
|
|
17
|
+
...props
|
|
18
|
+
}: DividerProps) {
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
role="separator"
|
|
22
|
+
aria-orientation={vertical ? "vertical" : "horizontal"}
|
|
23
|
+
className={cn(
|
|
24
|
+
"ph-divider",
|
|
25
|
+
vertical && "ph-divider--vertical",
|
|
26
|
+
thick && "ph-divider--thick",
|
|
27
|
+
dashed && "ph-divider--dashed",
|
|
28
|
+
!!label && "ph-divider--with-label",
|
|
29
|
+
!className && (vertical ? "mx-2 self-stretch" : "my-2"),
|
|
30
|
+
className
|
|
31
|
+
)}
|
|
32
|
+
{...props}
|
|
33
|
+
>
|
|
34
|
+
{label && <span className="ph-divider__label">{label}</span>}
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
Divider.displayName = "Divider";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface DotProps extends HTMLAttributes<HTMLSpanElement> {
|
|
5
|
+
color?: "green" | "yellow" | "red" | "blue" | "gray";
|
|
6
|
+
size?: "sm" | "md";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const colorMap = {
|
|
10
|
+
green: "bg-ph-success",
|
|
11
|
+
yellow: "bg-ph-warning",
|
|
12
|
+
red: "bg-ph-danger",
|
|
13
|
+
blue: "bg-ph-info",
|
|
14
|
+
gray: "bg-ph-mutedtext",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function Dot({ color = "gray", size = "md", className, ...props }: DotProps) {
|
|
18
|
+
return (
|
|
19
|
+
<span
|
|
20
|
+
className={cn(
|
|
21
|
+
"ph-dot inline-block shrink-0 rounded-full",
|
|
22
|
+
size === "sm" ? "h-1.5 w-1.5" : "h-2.5 w-2.5",
|
|
23
|
+
colorMap[color],
|
|
24
|
+
className
|
|
25
|
+
)}
|
|
26
|
+
aria-hidden="true"
|
|
27
|
+
{...props}
|
|
28
|
+
/>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Dot.displayName = "Dot";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ReactNode, type HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface InputGroupProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function InputGroup({ children, className, ...props }: InputGroupProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div className={cn("ph-input-group", className)} {...props}>
|
|
11
|
+
{children}
|
|
12
|
+
</div>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
InputGroup.displayName = "InputGroup";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { forwardRef, type HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface LettermarkProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
name?: string | number | null;
|
|
6
|
+
index?: number;
|
|
7
|
+
rounded?: boolean;
|
|
8
|
+
outlined?: boolean;
|
|
9
|
+
size?: "xs" | "sm" | "md" | "lg";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const Lettermark = forwardRef<HTMLDivElement, LettermarkProps>(function Lettermark({
|
|
13
|
+
name,
|
|
14
|
+
index,
|
|
15
|
+
outlined = false,
|
|
16
|
+
rounded = false,
|
|
17
|
+
size = "md",
|
|
18
|
+
className,
|
|
19
|
+
...props
|
|
20
|
+
}, ref) {
|
|
21
|
+
const letter = name
|
|
22
|
+
? typeof name === "number"
|
|
23
|
+
? String(Math.floor(name))
|
|
24
|
+
: String.fromCodePoint(name.codePointAt(0)!).toLocaleUpperCase()
|
|
25
|
+
: "?";
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div
|
|
29
|
+
ref={ref}
|
|
30
|
+
className={cn(
|
|
31
|
+
"ph-lettermark",
|
|
32
|
+
`ph-lettermark--${size}`,
|
|
33
|
+
typeof index === "number" && `ph-lettermark--v-${(index % 8) + 1}`,
|
|
34
|
+
outlined && "ph-lettermark--outlined",
|
|
35
|
+
rounded && "ph-lettermark--rounded",
|
|
36
|
+
letter === "?" && "ph-lettermark--unknown",
|
|
37
|
+
className
|
|
38
|
+
)}
|
|
39
|
+
aria-hidden="true"
|
|
40
|
+
{...props}
|
|
41
|
+
>
|
|
42
|
+
{letter}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
Lettermark.displayName = "Lettermark";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { forwardRef, type AnchorHTMLAttributes, type ReactNode } from "react";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
5
|
+
children: ReactNode;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const Link = forwardRef<HTMLAnchorElement, LinkProps>(function Link({
|
|
10
|
+
className,
|
|
11
|
+
disabled = false,
|
|
12
|
+
children,
|
|
13
|
+
...props
|
|
14
|
+
}, ref) {
|
|
15
|
+
return (
|
|
16
|
+
<a
|
|
17
|
+
ref={ref}
|
|
18
|
+
className={cn(
|
|
19
|
+
"ph-link",
|
|
20
|
+
disabled && "ph-link--disabled pointer-events-none opacity-50",
|
|
21
|
+
className
|
|
22
|
+
)}
|
|
23
|
+
tabIndex={disabled ? -1 : undefined}
|
|
24
|
+
aria-disabled={disabled || undefined}
|
|
25
|
+
{...props}
|
|
26
|
+
>
|
|
27
|
+
{children}
|
|
28
|
+
</a>
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
Link.displayName = "Link";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface LoadingBarProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
active?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function LoadingBar({ active = true, className, ...props }: LoadingBarProps) {
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
className={cn(
|
|
12
|
+
"ph-loading-bar",
|
|
13
|
+
active && "ph-loading-bar--active",
|
|
14
|
+
className
|
|
15
|
+
)}
|
|
16
|
+
role="progressbar"
|
|
17
|
+
aria-label="Loading"
|
|
18
|
+
{...props}
|
|
19
|
+
>
|
|
20
|
+
<div className="ph-loading-bar__fill" />
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
LoadingBar.displayName = "LoadingBar";
|
|
@@ -0,0 +1,66 @@
|
|
|
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 MenubarItem {
|
|
7
|
+
label: string;
|
|
8
|
+
items?: { label: string; onClick?: () => void; disabled?: boolean; separator?: boolean }[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface MenubarProps extends HTMLAttributes<HTMLDivElement> {
|
|
12
|
+
items: MenubarItem[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function Menubar({ items, className, ...props }: MenubarProps) {
|
|
16
|
+
const [openIndex, setOpenIndex] = useState(-1);
|
|
17
|
+
const barRef = useRef<HTMLDivElement>(null);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (openIndex < 0) return;
|
|
21
|
+
const handle = (e: MouseEvent) => {
|
|
22
|
+
if (barRef.current && !barRef.current.contains(e.target as Node)) {
|
|
23
|
+
setOpenIndex(-1);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
document.addEventListener("mousedown", handle);
|
|
27
|
+
return () => document.removeEventListener("mousedown", handle);
|
|
28
|
+
}, [openIndex]);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div ref={barRef} className={cn("ph-menubar", className)} {...props}>
|
|
32
|
+
{items.map((item, i) => (
|
|
33
|
+
<div key={i} className="ph-menubar__item">
|
|
34
|
+
<button
|
|
35
|
+
type="button"
|
|
36
|
+
onClick={() => setOpenIndex(openIndex === i ? -1 : i)}
|
|
37
|
+
className={cn("ph-menubar__trigger", openIndex === i && "ph-menubar__trigger--open")}
|
|
38
|
+
>
|
|
39
|
+
{item.label}
|
|
40
|
+
</button>
|
|
41
|
+
{openIndex === i && item.items && (
|
|
42
|
+
<div className="ph-menubar__dropdown">
|
|
43
|
+
{item.items.map((sub, j) =>
|
|
44
|
+
sub.separator ? (
|
|
45
|
+
<div key={j} className="ph-menubar__separator" />
|
|
46
|
+
) : (
|
|
47
|
+
<button
|
|
48
|
+
key={j}
|
|
49
|
+
type="button"
|
|
50
|
+
disabled={sub.disabled}
|
|
51
|
+
onClick={() => { sub.onClick?.(); setOpenIndex(-1); }}
|
|
52
|
+
className="ph-menubar__sub-item"
|
|
53
|
+
>
|
|
54
|
+
{sub.label}
|
|
55
|
+
</button>
|
|
56
|
+
)
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
))}
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
Menubar.displayName = "Menubar";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type ReactNode, type HTMLAttributes } from "react";
|
|
2
|
+
import { cn } from "@/lib/cn";
|
|
3
|
+
|
|
4
|
+
export interface MetricProps extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
value: string | number;
|
|
6
|
+
label: string;
|
|
7
|
+
badge?: ReactNode;
|
|
8
|
+
trend?: ReactNode;
|
|
9
|
+
icon?: ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function Metric({
|
|
13
|
+
value,
|
|
14
|
+
label,
|
|
15
|
+
badge,
|
|
16
|
+
trend,
|
|
17
|
+
icon,
|
|
18
|
+
className,
|
|
19
|
+
...props
|
|
20
|
+
}: MetricProps) {
|
|
21
|
+
return (
|
|
22
|
+
<div className={cn("ph-metric", className)} {...props}>
|
|
23
|
+
<div className="ph-metric__header">
|
|
24
|
+
{icon && <span className="ph-metric__icon">{icon}</span>}
|
|
25
|
+
<span className="ph-metric__label">{label}</span>
|
|
26
|
+
{badge && <span className="ph-metric__badge">{badge}</span>}
|
|
27
|
+
</div>
|
|
28
|
+
<div className="ph-metric__value">{value}</div>
|
|
29
|
+
{trend && <div className="ph-metric__trend">{trend}</div>}
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
Metric.displayName = "Metric";
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, type InputHTMLAttributes } from "react";
|
|
4
|
+
import { cn } from "@/lib/cn";
|
|
5
|
+
|
|
6
|
+
export interface NumberFieldProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "type" | "onChange"> {
|
|
7
|
+
value?: number;
|
|
8
|
+
onChange?: (value: number) => void;
|
|
9
|
+
min?: number;
|
|
10
|
+
max?: number;
|
|
11
|
+
step?: number;
|
|
12
|
+
label?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function NumberField({
|
|
16
|
+
value: controlledValue,
|
|
17
|
+
onChange,
|
|
18
|
+
min,
|
|
19
|
+
max,
|
|
20
|
+
step = 1,
|
|
21
|
+
label,
|
|
22
|
+
className,
|
|
23
|
+
disabled,
|
|
24
|
+
defaultValue,
|
|
25
|
+
...inputProps
|
|
26
|
+
}: NumberFieldProps) {
|
|
27
|
+
const [internal, setInternal] = useState(controlledValue ?? 0);
|
|
28
|
+
const value = controlledValue ?? internal;
|
|
29
|
+
|
|
30
|
+
const set = (v: number) => {
|
|
31
|
+
const clamped = Math.max(min ?? -Infinity, Math.min(max ?? Infinity, v));
|
|
32
|
+
setInternal(clamped);
|
|
33
|
+
onChange?.(clamped);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className={cn("ph-number-field", className)}>
|
|
38
|
+
{label && <label className="ph-label">{label}</label>}
|
|
39
|
+
<div className="ph-number-field__control">
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
disabled={disabled || (min !== undefined && value <= min)}
|
|
43
|
+
onClick={() => set(value - step)}
|
|
44
|
+
className="ph-number-field__btn"
|
|
45
|
+
aria-label="Decrease"
|
|
46
|
+
>
|
|
47
|
+
<svg width="12" height="2" viewBox="0 0 12 2" fill="currentColor"><rect y="0" width="12" height="2" rx="1" /></svg>
|
|
48
|
+
</button>
|
|
49
|
+
<input
|
|
50
|
+
type="number"
|
|
51
|
+
value={value}
|
|
52
|
+
onChange={(e) => set(Number(e.target.value))}
|
|
53
|
+
disabled={disabled}
|
|
54
|
+
min={min}
|
|
55
|
+
max={max}
|
|
56
|
+
step={step}
|
|
57
|
+
className="ph-number-field__input"
|
|
58
|
+
{...inputProps}
|
|
59
|
+
/>
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
disabled={disabled || (max !== undefined && value >= max)}
|
|
63
|
+
onClick={() => set(value + step)}
|
|
64
|
+
className="ph-number-field__btn"
|
|
65
|
+
aria-label="Increase"
|
|
66
|
+
>
|
|
67
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
68
|
+
<path d="M6 2v8M2 6h8" />
|
|
69
|
+
</svg>
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
NumberField.displayName = "NumberField";
|