@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.
Files changed (89) hide show
  1. package/dist/chunk-5HSOBJ4F.mjs +5968 -0
  2. package/dist/{index-D1RTT2Ap.d.mts → index-BgG8Homu.d.mts} +419 -2
  3. package/dist/{index-D1RTT2Ap.d.ts → index-BgG8Homu.d.ts} +419 -2
  4. package/dist/index.d.mts +198 -95
  5. package/dist/index.d.ts +198 -95
  6. package/dist/index.js +2995 -1136
  7. package/dist/index.mjs +629 -735
  8. package/dist/ui.d.mts +1 -1
  9. package/dist/ui.d.ts +1 -1
  10. package/dist/ui.js +1986 -156
  11. package/dist/ui.mjs +76 -3
  12. package/package.json +4 -14
  13. package/src/app/globals.css +976 -2
  14. package/src/components/charts/index.ts +9 -8
  15. package/src/components/charts/quill/BarChart.tsx +85 -0
  16. package/src/components/charts/quill/FunnelChart.tsx +49 -0
  17. package/src/components/charts/quill/InsightShell.tsx +27 -0
  18. package/src/components/charts/quill/MetricCard.tsx +44 -0
  19. package/src/components/charts/quill/PieChart.tsx +81 -0
  20. package/src/components/charts/quill/Sparkline.tsx +57 -0
  21. package/src/components/charts/quill/TimeSeriesLineChart.tsx +62 -0
  22. package/src/components/charts/quill/index.ts +9 -0
  23. package/src/components/icons/icons.tsx +2 -0
  24. package/src/components/icons/index.ts +3 -0
  25. package/src/components/icons/lemon-brand-icons.tsx +16 -0
  26. package/src/components/icons/lemon-category-icons.tsx +72 -0
  27. package/src/components/icons/lemon-icons.tsx +57 -0
  28. package/src/components/sections/IconShowcase.tsx +110 -7
  29. package/src/components/sections/NewComponentsShowcase.tsx +1052 -0
  30. package/src/components/sections/ProductLayoutsShowcase.tsx +57 -117
  31. package/src/components/sections/QuillChartShowcase.tsx +92 -0
  32. package/src/components/sections/QuillDashboardShowcase.tsx +149 -0
  33. package/src/components/sections/SidebarShowcase.tsx +152 -0
  34. package/src/components/sections/TableShowcase.tsx +2 -0
  35. package/src/components/sections/TooltipShowcase.tsx +19 -25
  36. package/src/components/ui/AlertDialog.tsx +62 -0
  37. package/src/components/ui/AppLayout.tsx +27 -0
  38. package/src/components/ui/Autocomplete.tsx +106 -0
  39. package/src/components/ui/Banner.tsx +63 -0
  40. package/src/components/ui/ButtonGroup.tsx +16 -0
  41. package/src/components/ui/Chip.tsx +46 -0
  42. package/src/components/ui/Collapsible.tsx +46 -0
  43. package/src/components/ui/Combobox.tsx +119 -0
  44. package/src/components/ui/ContextMenu.tsx +81 -0
  45. package/src/components/ui/DataTable.tsx +133 -0
  46. package/src/components/ui/DatePicker.tsx +96 -0
  47. package/src/components/ui/Dialog.tsx +75 -0
  48. package/src/components/ui/Divider.tsx +39 -0
  49. package/src/components/ui/Dot.tsx +32 -0
  50. package/src/components/ui/InputGroup.tsx +16 -0
  51. package/src/components/ui/Lettermark.tsx +47 -0
  52. package/src/components/ui/Link.tsx +32 -0
  53. package/src/components/ui/LoadingBar.tsx +25 -0
  54. package/src/components/ui/Menubar.tsx +66 -0
  55. package/src/components/ui/Metric.tsx +34 -0
  56. package/src/components/ui/NumberField.tsx +76 -0
  57. package/src/components/ui/Popover.tsx +73 -0
  58. package/src/components/ui/ProgressCircle.tsx +63 -0
  59. package/src/components/ui/Resizable.tsx +71 -0
  60. package/src/components/ui/Row.tsx +51 -0
  61. package/src/components/ui/ScrollArea.tsx +54 -0
  62. package/src/components/ui/Sidebar.tsx +77 -0
  63. package/src/components/ui/Snack.tsx +48 -0
  64. package/src/components/ui/Spinner.tsx +30 -0
  65. package/src/components/ui/Splotch.tsx +33 -0
  66. package/src/components/ui/Table.tsx +2 -0
  67. package/src/components/ui/Tag.tsx +65 -0
  68. package/src/components/ui/Toggle.tsx +62 -0
  69. package/src/components/ui/Tooltip.test.tsx +58 -0
  70. package/src/components/ui/Tooltip.tsx +211 -0
  71. package/src/components/ui/Widget.tsx +31 -0
  72. package/src/components/ui/index.ts +275 -21
  73. package/dist/chunk-FDMD3IIN.mjs +0 -3831
  74. package/src/components/charts/ph-insight-charts.tsx +0 -162
  75. package/src/components/dashboard/DashboardFiltersBar.tsx +0 -19
  76. package/src/components/dashboard/DashboardGrid.tsx +0 -23
  77. package/src/components/dashboard/DashboardInsightPlaceholder.tsx +0 -37
  78. package/src/components/dashboard/DashboardKpiCard.tsx +0 -25
  79. package/src/components/dashboard/DashboardToolbar.tsx +0 -23
  80. package/src/components/dashboard/DashboardWell.tsx +0 -10
  81. package/src/components/dashboard/InsightMiniCard.tsx +0 -26
  82. package/src/components/dashboard/InsightShell.tsx +0 -37
  83. package/src/components/dashboard/InsightShellHeader.tsx +0 -22
  84. package/src/components/dashboard/MiniTrendBars.tsx +0 -51
  85. package/src/components/dashboard/index.ts +0 -31
  86. package/src/components/dashboard/types.ts +0 -9
  87. package/src/components/sections/ChartShowcase.tsx +0 -190
  88. package/src/components/sections/DashboardShowcase.tsx +0 -191
  89. 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";