review theme

This commit is contained in:
Anaïs Saraiva
2026-08-25 12:24:40 +02:00
parent abf8736fdd
commit b1b4f03814
12 changed files with 124 additions and 184 deletions
+23 -23
View File
@@ -106,45 +106,45 @@ pub async fn auth(
pub fn render_password_form(key: &str, filename: &str, error: bool) -> Response { pub fn render_password_form(key: &str, filename: &str, error: bool) -> Response {
let message = if error { let message = if error {
"<p class=\"err\">Incorrect password. Please try again.</p>" "<p class=\"err\">Mot de passe incorrect. Réessayez.</p>"
} else { } else {
"" ""
}; };
let safe_filename = escape_html(filename); let safe_filename = escape_html(filename);
let html = format!( let html = format!(
r#"<!doctype html> r#"<!doctype html>
<html lang="en"> <html lang="fr">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="referrer" content="no-referrer" /> <meta name="referrer" content="no-referrer" />
<title>Protected file</title> <title>Fichier protégé</title>
<style> <style>
* {{ box-sizing: border-box; }} * {{ box-sizing: border-box; }}
body {{ body {{
margin: 0; font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; margin: 0; font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
background: #0e1013; color: #e7eaf0; min-height: 100vh; background: #fafafa; color: #171717; min-height: 100vh;
display: flex; align-items: center; justify-content: center; padding: 16px; display: flex; align-items: center; justify-content: center; padding: 16px;
}} }}
.card {{ .card {{
width: 100%; max-width: 400px; background: #15181d; border: 1px solid #262b33; width: 100%; max-width: 400px; background: #ffffff; border: 1px solid #e5e5e5;
border-radius: 14px; padding: 28px; box-shadow: 0 10px 40px rgba(0,0,0,.4); border-radius: 8px; padding: 28px;
}} }}
.brand {{ display: flex; align-items: center; gap: 10px; margin-bottom: 18px; }} .brand {{ display: flex; align-items: center; gap: 10px; margin-bottom: 20px; }}
.brand img {{ width: 34px; height: 34px; }} .brand img {{ width: 32px; height: 32px; }}
.brand span {{ font-size: 15px; font-weight: 650; }} .brand span {{ font-size: 15px; font-weight: 600; }}
h1 {{ margin: 0 0 6px; font-size: 20px; }} h1 {{ margin: 0 0 6px; font-size: 18px; }}
p {{ margin: 0 0 18px; color: #98a1af; font-size: 14px; word-break: break-all; }} p {{ margin: 0 0 18px; color: #737373; font-size: 14px; word-break: break-all; }}
.err {{ margin: 0 0 14px; color: #e05555; background: rgba(224,85,85,.08); .err {{ margin: 0 0 14px; color: #dc2626; background: #fef2f2;
border: 1px solid rgba(224,85,85,.3); border-radius: 8px; padding: 10px 12px; }} border: 1px solid #fecaca; border-radius: 6px; padding: 10px 12px; }}
input[type="password"] {{ input[type="password"] {{
width: 100%; background: #0e1013; border: 1px solid #262b33; color: #e7eaf0; width: 100%; background: #ffffff; border: 1px solid #e5e5e5; color: #171717;
border-radius: 8px; padding: 12px 13px; font-size: 14px; margin-bottom: 12px; border-radius: 6px; padding: 11px 12px; font-size: 14px; margin-bottom: 12px;
}} }}
input[type="password"]:focus {{ outline: none; border-color: #3f7bf6; }} input[type="password"]:focus {{ outline: none; border-color: #171717; }}
button {{ width: 100%; font: inherit; cursor: pointer; border: none; border-radius: 8px; button {{ width: 100%; font: inherit; cursor: pointer; border: none; border-radius: 6px;
padding: 12px 14px; font-size: 14px; font-weight: 600; background: #3f7bf6; color: #fff; }} padding: 11px 14px; font-size: 14px; font-weight: 500; background: #171717; color: #fff; }}
button:hover {{ background: #2f63d8; }} button:hover {{ background: #0a0a0a; }}
</style> </style>
</head> </head>
<body> <body>
@@ -153,12 +153,12 @@ pub fn render_password_form(key: &str, filename: &str, error: bool) -> Response
<img src="/logo.svg" alt="logo" /> <img src="/logo.svg" alt="logo" />
<span>File Drop</span> <span>File Drop</span>
</div> </div>
<h1>Protected file</h1> <h1>Fichier protégé</h1>
<p>Enter the password to download <strong>{safe_filename}</strong>.</p> <p>Entrez le mot de passe pour télécharger <strong>{safe_filename}</strong>.</p>
{message} {message}
<form method="post" action="/api/auth/{key}"> <form method="post" action="/api/auth/{key}">
<input type="password" name="password" placeholder="Password" autofocus autocomplete="off" /> <input type="password" name="password" placeholder="Mot de passe" autofocus autocomplete="off" />
<button type="submit">Download</button> <button type="submit">Télécharger</button>
</form> </form>
</main> </main>
</body> </body>
+1 -7
View File
@@ -1,11 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<defs> <rect x="4" y="4" width="56" height="56" rx="14" fill="#171717"/>
<linearGradient id="g" x1="4" y1="4" x2="60" y2="60" gradientUnits="userSpaceOnUse">
<stop stop-color="#5b8cff"/>
<stop offset="1" stop-color="#a78bfa"/>
</linearGradient>
</defs>
<rect x="4" y="4" width="56" height="56" rx="16" fill="url(#g)"/>
<path d="M32 42V24m0 0-6 6m6-6 6 6" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" fill="none"/> <path d="M32 42V24m0 0-6 6m6-6 6 6" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<path d="M20 42v2a4 4 0 0 0 4 4h16a4 4 0 0 0 4-4v-2" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" fill="none"/> <path d="M20 42v2a4 4 0 0 0 4 4h16a4 4 0 0 0 4-4v-2" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 624 B

After

Width:  |  Height:  |  Size: 413 B

+1 -7
View File
@@ -1,11 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<defs> <rect x="4" y="4" width="56" height="56" rx="14" fill="#171717"/>
<linearGradient id="g" x1="4" y1="4" x2="60" y2="60" gradientUnits="userSpaceOnUse">
<stop stop-color="#5b8cff"/>
<stop offset="1" stop-color="#a78bfa"/>
</linearGradient>
</defs>
<rect x="4" y="4" width="56" height="56" rx="16" fill="url(#g)"/>
<path d="M32 42V24m0 0-6 6m6-6 6 6" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" fill="none"/> <path d="M32 42V24m0 0-6 6m6-6 6 6" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<path d="M20 42v2a4 4 0 0 0 4 4h16a4 4 0 0 0 4-4v-2" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" fill="none"/> <path d="M20 42v2a4 4 0 0 0 4 4h16a4 4 0 0 0 4-4v-2" stroke="#fff" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 624 B

After

Width:  |  Height:  |  Size: 413 B

+19 -22
View File
@@ -21,7 +21,7 @@
const file = e.detail; const file = e.detail;
if (!file) return; if (!file) return;
if (file.size === 0) { if (file.size === 0) {
notify('Empty files are not supported.', 'error'); notify('Les fichiers vides ne sont pas pris en charge.', 'error');
return; return;
} }
currentFile = file; currentFile = file;
@@ -49,11 +49,11 @@
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
protected: !!data.protected protected: !!data.protected
}); });
notify('Upload complete.', 'success'); notify('Envoi terminé.', 'success');
currentFile = null; currentFile = null;
password = ''; password = '';
} catch (e) { } catch (e) {
notify(e.message || 'Upload failed.', 'error'); notify(e.message || 'Échec de l\'envoi.', 'error');
} finally { } finally {
uploading = false; uploading = false;
progress = 0; progress = 0;
@@ -62,33 +62,33 @@
</script> </script>
<div class="mx-auto max-w-6xl px-4 pb-16 sm:px-6"> <div class="mx-auto max-w-6xl px-4 pb-16 sm:px-6">
<header class="flex items-center justify-between gap-4 border-b border-edge/60 py-5"> <header class="flex items-center justify-between gap-4 py-6">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<img src="/logo.svg" alt="File Drop" class="h-9 w-9" /> <img src="/logo.svg" alt="File Drop" class="h-8 w-8" />
<div> <div>
<h1 class="text-lg font-bold leading-tight text-ink">File Drop</h1> <h1 class="text-base font-semibold leading-tight text-ink">File Drop</h1>
<p class="text-xs text-muted">Private file hosting</p> <p class="text-xs text-muted">Hébergement de fichiers privé</p>
</div> </div>
</div> </div>
<a href="/" class="btn btn-ghost px-4 py-2 text-xs">New upload</a> <a href="/" class="btn btn-ghost px-3 py-1.5 text-xs">Nouvel envoi</a>
</header> </header>
<main class="mt-6 grid grid-cols-1 items-start gap-5 lg:grid-cols-[360px_1fr]"> <main class="grid grid-cols-1 items-start gap-4 lg:grid-cols-[360px_1fr]">
<aside class="card p-5 lg:sticky lg:top-6"> <aside class="card p-5 lg:sticky lg:top-6">
<h2 class="mb-4 text-base font-semibold text-ink">Upload a file</h2> <h2 class="mb-4 text-sm font-semibold text-ink">Envoyer un fichier</h2>
<Dropzone bind:file={currentFile} disabled={uploading} on:file={onFile} /> <Dropzone bind:file={currentFile} disabled={uploading} on:file={onFile} />
<div class="mt-4"> <div class="mt-4">
<label for="pw" class="mb-1.5 block text-xs font-semibold text-muted"> <label for="pw" class="mb-1.5 block text-xs font-medium text-muted">
Password <span class="font-normal opacity-70">(optional)</span> Mot de passe <span class="font-normal opacity-70">(optionnel)</span>
</label> </label>
<input <input
id="pw" id="pw"
type="password" type="password"
class="field" class="field"
bind:value={password} bind:value={password}
placeholder="Protect the download" placeholder="Protéger le téléchargement"
autocomplete="new-password" autocomplete="new-password"
on:keydown={(e) => { on:keydown={(e) => {
if (e.key === 'Enter') submit(); if (e.key === 'Enter') submit();
@@ -98,26 +98,23 @@
{#if uploading} {#if uploading}
<div class="mt-4"> <div class="mt-4">
<div class="mb-2 flex items-center justify-between gap-2 text-xs text-muted"> <div class="mb-1.5 flex items-center justify-between gap-2 text-xs text-muted">
<span class="truncate">{currentFile ? currentFile.name : 'Uploading…'}</span> <span class="truncate">{currentFile ? currentFile.name : 'Envoi…'}</span>
<span class="tabular-nums">{progress}%</span> <span class="tabular-nums">{progress}%</span>
</div> </div>
<div class="h-1.5 w-full overflow-hidden rounded-full bg-surface"> <div class="h-1 w-full overflow-hidden rounded-full bg-raised">
<div <div class="h-full bg-accent transition-all duration-150" style="width: {progress}%"></div>
class="h-full rounded-full bg-gradient-to-r from-accent to-[#a78bfa] transition-all duration-150"
style="width: {progress}%"
></div>
</div> </div>
</div> </div>
{/if} {/if}
<button <button
type="button" type="button"
class="btn btn-primary mt-4 w-full py-3" class="btn btn-primary mt-4 w-full py-2.5"
disabled={!currentFile || uploading} disabled={!currentFile || uploading}
on:click={submit} on:click={submit}
> >
{uploading ? 'Uploading…' : 'Upload'} {uploading ? 'Envoi…' : 'Envoyer'}
</button> </button>
<div class="mt-4"> <div class="mt-4">
+9 -42
View File
@@ -11,35 +11,10 @@
body { body {
@apply bg-base text-ink font-sans; @apply bg-base text-ink font-sans;
min-height: 100vh; min-height: 100vh;
background-image:
radial-gradient(1200px 600px at 15% -10%, rgba(91, 140, 255, 0.14), transparent 60%),
radial-gradient(1000px 500px at 100% 0%, rgba(91, 140, 255, 0.08), transparent 55%);
background-attachment: fixed;
} }
::selection { ::selection {
background: rgba(91, 140, 255, 0.3); background: rgba(23, 23, 23, 0.08);
}
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #232936;
border-radius: 999px;
border: 2px solid transparent;
background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover {
background: #2f3746;
background-clip: content-box;
} }
summary { summary {
@@ -53,36 +28,28 @@
@layer components { @layer components {
.card { .card {
@apply rounded-2xl border border-edge bg-surface/80 backdrop-blur-sm; @apply rounded-lg border border-edge bg-surface;
} }
.btn { .btn {
@apply inline-flex items-center justify-center gap-2 rounded-xl text-sm font-semibold @apply inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium
transition-all duration-150 disabled:cursor-not-allowed disabled:opacity-50; transition-colors disabled:cursor-not-allowed disabled:opacity-50;
} }
.btn-primary { .btn-primary {
@apply bg-accent text-white hover:bg-accent-strong @apply bg-accent text-white hover:bg-accent-strong;
shadow-[0_8px_24px_-8px_rgba(91,140,255,0.7)];
} }
.btn-ghost { .btn-ghost {
@apply bg-transparent text-muted border border-edge hover:text-ink hover:border-muted/60; @apply border border-edge bg-surface text-ink hover:bg-raised;
} }
.btn-danger { .btn-danger {
@apply bg-transparent text-danger border border-danger/40 hover:bg-danger/10; @apply border border-edge bg-surface text-danger hover:border-danger/40;
} }
.field { .field {
@apply w-full rounded-xl border border-edge bg-base/70 px-3.5 py-3 text-sm text-ink @apply w-full rounded-md border border-edge bg-surface px-3 py-2.5 text-sm text-ink
placeholder:text-muted/60 outline-none transition focus:border-accent focus:ring-2 placeholder:text-muted outline-none transition-colors focus:border-ink;
focus:ring-accent/20;
}
}
@layer utilities {
.text-gradient {
@apply bg-gradient-to-r from-accent to-[#a78bfa] bg-clip-text text-transparent;
} }
} }
+12 -12
View File
@@ -50,8 +50,7 @@
</script> </script>
<div <div
class="group relative flex cursor-pointer flex-col items-center justify-center rounded-2xl border-2 border-dashed border-edge bg-surface/40 px-6 py-10 text-center transition-all duration-200 hover:border-accent/60 hover:bg-surface/70 {dragging ? 'border-accent bg-accent/10' : ''} {disabled ? 'cursor-not-allowed opacity-60' : ''}" class="flex cursor-pointer flex-col items-center justify-center rounded-lg border border-dashed border-edge bg-surface px-6 py-10 text-center transition-colors hover:border-ink {dragging ? 'border-ink bg-raised' : ''} {disabled ? 'cursor-not-allowed opacity-60' : ''}"
class:dragging
role="button" role="button"
tabindex="0" tabindex="0"
aria-label="Choose a file" aria-label="Choose a file"
@@ -72,20 +71,21 @@
}} }}
/> />
<div <div class="mb-3 flex h-10 w-10 items-center justify-center text-muted">
class="mb-4 flex h-14 w-14 items-center justify-center rounded-2xl bg-gradient-to-br from-accent/20 to-[#a78bfa]/20 text-accent transition-transform duration-200 {dragging ? 'scale-110' : 'group-hover:scale-105'}" <svg viewBox="0 0 24 24" fill="none" class="h-5 w-5" stroke="currentColor" stroke-width="1.5">
> <path
<svg viewBox="0 0 24 24" fill="none" class="h-7 w-7" stroke="currentColor" stroke-width="1.8"> stroke-linecap="round"
<path stroke-linecap="round" stroke-linejoin="round" d="M12 16V4m0 0L7 9m5-5l5 5" /> stroke-linejoin="round"
<path stroke-linecap="round" stroke-linejoin="round" d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2" /> d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5"
/>
</svg> </svg>
</div> </div>
{#if file} {#if file}
<p class="max-w-full truncate text-sm font-semibold text-ink">{file.name}</p> <p class="max-w-full truncate text-sm font-medium text-ink">{file.name}</p>
<p class="mt-1 text-xs text-muted">Drop another file to replace it</p> <p class="mt-1 text-xs text-muted">Déposez un autre fichier pour le remplacer</p>
{:else} {:else}
<p class="text-sm font-semibold text-ink">Drag &amp; drop a file here</p> <p class="text-sm font-medium text-ink">Glissez-déposez un fichier ici</p>
<p class="mt-1 text-xs text-muted">or click to browse — up to 10&nbsp;GB</p> <p class="mt-1 text-xs text-muted">ou cliquez pour parcourir</p>
{/if} {/if}
</div> </div>
+18 -14
View File
@@ -8,22 +8,22 @@
async function onDelete(item) { async function onDelete(item) {
const ok = window.confirm( const ok = window.confirm(
`Delete "${item.originalName || item.downloadName}" permanently? This cannot be undone.` `Supprimer définitivement « ${item.originalName || item.downloadName} » ?`
); );
if (!ok) return; if (!ok) return;
try { try {
await deleteFile(item); await deleteFile(item);
removeUpload(item.id); removeUpload(item.id);
dispatch('notify', { message: 'File deleted.', kind: 'success' }); dispatch('notify', { message: 'Fichier supprimé.', kind: 'success' });
} catch (e) { } catch (e) {
dispatch('notify', { message: e.message || 'Deletion failed.', kind: 'error' }); dispatch('notify', { message: e.message || 'Suppression impossible.', kind: 'error' });
} }
} }
function onClear() { function onClear() {
if ($uploads.length === 0) return; if ($uploads.length === 0) return;
const ok = window.confirm( const ok = window.confirm(
'Clear this list? This only removes entries from THIS browser. Files on the server are not touched.' 'Vider cette liste ? Seules les entrées de CE navigateur sont supprimées. Les fichiers restent sur le serveur.'
); );
if (!ok) return; if (!ok) return;
clearUploads(); clearUploads();
@@ -33,33 +33,37 @@
<section class="card p-5"> <section class="card p-5">
<div class="mb-4 flex items-center justify-between gap-3"> <div class="mb-4 flex items-center justify-between gap-3">
<div class="flex items-baseline gap-2"> <div class="flex items-baseline gap-2">
<h2 class="text-base font-semibold text-ink">Your files</h2> <h2 class="text-sm font-semibold text-ink">Fichiers</h2>
{#if $uploads.length} {#if $uploads.length}
<span class="text-xs text-muted">{$uploads.length} file{$uploads.length === 1 ? '' : 's'}</span> <span class="text-xs text-muted">{$uploads.length}</span>
{/if} {/if}
</div> </div>
{#if $uploads.length} {#if $uploads.length}
<button type="button" class="text-xs font-semibold text-muted transition hover:text-danger" on:click={onClear}> <button type="button" class="text-xs font-medium text-muted transition-colors hover:text-danger" on:click={onClear}>
Clear list Vider la liste
</button> </button>
{/if} {/if}
</div> </div>
{#if $uploads.length} {#if $uploads.length}
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-2">
{#each $uploads as item (item.id)} {#each $uploads as item (item.id)}
<FileRow {item} on:delete={(e) => onDelete(e.detail)} /> <FileRow {item} on:delete={(e) => onDelete(e.detail)} />
{/each} {/each}
</div> </div>
{:else} {:else}
<div class="flex flex-col items-center justify-center py-14 text-center"> <div class="flex flex-col items-center justify-center py-14 text-center">
<div class="mb-3 flex h-12 w-12 items-center justify-center rounded-2xl bg-surface"> <div class="mb-3 flex h-10 w-10 items-center justify-center text-muted/70">
<svg viewBox="0 0 24 24" fill="none" class="h-6 w-6 text-muted/60" stroke="currentColor" stroke-width="1.6"> <svg viewBox="0 0 24 24" fill="none" class="h-6 w-6" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M4 7V5a2 2 0 012-2h12a2 2 0 012 2v2M4 7h16M4 7v10a2 2 0 002 2h12a2 2 0 002-2V7" /> <path
stroke-linecap="round"
stroke-linejoin="round"
d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5m8.25 3v6.75m0 0l-3-3m3 3l3-3M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z"
/>
</svg> </svg>
</div> </div>
<p class="text-sm text-muted">No files yet.</p> <p class="text-sm text-muted">Aucun fichier pour l'instant.</p>
<p class="mt-1 text-xs text-muted/70">Upload your first file to see it here.</p> <p class="mt-1 text-xs text-muted">Envoyez votre premier fichier pour le voir ici.</p>
</div> </div>
{/if} {/if}
</section> </section>
+17 -26
View File
@@ -3,8 +3,6 @@
import { import {
forcedDownloadUrl, forcedDownloadUrl,
downloadUrl, downloadUrl,
displayName,
fileExtension,
formatBytes, formatBytes,
formatDate, formatDate,
copyText copyText
@@ -16,8 +14,7 @@
let copied = false; let copied = false;
$: name = displayName(item); $: name = item.originalName || item.downloadName || 'file';
$: ext = fileExtension(item);
async function copy() { async function copy() {
await copyText(downloadUrl(item)); await copyText(downloadUrl(item));
@@ -28,27 +25,21 @@
<div class="card flex flex-col gap-3 p-4 md:flex-row md:items-center md:justify-between"> <div class="card flex flex-col gap-3 p-4 md:flex-row md:items-center md:justify-between">
<div class="flex min-w-0 items-center gap-3"> <div class="flex min-w-0 items-center gap-3">
<div <div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-md border border-edge bg-raised">
class="relative flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-accent/20 to-[#a78bfa]/20" <svg viewBox="0 0 24 24" fill="none" class="h-5 w-5 text-muted" stroke="currentColor" stroke-width="1.5">
> <path
<svg viewBox="0 0 24 24" fill="none" class="h-5 w-5 text-accent" stroke="currentColor" stroke-width="1.7"> stroke-linecap="round"
<path stroke-linecap="round" stroke-linejoin="round" d="M7 3h7l4 4v11a2 2 0 01-2 2H7a2 2 0 01-2-2V5a2 2 0 012-2z" /> stroke-linejoin="round"
<path stroke-linecap="round" stroke-linejoin="round" d="M14 3v4h4" /> d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
/>
</svg> </svg>
{#if ext}
<span
class="absolute -bottom-1.5 -right-1.5 max-w-[40px] truncate rounded-md border border-edge bg-raised px-1.5 py-0.5 text-[9px] font-bold uppercase tracking-wide text-muted"
>
{ext}
</span>
{/if}
</div> </div>
<div class="min-w-0"> <div class="min-w-0">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="truncate text-sm font-semibold text-ink" title={name}>{name}</span> <span class="truncate text-sm font-medium text-ink" title={name}>{name}</span>
<span <span
class="shrink-0 rounded-full border px-2 py-0.5 text-[9px] font-bold uppercase tracking-wider {item.protected ? 'border-accent/40 bg-accent/10 text-accent' : 'border-ok/40 bg-ok/10 text-ok'}" class="shrink-0 rounded-md border px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide {item.protected ? 'border-edge bg-raised text-muted' : 'border-ok/30 bg-ok/10 text-ok'}"
> >
{item.protected ? 'Protected' : 'Public'} {item.protected ? 'Protected' : 'Public'}
</span> </span>
@@ -59,18 +50,18 @@
<div class="flex flex-col gap-2 md:items-end"> <div class="flex flex-col gap-2 md:items-end">
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<a href={forcedDownloadUrl(item)} class="btn btn-primary px-3 py-2 text-xs">Download</a> <a href={forcedDownloadUrl(item)} class="btn btn-primary px-3 py-1.5 text-xs">Télécharger</a>
<button type="button" class="btn btn-ghost px-3 py-2 text-xs" on:click={copy}> <button type="button" class="btn btn-ghost px-3 py-1.5 text-xs" on:click={copy}>
{copied ? 'Copied' : 'Copy'} {copied ? 'Copié' : 'Copier'}
</button> </button>
<button type="button" class="btn btn-danger px-3 py-2 text-xs" on:click={() => dispatch('delete', item)}> <button type="button" class="btn btn-danger px-3 py-1.5 text-xs" on:click={() => dispatch('delete', item)}>
Delete Supprimer
</button> </button>
</div> </div>
<details> <details>
<summary class="cursor-pointer select-none text-xs font-semibold text-muted transition hover:text-ink"> <summary class="cursor-pointer select-none text-xs font-medium text-muted transition-colors hover:text-ink">
Links ▾ Liens
</summary> </summary>
<div class="mt-2 md:flex md:justify-end"> <div class="mt-2 md:flex md:justify-end">
<LinkFormats {item} /> <LinkFormats {item} />
+1 -1
View File
@@ -21,7 +21,7 @@
<button <button
type="button" type="button"
title={fmt.value} title={fmt.value}
class="rounded-lg border border-edge bg-surface px-2.5 font-semibold text-muted transition hover:border-muted/60 hover:text-ink {size === 'sm' ? 'py-1.5 text-[11px]' : 'py-2 text-xs'}" class="rounded-md border border-edge bg-surface font-medium text-muted transition-colors hover:bg-raised hover:text-ink {size === 'sm' ? 'px-2.5 py-1 text-[11px]' : 'px-2.5 py-1.5 text-xs'}"
on:click={() => copy(fmt)} on:click={() => copy(fmt)}
> >
{copied === fmt.label ? 'Copied' : fmt.label} {copied === fmt.label ? 'Copied' : fmt.label}
+7 -7
View File
@@ -1,6 +1,6 @@
<script> <script>
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { downloadUrl, displayName, copyText } from '../lib/formats.js'; import { downloadUrl, copyText } from '../lib/formats.js';
import LinkFormats from './LinkFormats.svelte'; import LinkFormats from './LinkFormats.svelte';
export let result; export let result;
@@ -8,7 +8,7 @@
let copied = false; let copied = false;
$: url = result ? downloadUrl(result) : ''; $: url = result ? downloadUrl(result) : '';
$: name = result ? displayName(result) : ''; $: name = result ? result.originalName || result.downloadName || 'file' : '';
async function copy() { async function copy() {
await copyText(url); await copyText(url);
@@ -18,25 +18,25 @@
</script> </script>
{#if result} {#if result}
<div class="card p-4" transition:fade={{ duration: 180 }}> <div class="card p-4" transition:fade={{ duration: 150 }}>
<div class="mb-3 flex items-center justify-between gap-3"> <div class="mb-3 flex items-center justify-between gap-3">
<span class="truncate text-sm font-semibold text-ink" title={name}>{name}</span> <span class="truncate text-sm font-medium text-ink" title={name}>{name}</span>
<span <span
class="shrink-0 rounded-full border px-2 py-0.5 text-[10px] font-bold uppercase tracking-wider {result.protected ? 'border-accent/40 bg-accent/10 text-accent' : 'border-ok/40 bg-ok/10 text-ok'}" class="shrink-0 rounded-md border px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide {result.protected ? 'border-edge bg-raised text-muted' : 'border-ok/30 bg-ok/10 text-ok'}"
> >
{result.protected ? 'Protected' : 'Public'} {result.protected ? 'Protected' : 'Public'}
</span> </span>
</div> </div>
<div class="flex gap-2"> <div class="flex gap-2">
<input class="field !py-2.5 text-xs" type="text" readonly value={url} aria-label="Download URL" /> <input class="field !py-2 text-xs" type="text" readonly value={url} aria-label="Download URL" />
<button type="button" class="btn btn-primary px-4" on:click={copy}> <button type="button" class="btn btn-primary px-4" on:click={copy}>
{copied ? 'Copied' : 'Copy'} {copied ? 'Copied' : 'Copy'}
</button> </button>
</div> </div>
<div class="mt-3"> <div class="mt-3">
<p class="mb-2 text-[11px] font-semibold uppercase tracking-wider text-muted">Share links</p> <p class="mb-2 text-[11px] font-medium uppercase tracking-wide text-muted">Partager</p>
<LinkFormats item={result} size="md" /> <LinkFormats item={result} size="md" />
</div> </div>
</div> </div>
+5 -5
View File
@@ -27,13 +27,13 @@
</script> </script>
{#if visible} {#if visible}
<div class="fixed bottom-5 left-1/2 z-50 -translate-x-1/2 px-4" transition:fly={{ y: 12, duration: 180 }}> <div class="fixed bottom-5 left-1/2 z-50 -translate-x-1/2 px-4" transition:fly={{ y: 8, duration: 160 }}>
<div <div
class="rounded-xl border px-4 py-3 text-sm shadow-2xl backdrop-blur-md {kind === 'error' class="rounded-md border px-4 py-2.5 text-sm {kind === 'error'
? 'border-danger/40 bg-danger/15 text-danger' ? 'border-danger/40 bg-surface text-danger'
: kind === 'success' : kind === 'success'
? 'border-ok/40 bg-ok/15 text-ok' ? 'border-ok/40 bg-surface text-ok'
: 'border-edge bg-raised/95 text-ink'}" : 'border-edge bg-surface text-ink'}"
> >
{message} {message}
</div> </div>
+11 -18
View File
@@ -4,26 +4,19 @@ export default {
theme: { theme: {
extend: { extend: {
colors: { colors: {
base: '#0b0d12', base: '#fafafa',
surface: '#12151c', surface: '#ffffff',
raised: '#171b24', raised: '#f5f5f5',
edge: '#232936', edge: '#e5e5e5',
ink: '#e8ecf3', ink: '#171717',
muted: '#98a1b3', muted: '#737373',
accent: '#5b8cff', accent: '#171717',
'accent-strong': '#3f6ef0', 'accent-strong': '#0a0a0a',
danger: '#ef5b5b', danger: '#dc2626',
ok: '#46b684' ok: '#16a34a'
}, },
fontFamily: { fontFamily: {
sans: [ sans: ['system-ui', '-apple-system', 'Segoe UI', 'Roboto', 'sans-serif']
'Inter',
'system-ui',
'-apple-system',
'Segoe UI',
'Roboto',
'sans-serif'
]
} }
} }
}, },