review theme
This commit is contained in:
+23
-23
@@ -106,45 +106,45 @@ pub async fn auth(
|
||||
|
||||
pub fn render_password_form(key: &str, filename: &str, error: bool) -> Response {
|
||||
let message = if error {
|
||||
"<p class=\"err\">Incorrect password. Please try again.</p>"
|
||||
"<p class=\"err\">Mot de passe incorrect. Réessayez.</p>"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let safe_filename = escape_html(filename);
|
||||
let html = format!(
|
||||
r#"<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="referrer" content="no-referrer" />
|
||||
<title>Protected file</title>
|
||||
<title>Fichier protégé</title>
|
||||
<style>
|
||||
* {{ box-sizing: border-box; }}
|
||||
body {{
|
||||
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;
|
||||
}}
|
||||
.card {{
|
||||
width: 100%; max-width: 400px; background: #15181d; border: 1px solid #262b33;
|
||||
border-radius: 14px; padding: 28px; box-shadow: 0 10px 40px rgba(0,0,0,.4);
|
||||
width: 100%; max-width: 400px; background: #ffffff; border: 1px solid #e5e5e5;
|
||||
border-radius: 8px; padding: 28px;
|
||||
}}
|
||||
.brand {{ display: flex; align-items: center; gap: 10px; margin-bottom: 18px; }}
|
||||
.brand img {{ width: 34px; height: 34px; }}
|
||||
.brand span {{ font-size: 15px; font-weight: 650; }}
|
||||
h1 {{ margin: 0 0 6px; font-size: 20px; }}
|
||||
p {{ margin: 0 0 18px; color: #98a1af; font-size: 14px; word-break: break-all; }}
|
||||
.err {{ margin: 0 0 14px; color: #e05555; background: rgba(224,85,85,.08);
|
||||
border: 1px solid rgba(224,85,85,.3); border-radius: 8px; padding: 10px 12px; }}
|
||||
.brand {{ display: flex; align-items: center; gap: 10px; margin-bottom: 20px; }}
|
||||
.brand img {{ width: 32px; height: 32px; }}
|
||||
.brand span {{ font-size: 15px; font-weight: 600; }}
|
||||
h1 {{ margin: 0 0 6px; font-size: 18px; }}
|
||||
p {{ margin: 0 0 18px; color: #737373; font-size: 14px; word-break: break-all; }}
|
||||
.err {{ margin: 0 0 14px; color: #dc2626; background: #fef2f2;
|
||||
border: 1px solid #fecaca; border-radius: 6px; padding: 10px 12px; }}
|
||||
input[type="password"] {{
|
||||
width: 100%; background: #0e1013; border: 1px solid #262b33; color: #e7eaf0;
|
||||
border-radius: 8px; padding: 12px 13px; font-size: 14px; margin-bottom: 12px;
|
||||
width: 100%; background: #ffffff; border: 1px solid #e5e5e5; color: #171717;
|
||||
border-radius: 6px; padding: 11px 12px; font-size: 14px; margin-bottom: 12px;
|
||||
}}
|
||||
input[type="password"]:focus {{ outline: none; border-color: #3f7bf6; }}
|
||||
button {{ width: 100%; font: inherit; cursor: pointer; border: none; border-radius: 8px;
|
||||
padding: 12px 14px; font-size: 14px; font-weight: 600; background: #3f7bf6; color: #fff; }}
|
||||
button:hover {{ background: #2f63d8; }}
|
||||
input[type="password"]:focus {{ outline: none; border-color: #171717; }}
|
||||
button {{ width: 100%; font: inherit; cursor: pointer; border: none; border-radius: 6px;
|
||||
padding: 11px 14px; font-size: 14px; font-weight: 500; background: #171717; color: #fff; }}
|
||||
button:hover {{ background: #0a0a0a; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -153,12 +153,12 @@ pub fn render_password_form(key: &str, filename: &str, error: bool) -> Response
|
||||
<img src="/logo.svg" alt="logo" />
|
||||
<span>File Drop</span>
|
||||
</div>
|
||||
<h1>Protected file</h1>
|
||||
<p>Enter the password to download <strong>{safe_filename}</strong>.</p>
|
||||
<h1>Fichier protégé</h1>
|
||||
<p>Entrez le mot de passe pour télécharger <strong>{safe_filename}</strong>.</p>
|
||||
{message}
|
||||
<form method="post" action="/api/auth/{key}">
|
||||
<input type="password" name="password" placeholder="Password" autofocus autocomplete="off" />
|
||||
<button type="submit">Download</button>
|
||||
<input type="password" name="password" placeholder="Mot de passe" autofocus autocomplete="off" />
|
||||
<button type="submit">Télécharger</button>
|
||||
</form>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<defs>
|
||||
<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)"/>
|
||||
<rect x="4" y="4" width="56" height="56" rx="14" fill="#171717"/>
|
||||
<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"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 624 B After Width: | Height: | Size: 413 B |
@@ -1,11 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<defs>
|
||||
<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)"/>
|
||||
<rect x="4" y="4" width="56" height="56" rx="14" fill="#171717"/>
|
||||
<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"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 624 B After Width: | Height: | Size: 413 B |
+19
-22
@@ -21,7 +21,7 @@
|
||||
const file = e.detail;
|
||||
if (!file) return;
|
||||
if (file.size === 0) {
|
||||
notify('Empty files are not supported.', 'error');
|
||||
notify('Les fichiers vides ne sont pas pris en charge.', 'error');
|
||||
return;
|
||||
}
|
||||
currentFile = file;
|
||||
@@ -49,11 +49,11 @@
|
||||
createdAt: new Date().toISOString(),
|
||||
protected: !!data.protected
|
||||
});
|
||||
notify('Upload complete.', 'success');
|
||||
notify('Envoi terminé.', 'success');
|
||||
currentFile = null;
|
||||
password = '';
|
||||
} catch (e) {
|
||||
notify(e.message || 'Upload failed.', 'error');
|
||||
notify(e.message || 'Échec de l\'envoi.', 'error');
|
||||
} finally {
|
||||
uploading = false;
|
||||
progress = 0;
|
||||
@@ -62,33 +62,33 @@
|
||||
</script>
|
||||
|
||||
<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">
|
||||
<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>
|
||||
<h1 class="text-lg font-bold leading-tight text-ink">File Drop</h1>
|
||||
<p class="text-xs text-muted">Private file hosting</p>
|
||||
<h1 class="text-base font-semibold leading-tight text-ink">File Drop</h1>
|
||||
<p class="text-xs text-muted">Hébergement de fichiers privé</p>
|
||||
</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>
|
||||
|
||||
<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">
|
||||
<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} />
|
||||
|
||||
<div class="mt-4">
|
||||
<label for="pw" class="mb-1.5 block text-xs font-semibold text-muted">
|
||||
Password <span class="font-normal opacity-70">(optional)</span>
|
||||
<label for="pw" class="mb-1.5 block text-xs font-medium text-muted">
|
||||
Mot de passe <span class="font-normal opacity-70">(optionnel)</span>
|
||||
</label>
|
||||
<input
|
||||
id="pw"
|
||||
type="password"
|
||||
class="field"
|
||||
bind:value={password}
|
||||
placeholder="Protect the download"
|
||||
placeholder="Protéger le téléchargement"
|
||||
autocomplete="new-password"
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter') submit();
|
||||
@@ -98,26 +98,23 @@
|
||||
|
||||
{#if uploading}
|
||||
<div class="mt-4">
|
||||
<div class="mb-2 flex items-center justify-between gap-2 text-xs text-muted">
|
||||
<span class="truncate">{currentFile ? currentFile.name : 'Uploading…'}</span>
|
||||
<div class="mb-1.5 flex items-center justify-between gap-2 text-xs text-muted">
|
||||
<span class="truncate">{currentFile ? currentFile.name : 'Envoi…'}</span>
|
||||
<span class="tabular-nums">{progress}%</span>
|
||||
</div>
|
||||
<div class="h-1.5 w-full overflow-hidden rounded-full bg-surface">
|
||||
<div
|
||||
class="h-full rounded-full bg-gradient-to-r from-accent to-[#a78bfa] transition-all duration-150"
|
||||
style="width: {progress}%"
|
||||
></div>
|
||||
<div class="h-1 w-full overflow-hidden rounded-full bg-raised">
|
||||
<div class="h-full bg-accent transition-all duration-150" style="width: {progress}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<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}
|
||||
on:click={submit}
|
||||
>
|
||||
{uploading ? 'Uploading…' : 'Upload'}
|
||||
{uploading ? 'Envoi…' : 'Envoyer'}
|
||||
</button>
|
||||
|
||||
<div class="mt-4">
|
||||
|
||||
+9
-42
@@ -11,35 +11,10 @@
|
||||
body {
|
||||
@apply bg-base text-ink font-sans;
|
||||
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 {
|
||||
background: rgba(91, 140, 255, 0.3);
|
||||
}
|
||||
|
||||
::-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;
|
||||
background: rgba(23, 23, 23, 0.08);
|
||||
}
|
||||
|
||||
summary {
|
||||
@@ -53,36 +28,28 @@
|
||||
|
||||
@layer components {
|
||||
.card {
|
||||
@apply rounded-2xl border border-edge bg-surface/80 backdrop-blur-sm;
|
||||
@apply rounded-lg border border-edge bg-surface;
|
||||
}
|
||||
|
||||
.btn {
|
||||
@apply inline-flex items-center justify-center gap-2 rounded-xl text-sm font-semibold
|
||||
transition-all duration-150 disabled:cursor-not-allowed disabled:opacity-50;
|
||||
@apply inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium
|
||||
transition-colors disabled:cursor-not-allowed disabled:opacity-50;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
@apply bg-accent text-white hover:bg-accent-strong
|
||||
shadow-[0_8px_24px_-8px_rgba(91,140,255,0.7)];
|
||||
@apply bg-accent text-white hover:bg-accent-strong;
|
||||
}
|
||||
|
||||
.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 {
|
||||
@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 {
|
||||
@apply w-full rounded-xl border border-edge bg-base/70 px-3.5 py-3 text-sm text-ink
|
||||
placeholder:text-muted/60 outline-none transition focus:border-accent focus:ring-2
|
||||
focus:ring-accent/20;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-gradient {
|
||||
@apply bg-gradient-to-r from-accent to-[#a78bfa] bg-clip-text text-transparent;
|
||||
@apply w-full rounded-md border border-edge bg-surface px-3 py-2.5 text-sm text-ink
|
||||
placeholder:text-muted outline-none transition-colors focus:border-ink;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,8 +50,7 @@
|
||||
</script>
|
||||
|
||||
<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:dragging
|
||||
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' : ''}"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label="Choose a file"
|
||||
@@ -72,20 +71,21 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
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-7 w-7" stroke="currentColor" stroke-width="1.8">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 16V4m0 0L7 9m5-5l5 5" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 16v2a2 2 0 002 2h12a2 2 0 002-2v-2" />
|
||||
<div class="mb-3 flex h-10 w-10 items-center justify-center text-muted">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="h-5 w-5" stroke="currentColor" stroke-width="1.5">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
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>
|
||||
</div>
|
||||
|
||||
{#if file}
|
||||
<p class="max-w-full truncate text-sm font-semibold text-ink">{file.name}</p>
|
||||
<p class="mt-1 text-xs text-muted">Drop another file to replace it</p>
|
||||
<p class="max-w-full truncate text-sm font-medium text-ink">{file.name}</p>
|
||||
<p class="mt-1 text-xs text-muted">Déposez un autre fichier pour le remplacer</p>
|
||||
{:else}
|
||||
<p class="text-sm font-semibold text-ink">Drag & drop a file here</p>
|
||||
<p class="mt-1 text-xs text-muted">or click to browse — up to 10 GB</p>
|
||||
<p class="text-sm font-medium text-ink">Glissez-déposez un fichier ici</p>
|
||||
<p class="mt-1 text-xs text-muted">ou cliquez pour parcourir</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -8,22 +8,22 @@
|
||||
|
||||
async function onDelete(item) {
|
||||
const ok = window.confirm(
|
||||
`Delete "${item.originalName || item.downloadName}" permanently? This cannot be undone.`
|
||||
`Supprimer définitivement « ${item.originalName || item.downloadName} » ?`
|
||||
);
|
||||
if (!ok) return;
|
||||
try {
|
||||
await deleteFile(item);
|
||||
removeUpload(item.id);
|
||||
dispatch('notify', { message: 'File deleted.', kind: 'success' });
|
||||
dispatch('notify', { message: 'Fichier supprimé.', kind: 'success' });
|
||||
} catch (e) {
|
||||
dispatch('notify', { message: e.message || 'Deletion failed.', kind: 'error' });
|
||||
dispatch('notify', { message: e.message || 'Suppression impossible.', kind: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
function onClear() {
|
||||
if ($uploads.length === 0) return;
|
||||
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;
|
||||
clearUploads();
|
||||
@@ -33,33 +33,37 @@
|
||||
<section class="card p-5">
|
||||
<div class="mb-4 flex items-center justify-between gap-3">
|
||||
<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}
|
||||
<span class="text-xs text-muted">{$uploads.length} file{$uploads.length === 1 ? '' : 's'}</span>
|
||||
<span class="text-xs text-muted">{$uploads.length}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if $uploads.length}
|
||||
<button type="button" class="text-xs font-semibold text-muted transition hover:text-danger" on:click={onClear}>
|
||||
Clear list
|
||||
<button type="button" class="text-xs font-medium text-muted transition-colors hover:text-danger" on:click={onClear}>
|
||||
Vider la liste
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if $uploads.length}
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each $uploads as item (item.id)}
|
||||
<FileRow {item} on:delete={(e) => onDelete(e.detail)} />
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<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">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="h-6 w-6 text-muted/60" stroke="currentColor" stroke-width="1.6">
|
||||
<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" />
|
||||
<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" stroke="currentColor" stroke-width="1.5">
|
||||
<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>
|
||||
</div>
|
||||
<p class="text-sm text-muted">No files yet.</p>
|
||||
<p class="mt-1 text-xs text-muted/70">Upload your first file to see it here.</p>
|
||||
<p class="text-sm text-muted">Aucun fichier pour l'instant.</p>
|
||||
<p class="mt-1 text-xs text-muted">Envoyez votre premier fichier pour le voir ici.</p>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
import {
|
||||
forcedDownloadUrl,
|
||||
downloadUrl,
|
||||
displayName,
|
||||
fileExtension,
|
||||
formatBytes,
|
||||
formatDate,
|
||||
copyText
|
||||
@@ -16,8 +14,7 @@
|
||||
|
||||
let copied = false;
|
||||
|
||||
$: name = displayName(item);
|
||||
$: ext = fileExtension(item);
|
||||
$: name = item.originalName || item.downloadName || 'file';
|
||||
|
||||
async function copy() {
|
||||
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="flex min-w-0 items-center gap-3">
|
||||
<div
|
||||
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-accent" stroke="currentColor" stroke-width="1.7">
|
||||
<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" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14 3v4h4" />
|
||||
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-md border border-edge bg-raised">
|
||||
<svg viewBox="0 0 24 24" fill="none" class="h-5 w-5 text-muted" stroke="currentColor" stroke-width="1.5">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
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>
|
||||
{#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 class="min-w-0">
|
||||
<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
|
||||
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'}
|
||||
</span>
|
||||
@@ -59,18 +50,18 @@
|
||||
|
||||
<div class="flex flex-col gap-2 md:items-end">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href={forcedDownloadUrl(item)} class="btn btn-primary px-3 py-2 text-xs">Download</a>
|
||||
<button type="button" class="btn btn-ghost px-3 py-2 text-xs" on:click={copy}>
|
||||
{copied ? 'Copied' : 'Copy'}
|
||||
<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-1.5 text-xs" on:click={copy}>
|
||||
{copied ? 'Copié' : 'Copier'}
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger px-3 py-2 text-xs" on:click={() => dispatch('delete', item)}>
|
||||
Delete
|
||||
<button type="button" class="btn btn-danger px-3 py-1.5 text-xs" on:click={() => dispatch('delete', item)}>
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<details>
|
||||
<summary class="cursor-pointer select-none text-xs font-semibold text-muted transition hover:text-ink">
|
||||
Links ▾
|
||||
<summary class="cursor-pointer select-none text-xs font-medium text-muted transition-colors hover:text-ink">
|
||||
Liens
|
||||
</summary>
|
||||
<div class="mt-2 md:flex md:justify-end">
|
||||
<LinkFormats {item} />
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<button
|
||||
type="button"
|
||||
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)}
|
||||
>
|
||||
{copied === fmt.label ? 'Copied' : fmt.label}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
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';
|
||||
|
||||
export let result;
|
||||
@@ -8,7 +8,7 @@
|
||||
let copied = false;
|
||||
|
||||
$: url = result ? downloadUrl(result) : '';
|
||||
$: name = result ? displayName(result) : '';
|
||||
$: name = result ? result.originalName || result.downloadName || 'file' : '';
|
||||
|
||||
async function copy() {
|
||||
await copyText(url);
|
||||
@@ -18,25 +18,25 @@
|
||||
</script>
|
||||
|
||||
{#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">
|
||||
<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
|
||||
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'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<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}>
|
||||
{copied ? 'Copied' : 'Copy'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,13 +27,13 @@
|
||||
</script>
|
||||
|
||||
{#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
|
||||
class="rounded-xl border px-4 py-3 text-sm shadow-2xl backdrop-blur-md {kind === 'error'
|
||||
? 'border-danger/40 bg-danger/15 text-danger'
|
||||
class="rounded-md border px-4 py-2.5 text-sm {kind === 'error'
|
||||
? 'border-danger/40 bg-surface text-danger'
|
||||
: kind === 'success'
|
||||
? 'border-ok/40 bg-ok/15 text-ok'
|
||||
: 'border-edge bg-raised/95 text-ink'}"
|
||||
? 'border-ok/40 bg-surface text-ok'
|
||||
: 'border-edge bg-surface text-ink'}"
|
||||
>
|
||||
{message}
|
||||
</div>
|
||||
|
||||
+11
-18
@@ -4,26 +4,19 @@ export default {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
base: '#0b0d12',
|
||||
surface: '#12151c',
|
||||
raised: '#171b24',
|
||||
edge: '#232936',
|
||||
ink: '#e8ecf3',
|
||||
muted: '#98a1b3',
|
||||
accent: '#5b8cff',
|
||||
'accent-strong': '#3f6ef0',
|
||||
danger: '#ef5b5b',
|
||||
ok: '#46b684'
|
||||
base: '#fafafa',
|
||||
surface: '#ffffff',
|
||||
raised: '#f5f5f5',
|
||||
edge: '#e5e5e5',
|
||||
ink: '#171717',
|
||||
muted: '#737373',
|
||||
accent: '#171717',
|
||||
'accent-strong': '#0a0a0a',
|
||||
danger: '#dc2626',
|
||||
ok: '#16a34a'
|
||||
},
|
||||
fontFamily: {
|
||||
sans: [
|
||||
'Inter',
|
||||
'system-ui',
|
||||
'-apple-system',
|
||||
'Segoe UI',
|
||||
'Roboto',
|
||||
'sans-serif'
|
||||
]
|
||||
sans: ['system-ui', '-apple-system', 'Segoe UI', 'Roboto', 'sans-serif']
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user