This view is limited to 50 files because it contains too many changes. See the raw diff here.
Files changed (50) hide show
  1. .gitattributes +0 -1
  2. .gitignore +0 -32
  3. .prettierignore +0 -8
  4. .prettierrc +0 -6
  5. AGENTS.md +0 -10
  6. Dockerfile +0 -17
  7. README.md +0 -8
  8. bun.lock +0 -0
  9. bunfig.toml +0 -7
  10. components.json +0 -22
  11. components/studio/AdminPanel.tsx +0 -355
  12. components/studio/FeedbackDialog.tsx +0 -52
  13. components/studio/LoginDialog.tsx +0 -155
  14. components/studio/ProfileDialog.tsx +0 -159
  15. components/studio/ProjectsDialog.tsx +0 -159
  16. components/studio/Shape.tsx +0 -58
  17. components/studio/TattingStudio.tsx +0 -1198
  18. components/ui/accordion.tsx +0 -51
  19. components/ui/alert-dialog.tsx +0 -115
  20. components/ui/alert.tsx +0 -49
  21. components/ui/aspect-ratio.tsx +0 -5
  22. components/ui/avatar.tsx +0 -47
  23. components/ui/badge.tsx +0 -32
  24. components/ui/breadcrumb.tsx +0 -101
  25. components/ui/button.tsx +0 -49
  26. components/ui/calendar.tsx +0 -177
  27. components/ui/card.tsx +0 -55
  28. components/ui/carousel.tsx +0 -240
  29. components/ui/chart.tsx +0 -331
  30. components/ui/checkbox.tsx +0 -26
  31. components/ui/collapsible.tsx +0 -11
  32. components/ui/command.tsx +0 -143
  33. components/ui/context-menu.tsx +0 -186
  34. components/ui/dialog.tsx +0 -104
  35. components/ui/drawer.tsx +0 -98
  36. components/ui/dropdown-menu.tsx +0 -187
  37. components/ui/form.tsx +0 -171
  38. components/ui/hover-card.tsx +0 -27
  39. components/ui/input-otp.tsx +0 -73
  40. components/ui/input.tsx +0 -22
  41. components/ui/label.tsx +0 -21
  42. components/ui/menubar.tsx +0 -228
  43. components/ui/navigation-menu.tsx +0 -120
  44. components/ui/pagination.tsx +0 -98
  45. components/ui/popover.tsx +0 -31
  46. components/ui/progress.tsx +0 -25
  47. components/ui/radio-group.tsx +0 -36
  48. components/ui/resizable.tsx +0 -37
  49. components/ui/scroll-area.tsx +0 -44
  50. components/ui/select.tsx +0 -152
