ui: grayscale palette, square corners throughout

Removes all indigo/emerald/amber/red accent colors — everything now
uses the slate ramp. Strips rounded corners from containers, cards,
buttons, inputs, badges; the only remaining rounded element is the
circular logo image itself.

Affordances preserved without color:
- Primary buttons (Publish/Log in/Unlock/Copy link flash): bg-slate-100
  on dark background for high contrast
- Secondary buttons (Copy/Open): bg-slate-800
- Destructive (Delete): outlined with border-slate-500 so it reads as
  different from secondary
- Error messages: uppercase "ERROR" prefix + left border accent
- Success flash / ready drop state: uppercase "PUBLISHED"/"READY" labels
  and solid border-slate-200 vs. the dashed/empty state
- Protected lock: amber badge swapped for a slate-100 badge overlay
- Chart: slate-200 line on slate-400/20 fill instead of indigo

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-22 23:28:21 +02:00
parent 908b1381ba
commit 2b0ab836fe
5 changed files with 62 additions and 55 deletions

View File

@@ -109,8 +109,8 @@ func renderChart(series []store.DayPoint) string {
}
fmt.Fprintf(&area, "L%.1f,%d Z", float64(padLeft)+float64(n-1)*step, padTop+innerH)
fmt.Fprintf(&b, `<path d="%s" fill="rgb(99 102 241 / 0.15)" stroke="none"/>`, area.String())
fmt.Fprintf(&b, `<polyline fill="none" stroke="rgb(99 102 241)" stroke-width="2" points="%s"/>`, pts.String())
fmt.Fprintf(&b, `<path d="%s" fill="rgb(148 163 184 / 0.2)" stroke="none"/>`, area.String())
fmt.Fprintf(&b, `<polyline fill="none" stroke="rgb(226 232 240)" stroke-width="2" points="%s"/>`, pts.String())
// x-axis tick labels: first, middle, last day
labelIdx := []int{0, n / 2, n - 1}

View File

