ruler-components 0.1.1 → 0.1.3

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 (118) hide show
  1. package/dist/index.d.mts +458 -0
  2. package/dist/index.d.ts +458 -0
  3. package/dist/index.js +3618 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/index.mjs +3467 -0
  6. package/dist/index.mjs.map +1 -0
  7. package/package.json +21 -2
  8. package/AGENTS.md +0 -9
  9. package/CLAUDE.md +0 -1
  10. package/app/docs/[slug]/page.tsx +0 -147
  11. package/app/docs/layout.tsx +0 -26
  12. package/app/docs/page.tsx +0 -148
  13. package/app/favicon.ico +0 -0
  14. package/app/globals.css +0 -168
  15. package/app/icon.png +0 -0
  16. package/app/layout.tsx +0 -90
  17. package/app/opengraph-image.tsx +0 -10
  18. package/app/page.tsx +0 -34
  19. package/app/twitter-image.tsx +0 -10
  20. package/components/docs/accordion-doc-page.tsx +0 -352
  21. package/components/docs/alert-doc-page.tsx +0 -293
  22. package/components/docs/allinput-doc-page.tsx +0 -183
  23. package/components/docs/avatar-doc-page.tsx +0 -424
  24. package/components/docs/badge-doc-page.tsx +0 -363
  25. package/components/docs/button-doc-page.tsx +0 -406
  26. package/components/docs/card-doc-page.tsx +0 -364
  27. package/components/docs/checkbox-doc-page.tsx +0 -277
  28. package/components/docs/colors-doc-page.tsx +0 -430
  29. package/components/docs/dialog-doc-page.tsx +0 -364
  30. package/components/docs/docs-footer.tsx +0 -11
  31. package/components/docs/docs-mobile-menu-bar.tsx +0 -22
  32. package/components/docs/docs-mobile-nav-context.tsx +0 -27
  33. package/components/docs/docs-page.tsx +0 -263
  34. package/components/docs/docs-search.tsx +0 -303
  35. package/components/docs/docs-sidebar.tsx +0 -165
  36. package/components/docs/docs-topbar.tsx +0 -32
  37. package/components/docs/dropdown-doc-page.tsx +0 -522
  38. package/components/docs/getting-started-doc-page.tsx +0 -205
  39. package/components/docs/label-doc-page.tsx +0 -173
  40. package/components/docs/loaders-doc-page.tsx +0 -244
  41. package/components/docs/menu-doc-page.tsx +0 -373
  42. package/components/docs/modal-doc-page.tsx +0 -436
  43. package/components/docs/next-steps.tsx +0 -53
  44. package/components/docs/pagination-doc-page.tsx +0 -316
  45. package/components/docs/progress-doc-page.tsx +0 -387
  46. package/components/docs/radio-doc-page.tsx +0 -270
  47. package/components/docs/skeleton-doc-page.tsx +0 -230
  48. package/components/docs/stepper-doc-page.tsx +0 -309
  49. package/components/docs/switch-doc-page.tsx +0 -264
  50. package/components/docs/table-doc-page.tsx +0 -689
  51. package/components/docs/tabs-doc-page.tsx +0 -359
  52. package/components/docs/theming-doc-page.tsx +0 -243
  53. package/components/docs/tooltip-doc-page.tsx +0 -357
  54. package/components/docs/usage-section.tsx +0 -21
  55. package/components/icons/discord.tsx +0 -9
  56. package/components/icons/github.tsx +0 -9
  57. package/components/icons/x.tsx +0 -9
  58. package/components/installation-section.tsx +0 -34
  59. package/components/landing/feature-highlights.tsx +0 -105
  60. package/components/landing/footer-strip.tsx +0 -45
  61. package/components/landing/hero-card.tsx +0 -14
  62. package/components/landing/hero-spotlight.tsx +0 -126
  63. package/components/landing/hero.tsx +0 -64
  64. package/components/landing/navbar.tsx +0 -44
  65. package/components/landing/quick-start-section.tsx +0 -59
  66. package/components/landing/site-footer.tsx +0 -55
  67. package/components/theme-provider.tsx +0 -60
  68. package/components/ui/accordion.tsx +0 -174
  69. package/components/ui/alert.tsx +0 -103
  70. package/components/ui/allinput.tsx +0 -210
  71. package/components/ui/avatar.tsx +0 -179
  72. package/components/ui/badge.tsx +0 -52
  73. package/components/ui/button.tsx +0 -75
  74. package/components/ui/card.tsx +0 -105
  75. package/components/ui/checkbox.tsx +0 -130
  76. package/components/ui/code-block.tsx +0 -60
  77. package/components/ui/data-table.tsx +0 -235
  78. package/components/ui/dialog.tsx +0 -173
  79. package/components/ui/dropdown.tsx +0 -78
  80. package/components/ui/feature-card.tsx +0 -38
  81. package/components/ui/input.tsx +0 -44
  82. package/components/ui/label.tsx +0 -110
  83. package/components/ui/loaders.tsx +0 -139
  84. package/components/ui/menu.tsx +0 -118
  85. package/components/ui/modal.tsx +0 -399
  86. package/components/ui/package-manager-tabs.tsx +0 -40
  87. package/components/ui/pagination.tsx +0 -200
  88. package/components/ui/progress.tsx +0 -134
  89. package/components/ui/radio.tsx +0 -262
  90. package/components/ui/skeleton.tsx +0 -102
  91. package/components/ui/steppers.tsx +0 -309
  92. package/components/ui/switch.tsx +0 -153
  93. package/components/ui/table.tsx +0 -58
  94. package/components/ui/tabs.tsx +0 -166
  95. package/components/ui/theme-toggle.tsx +0 -60
  96. package/components/ui/tooltip.tsx +0 -238
  97. package/data/color-palette.ts +0 -35
  98. package/data/docs-nav.ts +0 -84
  99. package/eslint.config.mjs +0 -18
  100. package/index.ts +0 -8
  101. package/lib/cn.ts +0 -4
  102. package/lib/color.ts +0 -85
  103. package/lib/og-image.tsx +0 -138
  104. package/lib/theme.ts +0 -33
  105. package/next.config.ts +0 -7
  106. package/postcss.config.mjs +0 -7
  107. package/public/dark.png +0 -0
  108. package/public/dark1.png +0 -0
  109. package/public/dark2.png +0 -0
  110. package/public/file.svg +0 -1
  111. package/public/globe.svg +0 -1
  112. package/public/light.png +0 -0
  113. package/public/light1.png +0 -0
  114. package/public/next.svg +0 -1
  115. package/public/vercel.svg +0 -1
  116. package/public/video/hero-demo.mp4 +0 -0
  117. package/public/window.svg +0 -1
  118. package/tsconfig.json +0 -34
@@ -1,130 +0,0 @@
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 };
@@ -1,60 +0,0 @@
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
- }
@@ -1,235 +0,0 @@
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
- }
@@ -1,173 +0,0 @@
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
- }
@@ -1,78 +0,0 @@
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
- }