From 49795981ebd1a9fbd5886085e081fb7ad93c82bb Mon Sep 17 00:00:00 2001 From: Evandro Fonseca Junior Date: Tue, 20 Oct 2026 00:49:26 -0300 Subject: [PATCH] =?UTF-8?q?wip:=20snapshot=20das=20altera=C3=A7=C3=B5es=20?= =?UTF-8?q?n=C3=A3o=20commitadas=20(2026-10-20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Origem: D:/Documents/Github/Downloader Arquivos omitidos (segredos, bancos de dados, logs, >50 MB): 0 --- .claude/settings.local.json | 9 + .claude/worktrees/agent-a0cb243b | 1 + .claude/worktrees/agent-a6424abe | 1 + .claude/worktrees/agent-a85e5c45 | 1 + .claude/worktrees/agent-aa2cb71d | 1 + .graphifyignore | 21 ++ .openclaude-profile.json | 9 + package.json | 7 +- web/src/App.css | 290 -------------------- web/src/App.tsx | 128 ++++++--- web/src/components/feature/DownloadForm.tsx | 79 ++++-- web/src/components/feature/DownloadItem.tsx | 288 ++++++++++++------- web/src/components/feature/SessionTimer.tsx | 43 ++- web/src/components/ui/Alert.tsx | 26 +- web/src/components/ui/Button.tsx | 24 +- web/src/components/ui/Card.tsx | 6 +- web/src/components/ui/Input.tsx | 5 +- web/src/components/ui/Layout.tsx | 55 ++-- web/src/components/ui/ProgressBar.tsx | 4 +- web/src/components/ui/Textarea.tsx | 5 +- web/src/context/SessionContext.tsx | 160 +++++------ web/src/context/session-context.ts | 43 +++ web/src/index.css | 97 +++++-- 23 files changed, 673 insertions(+), 630 deletions(-) create mode 100644 .claude/settings.local.json create mode 160000 .claude/worktrees/agent-a0cb243b create mode 160000 .claude/worktrees/agent-a6424abe create mode 160000 .claude/worktrees/agent-a85e5c45 create mode 160000 .claude/worktrees/agent-aa2cb71d create mode 100644 .graphifyignore create mode 100644 .openclaude-profile.json create mode 100644 web/src/context/session-context.ts diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..b238b23 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "mcp__plugin_context-mode_context-mode__ctx_batch_execute", + "mcp__context-mode__ctx_batch_execute", + "mcp__context-mode__ctx_execute_file" + ] + } +} diff --git a/.claude/worktrees/agent-a0cb243b b/.claude/worktrees/agent-a0cb243b new file mode 160000 index 0000000..5b1600c --- /dev/null +++ b/.claude/worktrees/agent-a0cb243b @@ -0,0 +1 @@ +Subproject commit 5b1600c210a923cb33b0ae15c054cb4da64c62a5 diff --git a/.claude/worktrees/agent-a6424abe b/.claude/worktrees/agent-a6424abe new file mode 160000 index 0000000..5b1600c --- /dev/null +++ b/.claude/worktrees/agent-a6424abe @@ -0,0 +1 @@ +Subproject commit 5b1600c210a923cb33b0ae15c054cb4da64c62a5 diff --git a/.claude/worktrees/agent-a85e5c45 b/.claude/worktrees/agent-a85e5c45 new file mode 160000 index 0000000..5b1600c --- /dev/null +++ b/.claude/worktrees/agent-a85e5c45 @@ -0,0 +1 @@ +Subproject commit 5b1600c210a923cb33b0ae15c054cb4da64c62a5 diff --git a/.claude/worktrees/agent-aa2cb71d b/.claude/worktrees/agent-aa2cb71d new file mode 160000 index 0000000..5b1600c --- /dev/null +++ b/.claude/worktrees/agent-aa2cb71d @@ -0,0 +1 @@ +Subproject commit 5b1600c210a923cb33b0ae15c054cb4da64c62a5 diff --git a/.graphifyignore b/.graphifyignore new file mode 100644 index 0000000..d58fa88 --- /dev/null +++ b/.graphifyignore @@ -0,0 +1,21 @@ +node_modules/ +.next/ +dist/ +build/ +out/ +coverage/ +.git/ +.pnpm-store/ +.cache/ +*.log +.env +.env.* +__pycache__/ +*.pyc +.venv/ +venv/ +target/ +*.exe +*.dll +*.so +*.dylib diff --git a/.openclaude-profile.json b/.openclaude-profile.json new file mode 100644 index 0000000..6fc9980 --- /dev/null +++ b/.openclaude-profile.json @@ -0,0 +1,9 @@ +{ + "profile": "codex", + "env": { + "OPENAI_BASE_URL": "https://chatgpt.com/backend-api/codex", + "OPENAI_MODEL": "codexplan", + "CHATGPT_ACCOUNT_ID": "6dbec33c-c751-4383-b112-bfc776f39ed7" + }, + "createdAt": "2026-04-05T18:55:08.995Z" +} \ No newline at end of file diff --git a/package.json b/package.json index 156df23..80e66dc 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,6 @@ "workspaces": [ "server", "web" - ] -} - - + ], + "packageManager": "pnpm@10.28.1" +} \ No newline at end of file diff --git a/web/src/App.css b/web/src/App.css index 92614dd..e69de29 100644 --- a/web/src/App.css +++ b/web/src/App.css @@ -1,290 +0,0 @@ -.app-root { - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 2.5rem 1.5rem 3rem; - gap: 2.5rem; - background: radial-gradient(circle at top, #27272a 0, #09090b 45%, #020617 100%); -} - -.app-header { - max-width: 640px; - text-align: center; -} - -.app-header h1 { - font-size: 2rem; - margin-bottom: 0.5rem; -} - -.app-header p { - font-size: 0.98rem; - color: #e4e4e7; -} - -.app-header-meta { - margin-top: 0.5rem; - display: flex; - justify-content: center; - gap: 1rem; - flex-wrap: wrap; - font-size: 0.8rem; - color: #a1a1aa; -} - -.app-main { - width: 100%; - display: flex; - justify-content: center; - align-items: flex-start; - gap: 2rem; - max-width: 1200px; - margin: 0 auto; -} - -.download-form { - width: 100%; - max-width: 560px; - display: flex; - flex-direction: column; - gap: 0.75rem; - padding: 1.5rem 1.75rem; - border-radius: 1rem; - border: 1px solid rgba(63, 63, 70, 0.9); - background: radial-gradient(circle at top left, #18181b 0, #020617 60%); - box-shadow: - 0 18px 40px rgba(0, 0, 0, 0.7), - 0 0 0 1px rgba(24, 24, 27, 0.9); -} - -.field-label { - font-size: 0.9rem; - font-weight: 500; -} - -.field-helper { - font-size: 0.78rem; - color: #a1a1aa; - margin-top: -0.1rem; -} - -.url-input { - padding: 0.75rem 0.9rem; - border-radius: 0.5rem; - border: 1px solid #27272a; - background-color: #09090b; - color: #f4f4f5; - font-size: 0.95rem; - resize: vertical; -} - -.url-input--multiline { - line-height: 1.4; -} - -.url-input:focus { - outline: 2px solid #4f46e5; - outline-offset: 1px; - border-color: transparent; -} - -.primary-button { - margin-top: 0.5rem; - padding: 0.75rem 1rem; - border-radius: 0.5rem; - border: none; - background: #4f46e5; - color: #f9fafb; - font-weight: 600; - font-size: 0.95rem; - cursor: pointer; - transition: background 180ms ease-out, transform 160ms ease-out; -} - -.primary-button:hover { - background: #4338ca; -} - -.primary-button:active { - transform: translateY(1px) scale(0.99); -} - -.primary-button:focus-visible { - outline: 2px solid #a5b4fc; - outline-offset: 2px; -} - -.error-text { - font-size: 0.85rem; - color: #f97373; -} - -.downloads-section { - width: 100%; - max-width: 620px; - margin-top: 0; - padding: 1.25rem 1.5rem; - border-radius: 1rem; - border: 1px solid rgba(63, 63, 70, 0.9); - background-color: rgba(9, 9, 11, 0.9); - backdrop-filter: blur(14px); -} - -.downloads-title { - font-size: 1rem; - margin-bottom: 0.5rem; -} - -.downloads-empty { - margin-top: 0.5rem; - font-size: 0.85rem; - color: #a1a1aa; -} - -.downloads-list { - list-style: none; - padding: 0; - margin: 0; - display: flex; - flex-direction: column; - gap: 0.75rem; -} - -.download-item { - padding: 0.6rem 0.1rem 0.7rem; - border-bottom: 1px solid rgba(39, 39, 42, 0.8); -} - -.download-item:last-of-type { - border-bottom: none; -} - -.download-main { - display: flex; - justify-content: space-between; - align-items: center; - gap: 0.75rem; - margin-bottom: 0.35rem; -} - -.download-url { - font-size: 0.8rem; - color: #e5e5e5; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.badge { - padding: 0.2rem 0.55rem; - border-radius: 999px; - font-size: 0.7rem; - font-weight: 500; -} - -.badge--pending { - background-color: rgba(39, 39, 42, 0.9); - color: #e5e5e5; -} - -.badge--downloading { - background-color: rgba(37, 99, 235, 0.18); - color: #bfdbfe; -} - -.badge--completed { - background-color: rgba(22, 163, 74, 0.25); - color: #bbf7d0; -} - -.badge--error { - background-color: rgba(239, 68, 68, 0.18); - color: #fecaca; -} - -.progress-wrapper { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.progress-bar { - flex: 1; - height: 0.4rem; - border-radius: 999px; - background-color: #18181b; - overflow: hidden; -} - -.progress-bar__fill { - height: 100%; - border-radius: inherit; - background: linear-gradient(to right, #4f46e5, #22c55e); - transition: width 160ms ease-out; -} - -.progress-label { - font-size: 0.75rem; - color: #e4e4e7; - width: 3ch; - text-align: right; -} - -.download-error { - margin-top: 0.1rem; - font-size: 0.78rem; - color: #fecaca; -} - -.download-meta { - display: flex; - flex-wrap: wrap; - gap: 0.4rem; - margin-top: 0.25rem; - font-size: 0.8rem; - color: #d4d4d8; -} - -.download-footer { - margin-top: 0.35rem; - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.75rem; -} - -.secondary-button { - padding: 0.35rem 0.75rem; - border-radius: 999px; - border: 1px solid #3f3f46; - background-color: transparent; - color: #e4e4e7; - font-size: 0.78rem; - font-weight: 500; - cursor: pointer; - transition: - background-color 160ms ease-out, - border-color 160ms ease-out, - transform 120ms ease-out; -} - -.secondary-button:hover { - background-color: rgba(63, 63, 70, 0.4); - border-color: #71717a; -} - -.secondary-button:active { - transform: translateY(1px) scale(0.99); -} - -@media (min-width: 768px) { - .app-header h1 { - font-size: 2.25rem; - } - - .download-form { - padding: 1.75rem 2rem; - } -} diff --git a/web/src/App.tsx b/web/src/App.tsx index 26d7981..2ac471b 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,26 +1,43 @@ -import "./App.css"; +import { AlertCircle, HardDrive, Server } from "lucide-react"; import { DownloadForm } from "./components/feature/DownloadForm"; import { DownloadItem } from "./components/feature/DownloadItem"; -import { Layout } from "./components/ui/Layout"; -import { Alert, AlertDescription, AlertTitle } from "./components/ui/Alert"; -import { AlertCircle } from "lucide-react"; -import { SessionProvider, useSession } from "./context/SessionContext"; import { SessionTimer } from "./components/feature/SessionTimer"; +import { Alert, AlertDescription, AlertTitle } from "./components/ui/Alert"; +import { Layout } from "./components/ui/Layout"; +import { SessionProvider } from "./context/SessionContext"; +import { useSession } from "./context/session-context"; function DownloadList() { const { tasks, expiresAt } = useSession(); return ( -
-
-

Active Downloads

- +
+
+
+

+ Transfer activity +

+

+ Downloads in this session +

+

+ Monitor queue status, transfer speed and final files from a single workspace. +

+
+
{tasks.length === 0 ? ( -
-

No downloads in session

-

Files are removed after 1 hour of inactivity

+
+

+ Queue is empty +

+

+ No downloads yet +

+

+ Your queued, active and completed transfers will appear here with progress, speed and the final save action. +

) : (
@@ -29,50 +46,77 @@ function DownloadList() { ))}
)} -
+
); } function Main() { - const { queueDownload } = useSession(); + const { queueDownload, sessionId } = useSession(); - const handleDownload = async (url: string) => { - try { - await queueDownload(url); - } catch (error) { - console.error("Failed to queue", error); - } + const handleDownload = async (url: string): Promise => { + await queueDownload(url); }; return ( -
- {/* Left Column: Input Form */} -
-
-
-
-

New Download

-

- Paste a link to start. Files are downloaded to the server first. +

+
+
+ + Private download workspace + + +
+

+ Queue secure transfers from a cleaner control panel. +

+

+ Paste a file URL, let the server fetch it first, then keep every transfer visible until the session window expires. +

+
+ +
+
+ +

Server-side transfer

+

+ Downloads are fetched on the VPS first, then exposed as a final file in your session.

- -
+
+ +

Ephemeral storage

+

+ Files stay available for one hour of activity and are removed automatically after expiration. +

+
+
+
- - - Ephemeral Session - - Files are stored for 1 hour. The timer resets on interaction. - After expiration, all files are permanently deleted. - - -
+
+
+

New transfer

+

Start a new download

+

+ Paste a direct file URL to add it to the active session queue. +

+
+ +
+ +
+
+ + + + Ephemeral session policy + + Files are stored for 1 hour. The timer resets on activity, and once the session expires every stored file is deleted permanently. + +
- {/* Right Column: Downloads List */} -
+
diff --git a/web/src/components/feature/DownloadForm.tsx b/web/src/components/feature/DownloadForm.tsx index f5e1f05..081f37a 100644 --- a/web/src/components/feature/DownloadForm.tsx +++ b/web/src/components/feature/DownloadForm.tsx @@ -1,22 +1,32 @@ import { useState } from "react"; +import { Download, Loader2 } from "lucide-react"; +import { z } from "zod"; import { Button } from "../ui/Button"; import { Input } from "../ui/Input"; -import { Download } from "lucide-react"; -import { z } from "zod"; const formSchema = z.object({ url: z.string().url("Please enter a valid URL"), }); interface DownloadFormProps { - onDownload: (url: string) => void; + onDownload: (url: string) => Promise | void; + isReady: boolean; } -export function DownloadForm({ onDownload }: DownloadFormProps) { +function getErrorMessage(error: unknown): string { + if (error instanceof Error && error.message) { + return error.message; + } + + return "Unable to queue the download right now."; +} + +export function DownloadForm({ onDownload, isReady }: DownloadFormProps) { const [url, setUrl] = useState(""); const [error, setError] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(null); @@ -27,26 +37,53 @@ export function DownloadForm({ onDownload }: DownloadFormProps) { return; } - onDownload(url); - setUrl(""); + if (!isReady) { + setError("Session is still initializing. Try again in a moment."); + return; + } + + try { + setIsSubmitting(true); + await onDownload(url); + setUrl(""); + } catch (submitError: unknown) { + setError(getErrorMessage(submitError)); + } finally { + setIsSubmitting(false); + } }; return ( -
-
-
- setUrl(e.target.value)} - className={error ? "border-destructive focus-visible:ring-destructive" : ""} - /> - {error &&

{error}

} + +
+ + setUrl(e.target.value)} + disabled={isSubmitting} + className={error ? "border-destructive/60 focus-visible:ring-destructive/15" : ""} + /> +
+ Use a direct file link whenever possible for faster server-side fetching. + + {isReady ? "Validated before queueing" : "Preparing session"} +
-
+ +
+

+ The transfer starts on the server first, then becomes available in your session list. +

+
diff --git a/web/src/components/feature/DownloadItem.tsx b/web/src/components/feature/DownloadItem.tsx index 7849379..001117f 100644 --- a/web/src/components/feature/DownloadItem.tsx +++ b/web/src/components/feature/DownloadItem.tsx @@ -1,138 +1,218 @@ +import { useState } from "react"; import { FileCheck, FileWarning, + HardDrive, Loader2, Server, - HardDrive } from "lucide-react"; +import { DownloadStatus, DownloadTask, useSession } from "../../context/session-context"; +import { formatBytes, formatSpeed } from "../../lib/utils"; import { Button } from "../ui/Button"; import { Card, CardContent } from "../ui/Card"; import { Progress } from "../ui/ProgressBar"; -import { formatBytes, formatSpeed } from "../../lib/utils"; -const API_URL = import.meta.env.VITE_API_URL || "http://localhost:3000"; - -export type DownloadStatus = "queued" | "downloading" | "completed" | "error"; - -export type DownloadItemData = { - id: string; - sessionId: string; - url: string; - filename: string; - status: DownloadStatus; - progress: number; - totalBytes: number; - receivedBytes: number; - speed: number; - error?: string; - createdAt: number; -}; +const API_URL = import.meta.env.VITE_API_URL || ""; +const createdAtFormatter = new Intl.DateTimeFormat(undefined, { + hour: "2-digit", + minute: "2-digit", +}); interface DownloadItemProps { - item: DownloadItemData; + item: DownloadTask; +} + +const statusLabel: Record = { + queued: "Queued", + downloading: "Transferring", + completed: "Ready", + error: "Issue", +}; + +function getDownloadErrorMessage(error: unknown): string { + if (error instanceof Error && error.message) { + return error.message; + } + + return "Failed to download file."; } export function DownloadItem({ item }: DownloadItemProps) { + const { sessionId } = useSession(); + const [downloadError, setDownloadError] = useState(null); + const [isSaving, setIsSaving] = useState(false); const isCompleted = item.status === "completed"; const isError = item.status === "error"; const isQueued = item.status === "queued"; - const handleDownloadFile = () => { - const download = async () => { - try { - const sessionId = localStorage.getItem("downloader_session_id") || ""; - const res = await fetch(`${API_URL}/api/download/${item.id}`, { - headers: { "x-session-id": sessionId } - }); - if (!res.ok) throw new Error("Download failed"); + const statusBadgeClass = isCompleted + ? "border-emerald-400/20 bg-emerald-400/10 text-emerald-300" + : isError + ? "border-destructive/30 bg-destructive/10 text-destructive" + : isQueued + ? "border-amber-400/20 bg-amber-400/10 text-amber-300" + : "border-primary/20 bg-primary/10 text-primary"; - const blob = await res.blob(); - const url = window.URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = item.filename; - document.body.appendChild(a); - a.click(); - window.URL.revokeObjectURL(url); - document.body.removeChild(a); - } catch (e) { - alert("Failed to download file"); + const statusDetail = isCompleted + ? "File cached and ready to be saved locally." + : isError + ? "The transfer stopped before completion." + : isQueued + ? "Waiting for the server to start processing." + : "Receiving data from the source URL."; + + const handleDownloadFile = async (): Promise => { + try { + setDownloadError(null); + setIsSaving(true); + const response = await fetch(`${API_URL}/api/download/${item.id}`, { + headers: { "x-session-id": sessionId }, + }); + + if (!response.ok) { + throw new Error("Failed to fetch the stored file."); } - }; - download(); + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const anchor = document.createElement("a"); + anchor.href = url; + anchor.download = item.filename; + document.body.appendChild(anchor); + anchor.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(anchor); + } catch (error: unknown) { + setDownloadError(getDownloadErrorMessage(error)); + } finally { + setIsSaving(false); + } }; return ( - - -
-
- {isCompleted ? ( -
- + + +
+
+
+
+ {isCompleted ? ( +
+ +
+ ) : isError ? ( +
+ +
+ ) : isQueued ? ( +
+ +
+ ) : ( +
+ +
+ )}
- ) : isError ? ( -
- -
- ) : isQueued ? ( -
- -
- ) : ( -
- -
- )} -
-
-
-
-

- {item.filename || item.url} -

-

- {item.url} -

+
+
+ + {statusLabel[item.status]} + + + Created at {createdAtFormatter.format(item.createdAt)} + +
+ +
+

+ {item.filename || item.url} +

+

+ {item.url} +

+
- {isCompleted && ( - - )}
- {/* Progress Section */} - {!isCompleted && !isError && ( -
-
- {isQueued ? "Queued on server..." : item.status} - {formatBytes(item.receivedBytes || 0)} / {item.totalBytes ? formatBytes(item.totalBytes) : "??"} -
- -
- {item.speed ? formatSpeed(item.speed) : "0 B/s"} - {item.progress ? `${Math.round(item.progress)}%` : "0%"} -
-
- )} - - {isCompleted && ( -
- - Ready for download ({formatBytes(item.totalBytes)}) -
- )} - - {isError && ( -
- {item.error || "Unknown error occurred"} -
- )} + {isCompleted ? ( + + ) : null}
+ +
+
+

+ Status +

+

{statusDetail}

+
+
+

+ Size +

+

+ {item.totalBytes ? formatBytes(item.totalBytes) : "Unknown until available"} +

+
+
+

+ Throughput +

+

+ {item.speed ? formatSpeed(item.speed) : "Waiting for activity"} +

+
+
+ + {!isCompleted && !isError ? ( +
+
+ {isQueued ? "Waiting on server" : "Transfer progress"} + {item.progress ? `${Math.round(item.progress)}%` : "0%"} +
+ +
+ + {formatBytes(item.receivedBytes || 0)} / {item.totalBytes ? formatBytes(item.totalBytes) : "Unknown"} + + {item.speed ? formatSpeed(item.speed) : "No transfer speed yet"} +
+
+ ) : null} + + {isCompleted ? ( +
+

Transfer completed successfully

+

+ The file is ready in the current session and can be saved to disk at any time before expiration. +

+
+ ) : null} + + {downloadError ? ( +
+

Unable to save file

+

{downloadError}

+
+ ) : null} + + {isError ? ( +
+

Transfer failed

+

+ {item.error || "Unknown error occurred during the transfer."} +

+
+ ) : null}
diff --git a/web/src/components/feature/SessionTimer.tsx b/web/src/components/feature/SessionTimer.tsx index 874d0e8..40e77af 100644 --- a/web/src/components/feature/SessionTimer.tsx +++ b/web/src/components/feature/SessionTimer.tsx @@ -12,7 +12,9 @@ export function SessionTimer({ expiresAt, className }: SessionTimerProps) { const [isCritical, setIsCritical] = useState(false); useEffect(() => { - if (!expiresAt) return; + if (!expiresAt) { + return; + } const tick = () => { const now = Date.now(); @@ -29,20 +31,47 @@ export function SessionTimer({ expiresAt, className }: SessionTimerProps) { const seconds = Math.floor((diff % 60000) / 1000); setTimeLeft(`${minutes.toString().padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`); - setIsCritical(minutes < 5); // Critical if less than 5 minutes + setIsCritical(minutes < 5); }; - tick(); // Initial call + tick(); const interval = setInterval(tick, 1000); return () => clearInterval(interval); }, [expiresAt]); - if (!expiresAt) return null; + if (!expiresAt) { + return null; + } return ( -
- - Session: {timeLeft} +
+
+
+ +
+
+

+ Session window +

+

Expires in {timeLeft}

+
+
+ Auto cleanup
); } diff --git a/web/src/components/ui/Alert.tsx b/web/src/components/ui/Alert.tsx index 58ce965..98ba836 100644 --- a/web/src/components/ui/Alert.tsx +++ b/web/src/components/ui/Alert.tsx @@ -1,5 +1,5 @@ -import * as React from "react" -import { cn } from "../../lib/utils" +import * as React from "react"; +import { cn } from "../../lib/utils"; const Alert = React.forwardRef< HTMLDivElement, @@ -9,15 +9,15 @@ const Alert = React.forwardRef< ref={ref} role="alert" className={cn( - "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + "relative w-full rounded-[1.5rem] border border-border/70 bg-card/70 p-5 text-foreground shadow-[0_24px_80px_-40px_rgba(15,23,42,0.9)] backdrop-blur-xl [&>svg~*]:pl-8 [&>svg+div]:translate-y-[-2px] [&>svg]:absolute [&>svg]:left-5 [&>svg]:top-5 [&>svg]:text-foreground", variant === "destructive" && - "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + "border-destructive/30 bg-destructive/10 text-destructive-foreground [&>svg]:text-destructive", className )} {...props} /> -)) -Alert.displayName = "Alert" +)); +Alert.displayName = "Alert"; const AlertTitle = React.forwardRef< HTMLParagraphElement, @@ -25,11 +25,11 @@ const AlertTitle = React.forwardRef< >(({ className, ...props }, ref) => (
-)) -AlertTitle.displayName = "AlertTitle" +)); +AlertTitle.displayName = "AlertTitle"; const AlertDescription = React.forwardRef< HTMLParagraphElement, @@ -37,10 +37,10 @@ const AlertDescription = React.forwardRef< >(({ className, ...props }, ref) => (
-)) -AlertDescription.displayName = "AlertDescription" +)); +AlertDescription.displayName = "AlertDescription"; -export { Alert, AlertTitle, AlertDescription } +export { Alert, AlertTitle, AlertDescription }; diff --git a/web/src/components/ui/Button.tsx b/web/src/components/ui/Button.tsx index fbf21ec..bed2d3a 100644 --- a/web/src/components/ui/Button.tsx +++ b/web/src/components/ui/Button.tsx @@ -1,8 +1,7 @@ import * as React from "react"; import { cn } from "../../lib/utils"; -export interface ButtonProps - extends React.ButtonHTMLAttributes { +export interface ButtonProps extends React.ButtonHTMLAttributes { variant?: "default" | "secondary" | "outline" | "ghost" | "destructive"; size?: "default" | "sm" | "lg" | "icon"; } @@ -12,21 +11,22 @@ const Button = React.forwardRef( return (