ruler-components 0.1.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.
Files changed (114) hide show
  1. package/AGENTS.md +9 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +21 -0
  4. package/README.md +45 -0
  5. package/app/docs/[slug]/page.tsx +147 -0
  6. package/app/docs/layout.tsx +26 -0
  7. package/app/docs/page.tsx +148 -0
  8. package/app/favicon.ico +0 -0
  9. package/app/globals.css +168 -0
  10. package/app/icon.png +0 -0
  11. package/app/layout.tsx +90 -0
  12. package/app/opengraph-image.tsx +10 -0
  13. package/app/page.tsx +34 -0
  14. package/app/twitter-image.tsx +10 -0
  15. package/components/docs/accordion-doc-page.tsx +352 -0
  16. package/components/docs/alert-doc-page.tsx +293 -0
  17. package/components/docs/allinput-doc-page.tsx +183 -0
  18. package/components/docs/avatar-doc-page.tsx +424 -0
  19. package/components/docs/badge-doc-page.tsx +363 -0
  20. package/components/docs/button-doc-page.tsx +406 -0
  21. package/components/docs/card-doc-page.tsx +364 -0
  22. package/components/docs/checkbox-doc-page.tsx +277 -0
  23. package/components/docs/colors-doc-page.tsx +430 -0
  24. package/components/docs/dialog-doc-page.tsx +364 -0
  25. package/components/docs/docs-footer.tsx +11 -0
  26. package/components/docs/docs-mobile-menu-bar.tsx +22 -0
  27. package/components/docs/docs-mobile-nav-context.tsx +27 -0
  28. package/components/docs/docs-page.tsx +263 -0
  29. package/components/docs/docs-search.tsx +303 -0
  30. package/components/docs/docs-sidebar.tsx +165 -0
  31. package/components/docs/docs-topbar.tsx +32 -0
  32. package/components/docs/dropdown-doc-page.tsx +522 -0
  33. package/components/docs/getting-started-doc-page.tsx +205 -0
  34. package/components/docs/label-doc-page.tsx +173 -0
  35. package/components/docs/loaders-doc-page.tsx +244 -0
  36. package/components/docs/menu-doc-page.tsx +373 -0
  37. package/components/docs/modal-doc-page.tsx +436 -0
  38. package/components/docs/next-steps.tsx +53 -0
  39. package/components/docs/pagination-doc-page.tsx +316 -0
  40. package/components/docs/progress-doc-page.tsx +387 -0
  41. package/components/docs/radio-doc-page.tsx +270 -0
  42. package/components/docs/skeleton-doc-page.tsx +230 -0
  43. package/components/docs/stepper-doc-page.tsx +309 -0
  44. package/components/docs/switch-doc-page.tsx +264 -0
  45. package/components/docs/table-doc-page.tsx +689 -0
  46. package/components/docs/tabs-doc-page.tsx +359 -0
  47. package/components/docs/theming-doc-page.tsx +243 -0
  48. package/components/docs/tooltip-doc-page.tsx +357 -0
  49. package/components/docs/usage-section.tsx +21 -0
  50. package/components/icons/discord.tsx +9 -0
  51. package/components/icons/github.tsx +9 -0
  52. package/components/icons/x.tsx +9 -0
  53. package/components/installation-section.tsx +34 -0
  54. package/components/landing/feature-highlights.tsx +105 -0
  55. package/components/landing/footer-strip.tsx +45 -0
  56. package/components/landing/hero-card.tsx +14 -0
  57. package/components/landing/hero-spotlight.tsx +126 -0
  58. package/components/landing/hero.tsx +64 -0
  59. package/components/landing/navbar.tsx +44 -0
  60. package/components/landing/quick-start-section.tsx +59 -0
  61. package/components/landing/site-footer.tsx +55 -0
  62. package/components/theme-provider.tsx +60 -0
  63. package/components/ui/accordion.tsx +174 -0
  64. package/components/ui/alert.tsx +103 -0
  65. package/components/ui/allinput.tsx +210 -0
  66. package/components/ui/avatar.tsx +179 -0
  67. package/components/ui/badge.tsx +52 -0
  68. package/components/ui/button.tsx +75 -0
  69. package/components/ui/card.tsx +105 -0
  70. package/components/ui/checkbox.tsx +130 -0
  71. package/components/ui/code-block.tsx +60 -0
  72. package/components/ui/data-table.tsx +235 -0
  73. package/components/ui/dialog.tsx +173 -0
  74. package/components/ui/dropdown.tsx +78 -0
  75. package/components/ui/feature-card.tsx +38 -0
  76. package/components/ui/input.tsx +44 -0
  77. package/components/ui/label.tsx +110 -0
  78. package/components/ui/loaders.tsx +139 -0
  79. package/components/ui/menu.tsx +118 -0
  80. package/components/ui/modal.tsx +399 -0
  81. package/components/ui/package-manager-tabs.tsx +40 -0
  82. package/components/ui/pagination.tsx +200 -0
  83. package/components/ui/progress.tsx +134 -0
  84. package/components/ui/radio.tsx +262 -0
  85. package/components/ui/skeleton.tsx +102 -0
  86. package/components/ui/steppers.tsx +309 -0
  87. package/components/ui/switch.tsx +153 -0
  88. package/components/ui/table.tsx +58 -0
  89. package/components/ui/tabs.tsx +166 -0
  90. package/components/ui/theme-toggle.tsx +60 -0
  91. package/components/ui/tooltip.tsx +238 -0
  92. package/data/color-palette.ts +35 -0
  93. package/data/docs-nav.ts +84 -0
  94. package/eslint.config.mjs +18 -0
  95. package/index.ts +8 -0
  96. package/lib/cn.ts +4 -0
  97. package/lib/color.ts +85 -0
  98. package/lib/og-image.tsx +138 -0
  99. package/lib/theme.ts +33 -0
  100. package/next.config.ts +7 -0
  101. package/package.json +30 -0
  102. package/postcss.config.mjs +7 -0
  103. package/public/dark.png +0 -0
  104. package/public/dark1.png +0 -0
  105. package/public/dark2.png +0 -0
  106. package/public/file.svg +1 -0
  107. package/public/globe.svg +1 -0
  108. package/public/light.png +0 -0
  109. package/public/light1.png +0 -0
  110. package/public/next.svg +1 -0
  111. package/public/vercel.svg +1 -0
  112. package/public/video/hero-demo.mp4 +0 -0
  113. package/public/window.svg +1 -0
  114. package/tsconfig.json +34 -0