@@ -5,33 +5,33 @@
<!-- Stat tiles -->
<section class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-8">
<div class="rounded border border-slate-800 bg-slate-900/50 px-4 py-3">
<div class="border border-slate-800 bg-slate-900/50 px-4 py-3">
<div class="text-xs uppercase tracking-wide text-slate-400">Artifacts</div>
<div class="text-2xl font-semibold mt-1">{{ .Stats.TotalArtifacts }}</div>
</div>
<div class="rounded border border-slate-800 bg-slate-900/50 px-4 py-3">
<div class="border border-slate-800 bg-slate-900/50 px-4 py-3">
<div class="text-xs uppercase tracking-wide text-slate-400">Views (7d)</div>
<div class="text-2xl font-semibold mt-1">{{ .Stats.Views7d }}</div>
</div>
<div class="rounded border border-slate-800 bg-slate-900/50 px-4 py-3">
<div class="border border-slate-800 bg-slate-900/50 px-4 py-3">
<div class="text-xs uppercase tracking-wide text-slate-400">Views (30d)</div>
<div class="text-2xl font-semibold mt-1">{{ .Stats.Views30d }}</div>
</div>
<div class="rounded border border-slate-800 bg-slate-900/50 px-4 py-3">
<div class="border border-slate-800 bg-slate-900/50 px-4 py-3">
<div class="text-xs uppercase tracking-wide text-slate-400">Storage</div>
<div class="text-2xl font-semibold mt-1">{{ humanSize .Stats.StorageBytes }}</div>
</div>
</section>
<!-- Publish form -->
<section class="rounded border border-slate-800 bg-slate-900/50 p-4 mb-8">
<section class="border border-slate-800 bg-slate-900/50 p-4 mb-8">
<h2 class="text-lg font-semibold mb-3">Publish</h2>
<form method="post" action="/api/publish" enctype="multipart/form-data" class="space-y-3" id="publishForm">
<label for="dropInput"
id="dropZone"
class="relative flex flex-col items-center justify-center text-center cursor-pointer
border-2 border-dashed border-slate-700 hover:border-indigo-500
rounded-lg bg-slate-950/50 hover:bg-slate-950
border-2 border-dashed border-slate-700 hover:border-slate-400
bg-slate-950/50 hover:bg-slate-950
transition-[border-color,background-color,transform] duration-150
px-6 py-12 text-sm text-slate-400">
@@ -41,22 +41,22 @@
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M7 16a4 4 0 01-.88-7.9A5 5 0 0115.9 6.56 4 4 0 1118 16M8 13l4-4m0 0l4 4m-4-4v10"/>
</svg>
<div><span class="text-indigo-300 font-medium">Drop HTML file here</span> or click to browse</div>
<div><span class="text-slate-100 font-medium">Drop HTML file here</span> or click to browse</div>
<div class="text-xs text-slate-500 mt-1">.html · up to 5 MB</div>
</div>
<!-- Ready state (populated after a file is chosen) -->
<div class="hidden flex-col items-center w-full max-w-md" data-state="ready">
<div class="relative mb-3">
<svg class="w-12 h-12 text-emerald-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-12 h-12 text-slate-100" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
</svg>
<span class="absolute -bottom-1 -right-1 inline-flex items-center justify-center w-5 h-5 rounded-full bg-emerald-500 text-slate-950 text-[10px] font-bold ring-2 ring-slate-900"></span>
<span class="absolute -bottom-1 -right-1 inline-flex items-center justify-center w-5 h-5 bg-slate-100 text-slate-950 text-[10px] font-bold ring-2 ring-slate-900"></span>
</div>
<div class="flex items-center gap-2">
<span class="text-xs uppercase tracking-wider text-emerald-400 font-semibold">Ready</span>
<span class="w-1 h-1 rounded-full bg-slate-700"></span>
<span class="text-xs uppercase tracking-wider text-slate-200 font-semibold">Ready</span>
<span class="w-1 h-1 bg-slate-700"></span>
<span class="text-xs text-slate-500" id="dropSize"></span>
</div>
<div class="font-medium text-slate-100 mt-1 truncate max-w-full" id="dropFilename">file.html</div>
@@ -68,11 +68,14 @@
<!-- Error state -->
<div class="hidden flex-col items-center" data-state="error">
<svg class="w-10 h-10 mb-3 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg class="w-10 h-10 mb-3 text-slate-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
d="M12 9v2m0 4h.01M4.93 4.93l14.14 14.14M12 21a9 9 0 110-18 9 9 0 010 18z"/>
</svg>
<div class="text-red-300 font-medium" id="dropError">Only .html files are accepted</div>
<div class="text-slate-100 font-semibold" id="dropError">
<span class="uppercase tracking-wider text-xs text-slate-300 mr-2">Error</span>
<span id="dropErrorMsg">Only .html files are accepted</span>
</div>
<div class="text-xs text-slate-500 mt-1">try again</div>
</div>
@@ -82,21 +85,21 @@
<label class="block">
<span class="block text-xs text-slate-400 mb-1">Custom slug (optional)</span>
<input type="text" name="slug" pattern="[a-z0-9][a-z0-9-]{2,31}" placeholder="auto"
class="w-full bg-slate-950 border border-slate-800 rounded px-3 py-2 text-sm">
class="w-full bg-slate-950 border border-slate-800 px-3 py-2 text-sm focus:outline-none focus:border-slate-400">
</label>
<label class="block">
<span class="block text-xs text-slate-400 mb-1">Title (optional)</span>
<input type="text" name="title" placeholder="from &lt;title&gt; if blank"
class="w-full bg-slate-950 border border-slate-800 rounded px-3 py-2 text-sm">
class="w-full bg-slate-950 border border-slate-800 px-3 py-2 text-sm focus:outline-none focus:border-slate-400">
</label>
<label class="block">
<span class="block text-xs text-slate-400 mb-1">Password (optional)</span>
<input type="password" name="password" autocomplete="new-password"
class="w-full bg-slate-950 border border-slate-800 rounded px-3 py-2 text-sm">
class="w-full bg-slate-950 border border-slate-800 px-3 py-2 text-sm focus:outline-none focus:border-slate-400">
</label>
<label class="block">
<span class="block text-xs text-slate-400 mb-1">Expires</span>
<select name="expires_in" class="w-full bg-slate-950 border border-slate-800 rounded px-3 py-2 text-sm">
<select name="expires_in" class="w-full bg-slate-950 border border-slate-800 px-3 py-2 text-sm focus:outline-none focus:border-slate-400">
<option value="never">never</option>
<option value="1h">1 hour</option>
<option value="24h">1 day</option>
@@ -106,7 +109,7 @@
</label>
</div>
<div class="flex items-center justify-end">
<button class="bg-indigo-600 hover:bg-indigo-500 text-white rounded px-4 py-2 font-medium text-sm">Publish</button>
<button class="bg-slate-100 hover:bg-white text-slate-950 px-4 py-2 font-medium text-sm">Publish</button>
</div>
</form>
</section>
@@ -117,7 +120,7 @@
const input = document.getElementById('dropInput');
const filename = document.getElementById('dropFilename');
const sizeEl = document.getElementById('dropSize');
const errorEl = document.getElementById('dropError');
const errorMsg = document.getElementById('dropErrorMsg');
const clearBtn = document.getElementById('dropClear');
if (!zone || !input) return;
@@ -127,9 +130,9 @@
error: zone.querySelector('[data-state="error"]'),
};
const READY = ['border-emerald-500', '!border-solid', 'bg-emerald-500/5'];
const OVER = ['border-indigo-500', 'bg-indigo-500/10', 'scale-[1.01]'];
const ERROR = ['border-red-500', 'bg-red-500/5'];
const READY = ['border-slate-200', '!border-solid', 'bg-slate-400/10'];
const OVER = ['border-slate-300', 'bg-slate-400/15', 'scale-[1.01]'];
const ERROR = ['border-slate-200', 'bg-slate-400/10'];
function setState(name) {
for (const key of Object.keys(panels)) {
@@ -168,17 +171,16 @@
function handleFile(f) {
if (!/\.html?$/i.test(f.name) && f.type !== 'text/html') {
errorEl.textContent = `Not an HTML file: ${f.name}`;
errorMsg.textContent = `Not an HTML file: ${f.name}`;
setState('error');
setTimeout(() => { if (!input.files[0]) setState('empty'); }, 2500);
return;
}
if (f.size > 5 * 1024 * 1024) {
errorEl.textContent = `Too large (${fmtSize(f.size)}, max 5 MB)`;
errorMsg.textContent = `Too large (${fmtSize(f.size)}, max 5 MB)`;
setState('error');
return;
}
// Reflect drop selection back into the input so the form submits it.
try {
const dt = new DataTransfer();
dt.items.add(f);
@@ -198,17 +200,18 @@
</script>
{{ if .FlashSlug }}
<div class="rounded border border-emerald-800 bg-emerald-900/30 text-emerald-100 px-4 py-3 mb-6 text-sm flex items-center justify-between">
<div class="border-l-2 border-slate-100 bg-slate-900 text-slate-100 px-4 py-3 mb-6 text-sm flex items-center justify-between">
<div>
Published: <code class="bg-slate-900 px-2 py-0.5 rounded">{{ .BaseURL }}/p/{{ .FlashSlug }}</code>
<span class="uppercase tracking-wider text-xs font-bold text-slate-300 mr-2">Published</span>
<code class="bg-slate-950 border border-slate-800 px-2 py-0.5">{{ .BaseURL }}/p/{{ .FlashSlug }}</code>
</div>
<button onclick="navigator.clipboard.writeText('{{ .BaseURL }}/p/{{ .FlashSlug }}')"
class="bg-emerald-700 hover:bg-emerald-600 rounded px-3 py-1 text-xs">Copy link</button>
class="bg-slate-100 hover:bg-white text-slate-950 px-3 py-1 text-xs font-medium">Copy link</button>
</div>
{{ end }}
<!-- Artifact list -->
<section class="rounded border border-slate-800 bg-slate-900/30 overflow-hidden">
<section class="border border-slate-800 bg-slate-900/30 overflow-hidden">
<table class="w-full text-sm">
<thead class="bg-slate-900 text-slate-400 text-xs uppercase tracking-wide">
<tr>
@@ -225,25 +228,25 @@
{{ range .Items }}
<tr id="row-{{ .Slug }}" class="border-t border-slate-800 hover:bg-slate-900/50">
<td class="px-4 py-2">
<a class="font-mono text-indigo-300 hover:text-indigo-200" href="/a/{{ .Slug }}">{{ .Slug }}</a>
{{ if .HasPassword }}<span class="ml-1 text-xs text-amber-400" title="Password-protected">🔒</span>{{ end }}
<a class="font-mono text-slate-100 underline decoration-slate-600 hover:decoration-slate-200" href="/a/{{ .Slug }}">{{ .Slug }}</a>
{{ if .HasPassword }}<span class="ml-1 text-xs text-slate-400" title="Password-protected">🔒</span>{{ end }}
</td>
<td class="px-4 py-2 text-slate-300">{{ if .Title }}{{ .Title }}{{ else }}<span class="text-slate-500"></span>{{ end }}</td>
<td class="px-4 py-2 text-right tabular-nums">{{ .ViewCount }}</td>
<td class="px-4 py-2 text-slate-400">{{ humanTime .LastViewedAt }}</td>
<td class="px-4 py-2 spark text-indigo-300">{{ sparkline .Spark }}</td>
<td class="px-4 py-2 spark text-slate-200">{{ sparkline .Spark }}</td>
<td class="px-4 py-2 text-slate-400">{{ humanSize .SizeBytes }}</td>
<td class="px-4 py-2 text-right">
<td class="px-4 py-2 text-right whitespace-nowrap">
<button onclick="navigator.clipboard.writeText('{{ $.BaseURL }}/p/{{ .Slug }}')"
class="text-xs px-2 py-1 rounded bg-slate-800 hover:bg-slate-700">Copy</button>
<a href="/p/{{ .Slug }}" target="_blank" class="text-xs px-2 py-1 rounded bg-slate-800 hover:bg-slate-700">Open</a>
class="text-xs px-2 py-1 bg-slate-800 hover:bg-slate-700">Copy</button>
<a href="/p/{{ .Slug }}" target="_blank" class="text-xs px-2 py-1 bg-slate-800 hover:bg-slate-700">Open</a>
<button hx-delete="/a/{{ .Slug }}" hx-target="#row-{{ .Slug }}" hx-swap="outerHTML"
hx-confirm="Delete {{ .Slug }}? This cannot be undone."
class="text-xs px-2 py-1 rounded bg-red-900/60 hover:bg-red-800">Delete</button>
class="text-xs px-2 py-1 border border-slate-500 text-slate-200 hover:bg-slate-800">Delete</button>
</td>
</tr>
{{ else }}
<tr><td colspan="7" class="px-4 py-10 text-center text-slate-500">No artifacts yet. Paste some HTML above to publish your first one.</td></tr>
<tr><td colspan="7" class="px-4 py-10 text-center text-slate-500">No artifacts yet. Drop an HTML file above to publish your first one.</td></tr>
{{ end }}
</tbody>
</table>

View File

@@ -6,7 +6,7 @@
<div class="flex items-start justify-between mb-6">
<div>
<h1 class="text-2xl font-semibold font-mono">{{ .Artifact.Slug }} {{ if .Artifact.HasPassword }}<span class="text-base text-amber-400 align-middle">🔒</span>{{ end }}</h1>
<h1 class="text-2xl font-semibold font-mono">{{ .Artifact.Slug }} {{ if .Artifact.HasPassword }}<span class="text-base text-slate-400 align-middle">🔒</span>{{ end }}</h1>
{{ if .Artifact.Title }}<p class="text-slate-400 mt-1">{{ .Artifact.Title }}</p>{{ end }}
<p class="text-xs text-slate-500 mt-2">
Created {{ humanTime .Artifact.CreatedAt }} ·
@@ -14,23 +14,23 @@
{{ .Artifact.ViewCount }} views
{{ if .Artifact.ExpiresAt }} · expires {{ humanTime .Artifact.ExpiresAt }}{{ end }}
</p>
<p class="mt-2 text-sm"><a class="text-indigo-300 hover:text-indigo-200" href="{{ .BaseURL }}/p/{{ .Artifact.Slug }}" target="_blank">{{ .BaseURL }}/p/{{ .Artifact.Slug }}</a></p>
<p class="mt-2 text-sm"><a class="text-slate-100 underline decoration-slate-600 hover:decoration-slate-200" href="{{ .BaseURL }}/p/{{ .Artifact.Slug }}" target="_blank">{{ .BaseURL }}/p/{{ .Artifact.Slug }}</a></p>
</div>
<div class="flex gap-2">
<button onclick="navigator.clipboard.writeText('{{ .BaseURL }}/p/{{ .Artifact.Slug }}')"
class="text-sm px-3 py-1.5 rounded bg-slate-800 hover:bg-slate-700">Copy link</button>
class="text-sm px-3 py-1.5 bg-slate-800 hover:bg-slate-700">Copy link</button>
<button hx-delete="/a/{{ .Artifact.Slug }}" hx-confirm="Delete {{ .Artifact.Slug }}? This cannot be undone."
hx-on::after-request="window.location='/'"
class="text-sm px-3 py-1.5 rounded bg-red-900/60 hover:bg-red-800">Delete</button>
class="text-sm px-3 py-1.5 border border-slate-500 text-slate-200 hover:bg-slate-800">Delete</button>
</div>
</div>
<section class="rounded border border-slate-800 bg-slate-900/30 p-4 mb-6">
<section class="border border-slate-800 bg-slate-900/30 p-4 mb-6">
<h2 class="text-sm uppercase tracking-wide text-slate-400 mb-3">Views — last 30 days</h2>
{{ .ChartSVG }}
</section>
<section class="rounded border border-slate-800 bg-slate-900/30 p-4">
<section class="border border-slate-800 bg-slate-900/30 p-4">
<h2 class="text-sm uppercase tracking-wide text-slate-400 mb-3">Top referrers</h2>
{{ if .Referrers }}
<table class="w-full text-sm">

View File

@@ -4,13 +4,15 @@
<div class="w-full max-w-sm">
<div class="flex flex-col items-center text-center mb-8">
<img src="/static/logo.png" alt=""
class="w-20 h-20 rounded-full ring-1 ring-slate-700 mb-4 shadow-lg shadow-indigo-500/10">
class="w-20 h-20 rounded-full ring-1 ring-slate-700 mb-4 shadow-lg shadow-black/40">
<h1 class="text-3xl font-semibold tracking-tight">Artifacto</h1>
<p class="text-sm text-slate-400 mt-1">self-hosted HTML artifact publisher</p>
</div>
{{ if .Error }}
<div class="rounded bg-red-900/40 border border-red-800 text-red-200 px-3 py-2 mb-3 text-sm">{{ .Error }}</div>
<div class="border-l-2 border-slate-100 bg-slate-900 text-slate-100 px-3 py-2 mb-3 text-sm">
<span class="uppercase tracking-wider text-xs font-bold text-slate-300 mr-2">Error</span>{{ .Error }}
</div>
{{ end }}
<form method="post" action="/login" class="space-y-3">
@@ -18,9 +20,9 @@
<label class="block">
<span class="block text-sm text-slate-400 mb-1">Password</span>
<input type="password" name="password" required autofocus autocomplete="current-password"
class="w-full bg-slate-900 border border-slate-800 rounded px-3 py-2 focus:outline-none focus:border-indigo-500">
class="w-full bg-slate-900 border border-slate-800 px-3 py-2 focus:outline-none focus:border-slate-400">
</label>
<button class="w-full bg-indigo-600 hover:bg-indigo-500 text-white rounded px-3 py-2 font-medium">Log in</button>
<button class="w-full bg-slate-100 hover:bg-white text-slate-950 px-3 py-2 font-medium">Log in</button>
</form>
</div>
</main>

View File

@@ -5,8 +5,8 @@
<div class="flex flex-col items-center text-center mb-8">
<div class="relative mb-4">
<img src="/static/logo.png" alt=""
class="w-20 h-20 rounded-full ring-1 ring-slate-700 shadow-lg shadow-indigo-500/10">
<span class="absolute -bottom-1 -right-1 inline-flex items-center justify-center w-8 h-8 rounded-full bg-amber-500 text-slate-950 ring-2 ring-slate-950">
class="w-20 h-20 rounded-full ring-1 ring-slate-700 shadow-lg shadow-black/40">
<span class="absolute -bottom-1 -right-1 inline-flex items-center justify-center w-8 h-8 bg-slate-100 text-slate-950 ring-2 ring-slate-950">
<svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round"
d="M12 11v2m-4 4h8a2 2 0 002-2v-5a2 2 0 00-2-2H8a2 2 0 00-2 2v5a2 2 0 002 2zM10 9V7a2 2 0 114 0v2"/>
@@ -18,16 +18,18 @@
</div>
{{ if .Error }}
<div class="rounded bg-red-900/40 border border-red-800 text-red-200 px-3 py-2 mb-3 text-sm">{{ .Error }}</div>
<div class="border-l-2 border-slate-100 bg-slate-900 text-slate-100 px-3 py-2 mb-3 text-sm">
<span class="uppercase tracking-wider text-xs font-bold text-slate-300 mr-2">Error</span>{{ .Error }}
</div>
{{ end }}
<form method="post" action="/p/{{ .Slug }}/unlock" class="space-y-3">
<label class="block">
<span class="block text-sm text-slate-400 mb-1">Password</span>
<input type="password" name="password" required autofocus autocomplete="off"
class="w-full bg-slate-900 border border-slate-800 rounded px-3 py-2 focus:outline-none focus:border-indigo-500">
class="w-full bg-slate-900 border border-slate-800 px-3 py-2 focus:outline-none focus:border-slate-400">
</label>
<button class="w-full bg-indigo-600 hover:bg-indigo-500 text-white rounded px-3 py-2 font-medium">Unlock</button>
<button class="w-full bg-slate-100 hover:bg-white text-slate-950 px-3 py-2 font-medium">Unlock</button>
</form>
</div>
</main>