.gitattributes DELETED
@@ -1 +0,0 @@
1
- knot-studio.zip filter=lfs diff=lfs merge=lfs -text
 
 
.gitignore DELETED
@@ -1,32 +0,0 @@
1
- # Logs
2
- logs
3
- *.log
4
- npm-debug.log*
5
- yarn-debug.log*
6
- yarn-error.log*
7
- pnpm-debug.log*
8
- lerna-debug.log*
9
-
10
- node_modules
11
- dist
12
- dist-ssr
13
- .output
14
- .vinxi
15
- .tanstack/**
16
- .nitro
17
- *.local
18
-
19
- # Wrangler / Cloudflare
20
- .wrangler/
21
- .dev.vars
22
-
23
- # Editor directories and files
24
- .vscode/*
25
- !.vscode/extensions.json
26
- .idea
27
- .DS_Store
28
- *.suo
29
- *.ntvs*
30
- *.njsproj
31
- *.sln
32
- *.sw?
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.prettierignore DELETED
@@ -1,8 +0,0 @@
1
- node_modules
2
- dist
3
- .output
4
- .vinxi
5
- pnpm-lock.yaml
6
- package-lock.json
7
- bun.lock
8
- routeTree.gen.ts
 
 
 
 
 
 
 
 
 
.prettierrc DELETED
@@ -1,6 +0,0 @@
1
- {
2
- "printWidth": 100,
3
- "semi": true,
4
- "singleQuote": false,
5
- "trailingComma": "all"
6
- }
 
 
 
 
 
 
 
AGENTS.md DELETED
@@ -1,10 +0,0 @@
1
- <!-- LOVABLE:BEGIN -->
2
- > [!IMPORTANT]
3
- > This project is connected to [Lovable](https://lovable.dev). Avoid rewriting
4
- > published git history — force pushing, or rebasing/amending/squashing commits
5
- > that are already pushed — as it rewrites history on Lovable's side and the
6
- > user will likely lose their project history.
7
- >
8
- > Commits you push to the connected branch sync back to Lovable and show up in
9
- > the editor, so keep the branch in a working state.
10
- <!-- LOVABLE:END -->
 
 
 
 
 
 
 
 
 
 
 
Dockerfile DELETED
@@ -1,17 +0,0 @@
1
- FROM node:20-slim
2
-
3
- WORKDIR /app
4
-
5
- COPY package*.json bun.lock* ./
6
-
7
- RUN npm install -g bun && bun install
8
-
9
- COPY . .
10
-
11
- RUN bun run build
12
-
13
- RUN npm install -g serve
14
-
15
- EXPOSE 7860
16
-
17
- CMD ["serve", "-s", "dist", "-l", "7860"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md DELETED
@@ -1,8 +0,0 @@
1
- ---
2
- title: Test
3
- emoji: 🚀
4
- colorFrom: blue
5
- colorTo: indigo
6
- sdk: docker
7
- app_port: 7860
8
- ---
 
 
 
 
 
 
 
 
 
bun.lock DELETED
The diff for this file is too large to render. See raw diff
 
bunfig.toml DELETED
@@ -1,7 +0,0 @@
1
- [install]
2
- saveTextLockfile = true
3
- # 24h supply-chain guard: skip package versions published less than a day ago.
4
- minimumReleaseAge = 86400
5
- # Each entry bypasses the 24h guard for one package — confirm with the user
6
- # before adding any.
7
- minimumReleaseAgeExcludes = ["@lovable.dev/vite-tanstack-config", "@lovable.dev/mcp-js", "@lovable.dev/vite-plugin-dev-server-bridge", "@lovable.dev/vite-plugin-hmr-gate", "@lovable.dev/email-js", "@lovable.dev/webhooks-js"]
 
 
 
 
 
 
 
 
components.json DELETED
@@ -1,22 +0,0 @@
1
- {
2
- "$schema": "https://ui.shadcn.com/schema.json",
3
- "style": "new-york",
4
- "rsc": false,
5
- "tsx": true,
6
- "tailwind": {
7
- "css": "src/styles.css",
8
- "baseColor": "slate",
9
- "cssVariables": true,
10
- "prefix": ""
11
- },
12
- "iconLibrary": "lucide",
13
- "rtl": false,
14
- "aliases": {
15
- "components": "@/components",
16
- "utils": "@/lib/utils",
17
- "ui": "@/components/ui",
18
- "lib": "@/lib",
19
- "hooks": "@/hooks"
20
- },
21
- "registries": {}
22
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/studio/AdminPanel.tsx DELETED
@@ -1,355 +0,0 @@
1
- import { useCallback, useEffect, useState } from "react";
2
- import { Check, Copy, FolderOpen, KeyRound, MessageSquare, RotateCcw, Trash2, UserPlus, Users } from "lucide-react";
3
- import { Button } from "@/components/ui/button";
4
- import {
5
- AlertDialog,
6
- AlertDialogAction,
7
- AlertDialogCancel,
8
- AlertDialogContent,
9
- AlertDialogDescription,
10
- AlertDialogFooter,
11
- AlertDialogHeader,
12
- AlertDialogTitle,
13
- } from "@/components/ui/alert-dialog";
14
- import { Badge } from "@/components/ui/badge";
15
- import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
16
- import { Input } from "@/components/ui/input";
17
- import { Label } from "@/components/ui/label";
18
- import {
19
- Table,
20
- TableBody,
21
- TableCell,
22
- TableHead,
23
- TableHeader,
24
- TableRow,
25
- } from "@/components/ui/table";
26
- import {
27
- createUser,
28
- deleteUser,
29
- listUsers,
30
- resetPassword,
31
- roleLabel,
32
- statusLabel,
33
- type StoredUser,
34
- } from "@/lib/auth-service";
35
- import {
36
- countProjectsByOwner,
37
- formatStamp,
38
- listProjects,
39
- removeProjectsOfOwner,
40
- type StoredProject,
41
- } from "@/lib/studio-storage";
42
- import { listFeedback, markFeedbackRead, removeFeedback, type FeedbackItem } from "@/lib/feedback-service";
43
-
44
- type Props = {
45
- open: boolean;
46
- onOpenChange: (v: boolean) => void;
47
- };
48
-
49
- export default function AdminPanel({ open, onOpenChange }: Props) {
50
- const [users, setUsers] = useState<StoredUser[]>([]);
51
- const [counts, setCounts] = useState<Record<string, number>>({});
52
- const [createOpen, setCreateOpen] = useState(false);
53
- const [newLogin, setNewLogin] = useState("");
54
- const [createError, setCreateError] = useState<string | null>(null);
55
- const [credentials, setCredentials] = useState<{ username: string; password: string } | null>(null);
56
- const [toDelete, setToDelete] = useState<StoredUser | null>(null);
57
- const [viewing, setViewing] = useState<{ user: StoredUser; items: StoredProject[] } | null>(null);
58
- const [feedback, setFeedback] = useState<FeedbackItem[]>([]);
59
-
60
- const refresh = useCallback(() => {
61
- setUsers(listUsers());
62
- setCounts(countProjectsByOwner());
63
- setFeedback(listFeedback());
64
- }, []);
65
-
66
- useEffect(() => {
67
- if (open) refresh();
68
- }, [open, refresh]);
69
-
70
- const totalProjects = Object.values(counts).reduce((a, b) => a + b, 0);
71
- const activeUsers = users.filter((u) => u.status === "active").length;
72
- const pendingUsers = users.length - activeUsers;
73
-
74
- const copy = (value: string) => {
75
- void navigator.clipboard?.writeText(value);
76
- };
77
-
78
- const submitCreate = () => {
79
- const result = createUser(newLogin);
80
- if (!result.ok) {
81
- setCreateError(result.error);
82
- return;
83
- }
84
- setCreateError(null);
85
- setCreateOpen(false);
86
- setNewLogin("");
87
- setCredentials({ username: result.user.username, password: result.tempPassword });
88
- refresh();
89
- };
90
-
91
- return (
92
- <>
93
- <Dialog open={open} onOpenChange={onOpenChange}>
94
- <DialogContent className="max-h-[88vh] max-w-5xl overflow-y-auto">
95
- <DialogHeader>
96
- <DialogTitle className="flex items-center gap-2 font-display text-2xl">
97
- <Users className="h-6 w-6 text-accent" /> Панель админа
98
- </DialogTitle>
99
- <DialogDescription>Управление пользователями студии и обзор сохранённых схем.</DialogDescription>
100
- </DialogHeader>
101
-
102
- <div className="grid gap-3 sm:grid-cols-5">
103
- {[
104
- { label: "Пользователей", value: users.length },
105
- { label: "Активных", value: activeUsers },
106
- { label: "Ожидают входа", value: pendingUsers },
107
- { label: "Всего схем", value: totalProjects },
108
- { label: "Новых пожеланий", value: feedback.filter((f) => !f.read).length },
109
- ].map((s) => (
110
- <div key={s.label} className="rounded-2xl border border-border bg-secondary p-4">
111
- <p className="text-xs text-muted-foreground">{s.label}</p>
112
- <p className="font-display text-3xl leading-tight">{s.value}</p>
113
- </div>
114
- ))}
115
- </div>
116
-
117
- <section className="rounded-2xl border border-border">
118
- <h3 className="flex items-center gap-2 border-b border-border px-4 py-2 font-display text-lg">
119
- <MessageSquare className="h-5 w-5 text-accent" /> Пожелания пользователей
120
- </h3>
121
- {feedback.length === 0 ? (
122
- <p className="px-4 py-6 text-center text-sm text-muted-foreground">Пока нет сообщений.</p>
123
- ) : (
124
- <ul className="max-h-64 divide-y divide-border overflow-y-auto">
125
- {feedback.map((f) => (
126
- <li key={f.id} className="flex items-start gap-3 px-4 py-2.5">
127
- <div className="min-w-0 flex-1">
128
- <p className="text-xs text-muted-foreground">
129
- {f.username} · {formatStamp(f.createdAt)}
130
- {!f.read && <Badge className="ml-2 align-middle">новое</Badge>}
131
- </p>
132
- <p className="whitespace-pre-wrap text-sm">{f.text}</p>
133
- </div>
134
- <div className="flex shrink-0 gap-1">
135
- {!f.read && (
136
- <Button
137
- variant="outline"
138
- size="icon"
139
- className="h-8 w-8"
140
- title="Отметить прочитанным"
141
- onClick={() => setFeedback(markFeedbackRead(f.id))}
142
- >
143
- <Check className="h-4 w-4" />
144
- </Button>
145
- )}
146
- <Button
147
- variant="outline"
148
- size="icon"
149
- className="h-8 w-8 text-destructive"
150
- title="Удалить сообщение"
151
- onClick={() => setFeedback(removeFeedback(f.id))}
152
- >
153
- <Trash2 className="h-4 w-4" />
154
- </Button>
155
- </div>
156
- </li>
157
- ))}
158
- </ul>
159
- )}
160
- </section>
161
-
162
-
163
- <div>
164
- <Button size="lg" className="h-12 gap-2" onClick={() => setCreateOpen(true)}>
165
- <UserPlus className="h-5 w-5" /> Создать пользователя
166
- </Button>
167
- </div>
168
-
169
- <div className="overflow-x-auto rounded-2xl border border-border">
170
- <Table>
171
- <TableHeader>
172
- <TableRow>
173
- <TableHead>Логин</TableHead>
174
- <TableHead>Роль</TableHead>
175
- <TableHead>Создан</TableHead>
176
- <TableHead>Статус</TableHead>
177
- <TableHead>Схемы</TableHead>
178
- <TableHead className="text-right">Действия</TableHead>
179
- </TableRow>
180
- </TableHeader>
181
- <TableBody>
182
- {users.map((u) => (
183
- <TableRow key={u.id}>
184
- <TableCell className="font-medium">{u.username}</TableCell>
185
- <TableCell>{roleLabel(u.role)}</TableCell>
186
- <TableCell className="text-muted-foreground">{formatStamp(u.createdAt)}</TableCell>
187
- <TableCell>
188
- <Badge variant={u.status === "active" ? "secondary" : "outline"}>{statusLabel(u.status)}</Badge>
189
- </TableCell>
190
- <TableCell>{counts[u.id] ?? 0}</TableCell>
191
- <TableCell>
192
- <div className="flex flex-wrap justify-end gap-2">
193
- <Button
194
- variant="outline"
195
- size="sm"
196
- className="h-10 gap-1"
197
- title="Сбросить пароль и выдать временный"
198
- onClick={() => {
199
- const password = resetPassword(u.id);
200
- setCredentials({ username: u.username, password });
201
- refresh();
202
- }}
203
- >
204
- <RotateCcw className="h-4 w-4" /> Сбросить пароль
205
- </Button>
206
- <Button
207
- variant="outline"
208
- size="sm"
209
- className="h-10 gap-1"
210
- title="Просмотреть проекты пользователя"
211
- onClick={() => setViewing({ user: u, items: listProjects(u.id) })}
212
- >
213
- <FolderOpen className="h-4 w-4" /> Просмотреть проекты
214
- </Button>
215
- <Button
216
- variant="destructive"
217
- size="sm"
218
- className="h-10 gap-1"
219
- disabled={u.role === "admin"}
220
- title={
221
- u.role === "admin" ? "Администратора удалить нельзя" : "Удалить пользователя и его схемы"
222
- }
223
- onClick={() => setToDelete(u)}
224
- >
225
- <Trash2 className="h-4 w-4" /> Удалить
226
- </Button>
227
- </div>
228
- </TableCell>
229
- </TableRow>
230
- ))}
231
- </TableBody>
232
- </Table>
233
- </div>
234
- </DialogContent>
235
- </Dialog>
236
-
237
- {/* Создание пользователя */}
238
- <Dialog open={createOpen} onOpenChange={setCreateOpen}>
239
- <DialogContent className="max-w-sm">
240
- <DialogHeader>
241
- <DialogTitle className="font-display text-2xl">Создать пользователя</DialogTitle>
242
- <DialogDescription>
243
- Укажите логин — система выдаст одноразовый временный пароль для первого входа.
244
- </DialogDescription>
245
- </DialogHeader>
246
- <div className="space-y-2">
247
- <Label htmlFor="new-user-login">Логин</Label>
248
- <Input
249
- id="new-user-login"
250
- autoFocus
251
- value={newLogin}
252
- onChange={(e) => {
253
- setNewLogin(e.target.value);
254
- setCreateError(null);
255
- }}
256
- className="h-12"
257
- />
258
- {createError && <p className="text-sm text-destructive">{createError}</p>}
259
- </div>
260
- <Button size="lg" className="h-12 w-full" onClick={submitCreate}>
261
- <UserPlus className="h-5 w-5" /> Создать и выдать пароль
262
- </Button>
263
- </DialogContent>
264
- </Dialog>
265
-
266
- {/* Показ временного пароля */}
267
- <Dialog open={!!credentials} onOpenChange={(v) => !v && setCredentials(null)}>
268
- <DialogContent className="max-w-sm">
269
- <DialogHeader>
270
- <DialogTitle className="flex items-center gap-2 font-display text-2xl">
271
- <KeyRound className="h-6 w-6 text-accent" /> Временный пароль
272
- </DialogTitle>
273
- <DialogDescription>
274
- Передайте пароль пользователю «{credentials?.username}». При первом входе он задаст постоянный пароль.
275
- </DialogDescription>
276
- </DialogHeader>
277
- <div className="flex items-center gap-2">
278
- <code className="flex-1 rounded-xl border border-border bg-secondary px-4 py-3 text-center text-lg tracking-widest">
279
- {credentials?.password}
280
- </code>
281
- <Button
282
- variant="outline"
283
- size="lg"
284
- className="h-12 gap-2"
285
- title="Скопировать пароль"
286
- onClick={() => credentials && copy(credentials.password)}
287
- >
288
- <Copy className="h-5 w-5" />
289
- </Button>
290
- </div>
291
- <Button size="lg" className="h-12 w-full" onClick={() => setCredentials(null)}>
292
- Готово
293
- </Button>
294
- </DialogContent>
295
- </Dialog>
296
-
297
- {/* Проекты пользователя */}
298
- <Dialog open={!!viewing} onOpenChange={(v) => !v && setViewing(null)}>
299
- <DialogContent className="max-h-[80vh] max-w-3xl overflow-y-auto">
300
- <DialogHeader>
301
- <DialogTitle className="font-display text-2xl">Проекты «{viewing?.user.username}»</DialogTitle>
302
- <DialogDescription>Схемы, сохранённые этим пользователем.</DialogDescription>
303
- </DialogHeader>
304
- {viewing && viewing.items.length === 0 ? (
305
- <p className="py-8 text-center text-sm text-muted-foreground">У пользователя пока нет схем.</p>
306
- ) : (
307
- <div className="grid gap-4 sm:grid-cols-3">
308
- {viewing?.items.map((p) => (
309
- <article key={p.id} className="overflow-hidden rounded-2xl border border-border bg-card">
310
- <img
311
- src={p.thumb}
312
- alt={`Превью схемы «${p.title}»`}
313
- className="aspect-[4/3] w-full bg-secondary object-cover"
314
- />
315
- <div className="space-y-1 p-3">
316
- <h3 className="truncate font-medium">{p.title}</h3>
317
- <p className="text-xs text-muted-foreground">
318
- {formatStamp(p.updatedAt)} · элементов: {p.elements}
319
- </p>
320
- </div>
321
- </article>
322
- ))}
323
- </div>
324
- )}
325
- </DialogContent>
326
- </Dialog>
327
-
328
- <AlertDialog open={!!toDelete} onOpenChange={(v) => !v && setToDelete(null)}>
329
- <AlertDialogContent>
330
- <AlertDialogHeader>
331
- <AlertDialogTitle>Удалить пользователя?</AlertDialogTitle>
332
- <AlertDialogDescription>
333
- Пользователь «{toDelete?.username}» и все его схемы будут удалены безвозвратно.
334
- </AlertDialogDescription>
335
- </AlertDialogHeader>
336
- <AlertDialogFooter>
337
- <AlertDialogCancel>Отмена</AlertDialogCancel>
338
- <AlertDialogAction
339
- onClick={() => {
340
- if (toDelete) {
341
- removeProjectsOfOwner(toDelete.id);
342
- deleteUser(toDelete.id);
343
- }
344
- setToDelete(null);
345
- refresh();
346
- }}
347
- >
348
- Удалить
349
- </AlertDialogAction>
350
- </AlertDialogFooter>
351
- </AlertDialogContent>
352
- </AlertDialog>
353
- </>
354
- );
355
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/studio/FeedbackDialog.tsx DELETED
@@ -1,52 +0,0 @@
1
- import { useState } from "react";
2
- import { MessageSquarePlus, Send } from "lucide-react";
3
- import { Button } from "@/components/ui/button";
4
- import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
5
- import { Textarea } from "@/components/ui/textarea";
6
- import { addFeedback } from "@/lib/feedback-service";
7
- import type { StoredUser } from "@/lib/auth-service";
8
-
9
- type Props = {
10
- open: boolean;
11
- onOpenChange: (v: boolean) => void;
12
- user: StoredUser;
13
- };
14
-
15
- export default function FeedbackDialog({ open, onOpenChange, user }: Props) {
16
- const [text, setText] = useState("");
17
- const [sent, setSent] = useState(false);
18
-
19
- const submit = () => {
20
- if (text.trim().length < 3) return;
21
- addFeedback(user.id, user.username, text);
22
- setText("");
23
- setSent(true);
24
- window.setTimeout(() => {
25
- setSent(false);
26
- onOpenChange(false);
27
- }, 1200);
28
- };
29
-
30
- return (
31
- <Dialog open={open} onOpenChange={onOpenChange}>
32
- <DialogContent className="max-w-sm">
33
- <DialogHeader>
34
- <DialogTitle className="flex items-center gap-2 font-display text-2xl">
35
- <MessageSquarePlus className="h-5 w-5 text-accent" /> Чего не хватает?
36
- </DialogTitle>
37
- <DialogDescription>Напишите пожелание — оно придёт администратору студии.</DialogDescription>
38
- </DialogHeader>
39
- <Textarea
40
- rows={4}
41
- autoFocus
42
- value={text}
43
- placeholder="Например: добавьте элемент «трилистник» и печать на A3"
44
- onChange={(e) => setText(e.target.value)}
45
- />
46
- <Button className="h-11 w-full gap-2" disabled={text.trim().length < 3} onClick={submit}>
47
- <Send className="h-4 w-4" /> {sent ? "Отправлено!" : "Отправить"}
48
- </Button>
49
- </DialogContent>
50
- </Dialog>
51
- );
52
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/studio/LoginDialog.tsx DELETED
@@ -1,155 +0,0 @@
1
- import { useState, type FormEvent } from "react";
2
- import { KeyRound, ShieldCheck } from "lucide-react";
3
- import { Button } from "@/components/ui/button";
4
- import { Checkbox } from "@/components/ui/checkbox";
5
- import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
6
- import { Input } from "@/components/ui/input";
7
- import { Label } from "@/components/ui/label";
8
- import { login, setPermanentPassword, type StoredUser } from "@/lib/auth-service";
9
-
10
- export default function LoginDialog({ onSignedIn }: { onSignedIn: (user: StoredUser) => void }) {
11
- const [username, setUsername] = useState("");
12
- const [password, setPassword] = useState("");
13
- const [remember, setRemember] = useState(true);
14
- const [error, setError] = useState<string | null>(null);
15
-
16
- /** Пользователь вошёл по временному паролю — требуется постоянный. */
17
- const [pending, setPending] = useState<StoredUser | null>(null);
18
- const [next, setNext] = useState("");
19
- const [confirm, setConfirm] = useState("");
20
-
21
- const submitLogin = (e: FormEvent) => {
22
- e.preventDefault();
23
- const result = login(username, password, remember);
24
- if (!result.ok) {
25
- setError(result.error);
26
- return;
27
- }
28
- setError(null);
29
- if (result.mustChangePassword) {
30
- setPending(result.user);
31
- return;
32
- }
33
- onSignedIn(result.user);
34
- };
35
-
36
- const submitNewPassword = (e: FormEvent) => {
37
- e.preventDefault();
38
- if (!pending) return;
39
- if (next !== confirm) {
40
- setError("Пароли не совпадают.");
41
- return;
42
- }
43
- const result = setPermanentPassword(pending.id, next);
44
- if (!result.ok) {
45
- setError(result.error ?? "Не удалось сохранить пароль.");
46
- return;
47
- }
48
- onSignedIn({ ...pending, temporary: false, status: "active" });
49
- };
50
-
51
- if (pending) {
52
- return (
53
- <Dialog open>
54
- <DialogContent className="max-w-sm [&>button]:hidden">
55
- <DialogHeader>
56
- <DialogTitle className="flex items-center gap-2 font-display text-2xl">
57
- <ShieldCheck className="h-6 w-6 text-accent" /> Новый пароль
58
- </DialogTitle>
59
- <DialogDescription>
60
- Вы вошли по временному паролю. Задайте постоянный пароль, чтобы продолжить работу.
61
- </DialogDescription>
62
- </DialogHeader>
63
- <form onSubmit={submitNewPassword} className="space-y-4">
64
- <div className="space-y-2">
65
- <Label htmlFor="new-password">Новый пароль</Label>
66
- <Input
67
- id="new-password"
68
- autoFocus
69
- type="password"
70
- value={next}
71
- onChange={(e) => {
72
- setNext(e.target.value);
73
- setError(null);
74
- }}
75
- className="h-12"
76
- />
77
- </div>
78
- <div className="space-y-2">
79
- <Label htmlFor="confirm-password">Повторите пароль</Label>
80
- <Input
81
- id="confirm-password"
82
- type="password"
83
- value={confirm}
84
- onChange={(e) => {
85
- setConfirm(e.target.value);
86
- setError(null);
87
- }}
88
- className="h-12"
89
- />
90
- </div>
91
- {error && <p className="text-sm text-destructive">{error}</p>}
92
- <Button type="submit" size="lg" className="h-12 w-full">
93
- Сохранить и войти
94
- </Button>
95
- </form>
96
- </DialogContent>
97
- </Dialog>
98
- );
99
- }
100
-
101
- return (
102
- <Dialog open>
103
- <DialogContent className="max-w-sm [&>button]:hidden">
104
- <DialogHeader>
105
- <DialogTitle className="flex items-center gap-2 font-display text-2xl">
106
- <KeyRound className="h-6 w-6 text-accent" /> RealKnot Studio
107
- </DialogTitle>
108
- <DialogDescription>Войдите в аккаунт, чтобы открыть студию и свои проекты.</DialogDescription>
109
- </DialogHeader>
110
- <form onSubmit={submitLogin} className="space-y-4">
111
- <div className="space-y-2">
112
- <Label htmlFor="login">Логин</Label>
113
- <Input
114
- id="login"
115
- autoFocus
116
- autoComplete="username"
117
- placeholder="admin"
118
- value={username}
119
- onChange={(e) => {
120
- setUsername(e.target.value);
121
- setError(null);
122
- }}
123
- className="h-12"
124
- />
125
- </div>
126
- <div className="space-y-2">
127
- <Label htmlFor="password">Пароль</Label>
128
- <Input
129
- id="password"
130
- type="password"
131
- autoComplete="current-password"
132
- value={password}
133
- onChange={(e) => {
134
- setPassword(e.target.value);
135
- setError(null);
136
- }}
137
- className="h-12"
138
- />
139
- </div>
140
- {error && <p className="text-sm text-destructive">{error}</p>}
141
- <label className="flex cursor-pointer items-center gap-2 text-sm">
142
- <Checkbox checked={remember} onCheckedChange={(v) => setRemember(v === true)} />
143
- Запомнить меня
144
- </label>
145
- <Button type="submit" size="lg" className="h-12 w-full">
146
- Войти в студию
147
- </Button>
148
- <p className="text-center text-xs text-muted-foreground">
149
- Аккаунт создаёт администратор в «Панели админа».
150
- </p>
151
- </form>
152
- </DialogContent>
153
- </Dialog>
154
- );
155
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/studio/ProfileDialog.tsx DELETED
@@ -1,159 +0,0 @@
1
- import { useEffect, useRef, useState } from "react";
2
- import { Camera, FolderOpen, Save, UserRound } from "lucide-react";
3
- import { Button } from "@/components/ui/button";
4
- import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
5
- import { Input } from "@/components/ui/input";
6
- import { Label } from "@/components/ui/label";
7
- import { Textarea } from "@/components/ui/textarea";
8
- import { roleLabel, statusLabel, updateProfile, type StoredUser } from "@/lib/auth-service";
9
- import { formatStamp, listProjects, type StoredProject } from "@/lib/studio-storage";
10
-
11
- type Props = {
12
- open: boolean;
13
- onOpenChange: (v: boolean) => void;
14
- user: StoredUser;
15
- onUserChange: (u: StoredUser) => void;
16
- onOpenProject: (p: StoredProject) => void;
17
- };
18
-
19
- export default function ProfileDialog({ open, onOpenChange, user, onUserChange, onOpenProject }: Props) {
20
- const fileRef = useRef<HTMLInputElement | null>(null);
21
- const [displayName, setDisplayName] = useState(user.displayName ?? user.username);
22
- const [about, setAbout] = useState(user.about ?? "");
23
- const [avatar, setAvatar] = useState(user.avatar ?? "");
24
- const [items, setItems] = useState<StoredProject[]>([]);
25
- const [savedAt, setSavedAt] = useState(0);
26
-
27
- useEffect(() => {
28
- if (!open) return;
29
- setDisplayName(user.displayName ?? user.username);
30
- setAbout(user.about ?? "");
31
- setAvatar(user.avatar ?? "");
32
- setItems(listProjects(user.id));
33
- }, [open, user]);
34
-
35
- const pickAvatar = (file: File) => {
36
- const reader = new FileReader();
37
- reader.onload = () => setAvatar(String(reader.result ?? ""));
38
- reader.readAsDataURL(file);
39
- };
40
-
41
- const save = () => {
42
- const next = updateProfile(user.id, { displayName: displayName.trim(), about: about.trim(), avatar });
43
- if (next) onUserChange(next);
44
- setSavedAt(Date.now());
45
- };
46
-
47
- return (
48
- <Dialog open={open} onOpenChange={onOpenChange}>
49
- <DialogContent className="max-h-[88vh] max-w-3xl overflow-y-auto">
50
- <DialogHeader>
51
- <DialogTitle className="font-display text-2xl">Профиль</DialogTitle>
52
- <DialogDescription>Данные аккаунта, аватар и сохранённые работы.</DialogDescription>
53
- </DialogHeader>
54
-
55
- <input
56
- ref={fileRef}
57
- type="file"
58
- accept="image/*"
59
- className="hidden"
60
- onChange={(e) => {
61
- const f = e.target.files?.[0];
62
- if (f) pickAvatar(f);
63
- e.target.value = "";
64
- }}
65
- />
66
-
67
- <div className="grid gap-4 sm:grid-cols-[auto_minmax(0,1fr)]">
68
- <div className="flex flex-col items-center gap-2">
69
- <div className="grid h-24 w-24 place-items-center overflow-hidden rounded-full border border-border bg-secondary">
70
- {avatar ? (
71
- <img src={avatar} alt="Аватар пользователя" className="h-full w-full object-cover" />
72
- ) : (
73
- <UserRound className="h-10 w-10 text-muted-foreground" />
74
- )}
75
- </div>
76
- <Button variant="outline" size="sm" className="h-9 gap-1.5" onClick={() => fileRef.current?.click()}>
77
- <Camera className="h-4 w-4" /> Аватар
78
- </Button>
79
- </div>
80
-
81
- <div className="space-y-3">
82
- <div className="grid gap-2 sm:grid-cols-2">
83
- <div className="space-y-1.5">
84
- <Label htmlFor="pf-name">Отображаемое имя</Label>
85
- <Input id="pf-name" value={displayName} onChange={(e) => setDisplayName(e.target.value)} />
86
- </div>
87
- <div className="space-y-1.5">
88
- <Label>Логин</Label>
89
- <Input value={user.username} readOnly className="text-muted-foreground" />
90
- </div>
91
- </div>
92
- <div className="space-y-1.5">
93
- <Label htmlFor="pf-about">О себе</Label>
94
- <Textarea
95
- id="pf-about"
96
- rows={3}
97
- value={about}
98
- placeholder="Например: плету салфетки и мандалы"
99
- onChange={(e) => setAbout(e.target.value)}
100
- />
101
- </div>
102
- <dl className="grid grid-cols-2 gap-2 text-xs sm:grid-cols-4">
103
- {[
104
- { k: "Роль", v: roleLabel(user.role) },
105
- { k: "Статус", v: statusLabel(user.status) },
106
- { k: "Создан", v: formatStamp(user.createdAt) },
107
- { k: "Схем", v: String(items.length) },
108
- ].map((s) => (
109
- <div key={s.k} className="rounded-lg border border-border bg-secondary px-2 py-1.5">
110
- <dt className="text-muted-foreground">{s.k}</dt>
111
- <dd className="truncate font-medium">{s.v}</dd>
112
- </div>
113
- ))}
114
- </dl>
115
- <div className="flex items-center gap-2">
116
- <Button size="sm" className="h-10 gap-1.5" onClick={save}>
117
- <Save className="h-4 w-4" /> Сохранить профиль
118
- </Button>
119
- {savedAt > 0 && <span className="text-xs text-muted-foreground">Изменения сохранены</span>}
120
- </div>
121
- </div>
122
- </div>
123
-
124
- <div className="border-t border-border pt-3">
125
- <h3 className="font-display text-lg">Мои работы</h3>
126
- {items.length === 0 ? (
127
- <p className="py-6 text-center text-sm text-muted-foreground">Пока нет сохранённых схем.</p>
128
- ) : (
129
- <div className="mt-2 grid gap-3 sm:grid-cols-3">
130
- {items.map((p) => (
131
- <article key={p.id} className="overflow-hidden rounded-xl border border-border bg-card">
132
- <img
133
- src={p.thumb}
134
- alt={`Превью схемы «${p.title}»`}
135
- className="aspect-[4/3] w-full bg-secondary object-cover"
136
- />
137
- <div className="space-y-1.5 p-2">
138
- <h4 className="truncate text-sm font-medium">{p.title}</h4>
139
- <p className="text-[11px] text-muted-foreground">{formatStamp(p.updatedAt)}</p>
140
- <Button
141
- size="sm"
142
- className="h-8 w-full gap-1.5"
143
- onClick={() => {
144
- onOpenProject(p);
145
- onOpenChange(false);
146
- }}
147
- >
148
- <FolderOpen className="h-4 w-4" /> Открыть
149
- </Button>
150
- </div>
151
- </article>
152
- ))}
153
- </div>
154
- )}
155
- </div>
156
- </DialogContent>
157
- </Dialog>
158
- );
159
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/studio/ProjectsDialog.tsx DELETED
@@ -1,159 +0,0 @@
1
- import { useEffect, useState } from "react";
2
- import { Download, FolderOpen, Pencil, Trash2, Upload } from "lucide-react";
3
- import { Button } from "@/components/ui/button";
4
- import {
5
- AlertDialog,
6
- AlertDialogAction,
7
- AlertDialogCancel,
8
- AlertDialogContent,
9
- AlertDialogDescription,
10
- AlertDialogFooter,
11
- AlertDialogHeader,
12
- AlertDialogTitle,
13
- } from "@/components/ui/alert-dialog";
14
- import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog";
15
- import {
16
- formatStamp,
17
- listProjects,
18
- removeProject,
19
- renameProject,
20
- type StoredProject,
21
- } from "@/lib/studio-storage";
22
-
23
- type Props = {
24
- open: boolean;
25
- onOpenChange: (v: boolean) => void;
26
- onOpenProject: (p: StoredProject) => void;
27
- onImportFile: () => void;
28
- onSaveCurrent: () => void;
29
- ownerId: string;
30
- };
31
-
32
- export default function ProjectsDialog({ open, onOpenChange, onOpenProject, onImportFile, onSaveCurrent, ownerId }: Props) {
33
- const [items, setItems] = useState<StoredProject[]>([]);
34
- const [toDelete, setToDelete] = useState<StoredProject | null>(null);
35
-
36
- useEffect(() => {
37
- if (open) setItems(listProjects(ownerId));
38
- }, [open, ownerId]);
39
-
40
- const download = (p: StoredProject) => {
41
- const blob = new Blob([JSON.stringify(p.data, null, 2)], { type: "application/json" });
42
- const a = document.createElement("a");
43
- a.href = URL.createObjectURL(blob);
44
- a.download = `${p.title || "proekt"}.json`;
45
- a.click();
46
- URL.revokeObjectURL(a.href);
47
- };
48
-
49
- const rename = (p: StoredProject) => {
50
- const title = window.prompt("Новое название проекта", p.title);
51
- if (title && title.trim()) setItems(renameProject(p.id, title.trim(), ownerId));
52
- };
53
-
54
- return (
55
- <>
56
- <Dialog open={open} onOpenChange={onOpenChange}>
57
- <DialogContent className="max-h-[85vh] max-w-4xl overflow-y-auto">
58
- <DialogHeader>
59
- <DialogTitle className="font-display text-2xl">Мои проекты</DialogTitle>
60
- <DialogDescription>Схемы, сохранённые в памяти этого браузера.</DialogDescription>
61
- </DialogHeader>
62
-
63
- <div className="flex flex-wrap gap-2">
64
- <Button size="lg" className="h-12 gap-2" onClick={onSaveCurrent} title="Сохранить текущую схему в проекты">
65
- <FolderOpen className="h-5 w-5" /> Сохранить текущую схему
66
- </Button>
67
- <Button
68
- variant="outline"
69
- size="lg"
70
- className="h-12 gap-2"
71
- onClick={onImportFile}
72
- title="Загрузить проект из файла .json"
73
- >
74
- <Upload className="h-5 w-5" /> Загрузить .json
75
- </Button>
76
- </div>
77
-
78
- {items.length === 0 ? (
79
- <p className="py-8 text-center text-sm text-muted-foreground">
80
- Пока нет сохранённых проектов. Создайте схему и нажмите «Сохранить текущую схему».
81
- </p>
82
- ) : (
83
- <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
84
- {items.map((p) => (
85
- <article key={p.id} className="overflow-hidden rounded-2xl border border-border bg-card">
86
- <img
87
- src={p.thumb}
88
- alt={`Превью схемы «${p.title}»`}
89
- className="aspect-[4/3] w-full bg-secondary object-cover"
90
- />
91
- <div className="space-y-2 p-3">
92
- <h3 className="truncate font-medium">{p.title}</h3>
93
- <p className="text-xs text-muted-foreground">
94
- {formatStamp(p.updatedAt)} · элементов: {p.elements}
95
- </p>
96
- <div className="grid grid-cols-2 gap-2">
97
- <Button size="sm" className="h-10 gap-1" onClick={() => onOpenProject(p)} title="Открыть проект">
98
- <FolderOpen className="h-4 w-4" /> Открыть
99
- </Button>
100
- <Button
101
- variant="outline"
102
- size="sm"
103
- className="h-10 gap-1"
104
- onClick={() => rename(p)}
105
- title="Переименовать проект"
106
- >
107
- <Pencil className="h-4 w-4" /> Переименовать
108
- </Button>
109
- <Button
110
- variant="outline"
111
- size="sm"
112
- className="h-10 gap-1"
113
- onClick={() => download(p)}
114
- title="Скачать проект файлом .json"
115
- >
116
- <Download className="h-4 w-4" /> Скачать
117
- </Button>
118
- <Button
119
- variant="destructive"
120
- size="sm"
121
- className="h-10 gap-1"
122
- onClick={() => setToDelete(p)}
123
- title="Удалить проект"
124
- >
125
- <Trash2 className="h-4 w-4" /> Удалить
126
- </Button>
127
- </div>
128
- </div>
129
- </article>
130
- ))}
131
- </div>
132
- )}
133
- </DialogContent>
134
- </Dialog>
135
-
136
- <AlertDialog open={!!toDelete} onOpenChange={(v) => !v && setToDelete(null)}>
137
- <AlertDialogContent>
138
- <AlertDialogHeader>
139
- <AlertDialogTitle>Удалить проект?</AlertDialogTitle>
140
- <AlertDialogDescription>
141
- Вы уверены, что хотите удалить проект «{toDelete?.title}»? Действие нельзя отменить.
142
- </AlertDialogDescription>
143
- </AlertDialogHeader>
144
- <AlertDialogFooter>
145
- <AlertDialogCancel>Отмена</AlertDialogCancel>
146
- <AlertDialogAction
147
- onClick={() => {
148
- if (toDelete) setItems(removeProject(toDelete.id, ownerId));
149
- setToDelete(null);
150
- }}
151
- >
152
- Удалить
153
- </AlertDialogAction>
154
- </AlertDialogFooter>
155
- </AlertDialogContent>
156
- </AlertDialog>
157
- </>
158
- );
159
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/studio/Shape.tsx DELETED
@@ -1,58 +0,0 @@
1
- import type { ElementKind } from "@/lib/tatting";
2
-
3
- type Props = {
4
- kind: ElementKind;
5
- picots: number;
6
- radius: number;
7
- color: string;
8
- };
9
-
10
- const r3 = (n: number) => Math.round(n * 1000) / 1000;
11
-
12
- /** Renders a tatting motif centered at (0,0) in local coordinates. */
13
- export function Shape({ kind, picots, radius, color }: Props) {
14
- const picotR = Math.max(3, radius * 0.11);
15
-
16
- if (kind === "ring") {
17
- const dots = Array.from({ length: picots }, (_, i) => {
18
- const a = (i / picots) * Math.PI * 2 - Math.PI / 2;
19
- return {
20
- cx: r3(Math.cos(a) * (radius + picotR * 0.7)),
21
- cy: r3(Math.sin(a) * (radius + picotR * 0.7)),
22
- };
23
- });
24
- return (
25
- <g fill="none" stroke={color} strokeWidth={2.5} strokeLinecap="round">
26
- <circle cx={0} cy={0} r={radius} />
27
- <circle cx={0} cy={0} r={radius - 5} strokeWidth={1} opacity={0.45} />
28
- {dots.map((d, i) => (
29
- <circle key={i} cx={d.cx} cy={d.cy} r={picotR} />
30
- ))}
31
- </g>
32
- );
33
- }
34
-
35
- // Arc: half-circle bow with picots along the outer edge
36
- const start = { x: -radius, y: 0 };
37
- const end = { x: radius, y: 0 };
38
- const dots = Array.from({ length: picots }, (_, i) => {
39
- const t = (i + 1) / (picots + 1);
40
- const a = Math.PI - t * Math.PI;
41
- const rr = radius + picotR * 0.7;
42
- return { cx: r3(Math.cos(a) * rr), cy: r3(-Math.sin(a) * rr) };
43
- });
44
-
45
- return (
46
- <g fill="none" stroke={color} strokeWidth={2.5} strokeLinecap="round">
47
- <path d={`M ${start.x} ${start.y} A ${radius} ${radius} 0 0 1 ${end.x} ${end.y}`} />
48
- <path
49
- d={`M ${start.x + 5} 0 A ${radius - 5} ${radius - 5} 0 0 1 ${end.x - 5} 0`}
50
- strokeWidth={1}
51
- opacity={0.45}
52
- />
53
- {dots.map((d, i) => (
54
- <circle key={i} cx={d.cx} cy={d.cy} r={picotR} />
55
- ))}
56
- </g>
57
- );
58
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/studio/TattingStudio.tsx DELETED
@@ -1,1198 +0,0 @@
1
- import * as fabric from "fabric";
2
- import { jsPDF } from "jspdf";
3
- import { useCallback, useEffect, useRef, useState } from "react";
4
- import {
5
- ArrowDownToLine,
6
- ArrowUpToLine,
7
- Cloud,
8
- Copy,
9
- Download,
10
- Eraser,
11
- FileDown,
12
- FlipHorizontal,
13
- FlipVertical,
14
- FolderOpen,
15
- Grid3x3,
16
- Group,
17
- HelpCircle,
18
- Maximize,
19
- MousePointer2,
20
- Redo2,
21
- RotateCw,
22
- Ruler,
23
- Save,
24
- Scissors,
25
- Trash2,
26
- Undo2,
27
- Ungroup,
28
- ZoomIn,
29
- ZoomOut,
30
- LogOut,
31
- Users,
32
- UserRound,
33
- MessageSquarePlus,
34
- } from "lucide-react";
35
- import { Button } from "@/components/ui/button";
36
- import {
37
- Dialog,
38
- DialogContent,
39
- DialogDescription,
40
- DialogHeader,
41
- DialogTitle,
42
- } from "@/components/ui/dialog";
43
- import { Slider } from "@/components/ui/slider";
44
-
45
- import {
46
- CATEGORIES,
47
- LIBRARY,
48
- SNAP_STEPS,
49
- SYMMETRY_AXES,
50
- THREADS,
51
- threadMeters,
52
- type ElDef,
53
- type ThreadColor,
54
- } from "@/lib/tatting-ru";
55
- import { applyThread, applyWidth, buildElement, CUSTOM_PROPS, type TattingObject } from "@/lib/fabric-elements";
56
- import { AUTOSAVE_KEY, newId, upsertProject, type StoredProject } from "@/lib/studio-storage";
57
- import { currentUser, logout, roleLabel, type StoredUser } from "@/lib/auth-service";
58
- import LoginDialog from "@/components/studio/LoginDialog";
59
- import AdminPanel from "@/components/studio/AdminPanel";
60
- import ProjectsDialog from "@/components/studio/ProjectsDialog";
61
- import ProfileDialog from "@/components/studio/ProfileDialog";
62
- import FeedbackDialog from "@/components/studio/FeedbackDialog";
63
-
64
- const W = 1200;
65
- const H = 850;
66
-
67
- type Theme = "light" | "dark" | "grid";
68
-
69
- const THEME_CFG: Record<Theme, { bg: string; line: string; bold: string; label: string; hint: string }> = {
70
- light: { bg: "#fdfaf3", line: "#e6dfd0", bold: "#d6cbb4", label: "Светлая", hint: "Светлая тема" },
71
- dark: { bg: "#14161b", line: "#2a2f3a", bold: "#3f4757", label: "Тёмная", hint: "Тёмная тема с контрастной сеткой" },
72
- grid: { bg: "#eef5fb", line: "#c3d9ee", bold: "#8fb6dc", label: "Миллиметровка", hint: "Чертёжная миллиметровка" },
73
- };
74
-
75
- export default function TattingStudio() {
76
- const elRef = useRef<HTMLCanvasElement | null>(null);
77
- const wrapRef = useRef<HTMLDivElement | null>(null);
78
- const fcRef = useRef<fabric.Canvas | null>(null);
79
- const history = useRef<{ stack: string[]; index: number; lock: boolean }>({ stack: [], index: -1, lock: false });
80
- const fileRef = useRef<HTMLInputElement | null>(null);
81
- const pinch = useRef<{ dist: number; x: number; y: number } | null>(null);
82
- const drag = useRef<{ x: number; y: number } | null>(null);
83
- const savedTimer = useRef<number | null>(null);
84
-
85
- const [unlocked, setUnlocked] = useState(true);
86
- const [theme, setTheme] = useState<Theme>("light");
87
- const [snap, setSnap] = useState(10);
88
- const [showGrid, setShowGrid] = useState(true);
89
- const [showRulers, setShowRulers] = useState(true);
90
- const [zoom, setZoom] = useState(1);
91
- const [pan, setPan] = useState({ x: 0, y: 0 });
92
- const [panning, setPanning] = useState(false);
93
- const [booting, setBooting] = useState(true);
94
- const [symmetry, setSymmetry] = useState(1);
95
- const [thread, setThread] = useState<ThreadColor>(THREADS[0]!);
96
- const [width, setWidth] = useState(3);
97
- const [selected, setSelected] = useState<{ name: string; knots: number; thread: string } | null>(null);
98
- const [stats, setStats] = useState({ knots: 0, pathPx: 0, count: 0 });
99
- const [canUndo, setCanUndo] = useState(false);
100
- const [canRedo, setCanRedo] = useState(false);
101
- const [category, setCategory] = useState<string>(CATEGORIES[0]!);
102
- const [helpOpen, setHelpOpen] = useState(false);
103
- const [projectsOpen, setProjectsOpen] = useState(false);
104
- const [profileOpen, setProfileOpen] = useState(false);
105
- const [feedbackOpen, setFeedbackOpen] = useState(false);
106
- const [saved, setSaved] = useState(false);
107
- const [user, setUser] = useState<StoredUser | null>(null);
108
- const [adminOpen, setAdminOpen] = useState(false);
109
-
110
- useEffect(() => {
111
- const session = currentUser();
112
- setUser(session);
113
- setUnlocked(!!session && !session.temporary);
114
- }, []);
115
-
116
- const drawGrid = useCallback((canvas: fabric.Canvas, step: number, th: Theme, visible: boolean) => {
117
- const cfg = THEME_CFG[th];
118
- canvas.backgroundColor = cfg.bg;
119
- canvas.getObjects().forEach((o) => {
120
- if ((o as TattingObject).isGrid) canvas.remove(o);
121
- });
122
- if (visible) {
123
- const lines: fabric.FabricObject[] = [];
124
- const mk = (coords: [number, number, number, number], bold: boolean) => {
125
- const l = new fabric.Line(coords, {
126
- stroke: bold ? cfg.bold : cfg.line,
127
- strokeWidth: bold ? 1 : 0.5,
128
- selectable: false,
129
- evented: false,
130
- }) as unknown as TattingObject;
131
- l.isGrid = true;
132
- lines.push(l as unknown as fabric.FabricObject);
133
- };
134
- for (let x = 0; x <= W; x += step) mk([x, 0, x, H], x % (step * 5) === 0);
135
- for (let y = 0; y <= H; y += step) mk([0, y, W, y], y % (step * 5) === 0);
136
- lines.forEach((l) => {
137
- canvas.add(l);
138
- canvas.sendObjectToBack(l);
139
- });
140
- }
141
- canvas.requestRenderAll();
142
- }, []);
143
-
144
- const recalc = useCallback((canvas: fabric.Canvas) => {
145
- const objs = canvas.getObjects().filter((o) => !(o as TattingObject).isGrid);
146
- const knots = objs.reduce((s, o) => s + ((o as TattingObject).knots ?? 0), 0);
147
- const pathPx = objs.reduce((s, o) => s + ((o as TattingObject).pathPx ?? 0) * (o.scaleX ?? 1), 0);
148
- setStats({ knots, pathPx, count: objs.length });
149
- }, []);
150
-
151
- /** автосохранение текущей схемы в память браузера */
152
- const persist = useCallback((canvas: fabric.Canvas) => {
153
- try {
154
- const data = canvas.toObject(CUSTOM_PROPS) as { objects: { isGrid?: boolean }[] };
155
- data.objects = data.objects.filter((o) => !o.isGrid);
156
- localStorage.setItem(AUTOSAVE_KEY, JSON.stringify(data));
157
- setSaved(true);
158
- if (savedTimer.current) window.clearTimeout(savedTimer.current);
159
- savedTimer.current = window.setTimeout(() => setSaved(false), 1400);
160
- } catch {
161
- /* приватный режим — просто пропускаем */
162
- }
163
- }, []);
164
-
165
- const snapshot = useCallback(
166
- (canvas: fabric.Canvas) => {
167
- if (history.current.lock) return;
168
- const json = JSON.stringify(canvas.toObject(CUSTOM_PROPS));
169
- const h = history.current;
170
- h.stack = h.stack.slice(0, h.index + 1);
171
- h.stack.push(json);
172
- if (h.stack.length > 40) h.stack.shift();
173
- h.index = h.stack.length - 1;
174
- setCanUndo(h.index > 0);
175
- setCanRedo(false);
176
- recalc(canvas);
177
- persist(canvas);
178
- },
179
- [recalc, persist],
180
- );
181
-
182
- // init
183
- useEffect(() => {
184
- if (!unlocked || !elRef.current) return;
185
- const canvas = new fabric.Canvas(elRef.current, {
186
- width: W,
187
- height: H,
188
- backgroundColor: THEME_CFG.light.bg,
189
- preserveObjectStacking: true,
190
- });
191
- fcRef.current = canvas;
192
- drawGrid(canvas, 10, "light", true);
193
-
194
- const onSelect = () => {
195
- const a = canvas.getActiveObject() as TattingObject | undefined;
196
- setSelected(
197
- a && a.elName
198
- ? { name: a.elName, knots: a.knots ?? 0, thread: a.threadName ?? "—" }
199
- : a
200
- ? { name: "Несколько элементов", knots: 0, thread: "—" }
201
- : null,
202
- );
203
- };
204
- canvas.on("selection:created", onSelect);
205
- canvas.on("selection:updated", onSelect);
206
- canvas.on("selection:cleared", () => setSelected(null));
207
- canvas.on("object:added", () => snapshot(canvas));
208
- canvas.on("object:modified", () => snapshot(canvas));
209
- canvas.on("object:removed", () => snapshot(canvas));
210
-
211
- const stored = typeof localStorage !== "undefined" ? localStorage.getItem(AUTOSAVE_KEY) : null;
212
- if (stored) {
213
- history.current.lock = true;
214
- void canvas
215
- .loadFromJSON(stored)
216
- .then(() => {
217
- drawGrid(canvas, 10, "light", true);
218
- canvas.requestRenderAll();
219
- history.current.lock = false;
220
- snapshot(canvas);
221
- })
222
- .catch(() => {
223
- history.current.lock = false;
224
- })
225
- .finally(() => window.setTimeout(() => setBooting(false), 220));
226
- } else {
227
- window.setTimeout(() => setBooting(false), 220);
228
- }
229
-
230
-
231
- return () => {
232
- void canvas.dispose();
233
- fcRef.current = null;
234
- };
235
- }, [unlocked, drawGrid, snapshot]);
236
-
237
- // привязка к сетке
238
- useEffect(() => {
239
- const canvas = fcRef.current;
240
- if (!canvas) return;
241
- const handler = (e: { target: fabric.FabricObject }) => {
242
- const t = e.target;
243
- t.set({ left: Math.round(t.left / snap) * snap, top: Math.round(t.top / snap) * snap });
244
- };
245
- canvas.on("object:moving", handler as never);
246
- return () => canvas.off("object:moving", handler as never);
247
- }, [snap]);
248
-
249
- // тема / шаг сетки
250
- useEffect(() => {
251
- const canvas = fcRef.current;
252
- if (!canvas) return;
253
- history.current.lock = true;
254
- drawGrid(canvas, snap, theme, showGrid);
255
- history.current.lock = false;
256
- document.documentElement.classList.toggle("dark", theme === "dark");
257
- }, [theme, snap, showGrid, drawGrid]);
258
-
259
- /** не даём уехать схеме за пределы окна: сцена всегда перекрывает вид */
260
- const clampVpt = useCallback((canvas: fabric.Canvas) => {
261
- const z = canvas.getZoom();
262
- const vpt = canvas.viewportTransform;
263
- const limX = Math.max(0, W * z - W);
264
- const limY = Math.max(0, H * z - H);
265
- vpt[4] = Math.min(W * 0.25, Math.max(-limX - W * 0.25, vpt[4]));
266
- vpt[5] = Math.min(H * 0.25, Math.max(-limY - H * 0.25, vpt[5]));
267
- canvas.setViewportTransform(vpt);
268
- setZoom(z);
269
- setPan({ x: vpt[4], y: vpt[5] });
270
- }, []);
271
-
272
- const applyZoom = useCallback(
273
- (next: number, px?: number, py?: number) => {
274
- const canvas = fcRef.current;
275
- if (!canvas) return;
276
- const z = Math.min(6, Math.max(0.3, next));
277
- canvas.zoomToPoint(new fabric.Point(px ?? W / 2, py ?? H / 2), z);
278
- clampVpt(canvas);
279
- canvas.requestRenderAll();
280
- },
281
- [clampVpt],
282
- );
283
-
284
- const panBy = useCallback(
285
- (dx: number, dy: number) => {
286
- const canvas = fcRef.current;
287
- if (!canvas) return;
288
- const vpt = canvas.viewportTransform;
289
- vpt[4] += dx;
290
- vpt[5] += dy;
291
- clampVpt(canvas);
292
- canvas.requestRenderAll();
293
- },
294
- [clampVpt],
295
- );
296
-
297
- const fitToScreen = useCallback(() => {
298
- const canvas = fcRef.current;
299
- if (!canvas) return;
300
- canvas.setViewportTransform([1, 0, 0, 1, 0, 0]);
301
- setZoom(1);
302
- setPan({ x: 0, y: 0 });
303
- canvas.requestRenderAll();
304
- }, []);
305
-
306
- // колесо мыши, панорама правой кнопкой и жесты касания
307
- useEffect(() => {
308
- const wrap = wrapRef.current;
309
- if (!wrap || !unlocked) return;
310
- const scale = () => {
311
- const r = wrap.getBoundingClientRect();
312
- return { kx: W / (r.width || 1), ky: H / (r.height || 1), r };
313
- };
314
- const toScene = (cx: number, cy: number) => {
315
- const { kx, ky, r } = scale();
316
- return { x: (cx - r.left) * kx, y: (cy - r.top) * ky };
317
- };
318
-
319
- const onWheel = (e: WheelEvent) => {
320
- const canvas = fcRef.current;
321
- if (!canvas) return;
322
- e.preventDefault();
323
- const dy = e.deltaY * (e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? 100 : 1);
324
- const p = toScene(e.clientX, e.clientY);
325
- applyZoom(canvas.getZoom() * Math.exp(-dy * 0.0015), p.x, p.y);
326
- };
327
-
328
- // правая кнопка мыши — перетаскивание холста
329
- const onDown = (e: PointerEvent) => {
330
- if (e.button !== 2 && e.button !== 1) return;
331
- e.preventDefault();
332
- e.stopPropagation();
333
- drag.current = { x: e.clientX, y: e.clientY };
334
- setPanning(true);
335
- wrap.setPointerCapture(e.pointerId);
336
- };
337
- const onPointerMove = (e: PointerEvent) => {
338
- if (!drag.current) return;
339
- e.preventDefault();
340
- const { kx, ky } = scale();
341
- panBy((e.clientX - drag.current.x) * kx, (e.clientY - drag.current.y) * ky);
342
- drag.current = { x: e.clientX, y: e.clientY };
343
- };
344
- const onUp = () => {
345
- drag.current = null;
346
- setPanning(false);
347
- };
348
- const onContext = (e: MouseEvent) => e.preventDefault();
349
-
350
- const dist = (t: TouchList) => Math.hypot(t[0]!.clientX - t[1]!.clientX, t[0]!.clientY - t[1]!.clientY);
351
- const mid = (t: TouchList) => ({
352
- x: (t[0]!.clientX + t[1]!.clientX) / 2,
353
- y: (t[0]!.clientY + t[1]!.clientY) / 2,
354
- });
355
- const onStart = (e: TouchEvent) => {
356
- if (e.touches.length === 2) {
357
- const m = mid(e.touches);
358
- const c = toScene(m.x, m.y);
359
- pinch.current = { dist: dist(e.touches), x: c.x, y: c.y };
360
- drag.current = { x: m.x, y: m.y };
361
- }
362
- };
363
- const onTouchMove = (e: TouchEvent) => {
364
- const canvas = fcRef.current;
365
- if (!canvas || e.touches.length !== 2 || !pinch.current || !drag.current) return;
366
- e.preventDefault();
367
- const d = dist(e.touches);
368
- const m = mid(e.touches);
369
- const c = toScene(m.x, m.y);
370
- const { kx, ky } = scale();
371
- applyZoom(canvas.getZoom() * (d / pinch.current.dist), c.x, c.y);
372
- panBy((m.x - drag.current.x) * kx, (m.y - drag.current.y) * ky);
373
- pinch.current = { dist: d, x: c.x, y: c.y };
374
- drag.current = { x: m.x, y: m.y };
375
- };
376
- const onEnd = () => {
377
- pinch.current = null;
378
- drag.current = null;
379
- };
380
-
381
- wrap.addEventListener("wheel", onWheel, { passive: false });
382
- wrap.addEventListener("pointerdown", onDown, true);
383
- wrap.addEventListener("pointermove", onPointerMove, true);
384
- wrap.addEventListener("pointerup", onUp, true);
385
- wrap.addEventListener("pointercancel", onUp, true);
386
- wrap.addEventListener("contextmenu", onContext);
387
- wrap.addEventListener("touchstart", onStart, { passive: false });
388
- wrap.addEventListener("touchmove", onTouchMove, { passive: false });
389
- wrap.addEventListener("touchend", onEnd);
390
- return () => {
391
- wrap.removeEventListener("wheel", onWheel);
392
- wrap.removeEventListener("pointerdown", onDown, true);
393
- wrap.removeEventListener("pointermove", onPointerMove, true);
394
- wrap.removeEventListener("pointerup", onUp, true);
395
- wrap.removeEventListener("pointercancel", onUp, true);
396
- wrap.removeEventListener("contextmenu", onContext);
397
- wrap.removeEventListener("touchstart", onStart);
398
- wrap.removeEventListener("touchmove", onTouchMove);
399
- wrap.removeEventListener("touchend", onEnd);
400
- };
401
- }, [unlocked, applyZoom, panBy]);
402
-
403
-
404
- const addElement = (def: ElDef) => {
405
- const canvas = fcRef.current;
406
- if (!canvas) return;
407
- const cx = W / 2;
408
- const cy = H / 2;
409
- const axes = def.kind === "text" ? 1 : symmetry;
410
- if (axes <= 1) {
411
- const obj = buildElement(def, thread, width);
412
- obj.set({ left: cx, top: cy });
413
- canvas.add(obj);
414
- canvas.setActiveObject(obj);
415
- } else {
416
- const dist = 160;
417
- for (let i = 0; i < axes; i++) {
418
- const a = (i / axes) * Math.PI * 2;
419
- const obj = buildElement(def, thread, width);
420
- obj.set({
421
- left: Math.round((cx + Math.cos(a) * dist) / snap) * snap,
422
- top: Math.round((cy + Math.sin(a) * dist) / snap) * snap,
423
- angle: (a * 180) / Math.PI + 90,
424
- });
425
- canvas.add(obj);
426
- }
427
- }
428
- canvas.requestRenderAll();
429
- };
430
-
431
- const withActive = (fn: (obj: TattingObject, canvas: fabric.Canvas) => void) => {
432
- const canvas = fcRef.current;
433
- const a = canvas?.getActiveObject() as TattingObject | undefined;
434
- if (!canvas || !a) return;
435
- fn(a, canvas);
436
- canvas.requestRenderAll();
437
- };
438
-
439
- const duplicate = () =>
440
- withActive(async (a, canvas) => {
441
- const clone = (await a.clone(CUSTOM_PROPS as never)) as TattingObject;
442
- clone.set({ left: a.left + snap * 3, top: a.top + snap * 3 });
443
- canvas.add(clone);
444
- canvas.setActiveObject(clone);
445
- canvas.requestRenderAll();
446
- });
447
-
448
- const groupSelected = () => {
449
- const canvas = fcRef.current;
450
- const a = canvas?.getActiveObject();
451
- if (!canvas || !a || a.type !== "activeselection") return;
452
- const sel = a as fabric.ActiveSelection;
453
- const g = new fabric.Group(sel.removeAll()) as TattingObject;
454
- g.elName = "Группа элементов";
455
- g.knots = 0;
456
- canvas.add(g);
457
- canvas.setActiveObject(g);
458
- canvas.requestRenderAll();
459
- snapshot(canvas);
460
- };
461
-
462
- const ungroupSelected = () => {
463
- const canvas = fcRef.current;
464
- const a = canvas?.getActiveObject() as fabric.Group | undefined;
465
- if (!canvas || !a || a.type !== "group") return;
466
- const items = a.removeAll();
467
- canvas.remove(a);
468
- items.forEach((o) => canvas.add(o));
469
- canvas.discardActiveObject();
470
- canvas.requestRenderAll();
471
- snapshot(canvas);
472
- };
473
-
474
- const restore = (dir: -1 | 1) => {
475
- const canvas = fcRef.current;
476
- const h = history.current;
477
- const next = h.index + dir;
478
- if (!canvas || next < 0 || next >= h.stack.length) return;
479
- h.index = next;
480
- h.lock = true;
481
- void canvas.loadFromJSON(h.stack[next]!).then(() => {
482
- drawGrid(canvas, snap, theme, showGrid);
483
- canvas.requestRenderAll();
484
- h.lock = false;
485
- setCanUndo(h.index > 0);
486
- setCanRedo(h.index < h.stack.length - 1);
487
- recalc(canvas);
488
- });
489
- };
490
-
491
- useEffect(() => {
492
- const onKey = (e: KeyboardEvent) => {
493
- if (!(e.ctrlKey || e.metaKey)) return;
494
- if (e.key.toLowerCase() === "z") {
495
- e.preventDefault();
496
- restore(e.shiftKey ? 1 : -1);
497
- }
498
- if (e.key.toLowerCase() === "y") {
499
- e.preventDefault();
500
- restore(1);
501
- }
502
- };
503
- window.addEventListener("keydown", onKey);
504
- return () => window.removeEventListener("keydown", onKey);
505
- });
506
-
507
- const exportPng = () => {
508
- const canvas = fcRef.current;
509
- if (!canvas) return;
510
- const url = canvas.toDataURL({ format: "png", multiplier: 3 });
511
- const a = document.createElement("a");
512
- a.href = url;
513
- a.download = "frivolite-uzor.png";
514
- a.click();
515
- };
516
-
517
- const exportPdf = () => {
518
- const canvas = fcRef.current;
519
- if (!canvas) return;
520
- const url = canvas.toDataURL({ format: "png", multiplier: 3 });
521
- const pdf = new jsPDF({ orientation: "landscape", unit: "mm", format: "a4" });
522
- const ratio = Math.min((297 - 20) / W, (210 - 24) / H);
523
- pdf.setFontSize(12);
524
- pdf.text("Shema frivolite / RealKnot Studio", 10, 12);
525
- pdf.addImage(url, "PNG", 10, 16, W * ratio, H * ratio);
526
- pdf.save("frivolite-uzor.pdf");
527
- };
528
-
529
- const currentData = () => {
530
- const canvas = fcRef.current;
531
- if (!canvas) return null;
532
- const data = canvas.toObject(CUSTOM_PROPS) as { objects: { isGrid?: boolean }[] };
533
- data.objects = data.objects.filter((o) => !o.isGrid);
534
- return data;
535
- };
536
-
537
- const saveProjectFile = () => {
538
- const data = currentData();
539
- if (!data) return;
540
- const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
541
- const a = document.createElement("a");
542
- a.href = URL.createObjectURL(blob);
543
- a.download = "frivolite-proekt.json";
544
- a.click();
545
- URL.revokeObjectURL(a.href);
546
- };
547
-
548
- const saveToGallery = () => {
549
- const canvas = fcRef.current;
550
- const data = currentData();
551
- if (!canvas || !data) return;
552
- const title = window.prompt("Название проекта", `Схема ${new Date().toLocaleDateString("ru-RU")}`);
553
- if (!title) return;
554
- if (!user) return;
555
- const project: StoredProject = {
556
- id: newId(),
557
- ownerId: user.id,
558
- title: title.trim(),
559
- updatedAt: Date.now(),
560
- elements: stats.count,
561
- thumb: canvas.toDataURL({ format: "png", multiplier: 0.25 }),
562
- data,
563
- };
564
- upsertProject(project);
565
- setProjectsOpen(false);
566
- window.setTimeout(() => setProjectsOpen(true), 120);
567
- };
568
-
569
- const loadData = (payload: string | object) => {
570
- const canvas = fcRef.current;
571
- if (!canvas) return;
572
- history.current.lock = true;
573
- void canvas.loadFromJSON(payload as never).then(() => {
574
- drawGrid(canvas, snap, theme, showGrid);
575
- canvas.requestRenderAll();
576
- history.current.lock = false;
577
- snapshot(canvas);
578
- });
579
- };
580
-
581
- const openFile = (file: File) => void file.text().then((t) => loadData(t));
582
-
583
- const deleteSelected = () =>
584
- withActive((_a, c) => {
585
- c.getActiveObjects().forEach((o) => c.remove(o));
586
- c.discardActiveObject();
587
- c.requestRenderAll();
588
- });
589
-
590
- const clearCanvas = () => {
591
- const canvas = fcRef.current;
592
- if (!canvas) return;
593
- if (!window.confirm("Очистить холст? Все элементы схемы будут удалены.")) return;
594
- canvas.discardActiveObject();
595
- canvas
596
- .getObjects()
597
- .filter((o) => !(o as TattingObject).isGrid)
598
- .forEach((o) => canvas.remove(o));
599
- canvas.requestRenderAll();
600
- snapshot(canvas);
601
- };
602
-
603
- const meters = threadMeters(stats.pathPx);
604
-
605
- if (!unlocked || !user)
606
- return (
607
- <LoginDialog
608
- onSignedIn={(signed) => {
609
- setUser(signed);
610
- setUnlocked(true);
611
- }}
612
- />
613
- );
614
-
615
- return (
616
- <div className="flex h-[100dvh] flex-col overflow-hidden bg-background font-sans text-foreground">
617
- <input
618
- ref={fileRef}
619
- type="file"
620
- accept="application/json"
621
- className="hidden"
622
- onChange={(e) => {
623
- const f = e.target.files?.[0];
624
- if (f) openFile(f);
625
- e.target.value = "";
626
- }}
627
- />
628
-
629
- <header className="flex shrink-0 items-center gap-2 border-b border-border bg-card px-3 py-2">
630
- <div className="min-w-0 flex-1">
631
- <h1 className="truncate font-display text-xl leading-none lg:text-2xl">RealKnot Studio</h1>
632
- <p className="truncate text-[11px] text-muted-foreground">
633
- Редактор схем плетения · сетка {snap} px · правая кнопка — перемещение, колесо — масштаб
634
- </p>
635
- </div>
636
-
637
- <div className="hidden rounded-lg border border-border p-0.5 md:flex">
638
- {(Object.keys(THEME_CFG) as Theme[]).map((t) => (
639
- <button
640
- key={t}
641
- onClick={() => setTheme(t)}
642
- title={THEME_CFG[t].hint}
643
- className={`h-8 rounded-md px-2 text-xs font-medium transition-colors ${
644
- theme === t ? "bg-primary text-primary-foreground" : "text-muted-foreground"
645
- }`}
646
- >
647
- {THEME_CFG[t].label}
648
- </button>
649
- ))}
650
- </div>
651
-
652
- <button
653
- onClick={() => setProfileOpen(true)}
654
- title={`Профиль: ${user.username} · ${roleLabel(user.role)}`}
655
- className="hidden h-9 items-center gap-2 rounded-lg border border-border bg-secondary px-2.5 text-xs font-medium transition-colors hover:border-accent lg:flex"
656
- >
657
- {user.avatar ? (
658
- <img src={user.avatar} alt="" className="h-6 w-6 rounded-full object-cover" />
659
- ) : (
660
- <UserRound className="h-4 w-4 text-accent" />
661
- )}
662
- {user.displayName?.trim() || user.username}
663
- </button>
664
-
665
- <div className="flex items-center gap-1">
666
- <Button
667
- variant="outline"
668
- size="icon"
669
- className="h-9 w-9 lg:hidden"
670
- title="Профиль и мои работы"
671
- onClick={() => setProfileOpen(true)}
672
- >
673
- <UserRound className="h-4 w-4" />
674
- </Button>
675
- <Button
676
- variant="outline"
677
- size="icon"
678
- className="h-9 w-9"
679
- title="Написать: чего не хватает"
680
- onClick={() => setFeedbackOpen(true)}
681
- >
682
- <MessageSquarePlus className="h-4 w-4" />
683
- </Button>
684
- {user.role === "admin" && (
685
- <Button
686
- variant="outline"
687
- size="icon"
688
- className="h-9 w-9"
689
- title="Панель админа: пользователи и пожелания"
690
- onClick={() => setAdminOpen(true)}
691
- >
692
- <Users className="h-4 w-4" />
693
- </Button>
694
- )}
695
- <Button variant="outline" size="icon" className="h-9 w-9" title="Помощь" onClick={() => setHelpOpen(true)}>
696
- <HelpCircle className="h-4 w-4" />
697
- </Button>
698
- <Button
699
- variant="outline"
700
- size="icon"
701
- className="h-9 w-9"
702
- title="Мои проекты"
703
- onClick={() => setProjectsOpen(true)}
704
- >
705
- <FolderOpen className="h-4 w-4" />
706
- </Button>
707
- <Button variant="outline" size="icon" className="h-9 w-9" title="Скачать .json" onClick={saveProjectFile}>
708
- <Save className="h-4 w-4" />
709
- </Button>
710
- <Button variant="outline" size="icon" className="h-9 w-9" title="Экспорт в PDF A4" onClick={exportPdf}>
711
- <FileDown className="h-4 w-4" />
712
- </Button>
713
- <Button size="sm" className="h-9 gap-1.5" title="Экспорт изображения PNG" onClick={exportPng}>
714
- <Download className="h-4 w-4" /> PNG
715
- </Button>
716
- <Button
717
- variant="outline"
718
- size="icon"
719
- className="h-9 w-9"
720
- title="Выйти из аккаунта"
721
- onClick={() => {
722
- logout();
723
- setUser(null);
724
- setUnlocked(false);
725
- }}
726
- >
727
- <LogOut className="h-4 w-4" />
728
- </Button>
729
- </div>
730
- </header>
731
-
732
- <div className="flex min-h-0 flex-1 flex-col gap-2 overflow-y-auto p-2 lg:flex-row lg:overflow-hidden lg:gap-3 lg:p-3">
733
- {/* Библиотека */}
734
- <aside className="w-full shrink-0 overflow-y-auto rounded-xl border border-border bg-card p-3 lg:w-56 xl:w-64">
735
- <h2 className="font-display text-base">Библиотека элементов</h2>
736
-
737
- <div className="mt-2 flex flex-wrap gap-1">
738
- {CATEGORIES.map((c) => (
739
- <button
740
- key={c}
741
- onClick={() => setCategory(c)}
742
- title={`Раздел «${c}»`}
743
- className={`h-8 rounded-md border px-2 text-xs font-medium transition-colors ${
744
- category === c ? "border-accent bg-secondary" : "border-border text-muted-foreground"
745
- }`}
746
- >
747
- {c}
748
- </button>
749
- ))}
750
- </div>
751
- <div className="mt-2 grid grid-cols-2 gap-1.5 lg:grid-cols-1">
752
- {LIBRARY.filter((e) => e.category === category).map((def) => (
753
- <button
754
- key={def.id}
755
- onClick={() => addElement(def)}
756
- title={`Добавить: ${def.name} · ${def.kind === "text" ? "подпись" : `${def.knots} узлов`}`}
757
- className="flex w-full items-center gap-2 rounded-lg border border-border bg-background p-1.5 text-left transition-colors hover:border-accent hover:bg-secondary active:scale-[0.98]"
758
- >
759
- <ElementIcon def={def} color={thread.value} />
760
- <span className="min-w-0">
761
- <span className="block truncate text-xs font-medium">{def.name}</span>
762
- <span className="block text-[10px] text-muted-foreground">
763
- {def.kind === "text" ? "подпись" : `${def.knots} узлов`}
764
- </span>
765
- </span>
766
- </button>
767
- ))}
768
- </div>
769
- </aside>
770
-
771
- {/* Холст */}
772
- <main className="flex min-h-0 min-w-0 flex-1 flex-col">
773
-
774
- <div className="flex shrink-0 flex-wrap items-center gap-1 rounded-xl border border-border bg-card p-1.5">
775
- <Button variant="outline" size="icon" className="h-9 w-9" title="Отменить (Ctrl+Z)" disabled={!canUndo} onClick={() => restore(-1)}>
776
- <Undo2 className="h-4 w-4" />
777
- </Button>
778
- <Button variant="outline" size="icon" className="h-9 w-9" title="Повторить (Ctrl+Shift+Z)" disabled={!canRedo} onClick={() => restore(1)}>
779
- <Redo2 className="h-4 w-4" />
780
- </Button>
781
- <Button variant="outline" size="icon" className="h-9 w-9" title="Дублировать выбранный элемент" onClick={duplicate}>
782
- <Copy className="h-4 w-4" />
783
- </Button>
784
- <Button variant="outline" size="icon" className="h-9 w-9" title="Сгруппировать выделенные элементы" onClick={groupSelected}>
785
- <Group className="h-4 w-4" />
786
- </Button>
787
- <Button variant="outline" size="icon" className="h-9 w-9" title="Разгруппировать элементы" onClick={ungroupSelected}>
788
- <Ungroup className="h-4 w-4" />
789
- </Button>
790
- <Button
791
- variant="outline"
792
- size="icon"
793
- className="h-9 w-9"
794
- title="Отразить по горизонтали"
795
- onClick={() => withActive((a) => a.set("flipX", !a.flipX))}
796
- >
797
- <FlipHorizontal className="h-4 w-4" />
798
- </Button>
799
- <Button
800
- variant="outline"
801
- size="icon"
802
- className="h-9 w-9"
803
- title="Отразить по вертикали"
804
- onClick={() => withActive((a) => a.set("flipY", !a.flipY))}
805
- >
806
- <FlipVertical className="h-4 w-4" />
807
- </Button>
808
- <Button
809
- variant="outline"
810
- size="icon"
811
- className="h-9 w-9"
812
- title="На передний план"
813
- onClick={() => withActive((a, c) => c.bringObjectToFront(a))}
814
- >
815
- <ArrowUpToLine className="h-4 w-4" />
816
- </Button>
817
- <Button
818
- variant="outline"
819
- size="icon"
820
- className="h-9 w-9"
821
- title="На задний план"
822
- onClick={() =>
823
- withActive((a, c) => {
824
- c.sendObjectToBack(a);
825
- c.getObjects()
826
- .filter((o) => (o as TattingObject).isGrid)
827
- .forEach((g) => c.sendObjectToBack(g));
828
- })
829
- }
830
- >
831
- <ArrowDownToLine className="h-4 w-4" />
832
- </Button>
833
-
834
- <span className="mx-1 hidden h-6 w-px bg-border sm:block" />
835
-
836
- <Button variant="outline" size="icon" className="h-9 w-9" title="Приблизить" onClick={() => applyZoom(zoom * 1.2)}>
837
- <ZoomIn className="h-4 w-4" />
838
- </Button>
839
- <Button variant="outline" size="icon" className="h-9 w-9" title="Отдалить" onClick={() => applyZoom(zoom / 1.2)}>
840
- <ZoomOut className="h-4 w-4" />
841
- </Button>
842
- <Button variant="outline" size="sm" className="h-9 gap-1.5" title="Вписать схему в экран" onClick={fitToScreen}>
843
- <Maximize className="h-4 w-4" />
844
- <span className="tabular-nums">{Math.round(zoom * 100)}%</span>
845
- </Button>
846
- <Button
847
- variant={showGrid ? "default" : "outline"}
848
- size="icon"
849
- className="h-9 w-9"
850
- title="Показать или скрыть сетку"
851
- onClick={() => setShowGrid((v) => !v)}
852
- >
853
- <Grid3x3 className="h-4 w-4" />
854
- </Button>
855
- <Button
856
- variant={showRulers ? "default" : "outline"}
857
- size="icon"
858
- className="h-9 w-9"
859
- title="Показать или скрыть линейки"
860
- onClick={() => setShowRulers((v) => !v)}
861
- >
862
- <Ruler className="h-4 w-4" />
863
- </Button>
864
- </div>
865
-
866
- <div className="tatting-frame relative mt-2 min-h-0 flex-1 rounded-xl border border-border bg-card p-1.5 shadow-sm">
867
- <div className={`tatting-box ${showRulers ? "with-rulers" : ""}`}>
868
- {showRulers && <RulerBar axis="x" zoom={zoom} pan={pan.x} />}
869
- <div className="flex min-h-0 min-w-0 gap-1">
870
- {showRulers && <RulerBar axis="y" zoom={zoom} pan={pan.y} />}
871
- <div
872
- ref={wrapRef}
873
- className={`tatting-wrap relative min-w-0 flex-1 overflow-hidden rounded-lg ${
874
- panning ? "cursor-grabbing" : ""
875
- }`}
876
- >
877
- <canvas ref={elRef} className="touch-none" />
878
- {booting && (
879
- <div className="absolute inset-0 animate-pulse backdrop-blur-md">
880
- <div className="h-full w-full bg-gradient-to-br from-secondary/70 via-card/60 to-secondary/70" />
881
- </div>
882
- )}
883
- </div>
884
- </div>
885
- </div>
886
-
887
- {/* мягкий индикатор автосохранения в углу */}
888
- <div
889
- aria-live="polite"
890
- className={`pointer-events-none absolute bottom-3 right-3 flex items-center gap-1.5 rounded-full border border-border bg-card/90 px-2.5 py-1 text-[11px] text-muted-foreground shadow-sm transition-all duration-300 ${
891
- saved ? "translate-y-0 opacity-100" : "translate-y-1 opacity-0"
892
- }`}
893
- >
894
- <Cloud className="h-3.5 w-3.5 animate-pulse text-accent" /> сохранено
895
- </div>
896
- </div>
897
-
898
-
899
-
900
- </main>
901
-
902
- {/* Свойства */}
903
- <aside className="w-full shrink-0 space-y-3 overflow-y-auto rounded-xl border border-border bg-card p-3 lg:w-60 xl:w-72">
904
- <div>
905
- <h2 className="font-display text-base">Сетка и симметрия</h2>
906
- <div className="mt-1 text-xs text-muted-foreground">Шаг привязки</div>
907
- <div className="mt-1 flex gap-1">
908
- {SNAP_STEPS.map((s) => (
909
- <button
910
- key={s}
911
- onClick={() => setSnap(s)}
912
- title={`Привязка к сетке ${s} px`}
913
- className={`h-8 flex-1 rounded-lg border text-xs font-medium ${
914
- snap === s ? "border-accent bg-secondary" : "border-border text-muted-foreground"
915
- }`}
916
- >
917
- {s} px
918
- </button>
919
- ))}
920
- </div>
921
- <div className="mt-2 text-xs text-muted-foreground">Круговая симметрия (осей)</div>
922
- <div className="mt-1 flex gap-1">
923
- {SYMMETRY_AXES.map((s) => (
924
- <button
925
- key={s}
926
- onClick={() => setSymmetry(s)}
927
- title={s === 1 ? "Без симметрии" : `Радиальная симметрия: ${s} осей`}
928
- className={`h-8 flex-1 rounded-lg border text-xs font-medium ${
929
- symmetry === s ? "border-accent bg-secondary" : "border-border text-muted-foreground"
930
- }`}
931
- >
932
- {s === 1 ? "нет" : s}
933
- </button>
934
- ))}
935
- </div>
936
- </div>
937
-
938
- <div className="border-t border-border pt-3">
939
- <h2 className="font-display text-base">Нити и цвет</h2>
940
- {(["Металлик", "Пастель", "Градиент"] as const).map((g) => (
941
- <div key={g} className="mt-2">
942
- <div className="text-[10px] uppercase tracking-wide text-muted-foreground">{g}</div>
943
- <div className="mt-1 flex flex-wrap gap-1.5">
944
- {THREADS.filter((t) => t.group === g).map((t) => (
945
- <button
946
- key={t.name}
947
- title={`Нить: ${t.name}`}
948
- aria-label={t.name}
949
- onClick={() => {
950
- setThread(t);
951
- withActive((a) => applyThread(a, t));
952
- setSelected((s) => (s ? { ...s, thread: t.name } : s));
953
- }}
954
- style={{ background: t.to ? `linear-gradient(135deg, ${t.value}, ${t.to})` : t.value }}
955
- className={`h-8 w-8 rounded-full border-2 transition-transform active:scale-90 ${
956
- thread.name === t.name ? "scale-110 border-accent" : "border-border"
957
- }`}
958
- />
959
- ))}
960
- </div>
961
- </div>
962
- ))}
963
- <div className="mt-3">
964
- <div className="mb-1.5 flex items-center justify-between text-xs">
965
- <span className="flex items-center gap-1.5">
966
- <Scissors className="h-3.5 w-3.5" /> Толщина нити
967
- </span>
968
- <span className="tabular-nums text-muted-foreground">{width.toFixed(1)}</span>
969
- </div>
970
- <Slider
971
- value={[width]}
972
- min={1}
973
- max={10}
974
- step={0.5}
975
- onValueChange={([v]) => {
976
- const nv = v ?? 3;
977
- setWidth(nv);
978
- withActive((a) => applyWidth(a, nv));
979
- }}
980
- />
981
- </div>
982
- </div>
983
-
984
- <div className="border-t border-border pt-3">
985
- <h2 className="font-display text-base">Выбранный элемент</h2>
986
- {selected ? (
987
- <dl className="mt-1.5 space-y-1 text-xs">
988
- <div className="flex justify-between gap-2">
989
- <dt className="text-muted-foreground">Название</dt>
990
- <dd className="text-right font-medium">{selected.name}</dd>
991
- </div>
992
- <div className="flex justify-between gap-2">
993
- <dt className="text-muted-foreground">Узлов</dt>
994
- <dd className="font-medium tabular-nums">{selected.knots}</dd>
995
- </div>
996
- <div className="flex justify-between gap-2">
997
- <dt className="text-muted-foreground">Нить</dt>
998
- <dd className="font-medium">{selected.thread}</dd>
999
- </div>
1000
- </dl>
1001
- ) : (
1002
- <p className="mt-1.5 text-xs text-muted-foreground">
1003
- Выберите элемент на холсте, чтобы изменить цвет, толщину и положение.
1004
- </p>
1005
- )}
1006
- <Button
1007
- variant="destructive"
1008
- size="sm"
1009
- className="mt-2 h-9 w-full gap-2"
1010
- title="Удалить выбранный элемент"
1011
- disabled={!selected}
1012
- onClick={deleteSelected}
1013
- >
1014
- <Trash2 className="h-4 w-4" /> Удалить элемент
1015
- </Button>
1016
- </div>
1017
- </aside>
1018
- </div>
1019
-
1020
- {/* Нижняя строка состояния */}
1021
- <footer className="flex shrink-0 flex-wrap items-center justify-between gap-2 border-t border-border bg-card px-3 py-1.5">
1022
- <p className="text-xs tabular-nums">
1023
- Узлов: <strong>{stats.knots}</strong> · расход <strong>≈ {meters.toFixed(2)} м</strong>{" "}
1024
- <span className="text-muted-foreground">(запас 15%)</span>
1025
- </p>
1026
-
1027
- <div className="flex items-center gap-2">
1028
- <Button
1029
- variant="destructive"
1030
- size="sm"
1031
- className="h-10 gap-2"
1032
- title="Удалить выбранный элемент"
1033
- disabled={!selected}
1034
- onClick={deleteSelected}
1035
- >
1036
- <Trash2 className="h-4 w-4" /> Удалить
1037
- </Button>
1038
- <Button
1039
- variant="outline"
1040
- size="sm"
1041
- className="h-10 gap-2 border-destructive/40 text-destructive"
1042
- title="Очистить холст полностью"
1043
- onClick={clearCanvas}
1044
- >
1045
- <Eraser className="h-4 w-4" /> Очистить холст
1046
- </Button>
1047
- </div>
1048
- </footer>
1049
-
1050
- <ProjectsDialog
1051
- open={projectsOpen}
1052
- onOpenChange={setProjectsOpen}
1053
- onOpenProject={(p) => {
1054
- loadData(p.data as object);
1055
- setProjectsOpen(false);
1056
- }}
1057
- onImportFile={() => fileRef.current?.click()}
1058
- onSaveCurrent={saveToGallery}
1059
- ownerId={user.id}
1060
- />
1061
-
1062
- <AdminPanel open={adminOpen} onOpenChange={setAdminOpen} />
1063
-
1064
- <ProfileDialog
1065
- open={profileOpen}
1066
- onOpenChange={setProfileOpen}
1067
- user={user}
1068
- onUserChange={setUser}
1069
- onOpenProject={(p) => {
1070
- loadData(p.data as object);
1071
- setProfileOpen(false);
1072
- }}
1073
- />
1074
-
1075
- <FeedbackDialog open={feedbackOpen} onOpenChange={setFeedbackOpen} user={user} />
1076
-
1077
- <Dialog open={helpOpen} onOpenChange={setHelpOpen}>
1078
- <DialogContent className="max-w-lg">
1079
- <DialogHeader>
1080
- <DialogTitle className="font-display text-2xl">Как пользоваться студией</DialogTitle>
1081
- <DialogDescription>Три коротких шага для создания схемы фриволите.</DialogDescription>
1082
- </DialogHeader>
1083
- <ol className="space-y-4 text-sm">
1084
- <li className="flex gap-3">
1085
- <MousePointer2 className="mt-0.5 h-6 w-6 shrink-0 text-primary" />
1086
- <span>
1087
- <strong className="block">1. Добавьте и перетащите</strong>
1088
- Нажмите на элемент в «Библиотеке» — он появится на холсте. Включите круговую симметрию, чтобы сразу
1089
- получить мандалу или салфетку.
1090
- </span>
1091
- </li>
1092
- <li className="flex gap-3">
1093
- <RotateCw className="mt-0.5 h-6 w-6 shrink-0 text-primary" />
1094
- <span>
1095
- <strong className="block">2. Настройте</strong>
1096
- Меняйте цвет и толщину нити, группируйте элементы, отражайте и меняйте порядок слоёв. Ctrl+Z отменяет
1097
- действие.
1098
- </span>
1099
- </li>
1100
- <li className="flex gap-3">
1101
- <Download className="mt-0.5 h-6 w-6 shrink-0 text-primary" />
1102
- <span>
1103
- <strong className="block">3. Сохраните</strong>
1104
- «Мои проекты» хранит схемы в браузере, PNG и PDF A4 — для печати, «Скачать .json» — файл проекта.
1105
- </span>
1106
- </li>
1107
- </ol>
1108
- </DialogContent>
1109
- </Dialog>
1110
- </div>
1111
- );
1112
- }
1113
-
1114
- function RulerBar({ axis, zoom, pan }: { axis: "x" | "y"; zoom: number; pan: number }) {
1115
- const total = axis === "x" ? W : H;
1116
- const step = zoom >= 2.5 ? 25 : zoom >= 1.4 ? 50 : zoom >= 0.7 ? 100 : 200;
1117
- const from = Math.floor(-pan / zoom / step) * step;
1118
- const to = from + Math.ceil(total / zoom / step) * step + step;
1119
- const ticks: number[] = [];
1120
- for (let t = from; t <= to; t += step) ticks.push(t);
1121
- const pos = (t: number) => ((t * zoom + pan) / total) * 100;
1122
-
1123
- if (axis === "x")
1124
- return (
1125
- <div className="relative mb-1 ml-7 h-5 shrink-0 overflow-hidden rounded-md bg-secondary text-[10px] text-muted-foreground">
1126
- {ticks.map((t) => (
1127
- <span
1128
- key={t}
1129
- className="absolute top-0 flex h-full items-center border-l border-border pl-1"
1130
- style={{ left: `${pos(t)}%` }}
1131
- >
1132
- {t}
1133
- </span>
1134
- ))}
1135
- </div>
1136
- );
1137
-
1138
- return (
1139
- <div className="relative w-6 shrink-0 overflow-hidden rounded-md bg-secondary text-[10px] text-muted-foreground">
1140
- {ticks.map((t) => (
1141
- <span
1142
- key={t}
1143
- className="absolute left-0 flex w-full justify-center border-t border-border pt-0.5"
1144
- style={{ top: `${pos(t)}%` }}
1145
- >
1146
- {t}
1147
- </span>
1148
- ))}
1149
- </div>
1150
- );
1151
- }
1152
-
1153
-
1154
- function ElementIcon({ def, color }: { def: ElDef; color: string }) {
1155
- const r = 30;
1156
- const picotR = 5;
1157
- const dots = Array.from({ length: def.picots }, (_, i) => {
1158
- const a = (i / Math.max(def.picots, 1)) * Math.PI * 2 - Math.PI / 2;
1159
- return { cx: Math.cos(a) * (r + 4), cy: Math.sin(a) * (r + 4) };
1160
- });
1161
- return (
1162
- <svg viewBox="-45 -45 90 90" className="h-8 w-8 shrink-0" aria-hidden>
1163
- <g fill="none" stroke={color} strokeWidth={3}>
1164
- {def.kind === "ring" && (
1165
- <>
1166
- <circle r={r} />
1167
- {dots.map((d, i) => (
1168
- <circle key={i} cx={d.cx} cy={d.cy} r={picotR} />
1169
- ))}
1170
- </>
1171
- )}
1172
- {(def.kind === "arc" || def.kind === "arc-curved") && (
1173
- <>
1174
- <path d="M -34 12 A 36 36 0 0 1 34 12" />
1175
- {Array.from({ length: def.picots }, (_, i) => {
1176
- const t = (i + 1) / (def.picots + 1);
1177
- const a = Math.PI - t * Math.PI;
1178
- return <circle key={i} cx={Math.cos(a) * 40} cy={-Math.sin(a) * 40 + 12} r={4} />;
1179
- })}
1180
- </>
1181
- )}
1182
- {def.kind === "picot" && (
1183
- <>
1184
- <path d="M -26 8 Q 0 -34 26 8" />
1185
- <circle cy={-14} r={7} />
1186
- </>
1187
- )}
1188
- {def.kind === "bead" && <circle r={16} fill={color} />}
1189
- {def.kind === "crystal" && <polygon points="0,-28 20,0 0,28 -20,0" fill={color} />}
1190
- {def.kind === "text" && (
1191
- <text x="0" y="10" textAnchor="middle" fontSize="34" fill={color} stroke="none">
1192
- А1
1193
- </text>
1194
- )}
1195
- </g>
1196
- </svg>
1197
- );
1198
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/accordion.tsx DELETED
@@ -1,51 +0,0 @@
1
- import * as React from "react";
2
- import * as AccordionPrimitive from "@radix-ui/react-accordion";
3
- import { ChevronDown } from "lucide-react";
4
-
5
- import { cn } from "@/lib/utils";
6
-
7
- const Accordion = AccordionPrimitive.Root;
8
-
9
- const AccordionItem = React.forwardRef<
10
- React.ElementRef<typeof AccordionPrimitive.Item>,
11
- React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
12
- >(({ className, ...props }, ref) => (
13
- <AccordionPrimitive.Item ref={ref} className={cn("border-b", className)} {...props} />
14
- ));
15
- AccordionItem.displayName = "AccordionItem";
16
-
17
- const AccordionTrigger = React.forwardRef<
18
- React.ElementRef<typeof AccordionPrimitive.Trigger>,
19
- React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
20
- >(({ className, children, ...props }, ref) => (
21
- <AccordionPrimitive.Header className="flex">
22
- <AccordionPrimitive.Trigger
23
- ref={ref}
24
- className={cn(
25
- "flex flex-1 items-center justify-between py-4 text-sm font-medium cursor-pointer transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
26
- className,
27
- )}
28
- {...props}
29
- >
30
- {children}
31
- <ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
32
- </AccordionPrimitive.Trigger>
33
- </AccordionPrimitive.Header>
34
- ));
35
- AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
36
-
37
- const AccordionContent = React.forwardRef<
38
- React.ElementRef<typeof AccordionPrimitive.Content>,
39
- React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
40
- >(({ className, children, ...props }, ref) => (
41
- <AccordionPrimitive.Content
42
- ref={ref}
43
- className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
44
- {...props}
45
- >
46
- <div className={cn("pb-4 pt-0", className)}>{children}</div>
47
- </AccordionPrimitive.Content>
48
- ));
49
- AccordionContent.displayName = AccordionPrimitive.Content.displayName;
50
-
51
- export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/alert-dialog.tsx DELETED
@@ -1,115 +0,0 @@
1
- import * as React from "react";
2
- import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
3
-
4
- import { cn } from "@/lib/utils";
5
- import { buttonVariants } from "@/components/ui/button";
6
-
7
- const AlertDialog = AlertDialogPrimitive.Root;
8
-
9
- const AlertDialogTrigger = AlertDialogPrimitive.Trigger;
10
-
11
- const AlertDialogPortal = AlertDialogPrimitive.Portal;
12
-
13
- const AlertDialogOverlay = React.forwardRef<
14
- React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
15
- React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
16
- >(({ className, ...props }, ref) => (
17
- <AlertDialogPrimitive.Overlay
18
- className={cn(
19
- "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
20
- className,
21
- )}
22
- {...props}
23
- ref={ref}
24
- />
25
- ));
26
- AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName;
27
-
28
- const AlertDialogContent = React.forwardRef<
29
- React.ElementRef<typeof AlertDialogPrimitive.Content>,
30
- React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
31
- >(({ className, ...props }, ref) => (
32
- <AlertDialogPortal>
33
- <AlertDialogOverlay />
34
- <AlertDialogPrimitive.Content
35
- ref={ref}
36
- className={cn(
37
- "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 sm:rounded-lg",
38
- className,
39
- )}
40
- {...props}
41
- />
42
- </AlertDialogPortal>
43
- ));
44
- AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName;
45
-
46
- const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
47
- <div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
48
- );
49
- AlertDialogHeader.displayName = "AlertDialogHeader";
50
-
51
- const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
52
- <div
53
- className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
54
- {...props}
55
- />
56
- );
57
- AlertDialogFooter.displayName = "AlertDialogFooter";
58
-
59
- const AlertDialogTitle = React.forwardRef<
60
- React.ElementRef<typeof AlertDialogPrimitive.Title>,
61
- React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
62
- >(({ className, ...props }, ref) => (
63
- <AlertDialogPrimitive.Title
64
- ref={ref}
65
- className={cn("text-lg font-semibold", className)}
66
- {...props}
67
- />
68
- ));
69
- AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName;
70
-
71
- const AlertDialogDescription = React.forwardRef<
72
- React.ElementRef<typeof AlertDialogPrimitive.Description>,
73
- React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
74
- >(({ className, ...props }, ref) => (
75
- <AlertDialogPrimitive.Description
76
- ref={ref}
77
- className={cn("text-sm text-muted-foreground", className)}
78
- {...props}
79
- />
80
- ));
81
- AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName;
82
-
83
- const AlertDialogAction = React.forwardRef<
84
- React.ElementRef<typeof AlertDialogPrimitive.Action>,
85
- React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
86
- >(({ className, ...props }, ref) => (
87
- <AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
88
- ));
89
- AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName;
90
-
91
- const AlertDialogCancel = React.forwardRef<
92
- React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
93
- React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
94
- >(({ className, ...props }, ref) => (
95
- <AlertDialogPrimitive.Cancel
96
- ref={ref}
97
- className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
98
- {...props}
99
- />
100
- ));
101
- AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName;
102
-
103
- export {
104
- AlertDialog,
105
- AlertDialogPortal,
106
- AlertDialogOverlay,
107
- AlertDialogTrigger,
108
- AlertDialogContent,
109
- AlertDialogHeader,
110
- AlertDialogFooter,
111
- AlertDialogTitle,
112
- AlertDialogDescription,
113
- AlertDialogAction,
114
- AlertDialogCancel,
115
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/alert.tsx DELETED
@@ -1,49 +0,0 @@
1
- import * as React from "react";
2
- import { cva, type VariantProps } from "class-variance-authority";
3
-
4
- import { cn } from "@/lib/utils";
5
-
6
- const alertVariants = cva(
7
- "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
8
- {
9
- variants: {
10
- variant: {
11
- default: "bg-background text-foreground",
12
- destructive:
13
- "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
14
- },
15
- },
16
- defaultVariants: {
17
- variant: "default",
18
- },
19
- },
20
- );
21
-
22
- const Alert = React.forwardRef<
23
- HTMLDivElement,
24
- React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
25
- >(({ className, variant, ...props }, ref) => (
26
- <div ref={ref} role="alert" className={cn(alertVariants({ variant }), className)} {...props} />
27
- ));
28
- Alert.displayName = "Alert";
29
-
30
- const AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(
31
- ({ className, ...props }, ref) => (
32
- <h5
33
- ref={ref}
34
- className={cn("mb-1 font-medium leading-none tracking-tight", className)}
35
- {...props}
36
- />
37
- ),
38
- );
39
- AlertTitle.displayName = "AlertTitle";
40
-
41
- const AlertDescription = React.forwardRef<
42
- HTMLParagraphElement,
43
- React.HTMLAttributes<HTMLParagraphElement>
44
- >(({ className, ...props }, ref) => (
45
- <div ref={ref} className={cn("text-sm [&_p]:leading-relaxed", className)} {...props} />
46
- ));
47
- AlertDescription.displayName = "AlertDescription";
48
-
49
- export { Alert, AlertTitle, AlertDescription };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/aspect-ratio.tsx DELETED
@@ -1,5 +0,0 @@
1
- import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
2
-
3
- const AspectRatio = AspectRatioPrimitive.Root;
4
-
5
- export { AspectRatio };
 
 
 
 
 
 
components/ui/avatar.tsx DELETED
@@ -1,47 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import * as AvatarPrimitive from "@radix-ui/react-avatar";
5
-
6
- import { cn } from "@/lib/utils";
7
-
8
- const Avatar = React.forwardRef<
9
- React.ElementRef<typeof AvatarPrimitive.Root>,
10
- React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
11
- >(({ className, ...props }, ref) => (
12
- <AvatarPrimitive.Root
13
- ref={ref}
14
- className={cn("relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full", className)}
15
- {...props}
16
- />
17
- ));
18
- Avatar.displayName = AvatarPrimitive.Root.displayName;
19
-
20
- const AvatarImage = React.forwardRef<
21
- React.ElementRef<typeof AvatarPrimitive.Image>,
22
- React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
23
- >(({ className, ...props }, ref) => (
24
- <AvatarPrimitive.Image
25
- ref={ref}
26
- className={cn("aspect-square h-full w-full", className)}
27
- {...props}
28
- />
29
- ));
30
- AvatarImage.displayName = AvatarPrimitive.Image.displayName;
31
-
32
- const AvatarFallback = React.forwardRef<
33
- React.ElementRef<typeof AvatarPrimitive.Fallback>,
34
- React.ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
35
- >(({ className, ...props }, ref) => (
36
- <AvatarPrimitive.Fallback
37
- ref={ref}
38
- className={cn(
39
- "flex h-full w-full items-center justify-center rounded-full bg-muted",
40
- className,
41
- )}
42
- {...props}
43
- />
44
- ));
45
- AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
46
-
47
- export { Avatar, AvatarImage, AvatarFallback };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/badge.tsx DELETED
@@ -1,32 +0,0 @@
1
- import * as React from "react";
2
- import { cva, type VariantProps } from "class-variance-authority";
3
-
4
- import { cn } from "@/lib/utils";
5
-
6
- const badgeVariants = cva(
7
- "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8
- {
9
- variants: {
10
- variant: {
11
- default: "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
12
- secondary:
13
- "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
14
- destructive:
15
- "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
16
- outline: "text-foreground",
17
- },
18
- },
19
- defaultVariants: {
20
- variant: "default",
21
- },
22
- },
23
- );
24
-
25
- export interface BadgeProps
26
- extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
27
-
28
- function Badge({ className, variant, ...props }: BadgeProps) {
29
- return <div className={cn(badgeVariants({ variant }), className)} {...props} />;
30
- }
31
-
32
- export { Badge, badgeVariants };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/breadcrumb.tsx DELETED
@@ -1,101 +0,0 @@
1
- import * as React from "react";
2
- import { Slot } from "@radix-ui/react-slot";
3
- import { ChevronRight, MoreHorizontal } from "lucide-react";
4
-
5
- import { cn } from "@/lib/utils";
6
-
7
- const Breadcrumb = React.forwardRef<
8
- HTMLElement,
9
- React.ComponentPropsWithoutRef<"nav"> & {
10
- separator?: React.ReactNode;
11
- }
12
- >(({ ...props }, ref) => <nav ref={ref} aria-label="breadcrumb" {...props} />);
13
- Breadcrumb.displayName = "Breadcrumb";
14
-
15
- const BreadcrumbList = React.forwardRef<HTMLOListElement, React.ComponentPropsWithoutRef<"ol">>(
16
- ({ className, ...props }, ref) => (
17
- <ol
18
- ref={ref}
19
- className={cn(
20
- "flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
21
- className,
22
- )}
23
- {...props}
24
- />
25
- ),
26
- );
27
- BreadcrumbList.displayName = "BreadcrumbList";
28
-
29
- const BreadcrumbItem = React.forwardRef<HTMLLIElement, React.ComponentPropsWithoutRef<"li">>(
30
- ({ className, ...props }, ref) => (
31
- <li ref={ref} className={cn("inline-flex items-center gap-1.5", className)} {...props} />
32
- ),
33
- );
34
- BreadcrumbItem.displayName = "BreadcrumbItem";
35
-
36
- const BreadcrumbLink = React.forwardRef<
37
- HTMLAnchorElement,
38
- React.ComponentPropsWithoutRef<"a"> & {
39
- asChild?: boolean;
40
- }
41
- >(({ asChild, className, ...props }, ref) => {
42
- const Comp = asChild ? Slot : "a";
43
-
44
- return (
45
- <Comp
46
- ref={ref}
47
- className={cn("transition-colors hover:text-foreground", className)}
48
- {...props}
49
- />
50
- );
51
- });
52
- BreadcrumbLink.displayName = "BreadcrumbLink";
53
-
54
- const BreadcrumbPage = React.forwardRef<HTMLSpanElement, React.ComponentPropsWithoutRef<"span">>(
55
- ({ className, ...props }, ref) => (
56
- <span
57
- ref={ref}
58
- role="link"
59
- aria-disabled="true"
60
- aria-current="page"
61
- className={cn("font-normal text-foreground", className)}
62
- {...props}
63
- />
64
- ),
65
- );
66
- BreadcrumbPage.displayName = "BreadcrumbPage";
67
-
68
- const BreadcrumbSeparator = ({ children, className, ...props }: React.ComponentProps<"li">) => (
69
- <li
70
- role="presentation"
71
- aria-hidden="true"
72
- className={cn("[&>svg]:w-3.5 [&>svg]:h-3.5", className)}
73
- {...props}
74
- >
75
- {children ?? <ChevronRight />}
76
- </li>
77
- );
78
- BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
79
-
80
- const BreadcrumbEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
81
- <span
82
- role="presentation"
83
- aria-hidden="true"
84
- className={cn("flex h-9 w-9 items-center justify-center", className)}
85
- {...props}
86
- >
87
- <MoreHorizontal className="h-4 w-4" />
88
- <span className="sr-only">More</span>
89
- </span>
90
- );
91
- BreadcrumbEllipsis.displayName = "BreadcrumbElipssis";
92
-
93
- export {
94
- Breadcrumb,
95
- BreadcrumbList,
96
- BreadcrumbItem,
97
- BreadcrumbLink,
98
- BreadcrumbPage,
99
- BreadcrumbSeparator,
100
- BreadcrumbEllipsis,
101
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/button.tsx DELETED
@@ -1,49 +0,0 @@
1
- import * as React from "react";
2
- import { Slot } from "@radix-ui/react-slot";
3
- import { cva, type VariantProps } from "class-variance-authority";
4
-
5
- import { cn } from "@/lib/utils";
6
-
7
- const buttonVariants = cva(
8
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium cursor-pointer transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9
- {
10
- variants: {
11
- variant: {
12
- default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
13
- destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
14
- outline:
15
- "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
16
- secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
17
- ghost: "hover:bg-accent hover:text-accent-foreground",
18
- link: "text-primary underline-offset-4 hover:underline",
19
- },
20
- size: {
21
- default: "h-9 px-4 py-2",
22
- sm: "h-8 rounded-md px-3 text-xs",
23
- lg: "h-10 rounded-md px-8",
24
- icon: "h-9 w-9",
25
- },
26
- },
27
- defaultVariants: {
28
- variant: "default",
29
- size: "default",
30
- },
31
- },
32
- );
33
-
34
- export interface ButtonProps
35
- extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
36
- asChild?: boolean;
37
- }
38
-
39
- const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
40
- ({ className, variant, size, asChild = false, ...props }, ref) => {
41
- const Comp = asChild ? Slot : "button";
42
- return (
43
- <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
44
- );
45
- },
46
- );
47
- Button.displayName = "Button";
48
-
49
- export { Button, buttonVariants };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/calendar.tsx DELETED
@@ -1,177 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
5
- import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker";
6
-
7
- import { cn } from "@/lib/utils";
8
- import { Button, buttonVariants } from "@/components/ui/button";
9
-
10
- function Calendar({
11
- className,
12
- classNames,
13
- showOutsideDays = true,
14
- captionLayout = "label",
15
- buttonVariant = "ghost",
16
- formatters,
17
- components,
18
- ...props
19
- }: React.ComponentProps<typeof DayPicker> & {
20
- buttonVariant?: React.ComponentProps<typeof Button>["variant"];
21
- }) {
22
- const defaultClassNames = getDefaultClassNames();
23
-
24
- return (
25
- <DayPicker
26
- showOutsideDays={showOutsideDays}
27
- className={cn(
28
- "bg-background group/calendar p-3 [--cell-size:2rem] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
29
- String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
30
- String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
31
- className,
32
- )}
33
- captionLayout={captionLayout}
34
- formatters={{
35
- formatMonthDropdown: (date) => date.toLocaleString("default", { month: "short" }),
36
- ...formatters,
37
- }}
38
- classNames={{
39
- root: cn("w-fit", defaultClassNames.root),
40
- months: cn("relative flex flex-col gap-4 md:flex-row", defaultClassNames.months),
41
- month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
42
- nav: cn(
43
- "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
44
- defaultClassNames.nav,
45
- ),
46
- button_previous: cn(
47
- buttonVariants({ variant: buttonVariant }),
48
- "h-(--cell-size) w-(--cell-size) select-none p-0 aria-disabled:opacity-50",
49
- defaultClassNames.button_previous,
50
- ),
51
- button_next: cn(
52
- buttonVariants({ variant: buttonVariant }),
53
- "h-(--cell-size) w-(--cell-size) select-none p-0 aria-disabled:opacity-50",
54
- defaultClassNames.button_next,
55
- ),
56
- month_caption: cn(
57
- "flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)",
58
- defaultClassNames.month_caption,
59
- ),
60
- dropdowns: cn(
61
- "flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium",
62
- defaultClassNames.dropdowns,
63
- ),
64
- dropdown_root: cn(
65
- "has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border",
66
- defaultClassNames.dropdown_root,
67
- ),
68
- dropdown: cn("bg-popover absolute inset-0 opacity-0", defaultClassNames.dropdown),
69
- caption_label: cn(
70
- "select-none font-medium",
71
- captionLayout === "label"
72
- ? "text-sm"
73
- : "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
74
- defaultClassNames.caption_label,
75
- ),
76
- table: "w-full border-collapse",
77
- weekdays: cn("flex", defaultClassNames.weekdays),
78
- weekday: cn(
79
- "text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
80
- defaultClassNames.weekday,
81
- ),
82
- week: cn("mt-2 flex w-full", defaultClassNames.week),
83
- week_number_header: cn("w-(--cell-size) select-none", defaultClassNames.week_number_header),
84
- week_number: cn(
85
- "text-muted-foreground select-none text-[0.8rem]",
86
- defaultClassNames.week_number,
87
- ),
88
- day: cn(
89
- "group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
90
- defaultClassNames.day,
91
- ),
92
- range_start: cn("bg-accent rounded-l-md", defaultClassNames.range_start),
93
- range_middle: cn("rounded-none", defaultClassNames.range_middle),
94
- range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end),
95
- today: cn(
96
- "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
97
- defaultClassNames.today,
98
- ),
99
- outside: cn(
100
- "text-muted-foreground aria-selected:text-muted-foreground",
101
- defaultClassNames.outside,
102
- ),
103
- disabled: cn("text-muted-foreground opacity-50", defaultClassNames.disabled),
104
- hidden: cn("invisible", defaultClassNames.hidden),
105
- ...classNames,
106
- }}
107
- components={{
108
- Root: ({ className, rootRef, ...props }) => {
109
- return <div data-slot="calendar" ref={rootRef} className={cn(className)} {...props} />;
110
- },
111
- Chevron: ({ className, orientation, ...props }) => {
112
- if (orientation === "left") {
113
- return <ChevronLeftIcon className={cn("size-4", className)} {...props} />;
114
- }
115
-
116
- if (orientation === "right") {
117
- return <ChevronRightIcon className={cn("size-4", className)} {...props} />;
118
- }
119
-
120
- return <ChevronDownIcon className={cn("size-4", className)} {...props} />;
121
- },
122
- DayButton: CalendarDayButton,
123
- WeekNumber: ({ children, ...props }) => {
124
- return (
125
- <td {...props}>
126
- <div className="flex size-(--cell-size) items-center justify-center text-center">
127
- {children}
128
- </div>
129
- </td>
130
- );
131
- },
132
- ...components,
133
- }}
134
- {...props}
135
- />
136
- );
137
- }
138
-
139
- function CalendarDayButton({
140
- className,
141
- day,
142
- modifiers,
143
- ...props
144
- }: React.ComponentProps<typeof DayButton>) {
145
- const defaultClassNames = getDefaultClassNames();
146
-
147
- const ref = React.useRef<HTMLButtonElement>(null);
148
- React.useEffect(() => {
149
- if (modifiers["focused"]) ref.current?.focus();
150
- }, [modifiers]);
151
-
152
- return (
153
- <Button
154
- ref={ref}
155
- variant="ghost"
156
- size="icon"
157
- data-day={day.date.toLocaleDateString()}
158
- data-selected-single={
159
- modifiers["selected"] &&
160
- !modifiers["range_start"] &&
161
- !modifiers["range_end"] &&
162
- !modifiers["range_middle"]
163
- }
164
- data-range-start={modifiers["range_start"]}
165
- data-range-end={modifiers["range_end"]}
166
- data-range-middle={modifiers["range_middle"]}
167
- className={cn(
168
- "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 flex aspect-square h-auto w-full min-w-(--cell-size) flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] [&>span]:text-xs [&>span]:opacity-70",
169
- defaultClassNames.day,
170
- className,
171
- )}
172
- {...props}
173
- />
174
- );
175
- }
176
-
177
- export { Calendar, CalendarDayButton };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/card.tsx DELETED
@@ -1,55 +0,0 @@
1
- import * as React from "react";
2
-
3
- import { cn } from "@/lib/utils";
4
-
5
- const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
6
- ({ className, ...props }, ref) => (
7
- <div
8
- ref={ref}
9
- className={cn("rounded-xl border bg-card text-card-foreground shadow", className)}
10
- {...props}
11
- />
12
- ),
13
- );
14
- Card.displayName = "Card";
15
-
16
- const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
17
- ({ className, ...props }, ref) => (
18
- <div ref={ref} className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
19
- ),
20
- );
21
- CardHeader.displayName = "CardHeader";
22
-
23
- const CardTitle = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
24
- ({ className, ...props }, ref) => (
25
- <div
26
- ref={ref}
27
- className={cn("font-semibold leading-none tracking-tight", className)}
28
- {...props}
29
- />
30
- ),
31
- );
32
- CardTitle.displayName = "CardTitle";
33
-
34
- const CardDescription = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
35
- ({ className, ...props }, ref) => (
36
- <div ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
37
- ),
38
- );
39
- CardDescription.displayName = "CardDescription";
40
-
41
- const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
42
- ({ className, ...props }, ref) => (
43
- <div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
44
- ),
45
- );
46
- CardContent.displayName = "CardContent";
47
-
48
- const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
49
- ({ className, ...props }, ref) => (
50
- <div ref={ref} className={cn("flex items-center p-6 pt-0", className)} {...props} />
51
- ),
52
- );
53
- CardFooter.displayName = "CardFooter";
54
-
55
- export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/carousel.tsx DELETED
@@ -1,240 +0,0 @@
1
- import * as React from "react";
2
- import useEmblaCarousel, { type UseEmblaCarouselType } from "embla-carousel-react";
3
- import { ArrowLeft, ArrowRight } from "lucide-react";
4
-
5
- import { cn } from "@/lib/utils";
6
- import { Button } from "@/components/ui/button";
7
-
8
- type CarouselApi = UseEmblaCarouselType[1];
9
- type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
10
- type CarouselOptions = UseCarouselParameters[0];
11
- type CarouselPlugin = UseCarouselParameters[1];
12
-
13
- type CarouselProps = {
14
- opts?: CarouselOptions;
15
- plugins?: CarouselPlugin;
16
- orientation?: "horizontal" | "vertical";
17
- setApi?: (api: CarouselApi) => void;
18
- };
19
-
20
- type CarouselContextProps = {
21
- carouselRef: ReturnType<typeof useEmblaCarousel>[0];
22
- api: ReturnType<typeof useEmblaCarousel>[1];
23
- scrollPrev: () => void;
24
- scrollNext: () => void;
25
- canScrollPrev: boolean;
26
- canScrollNext: boolean;
27
- } & CarouselProps;
28
-
29
- const CarouselContext = React.createContext<CarouselContextProps | null>(null);
30
-
31
- function useCarousel() {
32
- const context = React.useContext(CarouselContext);
33
-
34
- if (!context) {
35
- throw new Error("useCarousel must be used within a <Carousel />");
36
- }
37
-
38
- return context;
39
- }
40
-
41
- const Carousel = React.forwardRef<
42
- HTMLDivElement,
43
- React.HTMLAttributes<HTMLDivElement> & CarouselProps
44
- >(({ orientation = "horizontal", opts, setApi, plugins, className, children, ...props }, ref) => {
45
- const [carouselRef, api] = useEmblaCarousel(
46
- {
47
- ...opts,
48
- axis: orientation === "horizontal" ? "x" : "y",
49
- },
50
- plugins,
51
- );
52
- const [canScrollPrev, setCanScrollPrev] = React.useState(false);
53
- const [canScrollNext, setCanScrollNext] = React.useState(false);
54
-
55
- const onSelect = React.useCallback((api: CarouselApi) => {
56
- if (!api) {
57
- return;
58
- }
59
-
60
- setCanScrollPrev(api.canScrollPrev());
61
- setCanScrollNext(api.canScrollNext());
62
- }, []);
63
-
64
- const scrollPrev = React.useCallback(() => {
65
- api?.scrollPrev();
66
- }, [api]);
67
-
68
- const scrollNext = React.useCallback(() => {
69
- api?.scrollNext();
70
- }, [api]);
71
-
72
- const handleKeyDown = React.useCallback(
73
- (event: React.KeyboardEvent<HTMLDivElement>) => {
74
- if (event.key === "ArrowLeft") {
75
- event.preventDefault();
76
- scrollPrev();
77
- } else if (event.key === "ArrowRight") {
78
- event.preventDefault();
79
- scrollNext();
80
- }
81
- },
82
- [scrollPrev, scrollNext],
83
- );
84
-
85
- React.useEffect(() => {
86
- if (!api || !setApi) {
87
- return;
88
- }
89
-
90
- setApi(api);
91
- }, [api, setApi]);
92
-
93
- React.useEffect(() => {
94
- if (!api) {
95
- return;
96
- }
97
-
98
- onSelect(api);
99
- api.on("reInit", onSelect);
100
- api.on("select", onSelect);
101
-
102
- return () => {
103
- api?.off("select", onSelect);
104
- };
105
- }, [api, onSelect]);
106
-
107
- return (
108
- <CarouselContext.Provider
109
- value={{
110
- carouselRef,
111
- api: api,
112
- opts,
113
- orientation: orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
114
- scrollPrev,
115
- scrollNext,
116
- canScrollPrev,
117
- canScrollNext,
118
- }}
119
- >
120
- <div
121
- ref={ref}
122
- onKeyDownCapture={handleKeyDown}
123
- className={cn("relative", className)}
124
- role="region"
125
- aria-roledescription="carousel"
126
- {...props}
127
- >
128
- {children}
129
- </div>
130
- </CarouselContext.Provider>
131
- );
132
- });
133
- Carousel.displayName = "Carousel";
134
-
135
- const CarouselContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
136
- ({ className, ...props }, ref) => {
137
- const { carouselRef, orientation } = useCarousel();
138
-
139
- return (
140
- <div ref={carouselRef} className="overflow-hidden">
141
- <div
142
- ref={ref}
143
- className={cn(
144
- "flex",
145
- orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
146
- className,
147
- )}
148
- {...props}
149
- />
150
- </div>
151
- );
152
- },
153
- );
154
- CarouselContent.displayName = "CarouselContent";
155
-
156
- const CarouselItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
157
- ({ className, ...props }, ref) => {
158
- const { orientation } = useCarousel();
159
-
160
- return (
161
- <div
162
- ref={ref}
163
- role="group"
164
- aria-roledescription="slide"
165
- className={cn(
166
- "min-w-0 shrink-0 grow-0 basis-full",
167
- orientation === "horizontal" ? "pl-4" : "pt-4",
168
- className,
169
- )}
170
- {...props}
171
- />
172
- );
173
- },
174
- );
175
- CarouselItem.displayName = "CarouselItem";
176
-
177
- const CarouselPrevious = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
178
- ({ className, variant = "outline", size = "icon", ...props }, ref) => {
179
- const { orientation, scrollPrev, canScrollPrev } = useCarousel();
180
-
181
- return (
182
- <Button
183
- ref={ref}
184
- variant={variant}
185
- size={size}
186
- className={cn(
187
- "absolute h-8 w-8 rounded-full",
188
- orientation === "horizontal"
189
- ? "-left-12 top-1/2 -translate-y-1/2"
190
- : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
191
- className,
192
- )}
193
- disabled={!canScrollPrev}
194
- onClick={scrollPrev}
195
- {...props}
196
- >
197
- <ArrowLeft className="h-4 w-4" />
198
- <span className="sr-only">Previous slide</span>
199
- </Button>
200
- );
201
- },
202
- );
203
- CarouselPrevious.displayName = "CarouselPrevious";
204
-
205
- const CarouselNext = React.forwardRef<HTMLButtonElement, React.ComponentProps<typeof Button>>(
206
- ({ className, variant = "outline", size = "icon", ...props }, ref) => {
207
- const { orientation, scrollNext, canScrollNext } = useCarousel();
208
-
209
- return (
210
- <Button
211
- ref={ref}
212
- variant={variant}
213
- size={size}
214
- className={cn(
215
- "absolute h-8 w-8 rounded-full",
216
- orientation === "horizontal"
217
- ? "-right-12 top-1/2 -translate-y-1/2"
218
- : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
219
- className,
220
- )}
221
- disabled={!canScrollNext}
222
- onClick={scrollNext}
223
- {...props}
224
- >
225
- <ArrowRight className="h-4 w-4" />
226
- <span className="sr-only">Next slide</span>
227
- </Button>
228
- );
229
- },
230
- );
231
- CarouselNext.displayName = "CarouselNext";
232
-
233
- export {
234
- type CarouselApi,
235
- Carousel,
236
- CarouselContent,
237
- CarouselItem,
238
- CarouselPrevious,
239
- CarouselNext,
240
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/chart.tsx DELETED
@@ -1,331 +0,0 @@
1
- import * as React from "react";
2
- import * as RechartsPrimitive from "recharts";
3
-
4
- import { cn } from "@/lib/utils";
5
-
6
- // Format: { THEME_NAME: CSS_SELECTOR }
7
- const THEMES = { light: "", dark: ".dark" } as const;
8
-
9
- export type ChartConfig = {
10
- [k in string]: {
11
- label?: React.ReactNode;
12
- icon?: React.ComponentType;
13
- } & (
14
- | { color?: string; theme?: never }
15
- | { color?: never; theme: Record<keyof typeof THEMES, string> }
16
- );
17
- };
18
-
19
- type ChartContextProps = {
20
- config: ChartConfig;
21
- };
22
-
23
- const ChartContext = React.createContext<ChartContextProps | null>(null);
24
-
25
- function useChart() {
26
- const context = React.useContext(ChartContext);
27
-
28
- if (!context) {
29
- throw new Error("useChart must be used within a <ChartContainer />");
30
- }
31
-
32
- return context;
33
- }
34
-
35
- const ChartContainer = React.forwardRef<
36
- HTMLDivElement,
37
- React.ComponentProps<"div"> & {
38
- config: ChartConfig;
39
- children: React.ComponentProps<typeof RechartsPrimitive.ResponsiveContainer>["children"];
40
- }
41
- >(({ id, className, children, config, ...props }, ref) => {
42
- const uniqueId = React.useId();
43
- const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
44
-
45
- return (
46
- <ChartContext.Provider value={{ config }}>
47
- <div
48
- data-chart={chartId}
49
- ref={ref}
50
- className={cn(
51
- "flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
52
- className,
53
- )}
54
- {...props}
55
- >
56
- <ChartStyle id={chartId} config={config} />
57
- <RechartsPrimitive.ResponsiveContainer>{children}</RechartsPrimitive.ResponsiveContainer>
58
- </div>
59
- </ChartContext.Provider>
60
- );
61
- });
62
- ChartContainer.displayName = "Chart";
63
-
64
- const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
65
- const colorConfig = Object.entries(config).filter(([, config]) => config.theme || config.color);
66
-
67
- if (!colorConfig.length) {
68
- return null;
69
- }
70
-
71
- return (
72
- <style
73
- dangerouslySetInnerHTML={{
74
- __html: Object.entries(THEMES)
75
- .map(
76
- ([theme, prefix]) => `
77
- ${prefix} [data-chart=${id}] {
78
- ${colorConfig
79
- .map(([key, itemConfig]) => {
80
- const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color;
81
- return color ? ` --color-${key}: ${color};` : null;
82
- })
83
- .join("\n")}
84
- }
85
- `,
86
- )
87
- .join("\n"),
88
- }}
89
- />
90
- );
91
- };
92
-
93
- const ChartTooltip = RechartsPrimitive.Tooltip;
94
-
95
- const ChartTooltipContent = React.forwardRef<
96
- HTMLDivElement,
97
- React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
98
- React.ComponentProps<"div"> & {
99
- hideLabel?: boolean;
100
- hideIndicator?: boolean;
101
- indicator?: "line" | "dot" | "dashed";
102
- nameKey?: string;
103
- labelKey?: string;
104
- }
105
- >(
106
- (
107
- {
108
- active,
109
- payload,
110
- className,
111
- indicator = "dot",
112
- hideLabel = false,
113
- hideIndicator = false,
114
- label,
115
- labelFormatter,
116
- labelClassName,
117
- formatter,
118
- color,
119
- nameKey,
120
- labelKey,
121
- },
122
- ref,
123
- ) => {
124
- const { config } = useChart();
125
-
126
- const tooltipLabel = React.useMemo(() => {
127
- if (hideLabel || !payload?.length) {
128
- return null;
129
- }
130
-
131
- const [item] = payload;
132
- const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
133
- const itemConfig = getPayloadConfigFromPayload(config, item, key);
134
- const value =
135
- !labelKey && typeof label === "string"
136
- ? config[label as keyof typeof config]?.label || label
137
- : itemConfig?.label;
138
-
139
- if (labelFormatter) {
140
- return (
141
- <div className={cn("font-medium", labelClassName)}>{labelFormatter(value, payload)}</div>
142
- );
143
- }
144
-
145
- if (!value) {
146
- return null;
147
- }
148
-
149
- return <div className={cn("font-medium", labelClassName)}>{value}</div>;
150
- }, [label, labelFormatter, payload, hideLabel, labelClassName, config, labelKey]);
151
-
152
- if (!active || !payload?.length) {
153
- return null;
154
- }
155
-
156
- const nestLabel = payload.length === 1 && indicator !== "dot";
157
-
158
- return (
159
- <div
160
- ref={ref}
161
- className={cn(
162
- "grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
163
- className,
164
- )}
165
- >
166
- {!nestLabel ? tooltipLabel : null}
167
- <div className="grid gap-1.5">
168
- {payload
169
- .filter((item) => item.type !== "none")
170
- .map((item, index) => {
171
- const key = `${nameKey || item.name || item.dataKey || "value"}`;
172
- const itemConfig = getPayloadConfigFromPayload(config, item, key);
173
- const indicatorColor = color || item.payload.fill || item.color;
174
-
175
- return (
176
- <div
177
- key={item.dataKey}
178
- className={cn(
179
- "flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
180
- indicator === "dot" && "items-center",
181
- )}
182
- >
183
- {formatter && item?.value !== undefined && item.name ? (
184
- formatter(item.value, item.name, item, index, item.payload)
185
- ) : (
186
- <>
187
- {itemConfig?.icon ? (
188
- <itemConfig.icon />
189
- ) : (
190
- !hideIndicator && (
191
- <div
192
- className={cn(
193
- "shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
194
- {
195
- "h-2.5 w-2.5": indicator === "dot",
196
- "w-1": indicator === "line",
197
- "w-0 border-[1.5px] border-dashed bg-transparent":
198
- indicator === "dashed",
199
- "my-0.5": nestLabel && indicator === "dashed",
200
- },
201
- )}
202
- style={
203
- {
204
- "--color-bg": indicatorColor,
205
- "--color-border": indicatorColor,
206
- } as React.CSSProperties
207
- }
208
- />
209
- )
210
- )}
211
- <div
212
- className={cn(
213
- "flex flex-1 justify-between leading-none",
214
- nestLabel ? "items-end" : "items-center",
215
- )}
216
- >
217
- <div className="grid gap-1.5">
218
- {nestLabel ? tooltipLabel : null}
219
- <span className="text-muted-foreground">
220
- {itemConfig?.label || item.name}
221
- </span>
222
- </div>
223
- {item.value && (
224
- <span className="font-mono font-medium tabular-nums text-foreground">
225
- {item.value.toLocaleString()}
226
- </span>
227
- )}
228
- </div>
229
- </>
230
- )}
231
- </div>
232
- );
233
- })}
234
- </div>
235
- </div>
236
- );
237
- },
238
- );
239
- ChartTooltipContent.displayName = "ChartTooltip";
240
-
241
- const ChartLegend = RechartsPrimitive.Legend;
242
-
243
- const ChartLegendContent = React.forwardRef<
244
- HTMLDivElement,
245
- React.ComponentProps<"div"> &
246
- Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
247
- hideIcon?: boolean;
248
- nameKey?: string;
249
- }
250
- >(({ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey }, ref) => {
251
- const { config } = useChart();
252
-
253
- if (!payload?.length) {
254
- return null;
255
- }
256
-
257
- return (
258
- <div
259
- ref={ref}
260
- className={cn(
261
- "flex items-center justify-center gap-4",
262
- verticalAlign === "top" ? "pb-3" : "pt-3",
263
- className,
264
- )}
265
- >
266
- {payload
267
- .filter((item) => item.type !== "none")
268
- .map((item) => {
269
- const key = `${nameKey || item.dataKey || "value"}`;
270
- const itemConfig = getPayloadConfigFromPayload(config, item, key);
271
-
272
- return (
273
- <div
274
- key={item.value}
275
- className={cn(
276
- "flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground",
277
- )}
278
- >
279
- {itemConfig?.icon && !hideIcon ? (
280
- <itemConfig.icon />
281
- ) : (
282
- <div
283
- className="h-2 w-2 shrink-0 rounded-[2px]"
284
- style={{
285
- backgroundColor: item.color,
286
- }}
287
- />
288
- )}
289
- {itemConfig?.label}
290
- </div>
291
- );
292
- })}
293
- </div>
294
- );
295
- });
296
- ChartLegendContent.displayName = "ChartLegend";
297
-
298
- // Helper to extract item config from a payload.
299
- function getPayloadConfigFromPayload(config: ChartConfig, payload: unknown, key: string) {
300
- if (typeof payload !== "object" || payload === null) {
301
- return undefined;
302
- }
303
-
304
- const payloadPayload =
305
- "payload" in payload && typeof payload.payload === "object" && payload.payload !== null
306
- ? payload.payload
307
- : undefined;
308
-
309
- let configLabelKey: string = key;
310
-
311
- if (key in payload && typeof payload[key as keyof typeof payload] === "string") {
312
- configLabelKey = payload[key as keyof typeof payload] as string;
313
- } else if (
314
- payloadPayload &&
315
- key in payloadPayload &&
316
- typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
317
- ) {
318
- configLabelKey = payloadPayload[key as keyof typeof payloadPayload] as string;
319
- }
320
-
321
- return configLabelKey in config ? config[configLabelKey] : config[key as keyof typeof config];
322
- }
323
-
324
- export {
325
- ChartContainer,
326
- ChartTooltip,
327
- ChartTooltipContent,
328
- ChartLegend,
329
- ChartLegendContent,
330
- ChartStyle,
331
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/checkbox.tsx DELETED
@@ -1,26 +0,0 @@
1
- import * as React from "react";
2
- import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
3
- import { Check } from "lucide-react";
4
-
5
- import { cn } from "@/lib/utils";
6
-
7
- const Checkbox = React.forwardRef<
8
- React.ElementRef<typeof CheckboxPrimitive.Root>,
9
- React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
10
- >(({ className, ...props }, ref) => (
11
- <CheckboxPrimitive.Root
12
- ref={ref}
13
- className={cn(
14
- "grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
15
- className,
16
- )}
17
- {...props}
18
- >
19
- <CheckboxPrimitive.Indicator className={cn("grid place-content-center text-current")}>
20
- <Check className="h-4 w-4" />
21
- </CheckboxPrimitive.Indicator>
22
- </CheckboxPrimitive.Root>
23
- ));
24
- Checkbox.displayName = CheckboxPrimitive.Root.displayName;
25
-
26
- export { Checkbox };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/collapsible.tsx DELETED
@@ -1,11 +0,0 @@
1
- "use client";
2
-
3
- import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
4
-
5
- const Collapsible = CollapsiblePrimitive.Root;
6
-
7
- const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
8
-
9
- const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
10
-
11
- export { Collapsible, CollapsibleTrigger, CollapsibleContent };
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/command.tsx DELETED
@@ -1,143 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import { type DialogProps } from "@radix-ui/react-dialog";
5
- import { Command as CommandPrimitive } from "cmdk";
6
- import { Search } from "lucide-react";
7
-
8
- import { cn } from "@/lib/utils";
9
- import { Dialog, DialogContent } from "@/components/ui/dialog";
10
-
11
- const Command = React.forwardRef<
12
- React.ElementRef<typeof CommandPrimitive>,
13
- React.ComponentPropsWithoutRef<typeof CommandPrimitive>
14
- >(({ className, ...props }, ref) => (
15
- <CommandPrimitive
16
- ref={ref}
17
- className={cn(
18
- "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
19
- className,
20
- )}
21
- {...props}
22
- />
23
- ));
24
- Command.displayName = CommandPrimitive.displayName;
25
-
26
- const CommandDialog = ({ children, ...props }: DialogProps) => {
27
- return (
28
- <Dialog {...props}>
29
- <DialogContent className="overflow-hidden p-0">
30
- <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
31
- {children}
32
- </Command>
33
- </DialogContent>
34
- </Dialog>
35
- );
36
- };
37
-
38
- const CommandInput = React.forwardRef<
39
- React.ElementRef<typeof CommandPrimitive.Input>,
40
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
41
- >(({ className, ...props }, ref) => (
42
- <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
43
- <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
44
- <CommandPrimitive.Input
45
- ref={ref}
46
- className={cn(
47
- "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
48
- className,
49
- )}
50
- {...props}
51
- />
52
- </div>
53
- ));
54
-
55
- CommandInput.displayName = CommandPrimitive.Input.displayName;
56
-
57
- const CommandList = React.forwardRef<
58
- React.ElementRef<typeof CommandPrimitive.List>,
59
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
60
- >(({ className, ...props }, ref) => (
61
- <CommandPrimitive.List
62
- ref={ref}
63
- className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
64
- {...props}
65
- />
66
- ));
67
-
68
- CommandList.displayName = CommandPrimitive.List.displayName;
69
-
70
- const CommandEmpty = React.forwardRef<
71
- React.ElementRef<typeof CommandPrimitive.Empty>,
72
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
73
- >((props, ref) => (
74
- <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />
75
- ));
76
-
77
- CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
78
-
79
- const CommandGroup = React.forwardRef<
80
- React.ElementRef<typeof CommandPrimitive.Group>,
81
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
82
- >(({ className, ...props }, ref) => (
83
- <CommandPrimitive.Group
84
- ref={ref}
85
- className={cn(
86
- "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
87
- className,
88
- )}
89
- {...props}
90
- />
91
- ));
92
-
93
- CommandGroup.displayName = CommandPrimitive.Group.displayName;
94
-
95
- const CommandSeparator = React.forwardRef<
96
- React.ElementRef<typeof CommandPrimitive.Separator>,
97
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
98
- >(({ className, ...props }, ref) => (
99
- <CommandPrimitive.Separator
100
- ref={ref}
101
- className={cn("-mx-1 h-px bg-border", className)}
102
- {...props}
103
- />
104
- ));
105
- CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
106
-
107
- const CommandItem = React.forwardRef<
108
- React.ElementRef<typeof CommandPrimitive.Item>,
109
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
110
- >(({ className, ...props }, ref) => (
111
- <CommandPrimitive.Item
112
- ref={ref}
113
- className={cn(
114
- "relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
115
- className,
116
- )}
117
- {...props}
118
- />
119
- ));
120
-
121
- CommandItem.displayName = CommandPrimitive.Item.displayName;
122
-
123
- const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
124
- return (
125
- <span
126
- className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
127
- {...props}
128
- />
129
- );
130
- };
131
- CommandShortcut.displayName = "CommandShortcut";
132
-
133
- export {
134
- Command,
135
- CommandDialog,
136
- CommandInput,
137
- CommandList,
138
- CommandEmpty,
139
- CommandGroup,
140
- CommandItem,
141
- CommandShortcut,
142
- CommandSeparator,
143
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/context-menu.tsx DELETED
@@ -1,186 +0,0 @@
1
- import * as React from "react";
2
- import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
3
- import { Check, ChevronRight, Circle } from "lucide-react";
4
-
5
- import { cn } from "@/lib/utils";
6
-
7
- const ContextMenu = ContextMenuPrimitive.Root;
8
-
9
- const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
10
-
11
- const ContextMenuGroup = ContextMenuPrimitive.Group;
12
-
13
- const ContextMenuPortal = ContextMenuPrimitive.Portal;
14
-
15
- const ContextMenuSub = ContextMenuPrimitive.Sub;
16
-
17
- const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
18
-
19
- const ContextMenuSubTrigger = React.forwardRef<
20
- React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
21
- React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
22
- inset?: boolean;
23
- }
24
- >(({ className, inset, children, ...props }, ref) => (
25
- <ContextMenuPrimitive.SubTrigger
26
- ref={ref}
27
- className={cn(
28
- "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
29
- inset && "pl-8",
30
- className,
31
- )}
32
- {...props}
33
- >
34
- {children}
35
- <ChevronRight className="ml-auto h-4 w-4" />
36
- </ContextMenuPrimitive.SubTrigger>
37
- ));
38
- ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
39
-
40
- const ContextMenuSubContent = React.forwardRef<
41
- React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
42
- React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
43
- >(({ className, ...props }, ref) => (
44
- <ContextMenuPrimitive.SubContent
45
- ref={ref}
46
- className={cn(
47
- "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-context-menu-content-transform-origin)",
48
- className,
49
- )}
50
- {...props}
51
- />
52
- ));
53
- ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
54
-
55
- const ContextMenuContent = React.forwardRef<
56
- React.ElementRef<typeof ContextMenuPrimitive.Content>,
57
- React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
58
- >(({ className, ...props }, ref) => (
59
- <ContextMenuPrimitive.Portal>
60
- <ContextMenuPrimitive.Content
61
- ref={ref}
62
- className={cn(
63
- "z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-context-menu-content-transform-origin)",
64
- className,
65
- )}
66
- {...props}
67
- />
68
- </ContextMenuPrimitive.Portal>
69
- ));
70
- ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
71
-
72
- const ContextMenuItem = React.forwardRef<
73
- React.ElementRef<typeof ContextMenuPrimitive.Item>,
74
- React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
75
- inset?: boolean;
76
- }
77
- >(({ className, inset, ...props }, ref) => (
78
- <ContextMenuPrimitive.Item
79
- ref={ref}
80
- className={cn(
81
- "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
82
- inset && "pl-8",
83
- className,
84
- )}
85
- {...props}
86
- />
87
- ));
88
- ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
89
-
90
- const ContextMenuCheckboxItem = React.forwardRef<
91
- React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
92
- React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
93
- >(({ className, children, ...props }, ref) => (
94
- <ContextMenuPrimitive.CheckboxItem
95
- ref={ref}
96
- className={cn(
97
- "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
98
- className,
99
- )}
100
- {...props}
101
- >
102
- <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
103
- <ContextMenuPrimitive.ItemIndicator>
104
- <Check className="h-4 w-4" />
105
- </ContextMenuPrimitive.ItemIndicator>
106
- </span>
107
- {children}
108
- </ContextMenuPrimitive.CheckboxItem>
109
- ));
110
- ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName;
111
-
112
- const ContextMenuRadioItem = React.forwardRef<
113
- React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
114
- React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
115
- >(({ className, children, ...props }, ref) => (
116
- <ContextMenuPrimitive.RadioItem
117
- ref={ref}
118
- className={cn(
119
- "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
120
- className,
121
- )}
122
- {...props}
123
- >
124
- <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
125
- <ContextMenuPrimitive.ItemIndicator>
126
- <Circle className="h-4 w-4 fill-current" />
127
- </ContextMenuPrimitive.ItemIndicator>
128
- </span>
129
- {children}
130
- </ContextMenuPrimitive.RadioItem>
131
- ));
132
- ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
133
-
134
- const ContextMenuLabel = React.forwardRef<
135
- React.ElementRef<typeof ContextMenuPrimitive.Label>,
136
- React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
137
- inset?: boolean;
138
- }
139
- >(({ className, inset, ...props }, ref) => (
140
- <ContextMenuPrimitive.Label
141
- ref={ref}
142
- className={cn("px-2 py-1.5 text-sm font-semibold text-foreground", inset && "pl-8", className)}
143
- {...props}
144
- />
145
- ));
146
- ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
147
-
148
- const ContextMenuSeparator = React.forwardRef<
149
- React.ElementRef<typeof ContextMenuPrimitive.Separator>,
150
- React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
151
- >(({ className, ...props }, ref) => (
152
- <ContextMenuPrimitive.Separator
153
- ref={ref}
154
- className={cn("-mx-1 my-1 h-px bg-border", className)}
155
- {...props}
156
- />
157
- ));
158
- ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
159
-
160
- const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
161
- return (
162
- <span
163
- className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
164
- {...props}
165
- />
166
- );
167
- };
168
- ContextMenuShortcut.displayName = "ContextMenuShortcut";
169
-
170
- export {
171
- ContextMenu,
172
- ContextMenuTrigger,
173
- ContextMenuContent,
174
- ContextMenuItem,
175
- ContextMenuCheckboxItem,
176
- ContextMenuRadioItem,
177
- ContextMenuLabel,
178
- ContextMenuSeparator,
179
- ContextMenuShortcut,
180
- ContextMenuGroup,
181
- ContextMenuPortal,
182
- ContextMenuSub,
183
- ContextMenuSubContent,
184
- ContextMenuSubTrigger,
185
- ContextMenuRadioGroup,
186
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/dialog.tsx DELETED
@@ -1,104 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import * as DialogPrimitive from "@radix-ui/react-dialog";
5
- import { X } from "lucide-react";
6
-
7
- import { cn } from "@/lib/utils";
8
-
9
- const Dialog = DialogPrimitive.Root;
10
-
11
- const DialogTrigger = DialogPrimitive.Trigger;
12
-
13
- const DialogPortal = DialogPrimitive.Portal;
14
-
15
- const DialogClose = DialogPrimitive.Close;
16
-
17
- const DialogOverlay = React.forwardRef<
18
- React.ElementRef<typeof DialogPrimitive.Overlay>,
19
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
20
- >(({ className, ...props }, ref) => (
21
- <DialogPrimitive.Overlay
22
- ref={ref}
23
- className={cn(
24
- "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
25
- className,
26
- )}
27
- {...props}
28
- />
29
- ));
30
- DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
31
-
32
- const DialogContent = React.forwardRef<
33
- React.ElementRef<typeof DialogPrimitive.Content>,
34
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
35
- >(({ className, children, ...props }, ref) => (
36
- <DialogPortal>
37
- <DialogOverlay />
38
- <DialogPrimitive.Content
39
- ref={ref}
40
- className={cn(
41
- "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 sm:rounded-lg",
42
- className,
43
- )}
44
- {...props}
45
- >
46
- {children}
47
- <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background cursor-pointer transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
48
- <X className="h-4 w-4" />
49
- <span className="sr-only">Close</span>
50
- </DialogPrimitive.Close>
51
- </DialogPrimitive.Content>
52
- </DialogPortal>
53
- ));
54
- DialogContent.displayName = DialogPrimitive.Content.displayName;
55
-
56
- const DialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
57
- <div className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} {...props} />
58
- );
59
- DialogHeader.displayName = "DialogHeader";
60
-
61
- const DialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
62
- <div
63
- className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
64
- {...props}
65
- />
66
- );
67
- DialogFooter.displayName = "DialogFooter";
68
-
69
- const DialogTitle = React.forwardRef<
70
- React.ElementRef<typeof DialogPrimitive.Title>,
71
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
72
- >(({ className, ...props }, ref) => (
73
- <DialogPrimitive.Title
74
- ref={ref}
75
- className={cn("text-lg font-semibold leading-none tracking-tight", className)}
76
- {...props}
77
- />
78
- ));
79
- DialogTitle.displayName = DialogPrimitive.Title.displayName;
80
-
81
- const DialogDescription = React.forwardRef<
82
- React.ElementRef<typeof DialogPrimitive.Description>,
83
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
84
- >(({ className, ...props }, ref) => (
85
- <DialogPrimitive.Description
86
- ref={ref}
87
- className={cn("text-sm text-muted-foreground", className)}
88
- {...props}
89
- />
90
- ));
91
- DialogDescription.displayName = DialogPrimitive.Description.displayName;
92
-
93
- export {
94
- Dialog,
95
- DialogPortal,
96
- DialogOverlay,
97
- DialogTrigger,
98
- DialogClose,
99
- DialogContent,
100
- DialogHeader,
101
- DialogFooter,
102
- DialogTitle,
103
- DialogDescription,
104
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/drawer.tsx DELETED
@@ -1,98 +0,0 @@
1
- import * as React from "react";
2
- import { Drawer as DrawerPrimitive } from "vaul";
3
-
4
- import { cn } from "@/lib/utils";
5
-
6
- const Drawer = ({
7
- shouldScaleBackground = true,
8
- ...props
9
- }: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
10
- <DrawerPrimitive.Root shouldScaleBackground={shouldScaleBackground} {...props} />
11
- );
12
- Drawer.displayName = "Drawer";
13
-
14
- const DrawerTrigger = DrawerPrimitive.Trigger;
15
-
16
- const DrawerPortal = DrawerPrimitive.Portal;
17
-
18
- const DrawerClose = DrawerPrimitive.Close;
19
-
20
- const DrawerOverlay = React.forwardRef<
21
- React.ElementRef<typeof DrawerPrimitive.Overlay>,
22
- React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
23
- >(({ className, ...props }, ref) => (
24
- <DrawerPrimitive.Overlay
25
- ref={ref}
26
- className={cn("fixed inset-0 z-50 bg-black/80", className)}
27
- {...props}
28
- />
29
- ));
30
- DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
31
-
32
- const DrawerContent = React.forwardRef<
33
- React.ElementRef<typeof DrawerPrimitive.Content>,
34
- React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
35
- >(({ className, children, ...props }, ref) => (
36
- <DrawerPortal>
37
- <DrawerOverlay />
38
- <DrawerPrimitive.Content
39
- ref={ref}
40
- className={cn(
41
- "fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
42
- className,
43
- )}
44
- {...props}
45
- >
46
- <div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
47
- {children}
48
- </DrawerPrimitive.Content>
49
- </DrawerPortal>
50
- ));
51
- DrawerContent.displayName = "DrawerContent";
52
-
53
- const DrawerHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
54
- <div className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)} {...props} />
55
- );
56
- DrawerHeader.displayName = "DrawerHeader";
57
-
58
- const DrawerFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
59
- <div className={cn("mt-auto flex flex-col gap-2 p-4", className)} {...props} />
60
- );
61
- DrawerFooter.displayName = "DrawerFooter";
62
-
63
- const DrawerTitle = React.forwardRef<
64
- React.ElementRef<typeof DrawerPrimitive.Title>,
65
- React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
66
- >(({ className, ...props }, ref) => (
67
- <DrawerPrimitive.Title
68
- ref={ref}
69
- className={cn("text-lg font-semibold leading-none tracking-tight", className)}
70
- {...props}
71
- />
72
- ));
73
- DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
74
-
75
- const DrawerDescription = React.forwardRef<
76
- React.ElementRef<typeof DrawerPrimitive.Description>,
77
- React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
78
- >(({ className, ...props }, ref) => (
79
- <DrawerPrimitive.Description
80
- ref={ref}
81
- className={cn("text-sm text-muted-foreground", className)}
82
- {...props}
83
- />
84
- ));
85
- DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
86
-
87
- export {
88
- Drawer,
89
- DrawerPortal,
90
- DrawerOverlay,
91
- DrawerTrigger,
92
- DrawerClose,
93
- DrawerContent,
94
- DrawerHeader,
95
- DrawerFooter,
96
- DrawerTitle,
97
- DrawerDescription,
98
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/dropdown-menu.tsx DELETED
@@ -1,187 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
5
- import { Check, ChevronRight, Circle } from "lucide-react";
6
-
7
- import { cn } from "@/lib/utils";
8
-
9
- const DropdownMenu = DropdownMenuPrimitive.Root;
10
-
11
- const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
12
-
13
- const DropdownMenuGroup = DropdownMenuPrimitive.Group;
14
-
15
- const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
16
-
17
- const DropdownMenuSub = DropdownMenuPrimitive.Sub;
18
-
19
- const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
20
-
21
- const DropdownMenuSubTrigger = React.forwardRef<
22
- React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
23
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
24
- inset?: boolean;
25
- }
26
- >(({ className, inset, children, ...props }, ref) => (
27
- <DropdownMenuPrimitive.SubTrigger
28
- ref={ref}
29
- className={cn(
30
- "flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
31
- inset && "pl-8",
32
- className,
33
- )}
34
- {...props}
35
- >
36
- {children}
37
- <ChevronRight className="ml-auto" />
38
- </DropdownMenuPrimitive.SubTrigger>
39
- ));
40
- DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
41
-
42
- const DropdownMenuSubContent = React.forwardRef<
43
- React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
44
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
45
- >(({ className, ...props }, ref) => (
46
- <DropdownMenuPrimitive.SubContent
47
- ref={ref}
48
- className={cn(
49
- "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-dropdown-menu-content-transform-origin)",
50
- className,
51
- )}
52
- {...props}
53
- />
54
- ));
55
- DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
56
-
57
- const DropdownMenuContent = React.forwardRef<
58
- React.ElementRef<typeof DropdownMenuPrimitive.Content>,
59
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
60
- >(({ className, sideOffset = 4, ...props }, ref) => (
61
- <DropdownMenuPrimitive.Portal>
62
- <DropdownMenuPrimitive.Content
63
- ref={ref}
64
- sideOffset={sideOffset}
65
- className={cn(
66
- "z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
67
- "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-dropdown-menu-content-transform-origin)",
68
- className,
69
- )}
70
- {...props}
71
- />
72
- </DropdownMenuPrimitive.Portal>
73
- ));
74
- DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
75
-
76
- const DropdownMenuItem = React.forwardRef<
77
- React.ElementRef<typeof DropdownMenuPrimitive.Item>,
78
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
79
- inset?: boolean;
80
- }
81
- >(({ className, inset, ...props }, ref) => (
82
- <DropdownMenuPrimitive.Item
83
- ref={ref}
84
- className={cn(
85
- "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
86
- inset && "pl-8",
87
- className,
88
- )}
89
- {...props}
90
- />
91
- ));
92
- DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
93
-
94
- const DropdownMenuCheckboxItem = React.forwardRef<
95
- React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
96
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
97
- >(({ className, children, ...props }, ref) => (
98
- <DropdownMenuPrimitive.CheckboxItem
99
- ref={ref}
100
- className={cn(
101
- "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
102
- className,
103
- )}
104
- {...props}
105
- >
106
- <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
107
- <DropdownMenuPrimitive.ItemIndicator>
108
- <Check className="h-4 w-4" />
109
- </DropdownMenuPrimitive.ItemIndicator>
110
- </span>
111
- {children}
112
- </DropdownMenuPrimitive.CheckboxItem>
113
- ));
114
- DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName;
115
-
116
- const DropdownMenuRadioItem = React.forwardRef<
117
- React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
118
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
119
- >(({ className, children, ...props }, ref) => (
120
- <DropdownMenuPrimitive.RadioItem
121
- ref={ref}
122
- className={cn(
123
- "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
124
- className,
125
- )}
126
- {...props}
127
- >
128
- <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
129
- <DropdownMenuPrimitive.ItemIndicator>
130
- <Circle className="h-2 w-2 fill-current" />
131
- </DropdownMenuPrimitive.ItemIndicator>
132
- </span>
133
- {children}
134
- </DropdownMenuPrimitive.RadioItem>
135
- ));
136
- DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
137
-
138
- const DropdownMenuLabel = React.forwardRef<
139
- React.ElementRef<typeof DropdownMenuPrimitive.Label>,
140
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
141
- inset?: boolean;
142
- }
143
- >(({ className, inset, ...props }, ref) => (
144
- <DropdownMenuPrimitive.Label
145
- ref={ref}
146
- className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
147
- {...props}
148
- />
149
- ));
150
- DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
151
-
152
- const DropdownMenuSeparator = React.forwardRef<
153
- React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
154
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
155
- >(({ className, ...props }, ref) => (
156
- <DropdownMenuPrimitive.Separator
157
- ref={ref}
158
- className={cn("-mx-1 my-1 h-px bg-muted", className)}
159
- {...props}
160
- />
161
- ));
162
- DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
163
-
164
- const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
165
- return (
166
- <span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
167
- );
168
- };
169
- DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
170
-
171
- export {
172
- DropdownMenu,
173
- DropdownMenuTrigger,
174
- DropdownMenuContent,
175
- DropdownMenuItem,
176
- DropdownMenuCheckboxItem,
177
- DropdownMenuRadioItem,
178
- DropdownMenuLabel,
179
- DropdownMenuSeparator,
180
- DropdownMenuShortcut,
181
- DropdownMenuGroup,
182
- DropdownMenuPortal,
183
- DropdownMenuSub,
184
- DropdownMenuSubContent,
185
- DropdownMenuSubTrigger,
186
- DropdownMenuRadioGroup,
187
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/form.tsx DELETED
@@ -1,171 +0,0 @@
1
- import * as React from "react";
2
- import * as LabelPrimitive from "@radix-ui/react-label";
3
- import { Slot } from "@radix-ui/react-slot";
4
- import {
5
- Controller,
6
- FormProvider,
7
- useFormContext,
8
- type ControllerProps,
9
- type FieldPath,
10
- type FieldValues,
11
- } from "react-hook-form";
12
-
13
- import { cn } from "@/lib/utils";
14
- import { Label } from "@/components/ui/label";
15
-
16
- const Form = FormProvider;
17
-
18
- type FormFieldContextValue<
19
- TFieldValues extends FieldValues = FieldValues,
20
- TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
21
- > = {
22
- name: TName;
23
- };
24
-
25
- const FormFieldContext = React.createContext<FormFieldContextValue | null>(null);
26
-
27
- const FormField = <
28
- TFieldValues extends FieldValues = FieldValues,
29
- TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
30
- >({
31
- ...props
32
- }: ControllerProps<TFieldValues, TName>) => {
33
- return (
34
- <FormFieldContext.Provider value={{ name: props.name }}>
35
- <Controller {...props} />
36
- </FormFieldContext.Provider>
37
- );
38
- };
39
-
40
- const useFormField = () => {
41
- const fieldContext = React.useContext(FormFieldContext);
42
- const itemContext = React.useContext(FormItemContext);
43
- const { getFieldState, formState } = useFormContext();
44
-
45
- if (!fieldContext) {
46
- throw new Error("useFormField should be used within <FormField>");
47
- }
48
-
49
- if (!itemContext) {
50
- throw new Error("useFormField should be used within <FormItem>");
51
- }
52
-
53
- const fieldState = getFieldState(fieldContext.name, formState);
54
-
55
- const { id } = itemContext;
56
-
57
- return {
58
- id,
59
- name: fieldContext.name,
60
- formItemId: `${id}-form-item`,
61
- formDescriptionId: `${id}-form-item-description`,
62
- formMessageId: `${id}-form-item-message`,
63
- ...fieldState,
64
- };
65
- };
66
-
67
- type FormItemContextValue = {
68
- id: string;
69
- };
70
-
71
- const FormItemContext = React.createContext<FormItemContextValue | null>(null);
72
-
73
- const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
74
- ({ className, ...props }, ref) => {
75
- const id = React.useId();
76
-
77
- return (
78
- <FormItemContext.Provider value={{ id }}>
79
- <div ref={ref} className={cn("space-y-2", className)} {...props} />
80
- </FormItemContext.Provider>
81
- );
82
- },
83
- );
84
- FormItem.displayName = "FormItem";
85
-
86
- const FormLabel = React.forwardRef<
87
- React.ElementRef<typeof LabelPrimitive.Root>,
88
- React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
89
- >(({ className, ...props }, ref) => {
90
- const { error, formItemId } = useFormField();
91
-
92
- return (
93
- <Label
94
- ref={ref}
95
- className={cn(error && "text-destructive", className)}
96
- htmlFor={formItemId}
97
- {...props}
98
- />
99
- );
100
- });
101
- FormLabel.displayName = "FormLabel";
102
-
103
- const FormControl = React.forwardRef<
104
- React.ElementRef<typeof Slot>,
105
- React.ComponentPropsWithoutRef<typeof Slot>
106
- >(({ ...props }, ref) => {
107
- const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
108
-
109
- return (
110
- <Slot
111
- ref={ref}
112
- id={formItemId}
113
- aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
114
- aria-invalid={!!error}
115
- {...props}
116
- />
117
- );
118
- });
119
- FormControl.displayName = "FormControl";
120
-
121
- const FormDescription = React.forwardRef<
122
- HTMLParagraphElement,
123
- React.HTMLAttributes<HTMLParagraphElement>
124
- >(({ className, ...props }, ref) => {
125
- const { formDescriptionId } = useFormField();
126
-
127
- return (
128
- <p
129
- ref={ref}
130
- id={formDescriptionId}
131
- className={cn("text-[0.8rem] text-muted-foreground", className)}
132
- {...props}
133
- />
134
- );
135
- });
136
- FormDescription.displayName = "FormDescription";
137
-
138
- const FormMessage = React.forwardRef<
139
- HTMLParagraphElement,
140
- React.HTMLAttributes<HTMLParagraphElement>
141
- >(({ className, children, ...props }, ref) => {
142
- const { error, formMessageId } = useFormField();
143
- const body = error ? String(error?.message ?? "") : children;
144
-
145
- if (!body) {
146
- return null;
147
- }
148
-
149
- return (
150
- <p
151
- ref={ref}
152
- id={formMessageId}
153
- className={cn("text-[0.8rem] font-medium text-destructive", className)}
154
- {...props}
155
- >
156
- {body}
157
- </p>
158
- );
159
- });
160
- FormMessage.displayName = "FormMessage";
161
-
162
- export {
163
- useFormField,
164
- Form,
165
- FormItem,
166
- FormLabel,
167
- FormControl,
168
- FormDescription,
169
- FormMessage,
170
- FormField,
171
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/hover-card.tsx DELETED
@@ -1,27 +0,0 @@
1
- import * as React from "react";
2
- import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
3
-
4
- import { cn } from "@/lib/utils";
5
-
6
- const HoverCard = HoverCardPrimitive.Root;
7
-
8
- const HoverCardTrigger = HoverCardPrimitive.Trigger;
9
-
10
- const HoverCardContent = React.forwardRef<
11
- React.ElementRef<typeof HoverCardPrimitive.Content>,
12
- React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
13
- >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
14
- <HoverCardPrimitive.Content
15
- ref={ref}
16
- align={align}
17
- sideOffset={sideOffset}
18
- className={cn(
19
- "z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-hover-card-content-transform-origin)",
20
- className,
21
- )}
22
- {...props}
23
- />
24
- ));
25
- HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
26
-
27
- export { HoverCard, HoverCardTrigger, HoverCardContent };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/input-otp.tsx DELETED
@@ -1,73 +0,0 @@
1
- import * as React from "react";
2
- import { OTPInput, OTPInputContext } from "input-otp";
3
- import { Minus } from "lucide-react";
4
-
5
- import { cn } from "@/lib/utils";
6
-
7
- const InputOTP = React.forwardRef<
8
- React.ElementRef<typeof OTPInput>,
9
- React.ComponentPropsWithoutRef<typeof OTPInput>
10
- >(({ className, containerClassName, ...props }, ref) => (
11
- <OTPInput
12
- ref={ref}
13
- containerClassName={cn(
14
- "flex items-center gap-2 has-[:disabled]:opacity-50",
15
- containerClassName,
16
- )}
17
- className={cn("disabled:cursor-not-allowed", className)}
18
- {...props}
19
- />
20
- ));
21
- InputOTP.displayName = "InputOTP";
22
-
23
- const InputOTPGroup = React.forwardRef<
24
- React.ElementRef<"div">,
25
- React.ComponentPropsWithoutRef<"div">
26
- >(({ className, ...props }, ref) => (
27
- <div ref={ref} className={cn("flex items-center", className)} {...props} />
28
- ));
29
- InputOTPGroup.displayName = "InputOTPGroup";
30
-
31
- const InputOTPSlot = React.forwardRef<
32
- React.ElementRef<"div">,
33
- React.ComponentPropsWithoutRef<"div"> & { index: number }
34
- >(({ index, className, ...props }, ref) => {
35
- const inputOTPContext = React.useContext(OTPInputContext);
36
- const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index] ?? {
37
- char: null,
38
- hasFakeCaret: false,
39
- isActive: false,
40
- };
41
-
42
- return (
43
- <div
44
- ref={ref}
45
- className={cn(
46
- "relative flex h-9 w-9 items-center justify-center border-y border-r border-input text-sm shadow-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
47
- isActive && "z-10 ring-1 ring-ring",
48
- className,
49
- )}
50
- {...props}
51
- >
52
- {char}
53
- {hasFakeCaret && (
54
- <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
55
- <div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
56
- </div>
57
- )}
58
- </div>
59
- );
60
- });
61
- InputOTPSlot.displayName = "InputOTPSlot";
62
-
63
- const InputOTPSeparator = React.forwardRef<
64
- React.ElementRef<"div">,
65
- React.ComponentPropsWithoutRef<"div">
66
- >(({ ...props }, ref) => (
67
- <div ref={ref} role="separator" {...props}>
68
- <Minus />
69
- </div>
70
- ));
71
- InputOTPSeparator.displayName = "InputOTPSeparator";
72
-
73
- export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/input.tsx DELETED
@@ -1,22 +0,0 @@
1
- import * as React from "react";
2
-
3
- import { cn } from "@/lib/utils";
4
-
5
- const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
6
- ({ className, type, ...props }, ref) => {
7
- return (
8
- <input
9
- type={type}
10
- className={cn(
11
- "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
12
- className,
13
- )}
14
- ref={ref}
15
- {...props}
16
- />
17
- );
18
- },
19
- );
20
- Input.displayName = "Input";
21
-
22
- export { Input };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/label.tsx DELETED
@@ -1,21 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import * as LabelPrimitive from "@radix-ui/react-label";
5
- import { cva, type VariantProps } from "class-variance-authority";
6
-
7
- import { cn } from "@/lib/utils";
8
-
9
- const labelVariants = cva(
10
- "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
11
- );
12
-
13
- const Label = React.forwardRef<
14
- React.ElementRef<typeof LabelPrimitive.Root>,
15
- React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> & VariantProps<typeof labelVariants>
16
- >(({ className, ...props }, ref) => (
17
- <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
18
- ));
19
- Label.displayName = LabelPrimitive.Root.displayName;
20
-
21
- export { Label };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/menubar.tsx DELETED
@@ -1,228 +0,0 @@
1
- import * as React from "react";
2
- import * as MenubarPrimitive from "@radix-ui/react-menubar";
3
- import { Check, ChevronRight, Circle } from "lucide-react";
4
-
5
- import { cn } from "@/lib/utils";
6
-
7
- function MenubarMenu({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
8
- return <MenubarPrimitive.Menu {...props} />;
9
- }
10
-
11
- function MenubarGroup({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Group>) {
12
- return <MenubarPrimitive.Group {...props} />;
13
- }
14
-
15
- function MenubarPortal({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
16
- return <MenubarPrimitive.Portal {...props} />;
17
- }
18
-
19
- function MenubarRadioGroup({ ...props }: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
20
- return <MenubarPrimitive.RadioGroup {...props} />;
21
- }
22
-
23
- function MenubarSub({ ...props }: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
24
- return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />;
25
- }
26
-
27
- const Menubar = React.forwardRef<
28
- React.ElementRef<typeof MenubarPrimitive.Root>,
29
- React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
30
- >(({ className, ...props }, ref) => (
31
- <MenubarPrimitive.Root
32
- ref={ref}
33
- className={cn(
34
- "flex h-9 items-center space-x-1 rounded-md border bg-background p-1 shadow-sm",
35
- className,
36
- )}
37
- {...props}
38
- />
39
- ));
40
- Menubar.displayName = MenubarPrimitive.Root.displayName;
41
-
42
- const MenubarTrigger = React.forwardRef<
43
- React.ElementRef<typeof MenubarPrimitive.Trigger>,
44
- React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
45
- >(({ className, ...props }, ref) => (
46
- <MenubarPrimitive.Trigger
47
- ref={ref}
48
- className={cn(
49
- "flex cursor-default select-none items-center rounded-sm px-3 py-1 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
50
- className,
51
- )}
52
- {...props}
53
- />
54
- ));
55
- MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
56
-
57
- const MenubarSubTrigger = React.forwardRef<
58
- React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
59
- React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
60
- inset?: boolean;
61
- }
62
- >(({ className, inset, children, ...props }, ref) => (
63
- <MenubarPrimitive.SubTrigger
64
- ref={ref}
65
- className={cn(
66
- "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
67
- inset && "pl-8",
68
- className,
69
- )}
70
- {...props}
71
- >
72
- {children}
73
- <ChevronRight className="ml-auto h-4 w-4" />
74
- </MenubarPrimitive.SubTrigger>
75
- ));
76
- MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
77
-
78
- const MenubarSubContent = React.forwardRef<
79
- React.ElementRef<typeof MenubarPrimitive.SubContent>,
80
- React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
81
- >(({ className, ...props }, ref) => (
82
- <MenubarPrimitive.SubContent
83
- ref={ref}
84
- className={cn(
85
- "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-menubar-content-transform-origin)",
86
- className,
87
- )}
88
- {...props}
89
- />
90
- ));
91
- MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
92
-
93
- const MenubarContent = React.forwardRef<
94
- React.ElementRef<typeof MenubarPrimitive.Content>,
95
- React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
96
- >(({ className, align = "start", alignOffset = -4, sideOffset = 8, ...props }, ref) => (
97
- <MenubarPrimitive.Portal>
98
- <MenubarPrimitive.Content
99
- ref={ref}
100
- align={align}
101
- alignOffset={alignOffset}
102
- sideOffset={sideOffset}
103
- className={cn(
104
- "z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-menubar-content-transform-origin)",
105
- className,
106
- )}
107
- {...props}
108
- />
109
- </MenubarPrimitive.Portal>
110
- ));
111
- MenubarContent.displayName = MenubarPrimitive.Content.displayName;
112
-
113
- const MenubarItem = React.forwardRef<
114
- React.ElementRef<typeof MenubarPrimitive.Item>,
115
- React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
116
- inset?: boolean;
117
- }
118
- >(({ className, inset, ...props }, ref) => (
119
- <MenubarPrimitive.Item
120
- ref={ref}
121
- className={cn(
122
- "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
123
- inset && "pl-8",
124
- className,
125
- )}
126
- {...props}
127
- />
128
- ));
129
- MenubarItem.displayName = MenubarPrimitive.Item.displayName;
130
-
131
- const MenubarCheckboxItem = React.forwardRef<
132
- React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
133
- React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
134
- >(({ className, children, ...props }, ref) => (
135
- <MenubarPrimitive.CheckboxItem
136
- ref={ref}
137
- className={cn(
138
- "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
139
- className,
140
- )}
141
- {...props}
142
- >
143
- <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
144
- <MenubarPrimitive.ItemIndicator>
145
- <Check className="h-4 w-4" />
146
- </MenubarPrimitive.ItemIndicator>
147
- </span>
148
- {children}
149
- </MenubarPrimitive.CheckboxItem>
150
- ));
151
- MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
152
-
153
- const MenubarRadioItem = React.forwardRef<
154
- React.ElementRef<typeof MenubarPrimitive.RadioItem>,
155
- React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
156
- >(({ className, children, ...props }, ref) => (
157
- <MenubarPrimitive.RadioItem
158
- ref={ref}
159
- className={cn(
160
- "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
161
- className,
162
- )}
163
- {...props}
164
- >
165
- <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
166
- <MenubarPrimitive.ItemIndicator>
167
- <Circle className="h-4 w-4 fill-current" />
168
- </MenubarPrimitive.ItemIndicator>
169
- </span>
170
- {children}
171
- </MenubarPrimitive.RadioItem>
172
- ));
173
- MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
174
-
175
- const MenubarLabel = React.forwardRef<
176
- React.ElementRef<typeof MenubarPrimitive.Label>,
177
- React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
178
- inset?: boolean;
179
- }
180
- >(({ className, inset, ...props }, ref) => (
181
- <MenubarPrimitive.Label
182
- ref={ref}
183
- className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
184
- {...props}
185
- />
186
- ));
187
- MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
188
-
189
- const MenubarSeparator = React.forwardRef<
190
- React.ElementRef<typeof MenubarPrimitive.Separator>,
191
- React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
192
- >(({ className, ...props }, ref) => (
193
- <MenubarPrimitive.Separator
194
- ref={ref}
195
- className={cn("-mx-1 my-1 h-px bg-muted", className)}
196
- {...props}
197
- />
198
- ));
199
- MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
200
-
201
- const MenubarShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
202
- return (
203
- <span
204
- className={cn("ml-auto text-xs tracking-widest text-muted-foreground", className)}
205
- {...props}
206
- />
207
- );
208
- };
209
- MenubarShortcut.displayname = "MenubarShortcut";
210
-
211
- export {
212
- Menubar,
213
- MenubarMenu,
214
- MenubarTrigger,
215
- MenubarContent,
216
- MenubarItem,
217
- MenubarSeparator,
218
- MenubarLabel,
219
- MenubarCheckboxItem,
220
- MenubarRadioGroup,
221
- MenubarRadioItem,
222
- MenubarPortal,
223
- MenubarSubContent,
224
- MenubarSubTrigger,
225
- MenubarGroup,
226
- MenubarSub,
227
- MenubarShortcut,
228
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/navigation-menu.tsx DELETED
@@ -1,120 +0,0 @@
1
- import * as React from "react";
2
- import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
3
- import { cva } from "class-variance-authority";
4
- import { ChevronDown } from "lucide-react";
5
-
6
- import { cn } from "@/lib/utils";
7
-
8
- const NavigationMenu = React.forwardRef<
9
- React.ElementRef<typeof NavigationMenuPrimitive.Root>,
10
- React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Root>
11
- >(({ className, children, ...props }, ref) => (
12
- <NavigationMenuPrimitive.Root
13
- ref={ref}
14
- className={cn("relative z-10 flex max-w-max flex-1 items-center justify-center", className)}
15
- {...props}
16
- >
17
- {children}
18
- <NavigationMenuViewport />
19
- </NavigationMenuPrimitive.Root>
20
- ));
21
- NavigationMenu.displayName = NavigationMenuPrimitive.Root.displayName;
22
-
23
- const NavigationMenuList = React.forwardRef<
24
- React.ElementRef<typeof NavigationMenuPrimitive.List>,
25
- React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.List>
26
- >(({ className, ...props }, ref) => (
27
- <NavigationMenuPrimitive.List
28
- ref={ref}
29
- className={cn("group flex flex-1 list-none items-center justify-center space-x-1", className)}
30
- {...props}
31
- />
32
- ));
33
- NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName;
34
-
35
- const NavigationMenuItem = NavigationMenuPrimitive.Item;
36
-
37
- const navigationMenuTriggerStyle = cva(
38
- "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium cursor-pointer transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 disabled:cursor-not-allowed data-[state=open]:text-accent-foreground data-[state=open]:bg-accent/50 data-[state=open]:hover:bg-accent data-[state=open]:focus:bg-accent",
39
- );
40
-
41
- const NavigationMenuTrigger = React.forwardRef<
42
- React.ElementRef<typeof NavigationMenuPrimitive.Trigger>,
43
- React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Trigger>
44
- >(({ className, children, ...props }, ref) => (
45
- <NavigationMenuPrimitive.Trigger
46
- ref={ref}
47
- className={cn(navigationMenuTriggerStyle(), "group", className)}
48
- {...props}
49
- >
50
- {children}{" "}
51
- <ChevronDown
52
- className="relative top-[1px] ml-1 h-3 w-3 transition duration-300 group-data-[state=open]:rotate-180"
53
- aria-hidden="true"
54
- />
55
- </NavigationMenuPrimitive.Trigger>
56
- ));
57
- NavigationMenuTrigger.displayName = NavigationMenuPrimitive.Trigger.displayName;
58
-
59
- const NavigationMenuContent = React.forwardRef<
60
- React.ElementRef<typeof NavigationMenuPrimitive.Content>,
61
- React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Content>
62
- >(({ className, ...props }, ref) => (
63
- <NavigationMenuPrimitive.Content
64
- ref={ref}
65
- className={cn(
66
- "left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
67
- className,
68
- )}
69
- {...props}
70
- />
71
- ));
72
- NavigationMenuContent.displayName = NavigationMenuPrimitive.Content.displayName;
73
-
74
- const NavigationMenuLink = NavigationMenuPrimitive.Link;
75
-
76
- const NavigationMenuViewport = React.forwardRef<
77
- React.ElementRef<typeof NavigationMenuPrimitive.Viewport>,
78
- React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Viewport>
79
- >(({ className, ...props }, ref) => (
80
- <div className={cn("absolute left-0 top-full flex justify-center")}>
81
- <NavigationMenuPrimitive.Viewport
82
- className={cn(
83
- "origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
84
- className,
85
- )}
86
- ref={ref}
87
- {...props}
88
- />
89
- </div>
90
- ));
91
- NavigationMenuViewport.displayName = NavigationMenuPrimitive.Viewport.displayName;
92
-
93
- const NavigationMenuIndicator = React.forwardRef<
94
- React.ElementRef<typeof NavigationMenuPrimitive.Indicator>,
95
- React.ComponentPropsWithoutRef<typeof NavigationMenuPrimitive.Indicator>
96
- >(({ className, ...props }, ref) => (
97
- <NavigationMenuPrimitive.Indicator
98
- ref={ref}
99
- className={cn(
100
- "top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
101
- className,
102
- )}
103
- {...props}
104
- >
105
- <div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
106
- </NavigationMenuPrimitive.Indicator>
107
- ));
108
- NavigationMenuIndicator.displayName = NavigationMenuPrimitive.Indicator.displayName;
109
-
110
- export {
111
- navigationMenuTriggerStyle,
112
- NavigationMenu,
113
- NavigationMenuList,
114
- NavigationMenuItem,
115
- NavigationMenuContent,
116
- NavigationMenuTrigger,
117
- NavigationMenuLink,
118
- NavigationMenuIndicator,
119
- NavigationMenuViewport,
120
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/pagination.tsx DELETED
@@ -1,98 +0,0 @@
1
- import * as React from "react";
2
- import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
3
-
4
- import { cn } from "@/lib/utils";
5
- import { ButtonProps, buttonVariants } from "@/components/ui/button";
6
-
7
- const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
8
- <nav
9
- role="navigation"
10
- aria-label="pagination"
11
- className={cn("mx-auto flex w-full justify-center", className)}
12
- {...props}
13
- />
14
- );
15
- Pagination.displayName = "Pagination";
16
-
17
- const PaginationContent = React.forwardRef<HTMLUListElement, React.ComponentProps<"ul">>(
18
- ({ className, ...props }, ref) => (
19
- <ul ref={ref} className={cn("flex flex-row items-center gap-1", className)} {...props} />
20
- ),
21
- );
22
- PaginationContent.displayName = "PaginationContent";
23
-
24
- const PaginationItem = React.forwardRef<HTMLLIElement, React.ComponentProps<"li">>(
25
- ({ className, ...props }, ref) => <li ref={ref} className={cn("", className)} {...props} />,
26
- );
27
- PaginationItem.displayName = "PaginationItem";
28
-
29
- type PaginationLinkProps = {
30
- isActive?: boolean;
31
- } & Pick<ButtonProps, "size"> &
32
- React.ComponentProps<"a">;
33
-
34
- const PaginationLink = ({ className, isActive, size = "icon", ...props }: PaginationLinkProps) => (
35
- <a
36
- aria-current={isActive ? "page" : undefined}
37
- className={cn(
38
- buttonVariants({
39
- variant: isActive ? "outline" : "ghost",
40
- size,
41
- }),
42
- className,
43
- )}
44
- {...props}
45
- />
46
- );
47
- PaginationLink.displayName = "PaginationLink";
48
-
49
- const PaginationPrevious = ({
50
- className,
51
- ...props
52
- }: React.ComponentProps<typeof PaginationLink>) => (
53
- <PaginationLink
54
- aria-label="Go to previous page"
55
- size="default"
56
- className={cn("gap-1 pl-2.5", className)}
57
- {...props}
58
- >
59
- <ChevronLeft className="h-4 w-4" />
60
- <span>Previous</span>
61
- </PaginationLink>
62
- );
63
- PaginationPrevious.displayName = "PaginationPrevious";
64
-
65
- const PaginationNext = ({ className, ...props }: React.ComponentProps<typeof PaginationLink>) => (
66
- <PaginationLink
67
- aria-label="Go to next page"
68
- size="default"
69
- className={cn("gap-1 pr-2.5", className)}
70
- {...props}
71
- >
72
- <span>Next</span>
73
- <ChevronRight className="h-4 w-4" />
74
- </PaginationLink>
75
- );
76
- PaginationNext.displayName = "PaginationNext";
77
-
78
- const PaginationEllipsis = ({ className, ...props }: React.ComponentProps<"span">) => (
79
- <span
80
- aria-hidden
81
- className={cn("flex h-9 w-9 items-center justify-center", className)}
82
- {...props}
83
- >
84
- <MoreHorizontal className="h-4 w-4" />
85
- <span className="sr-only">More pages</span>
86
- </span>
87
- );
88
- PaginationEllipsis.displayName = "PaginationEllipsis";
89
-
90
- export {
91
- Pagination,
92
- PaginationContent,
93
- PaginationLink,
94
- PaginationItem,
95
- PaginationPrevious,
96
- PaginationNext,
97
- PaginationEllipsis,
98
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/popover.tsx DELETED
@@ -1,31 +0,0 @@
1
- import * as React from "react";
2
- import * as PopoverPrimitive from "@radix-ui/react-popover";
3
-
4
- import { cn } from "@/lib/utils";
5
-
6
- const Popover = PopoverPrimitive.Root;
7
-
8
- const PopoverTrigger = PopoverPrimitive.Trigger;
9
-
10
- const PopoverAnchor = PopoverPrimitive.Anchor;
11
-
12
- const PopoverContent = React.forwardRef<
13
- React.ElementRef<typeof PopoverPrimitive.Content>,
14
- React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
15
- >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16
- <PopoverPrimitive.Portal>
17
- <PopoverPrimitive.Content
18
- ref={ref}
19
- align={align}
20
- sideOffset={sideOffset}
21
- className={cn(
22
- "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-popover-content-transform-origin)",
23
- className,
24
- )}
25
- {...props}
26
- />
27
- </PopoverPrimitive.Portal>
28
- ));
29
- PopoverContent.displayName = PopoverPrimitive.Content.displayName;
30
-
31
- export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/progress.tsx DELETED
@@ -1,25 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import * as ProgressPrimitive from "@radix-ui/react-progress";
5
-
6
- import { cn } from "@/lib/utils";
7
-
8
- const Progress = React.forwardRef<
9
- React.ElementRef<typeof ProgressPrimitive.Root>,
10
- React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
11
- >(({ className, value, ...props }, ref) => (
12
- <ProgressPrimitive.Root
13
- ref={ref}
14
- className={cn("relative h-2 w-full overflow-hidden rounded-full bg-primary/20", className)}
15
- {...props}
16
- >
17
- <ProgressPrimitive.Indicator
18
- className="h-full w-full flex-1 bg-primary transition-all"
19
- style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
20
- />
21
- </ProgressPrimitive.Root>
22
- ));
23
- Progress.displayName = ProgressPrimitive.Root.displayName;
24
-
25
- export { Progress };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/radio-group.tsx DELETED
@@ -1,36 +0,0 @@
1
- import * as React from "react";
2
- import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
3
- import { Circle } from "lucide-react";
4
-
5
- import { cn } from "@/lib/utils";
6
-
7
- const RadioGroup = React.forwardRef<
8
- React.ElementRef<typeof RadioGroupPrimitive.Root>,
9
- React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
10
- >(({ className, ...props }, ref) => {
11
- return <RadioGroupPrimitive.Root className={cn("grid gap-2", className)} {...props} ref={ref} />;
12
- });
13
- RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
14
-
15
- const RadioGroupItem = React.forwardRef<
16
- React.ElementRef<typeof RadioGroupPrimitive.Item>,
17
- React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
18
- >(({ className, ...props }, ref) => {
19
- return (
20
- <RadioGroupPrimitive.Item
21
- ref={ref}
22
- className={cn(
23
- "aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow cursor-pointer focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
24
- className,
25
- )}
26
- {...props}
27
- >
28
- <RadioGroupPrimitive.Indicator className="flex items-center justify-center">
29
- <Circle className="h-3.5 w-3.5 fill-primary" />
30
- </RadioGroupPrimitive.Indicator>
31
- </RadioGroupPrimitive.Item>
32
- );
33
- });
34
- RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
35
-
36
- export { RadioGroup, RadioGroupItem };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/resizable.tsx DELETED
@@ -1,37 +0,0 @@
1
- import { GripVertical } from "lucide-react";
2
- import { Group, Panel, Separator } from "react-resizable-panels";
3
-
4
- import { cn } from "@/lib/utils";
5
-
6
- const ResizablePanelGroup = ({ className, ...props }: React.ComponentProps<typeof Group>) => (
7
- <Group
8
- className={cn("flex h-full w-full data-[panel-group-direction=vertical]:flex-col", className)}
9
- {...props}
10
- />
11
- );
12
-
13
- const ResizablePanel = Panel;
14
-
15
- const ResizableHandle = ({
16
- withHandle,
17
- className,
18
- ...props
19
- }: React.ComponentProps<typeof Separator> & {
20
- withHandle?: boolean;
21
- }) => (
22
- <Separator
23
- className={cn(
24
- "relative flex w-px items-center justify-center bg-border after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring focus-visible:ring-offset-1 data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
25
- className,
26
- )}
27
- {...props}
28
- >
29
- {withHandle && (
30
- <div className="z-10 flex h-4 w-3 items-center justify-center rounded-sm border bg-border">
31
- <GripVertical className="h-2.5 w-2.5" />
32
- </div>
33
- )}
34
- </Separator>
35
- );
36
-
37
- export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/scroll-area.tsx DELETED
@@ -1,44 +0,0 @@
1
- import * as React from "react";
2
- import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
3
-
4
- import { cn } from "@/lib/utils";
5
-
6
- const ScrollArea = React.forwardRef<
7
- React.ElementRef<typeof ScrollAreaPrimitive.Root>,
8
- React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
9
- >(({ className, children, ...props }, ref) => (
10
- <ScrollAreaPrimitive.Root
11
- ref={ref}
12
- className={cn("relative overflow-hidden", className)}
13
- {...props}
14
- >
15
- <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
16
- {children}
17
- </ScrollAreaPrimitive.Viewport>
18
- <ScrollBar />
19
- <ScrollAreaPrimitive.Corner />
20
- </ScrollAreaPrimitive.Root>
21
- ));
22
- ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
23
-
24
- const ScrollBar = React.forwardRef<
25
- React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
26
- React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
27
- >(({ className, orientation = "vertical", ...props }, ref) => (
28
- <ScrollAreaPrimitive.ScrollAreaScrollbar
29
- ref={ref}
30
- orientation={orientation}
31
- className={cn(
32
- "flex touch-none select-none transition-colors",
33
- orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent p-[1px]",
34
- orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent p-[1px]",
35
- className,
36
- )}
37
- {...props}
38
- >
39
- <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
40
- </ScrollAreaPrimitive.ScrollAreaScrollbar>
41
- ));
42
- ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
43
-
44
- export { ScrollArea, ScrollBar };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
components/ui/select.tsx DELETED
@@ -1,152 +0,0 @@
1
- "use client";
2
-
3
- import * as React from "react";
4
- import * as SelectPrimitive from "@radix-ui/react-select";
5
- import { Check, ChevronDown, ChevronUp } from "lucide-react";
6
-
7
- import { cn } from "@/lib/utils";
8
-
9
- const Select = SelectPrimitive.Root;
10
-
11
- const SelectGroup = SelectPrimitive.Group;
12
-
13
- const SelectValue = SelectPrimitive.Value;
14
-
15
- const SelectTrigger = React.forwardRef<
16
- React.ElementRef<typeof SelectPrimitive.Trigger>,
17
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
18
- >(({ className, children, ...props }, ref) => (
19
- <SelectPrimitive.Trigger
20
- ref={ref}
21
- className={cn(
22
- "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background cursor-pointer data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
23
- className,
24
- )}
25
- {...props}
26
- >
27
- {children}
28
- <SelectPrimitive.Icon asChild>
29
- <ChevronDown className="h-4 w-4 opacity-50" />
30
- </SelectPrimitive.Icon>
31
- </SelectPrimitive.Trigger>
32
- ));
33
- SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
34
-
35
- const SelectScrollUpButton = React.forwardRef<
36
- React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
37
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
38
- >(({ className, ...props }, ref) => (
39
- <SelectPrimitive.ScrollUpButton
40
- ref={ref}
41
- className={cn("flex cursor-default items-center justify-center py-1", className)}
42
- {...props}
43
- >
44
- <ChevronUp className="h-4 w-4" />
45
- </SelectPrimitive.ScrollUpButton>
46
- ));
47
- SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
48
-
49
- const SelectScrollDownButton = React.forwardRef<
50
- React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
51
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
52
- >(({ className, ...props }, ref) => (
53
- <SelectPrimitive.ScrollDownButton
54
- ref={ref}
55
- className={cn("flex cursor-default items-center justify-center py-1", className)}
56
- {...props}
57
- >
58
- <ChevronDown className="h-4 w-4" />
59
- </SelectPrimitive.ScrollDownButton>
60
- ));
61
- SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
62
-
63
- const SelectContent = React.forwardRef<
64
- React.ElementRef<typeof SelectPrimitive.Content>,
65
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
66
- >(({ className, children, position = "popper", ...props }, ref) => (
67
- <SelectPrimitive.Portal>
68
- <SelectPrimitive.Content
69
- ref={ref}
70
- className={cn(
71
- "relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-(--radix-select-content-transform-origin)",
72
- position === "popper" &&
73
- "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
74
- className,
75
- )}
76
- position={position}
77
- {...props}
78
- >
79
- <SelectScrollUpButton />
80
- <SelectPrimitive.Viewport
81
- className={cn(
82
- "p-1",
83
- position === "popper" &&
84
- "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]",
85
- )}
86
- >
87
- {children}
88
- </SelectPrimitive.Viewport>
89
- <SelectScrollDownButton />
90
- </SelectPrimitive.Content>
91
- </SelectPrimitive.Portal>
92
- ));
93
- SelectContent.displayName = SelectPrimitive.Content.displayName;
94
-
95
- const SelectLabel = React.forwardRef<
96
- React.ElementRef<typeof SelectPrimitive.Label>,
97
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
98
- >(({ className, ...props }, ref) => (
99
- <SelectPrimitive.Label
100
- ref={ref}
101
- className={cn("px-2 py-1.5 text-sm font-semibold", className)}
102
- {...props}
103
- />
104
- ));
105
- SelectLabel.displayName = SelectPrimitive.Label.displayName;
106
-
107
- const SelectItem = React.forwardRef<
108
- React.ElementRef<typeof SelectPrimitive.Item>,
109
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
110
- >(({ className, children, ...props }, ref) => (
111
- <SelectPrimitive.Item
112
- ref={ref}
113
- className={cn(
114
- "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
115
- className,
116
- )}
117
- {...props}
118
- >
119
- <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
120
- <SelectPrimitive.ItemIndicator>
121
- <Check className="h-4 w-4" />
122
- </SelectPrimitive.ItemIndicator>
123
- </span>
124
- <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
125
- </SelectPrimitive.Item>
126
- ));
127
- SelectItem.displayName = SelectPrimitive.Item.displayName;
128
-
129
- const SelectSeparator = React.forwardRef<
130
- React.ElementRef<typeof SelectPrimitive.Separator>,
131
- React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
132
- >(({ className, ...props }, ref) => (
133
- <SelectPrimitive.Separator
134
- ref={ref}
135
- className={cn("-mx-1 my-1 h-px bg-muted", className)}
136
- {...props}
137
- />
138
- ));
139
- SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
140
-
141
- export {
142
- Select,
143
- SelectGroup,
144
- SelectValue,
145
- SelectTrigger,
146
- SelectContent,
147
- SelectLabel,
148
- SelectItem,
149
- SelectSeparator,
150
- SelectScrollUpButton,
151
- SelectScrollDownButton,
152
- };