@@ -0,0 +1,130 @@
1
+ import * as React from "react";
2
+ import { Check } from "lucide-react";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import { cn } from "@/lib/cn";
5
+
6
+ const checkboxControlVariants = cva(
7
+ [
8
+ "pointer-events-none relative flex shrink-0 items-center justify-center rounded-[3px] border-2 bg-white transition-all duration-200",
9
+ "border-zinc-500 group-hover:border-zinc-700",
10
+ "peer-checked:border-zinc-900 peer-checked:bg-zinc-900",
11
+ "peer-focus-visible:outline-none peer-focus-visible:ring-2 peer-focus-visible:ring-zinc-900/30 peer-focus-visible:ring-offset-2",
12
+ "peer-disabled:border-zinc-300 peer-disabled:bg-zinc-100",
13
+ "peer-disabled:peer-checked:border-zinc-300 peer-disabled:peer-checked:bg-zinc-300",
14
+ "dark:border-zinc-500 dark:bg-zinc-900 dark:group-hover:border-zinc-400",
15
+ "dark:peer-checked:border-zinc-100 dark:peer-checked:bg-zinc-100",
16
+ "dark:peer-disabled:border-zinc-700 dark:peer-disabled:bg-zinc-800",
17
+ "dark:peer-disabled:peer-checked:border-zinc-600 dark:peer-disabled:peer-checked:bg-zinc-600",
18
+ "peer-checked:[&_svg]:scale-100 peer-checked:[&_svg]:opacity-100",
19
+ ],
20
+ {
21
+ variants: {
22
+ size: {
23
+ sm: "h-4 w-4 [&_svg]:h-2.5 [&_svg]:w-2.5",
24
+ md: "h-[18px] w-[18px] [&_svg]:h-3 [&_svg]:w-3",
25
+ lg: "h-5 w-5 [&_svg]:h-3.5 [&_svg]:w-3.5",
26
+ },
27
+ },
28
+ defaultVariants: { size: "md" },
29
+ },
30
+ );
31
+
32
+ const checkboxWrapperVariants = cva("group flex w-full transition-colors duration-150", {
33
+ variants: {
34
+ variant: {
35
+ inline: "gap-3",
36
+ card: [
37
+ "gap-3 rounded-lg border border-zinc-200 bg-white px-3.5 py-3",
38
+ "hover:border-zinc-300 hover:bg-zinc-50/80",
39
+ "has-[:checked]:border-zinc-900",
40
+ "dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-700 dark:hover:bg-zinc-900/50",
41
+ "dark:has-[:checked]:border-zinc-500",
42
+ ],
43
+ },
44
+ },
45
+ defaultVariants: { variant: "inline" },
46
+ });
47
+
48
+ export interface CheckboxProps
49
+ extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type" | "size">,
50
+ VariantProps<typeof checkboxControlVariants> {
51
+ label?: string;
52
+ description?: string;
53
+ variant?: "inline" | "card";
54
+ }
55
+
56
+ const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
57
+ (
58
+ {
59
+ className,
60
+ size,
61
+ variant = "inline",
62
+ label,
63
+ description,
64
+ disabled,
65
+ id,
66
+ ...props
67
+ },
68
+ ref,
69
+ ) => {
70
+ const generatedId = React.useId();
71
+ const inputId = id ?? generatedId;
72
+ const resolvedVariant = variant === "card" || description ? "card" : "inline";
73
+ const alignStart = resolvedVariant === "card" || Boolean(description);
74
+
75
+ return (
76
+ <label
77
+ htmlFor={inputId}
78
+ className={cn(
79
+ checkboxWrapperVariants({ variant: resolvedVariant }),
80
+ alignStart ? "items-start" : "items-center",
81
+ disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
82
+ className,
83
+ )}
84
+ >
85
+ <span
86
+ className={cn(
87
+ "relative flex shrink-0 items-center justify-center p-2.5 -m-2.5",
88
+ alignStart ? "mt-0.5" : undefined,
89
+ )}
90
+ >
91
+ <input
92
+ ref={ref}
93
+ id={inputId}
94
+ type="checkbox"
95
+ disabled={disabled}
96
+ className="peer absolute inset-0 z-10 cursor-pointer opacity-0 disabled:cursor-not-allowed"
97
+ {...props}
98
+ />
99
+ <span
100
+ aria-hidden
101
+ className="pointer-events-none absolute inset-0 rounded-full bg-zinc-900/0 transition-colors duration-200 group-hover:bg-zinc-900/[0.08] group-active:bg-zinc-900/[0.12] peer-disabled:group-hover:bg-transparent dark:group-hover:bg-white/[0.08] dark:group-active:bg-white/[0.12]"
102
+ />
103
+ <span className={checkboxControlVariants({ size })}>
104
+ <Check
105
+ className="text-white opacity-0 transition-all duration-200 dark:text-zinc-900"
106
+ strokeWidth={3}
107
+ />
108
+ </span>
109
+ </span>
110
+ {(label || description) && (
111
+ <span className="flex min-w-0 flex-1 flex-col gap-0.5">
112
+ {label && (
113
+ <span className="text-sm leading-snug text-zinc-800 dark:text-zinc-100">
114
+ {label}
115
+ </span>
116
+ )}
117
+ {description && (
118
+ <span className="text-xs leading-relaxed text-zinc-500 dark:text-zinc-400">
119
+ {description}
120
+ </span>
121
+ )}
122
+ </span>
123
+ )}
124
+ </label>
125
+ );
126
+ },
127
+ );
128
+ Checkbox.displayName = "Checkbox";
129
+
130
+ export { Checkbox, checkboxControlVariants };
@@ -0,0 +1,60 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { Check, Copy } from "lucide-react";
5
+ import { cn } from "@/lib/cn";
6
+
7
+ export function CodeBlock({
8
+ code,
9
+ filename,
10
+ className,
11
+ }: {
12
+ code: string;
13
+ filename?: string;
14
+ className?: string;
15
+ }) {
16
+ const [copied, setCopied] = useState(false);
17
+
18
+ const onCopy = async () => {
19
+ await navigator.clipboard.writeText(code);
20
+ setCopied(true);
21
+ setTimeout(() => setCopied(false), 1500);
22
+ };
23
+
24
+ const copyButton = (
25
+ <button
26
+ type="button"
27
+ onClick={onCopy}
28
+ aria-label="Copy code"
29
+ className="flex h-7 w-7 items-center justify-center rounded-md text-zinc-500 transition-colors hover:bg-white/10 hover:text-zinc-200"
30
+ >
31
+ {copied ? (
32
+ <Check className="h-3.5 w-3.5 text-white" />
33
+ ) : (
34
+ <Copy className="h-3.5 w-3.5" />
35
+ )}
36
+ </button>
37
+ );
38
+
39
+ return (
40
+ <div
41
+ className={cn(
42
+ "group relative overflow-x-auto rounded-xl bg-zinc-900 ring-1 ring-white/10",
43
+ className,
44
+ )}
45
+ >
46
+ {filename ? (
47
+ <div className="flex items-center justify-between border-b border-white/10 px-4 py-2.5">
48
+ <span className="font-mono text-xs text-zinc-500">{filename}</span>
49
+ {copyButton}
50
+ </div>
51
+ ) : null}
52
+ <pre className={cn("max-w-full overflow-x-auto px-4 py-3.5 text-[13px] leading-relaxed text-zinc-100", !filename && "pr-12")}>
53
+ <code className="font-mono">{code}</code>
54
+ </pre>
55
+ {filename ? null : (
56
+ <div className="absolute right-3 top-3">{copyButton}</div>
57
+ )}
58
+ </div>
59
+ );
60
+ }
@@ -0,0 +1,235 @@
1
+ "use client";
2
+
3
+ import React, { useMemo, useState } from "react";
4
+ import { ArrowDown, ArrowUp, ArrowUpDown, Filter, Search, X } from "lucide-react";
5
+ import { cn } from "@/lib/cn";
6
+ import { Button } from "./button";
7
+ import { Input } from "./input";
8
+ import { Pagination } from "./pagination";
9
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./table";
10
+ import { Dropdown, DropdownContent, DropdownDivider, DropdownItem, DropdownLabel, DropdownTrigger } from "./dropdown";
11
+
12
+ export interface DataTableColumn<T> {
13
+ key: string;
14
+ header: string;
15
+ sortable?: boolean;
16
+ align?: "left" | "center" | "right";
17
+ filterOptions?: string[];
18
+ accessor?: (row: T) => string | number;
19
+ render?: (row: T) => React.ReactNode;
20
+ className?: string;
21
+ }
22
+
23
+ export interface DataTableProps<T> {
24
+ data: T[];
25
+ columns: DataTableColumn<T>[];
26
+ searchPlaceholder?: string;
27
+ pageSize?: number;
28
+ className?: string;
29
+ }
30
+
31
+ type SortDirection = "asc" | "desc" | null;
32
+
33
+ export function DataTable<T extends object>({
34
+ data,
35
+ columns,
36
+ searchPlaceholder = "Search...",
37
+ pageSize = 5,
38
+ className,
39
+ }: DataTableProps<T>) {
40
+ const [search, setSearch] = useState("");
41
+ const [sortKey, setSortKey] = useState<string | null>(null);
42
+ const [sortDir, setSortDir] = useState<SortDirection>(null);
43
+ const [activeFilters, setActiveFilters] = useState<Record<string, string>>({});
44
+ const [page, setPage] = useState(1);
45
+
46
+ const getValue = (row: T, col: DataTableColumn<T>) =>
47
+ col.accessor ? col.accessor(row) : ((row as Record<string, unknown>)[col.key] as string | number);
48
+
49
+ const filterableColumns = columns.filter((c) => c.filterOptions?.length);
50
+ const activeFilterCount = Object.values(activeFilters).filter(Boolean).length;
51
+
52
+ const filtered = useMemo(() => {
53
+ let rows = data;
54
+
55
+ if (search.trim()) {
56
+ const q = search.trim().toLowerCase();
57
+ rows = rows.filter((row) => columns.some((col) => String(getValue(row, col) ?? "").toLowerCase().includes(q)));
58
+ }
59
+
60
+ Object.entries(activeFilters).forEach(([key, value]) => {
61
+ if (!value) return;
62
+ const col = columns.find((c) => c.key === key);
63
+ if (!col) return;
64
+ rows = rows.filter((row) => String(getValue(row, col)) === value);
65
+ });
66
+
67
+ return rows;
68
+ // eslint-disable-next-line react-hooks/exhaustive-deps
69
+ }, [data, search, activeFilters]);
70
+
71
+ const sorted = useMemo(() => {
72
+ if (!sortKey || !sortDir) return filtered;
73
+ const col = columns.find((c) => c.key === sortKey);
74
+ if (!col) return filtered;
75
+ return [...filtered].sort((a, b) => {
76
+ const av = getValue(a, col);
77
+ const bv = getValue(b, col);
78
+ if (typeof av === "number" && typeof bv === "number") return sortDir === "asc" ? av - bv : bv - av;
79
+ return sortDir === "asc" ? String(av).localeCompare(String(bv)) : String(bv).localeCompare(String(av));
80
+ });
81
+ // eslint-disable-next-line react-hooks/exhaustive-deps
82
+ }, [filtered, sortKey, sortDir]);
83
+
84
+ const totalPages = Math.max(1, Math.ceil(sorted.length / pageSize));
85
+ const currentPage = Math.min(page, totalPages);
86
+ const paged = sorted.slice((currentPage - 1) * pageSize, currentPage * pageSize);
87
+
88
+ const handleSort = (col: DataTableColumn<T>) => {
89
+ if (!col.sortable) return;
90
+ if (sortKey !== col.key) {
91
+ setSortKey(col.key);
92
+ setSortDir("asc");
93
+ } else if (sortDir === "asc") {
94
+ setSortDir("desc");
95
+ } else {
96
+ setSortKey(null);
97
+ setSortDir(null);
98
+ }
99
+ setPage(1);
100
+ };
101
+
102
+ const clearAll = () => {
103
+ setSearch("");
104
+ setActiveFilters({});
105
+ setPage(1);
106
+ };
107
+
108
+ return (
109
+ <div className={cn("w-full", className)}>
110
+ <div className="flex flex-col gap-3 pb-4 sm:flex-row sm:items-center sm:justify-between">
111
+ <div className="relative w-full sm:max-w-xs">
112
+ <Search size={15} className="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400" />
113
+ <Input
114
+ value={search}
115
+ onChange={(e) => {
116
+ setSearch(e.target.value);
117
+ setPage(1);
118
+ }}
119
+ placeholder={searchPlaceholder}
120
+ className="pl-9"
121
+ />
122
+ </div>
123
+
124
+ {(filterableColumns.length > 0 || activeFilterCount > 0) && (
125
+ <div className="flex flex-wrap items-center gap-2">
126
+ {filterableColumns.map((col) => (
127
+ <Dropdown key={col.key}>
128
+ <DropdownTrigger>
129
+ <Button
130
+ variant="outline"
131
+ size="sm"
132
+ className={cn(activeFilters[col.key] && "border-zinc-900 text-zinc-900 dark:border-zinc-100 dark:text-white")}
133
+ >
134
+ <Filter size={13} />
135
+ {activeFilters[col.key] || col.header}
136
+ </Button>
137
+ </DropdownTrigger>
138
+ <DropdownContent align="right">
139
+ <DropdownLabel>Filter by {col.header}</DropdownLabel>
140
+ <DropdownItem label="All" selected={!activeFilters[col.key]} onClick={() => setActiveFilters((f) => ({ ...f, [col.key]: "" }))} />
141
+ <DropdownDivider />
142
+ {col.filterOptions!.map((opt) => (
143
+ <DropdownItem
144
+ key={opt}
145
+ label={opt}
146
+ selected={activeFilters[col.key] === opt}
147
+ onClick={() => {
148
+ setActiveFilters((f) => ({ ...f, [col.key]: opt }));
149
+ setPage(1);
150
+ }}
151
+ />
152
+ ))}
153
+ </DropdownContent>
154
+ </Dropdown>
155
+ ))}
156
+
157
+ {(activeFilterCount > 0 || search) && (
158
+ <Button variant="ghost" size="sm" onClick={clearAll}>
159
+ <X size={13} />
160
+ Clear
161
+ </Button>
162
+ )}
163
+ </div>
164
+ )}
165
+ </div>
166
+
167
+ <div className="overflow-hidden rounded-xl border border-zinc-200 dark:border-zinc-800">
168
+ <Table>
169
+ <TableHeader>
170
+ <TableRow>
171
+ {columns.map((col) => (
172
+ <TableHead
173
+ key={col.key}
174
+ onClick={() => handleSort(col)}
175
+ className={cn(
176
+ col.align === "right" && "text-right",
177
+ col.align === "center" && "text-center",
178
+ col.sortable && "cursor-pointer select-none hover:text-zinc-700 dark:hover:text-zinc-200",
179
+ col.className,
180
+ )}
181
+ >
182
+ <span
183
+ className={cn(
184
+ "inline-flex items-center gap-1",
185
+ col.align === "right" && "flex-row-reverse",
186
+ col.align === "center" && "justify-center",
187
+ )}
188
+ >
189
+ {col.header}
190
+ {col.sortable &&
191
+ (sortKey === col.key ? (
192
+ sortDir === "asc" ? (
193
+ <ArrowUp size={12} />
194
+ ) : (
195
+ <ArrowDown size={12} />
196
+ )
197
+ ) : (
198
+ <ArrowUpDown size={12} className="opacity-30" />
199
+ ))}
200
+ </span>
201
+ </TableHead>
202
+ ))}
203
+ </TableRow>
204
+ </TableHeader>
205
+ <TableBody>
206
+ {paged.length === 0 ? (
207
+ <TableRow className="hover:bg-transparent">
208
+ <TableCell colSpan={columns.length} className="py-10 text-center text-zinc-400">
209
+ No results found.
210
+ </TableCell>
211
+ </TableRow>
212
+ ) : (
213
+ paged.map((row, i) => (
214
+ <TableRow key={i}>
215
+ {columns.map((col) => (
216
+ <TableCell key={col.key} className={cn(col.align === "right" && "text-right", col.align === "center" && "text-center", col.className)}>
217
+ {col.render ? col.render(row) : String(getValue(row, col) ?? "")}
218
+ </TableCell>
219
+ ))}
220
+ </TableRow>
221
+ ))
222
+ )}
223
+ </TableBody>
224
+ </Table>
225
+ </div>
226
+
227
+ <div className="flex flex-col-reverse items-center justify-between gap-3 pt-4 sm:flex-row">
228
+ <p className="text-xs text-zinc-500 dark:text-zinc-400">
229
+ {sorted.length === 0 ? "0 results" : `Showing ${(currentPage - 1) * pageSize + 1}–${Math.min(currentPage * pageSize, sorted.length)} of ${sorted.length}`}
230
+ </p>
231
+ <Pagination page={currentPage} totalPages={totalPages} onPageChange={setPage} />
232
+ </div>
233
+ </div>
234
+ );
235
+ }
@@ -0,0 +1,173 @@
1
+ "use client";
2
+
3
+ import React, { createContext, useContext, useState, useEffect } from "react";
4
+ import { createPortal } from "react-dom";
5
+ import { User, Plus } from "lucide-react";
6
+ import { cn } from "@/lib/cn";
7
+
8
+ type DialogContextType = {
9
+ isOpen: boolean;
10
+ setIsOpen: (open: boolean) => void;
11
+ };
12
+
13
+ const DialogContext = createContext<DialogContextType | null>(null);
14
+
15
+ export function Dialog({ children, open, onOpenChange }: { children: React.ReactNode; open?: boolean; onOpenChange?: (open: boolean) => void }) {
16
+ const [isOpenState, setIsOpenState] = useState(false);
17
+ const isOpen = open !== undefined ? open : isOpenState;
18
+ const setIsOpen = onOpenChange || setIsOpenState;
19
+
20
+ useEffect(() => {
21
+ if (isOpen) document.body.style.overflow = "hidden";
22
+ else document.body.style.overflow = "unset";
23
+ return () => {
24
+ document.body.style.overflow = "unset";
25
+ };
26
+ }, [isOpen]);
27
+
28
+ return (
29
+ <DialogContext.Provider value={{ isOpen, setIsOpen }}>
30
+ {children}
31
+ </DialogContext.Provider>
32
+ );
33
+ }
34
+
35
+ export function DialogTrigger({ children }: { children: React.ReactElement<any> }) {
36
+ const { setIsOpen } = useContext(DialogContext)!;
37
+
38
+ return React.cloneElement(children, {
39
+ onClick: (e: React.MouseEvent) => {
40
+ if (children.props?.onClick) children.props.onClick(e);
41
+ setIsOpen(true);
42
+ },
43
+ });
44
+ }
45
+
46
+ export function DialogContent({ children, className = "" }: { children: React.ReactNode; className?: string }) {
47
+ const { isOpen, setIsOpen } = useContext(DialogContext)!;
48
+ const [visible, setVisible] = useState(false);
49
+
50
+ useEffect(() => {
51
+ if (!isOpen) {
52
+ setVisible(false);
53
+ return;
54
+ }
55
+ const frame = requestAnimationFrame(() => setVisible(true));
56
+ return () => cancelAnimationFrame(frame);
57
+ }, [isOpen]);
58
+
59
+ useEffect(() => {
60
+ if (!isOpen) return;
61
+ const handleKeyDown = (e: KeyboardEvent) => {
62
+ if (e.key === "Escape") setIsOpen(false);
63
+ };
64
+ window.addEventListener("keydown", handleKeyDown);
65
+ return () => window.removeEventListener("keydown", handleKeyDown);
66
+ }, [isOpen, setIsOpen]);
67
+
68
+ if (!isOpen) return null;
69
+
70
+ return createPortal(
71
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4">
72
+ <div
73
+ className={cn(
74
+ "fixed inset-0 bg-zinc-900/45 backdrop-blur-[2px] transition-opacity duration-200 ease-out dark:bg-black/65",
75
+ visible ? "opacity-100" : "opacity-0",
76
+ )}
77
+ onClick={() => setIsOpen(false)}
78
+ aria-hidden
79
+ />
80
+ <div
81
+ role="dialog"
82
+ aria-modal="true"
83
+ onClick={(e) => e.stopPropagation()}
84
+ className={cn(
85
+ "relative z-10 w-full max-w-md rounded-2xl border border-zinc-200/90 bg-white p-6 text-zinc-900",
86
+ "shadow-[0_20px_60px_rgba(0,0,0,0.12),0_0_0_1px_rgba(0,0,0,0.04)]",
87
+ "dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-100",
88
+ "dark:shadow-[0_20px_60px_rgba(0,0,0,0.5),0_0_0_1px_rgba(255,255,255,0.06)]",
89
+ "transition-[opacity,transform] duration-200 ease-out",
90
+ visible ? "translate-y-0 scale-100 opacity-100" : "translate-y-1 scale-[0.97] opacity-0",
91
+ className,
92
+ )}
93
+ >
94
+ {children}
95
+ </div>
96
+ </div>,
97
+ document.body,
98
+ );
99
+ }
100
+
101
+ export function DialogHeader({ title, description }: { title?: string; description?: string }) {
102
+ return (
103
+ <div className="mb-5">
104
+ {title && <h2 className="text-[17px] font-semibold leading-snug tracking-tight text-zinc-900 dark:text-white">{title}</h2>}
105
+ {description && <p className="mt-2 text-[14px] leading-relaxed text-zinc-500 dark:text-zinc-400">{description}</p>}
106
+ </div>
107
+ );
108
+ }
109
+
110
+ export function DialogFooter({ children }: { children: React.ReactNode }) {
111
+ return <div className="mt-6 flex items-center justify-end gap-1">{children}</div>;
112
+ }
113
+
114
+ export function DialogAction({ label, onClick, variant = "primary" }: { label: string; onClick?: () => void; variant?: "primary" | "destructive" }) {
115
+ const { setIsOpen } = useContext(DialogContext)!;
116
+
117
+ const handleClick = () => {
118
+ if (onClick) onClick();
119
+ setIsOpen(false);
120
+ };
121
+
122
+ const colorClass =
123
+ variant === "destructive"
124
+ ? "text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-500/10"
125
+ : "text-zinc-700 hover:bg-zinc-100 dark:text-zinc-200 dark:hover:bg-zinc-800";
126
+
127
+ return (
128
+ <button
129
+ onClick={handleClick}
130
+ className={cn(
131
+ "rounded-lg px-3.5 py-2 text-[13px] font-semibold tracking-tight outline-none transition-colors duration-150",
132
+ "focus-visible:ring-2 focus-visible:ring-zinc-900/15 dark:focus-visible:ring-white/20",
133
+ colorClass,
134
+ )}
135
+ >
136
+ {label}
137
+ </button>
138
+ );
139
+ }
140
+
141
+ export function DialogItem({
142
+ label,
143
+ icon,
144
+ onClick,
145
+ variant = "account",
146
+ }: {
147
+ label: string;
148
+ icon?: React.ReactNode;
149
+ onClick?: () => void;
150
+ variant?: "account" | "action";
151
+ }) {
152
+ const { setIsOpen } = useContext(DialogContext)!;
153
+
154
+ const handleClick = () => {
155
+ if (onClick) onClick();
156
+ setIsOpen(false);
157
+ };
158
+
159
+ const iconBg = variant === "account" ? "bg-[#0070BA]/10 dark:bg-[#0070BA]/20" : "bg-zinc-100 dark:bg-zinc-800";
160
+ const iconColor = variant === "account" ? "text-[#0070BA] dark:text-[#4DA3E0]" : "text-zinc-500 dark:text-zinc-400";
161
+
162
+ return (
163
+ <button
164
+ onClick={handleClick}
165
+ className="flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-left outline-none transition-colors duration-150 hover:bg-zinc-50 dark:hover:bg-zinc-900/60"
166
+ >
167
+ <div className={cn("flex h-9 w-9 shrink-0 items-center justify-center rounded-full", iconBg, iconColor)}>
168
+ {icon || (variant === "account" ? <User size={18} /> : <Plus size={18} />)}
169
+ </div>
170
+ <span className="text-[14px] font-medium text-zinc-900 dark:text-white">{label}</span>
171
+ </button>
172
+ );
173
+ }
@@ -0,0 +1,78 @@
1
+ "use client";
2
+
3
+ import React, { useState, useRef, useEffect } from "react";
4
+ import { Check, ChevronDown, Search, User, Settings, LogOut, Filter, ArrowUpDown, Calendar, ChevronRight, X } from "lucide-react";
5
+
6
+ // --- Base Components ---
7
+ export function Dropdown({ children }: { children: React.ReactNode }) {
8
+ const [isOpen, setIsOpen] = useState(false);
9
+ const containerRef = useRef<HTMLDivElement>(null);
10
+
11
+ useEffect(() => {
12
+ const handleClickOutside = (e: MouseEvent) => {
13
+ if (containerRef.current && !containerRef.current.contains(e.target as Node)) setIsOpen(false);
14
+ };
15
+ document.addEventListener("mousedown", handleClickOutside);
16
+ return () => document.removeEventListener("mousedown", handleClickOutside);
17
+ }, []);
18
+
19
+ useEffect(() => {
20
+ if (!isOpen) return;
21
+ const handleKeyDown = (e: KeyboardEvent) => {
22
+ if (e.key === "Escape") setIsOpen(false);
23
+ };
24
+ document.addEventListener("keydown", handleKeyDown);
25
+ return () => document.removeEventListener("keydown", handleKeyDown);
26
+ }, [isOpen]);
27
+
28
+ return (
29
+ <div className="relative inline-block text-left" ref={containerRef}>
30
+ {React.Children.map(children, (child) => {
31
+ if (React.isValidElement(child)) {
32
+ return React.cloneElement(child as React.ReactElement<any>, { isOpen, setIsOpen });
33
+ }
34
+ return child;
35
+ })}
36
+ </div>
37
+ );
38
+ }
39
+
40
+ export function DropdownTrigger({ children, isOpen, setIsOpen }: any) {
41
+ return React.cloneElement(children, {
42
+ onClick: () => setIsOpen(!isOpen),
43
+ "aria-haspopup": "menu",
44
+ "aria-expanded": isOpen,
45
+ className: `${children.props.className} ${isOpen ? "ring-2 ring-zinc-200 dark:ring-zinc-800" : ""}`
46
+ });
47
+ }
48
+
49
+ export function DropdownContent({ children, isOpen, className = "", align = "left" }: any) {
50
+ if (!isOpen) return null;
51
+ const alignClass = align === "right" ? "right-0" : "left-0";
52
+ return (
53
+ <div role="menu" className={`absolute ${alignClass} z-50 mt-2 w-56 origin-top-right rounded-lg border border-zinc-200 bg-white p-1 shadow-xl outline-none dark:border-zinc-800 dark:bg-zinc-950 ${className} animate-in fade-in zoom-in-95 duration-200`}>
54
+ {children}
55
+ </div>
56
+ );
57
+ }
58
+
59
+ export function DropdownItem({ label, icon: Icon, selected, onClick, className = "", variant = "default" }: any) {
60
+ const baseClass = "flex w-full items-center gap-2 rounded-md px-3 py-2 text-left text-sm transition-colors outline-none cursor-pointer";
61
+ const variantClass = variant === "destructive" ? "text-red-600 hover:bg-red-50 dark:hover:bg-red-950/30" : "text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-900";
62
+
63
+ return (
64
+ <button type="button" role="menuitem" className={`${baseClass} ${variantClass} ${className}`} onClick={onClick}>
65
+ {Icon && <Icon size={16} className="opacity-70" />}
66
+ <span className="flex-1">{label}</span>
67
+ {selected && <Check size={14} className="text-zinc-900 dark:text-white" />}
68
+ </button>
69
+ );
70
+ }
71
+
72
+ export function DropdownLabel({ children }: { children: React.ReactNode }) {
73
+ return <div className="px-3 py-2 text-[11px] font-bold uppercase tracking-wider text-zinc-400">{children}</div>;
74
+ }
75
+
76
+ export function DropdownDivider() {
77
+ return <div role="separator" className="my-1 h-px bg-zinc-100 dark:bg-zinc-800" />;
78
+ }