2 Commits
Author SHA1 Message Date
sittichok OuamsiriandClaude Opus 5.5 0b5b811d17 feat(redirect): restyle PIN and 404 pages to match the redesign
Ink palette, no gradients/glow, same card/field/button tokens as the SPA,
custom ease-out, press feedback, 'Unlocking…' state on auto-submit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 23:17:08 +07:00
sittichok OuamsiriandClaude Opus 5.5 f788ef3588 ci: bump workflow-templates to debabb1, drop registry override
Template now defaults to 10.2.0.10:30008 itself.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-10 00:26:59 +07:00
3 changed files with 80 additions and 86 deletions

No files matched your search

+2 -5
View File
@@ -12,20 +12,17 @@ permissions:
jobs:
trigger-internal-build:
uses: infras/workflow-templates/.gitea/workflows/docker-template.yml@736cd715b4b9c80f20ec51ef8445cb630c8299f5
uses: infras/workflow-templates/.gitea/workflows/docker-template.yml@debabb17cbf09dfadaafa14f9bcab88bd8a3512e
with:
image_name: 'snip'
image_tag: ${{ gitea.ref_name }}
# Template default (192.168.1.17) is the pre-move LAN address; the
# registry (Gitea on TrueNAS) now lives on 10.2.0.10.
registry_domain: '10.2.0.10:30008'
secrets:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
bump-compose:
needs: trigger-internal-build
uses: infras/workflow-templates/.gitea/workflows/bump-compose-version.yml@736cd715b4b9c80f20ec51ef8445cb630c8299f5
uses: infras/workflow-templates/.gitea/workflows/bump-compose-version.yml@debabb17cbf09dfadaafa14f9bcab88bd8a3512e
with:
app: snip
updates: |
+24 -24
View File
@@ -8,29 +8,29 @@ func notFoundHTML(shortHost, homeURL string) string {
href := html.EscapeString(homeURL)
return `<!doctype html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="theme-color" content="#fbfbfa" media="(prefers-color-scheme:light)">
<meta name="theme-color" content="#09090b" media="(prefers-color-scheme:dark)">
<meta name="theme-color" content="#fafafa" media="(prefers-color-scheme:light)">
<meta name="theme-color" content="#0a0a0b" media="(prefers-color-scheme:dark)">
<title>Link not found</title><style>
:root{--bg:#fbfbfa;--surface:#fff;--ink:#18181b;--muted:#71717a;--line:#e9e9eb;
--grad-from:#2563eb;--grad-to:#7c3aed;--grad-ink:#fff;--glow-1:rgba(37,99,235,.16);--glow-2:rgba(124,58,237,.14)}
@media(prefers-color-scheme:dark){:root{--bg:#09090b;--surface:#111113;--ink:#fafafa;--muted:#a1a1aa;--line:#232327;
--grad-from:#60a5fa;--grad-to:#a78bfa;--grad-ink:#09090b;--glow-1:rgba(96,165,250,.13);--glow-2:rgba(167,139,250,.12)}}
body{margin:0;min-height:100vh;display:grid;place-items:center;background-color:var(--bg);color:var(--ink);
font-family:ui-sans-serif,system-ui,-apple-system,sans-serif;-webkit-font-smoothing:antialiased;
text-align:center;padding:20px;
background-image:radial-gradient(60% 45% at 12% 0%,var(--glow-1),transparent 70%),
radial-gradient(55% 40% at 92% 100%,var(--glow-2),transparent 70%);
background-attachment:fixed;background-repeat:no-repeat}
.card{background:var(--surface);border:1px solid var(--line);border-radius:16px;padding:32px;width:100%;max-width:360px}
h1{font-size:44px;font-weight:600;margin:0;letter-spacing:-.03em;
background-image:linear-gradient(135deg,var(--grad-from),var(--grad-to));
-webkit-background-clip:text;background-clip:text;color:transparent}
p{color:var(--muted);font-size:14px;line-height:1.5;margin:8px 0 20px}
a{display:block;background-image:linear-gradient(135deg,var(--grad-from),var(--grad-to));color:var(--grad-ink);
text-decoration:none;font-size:15px;font-weight:500;
padding:13px 20px;border-radius:12px;transition:opacity .15s}
a:hover{opacity:.9}
</style></head><body><div class="card"><h1>404</h1>
<p>This short link doesn't exist or was removed.</p>
<a href="` + href + `">Go to ` + h + `</a></div></body></html>`
:root{--bg:#fafafa;--surface:#fff;--ink:#111113;--muted:#6f6f78;--line:#e7e7ea;
--shadow:0 1px 2px rgba(0,0,0,.04),0 2px 8px -2px rgba(0,0,0,.04);--out:cubic-bezier(.23,1,.32,1)}
@media(prefers-color-scheme:dark){:root{--bg:#0a0a0b;--surface:#121214;--ink:#f4f4f5;--muted:#9b9ba4;--line:#242428;
--shadow:0 1px 2px rgba(0,0,0,.4)}}
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
body{margin:0;min-height:100vh;min-height:100dvh;display:grid;place-items:center;background:var(--bg);color:var(--ink);
font-family:Inter,ui-sans-serif,system-ui,-apple-system,sans-serif;-webkit-font-smoothing:antialiased;padding:16px}
.card{background:var(--surface);border:1px solid var(--line);border-radius:16px;padding:28px;width:100%;max-width:384px;
box-shadow:var(--shadow);animation:rise .28s var(--out) backwards}
@keyframes rise{from{opacity:0;transform:translateY(6px)}}
.code{font-family:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;color:var(--muted);margin:0 0 8px}
h1{font-size:22px;font-weight:600;letter-spacing:-.02em;margin:0}
p{color:var(--muted);font-size:14px;line-height:1.5;margin:6px 0 20px}
a{display:flex;align-items:center;justify-content:center;height:48px;background:var(--ink);color:var(--bg);
text-decoration:none;font-size:15px;font-weight:500;border-radius:12px;transition:opacity .15s ease,transform .16s var(--out)}
@media(hover:hover){a:hover{opacity:.85}}
a:active{transform:scale(.97)}
a:focus-visible{outline:none;box-shadow:0 0 0 2px var(--bg),0 0 0 4px var(--ink)}
@media(prefers-reduced-motion:reduce){.card{animation:none}a:active{transform:none}}
</style></head><body><div class="card"><p class="code">404</p><h1>Link not found</h1>
<p>This short link doesn't exist, or it was removed.</p>
<a href="` + href + `">Shorten a link on ` + h + `</a></div></body></html>`
}
+54 -57
View File
@@ -22,81 +22,76 @@ var pinTmpl = template.Must(template.New("pin").Parse(`<!doctype html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#fbfbfa" media="(prefers-color-scheme:light)">
<meta name="theme-color" content="#09090b" media="(prefers-color-scheme:dark)">
<meta name="theme-color" content="#fafafa" media="(prefers-color-scheme:light)">
<meta name="theme-color" content="#0a0a0b" media="(prefers-color-scheme:dark)">
<meta name="robots" content="noindex">
<title>Enter PIN · {{.ShortHost}}/{{.Code}}</title>
<style>
:root{--bg:#fbfbfa;--surface:#fff;--surface-2:#f4f4f5;--ink:#18181b;--muted:#71717a;
--line:#e9e9eb;--accent:#2563eb;--accent-soft:rgba(37,99,235,.1);--danger:#dc2626;
--grad-from:#2563eb;--grad-to:#7c3aed;--grad-ink:#fff;
--glow-1:rgba(37,99,235,.16);--glow-2:rgba(124,58,237,.14)}
@media(prefers-color-scheme:dark){:root{--bg:#09090b;--surface:#111113;--surface-2:#19191c;--ink:#fafafa;
--muted:#a1a1aa;--line:#232327;--accent:#60a5fa;--accent-soft:rgba(96,165,250,.14);--danger:#f87171;
--grad-from:#60a5fa;--grad-to:#a78bfa;--grad-ink:#09090b;
--glow-1:rgba(96,165,250,.13);--glow-2:rgba(167,139,250,.12)}}
:root{--bg:#fafafa;--surface:#fff;--surface-2:#f4f4f5;--ink:#111113;--muted:#6f6f78;--line:#e7e7ea;
--ring:#d4d4d8;--soft:rgba(17,17,19,.08);--danger:#dc2626;
--shadow:0 1px 2px rgba(0,0,0,.04),0 2px 8px -2px rgba(0,0,0,.04);--out:cubic-bezier(.23,1,.32,1)}
@media(prefers-color-scheme:dark){:root{--bg:#0a0a0b;--surface:#121214;--surface-2:#1a1a1d;--ink:#f4f4f5;
--muted:#9b9ba4;--line:#242428;--ring:#33333a;--soft:rgba(244,244,245,.12);--danger:#f87171;
--shadow:0 1px 2px rgba(0,0,0,.4)}}
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
body{margin:0;min-height:100vh;display:grid;place-items:center;padding:20px;
background-color:var(--bg);color:var(--ink);
font-family:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
-webkit-font-smoothing:antialiased;
background-image:radial-gradient(60% 45% at 12% 0%,var(--glow-1),transparent 70%),
radial-gradient(55% 40% at 92% 100%,var(--glow-2),transparent 70%);
background-attachment:fixed;background-repeat:no-repeat}
.card{width:100%;max-width:380px;background:var(--surface);border:1px solid var(--line);
border-radius:16px;padding:24px;animation:rise .3s ease-out}
body{margin:0;min-height:100vh;min-height:100dvh;display:grid;place-items:center;padding:16px;
background:var(--bg);color:var(--ink);
font-family:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
-webkit-font-smoothing:antialiased}
.mono{font-family:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,monospace}
.card{width:100%;max-width:384px;background:var(--surface);border:1px solid var(--line);
border-radius:16px;padding:24px;box-shadow:var(--shadow);animation:rise .28s var(--out) backwards}
@media(min-width:420px){.card{padding:28px}}
@keyframes rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
.lock{width:40px;height:40px;border-radius:12px;color:var(--grad-ink);
background-image:linear-gradient(135deg,var(--grad-from),var(--grad-to));
display:grid;place-items:center;margin-bottom:16px}
.lock svg{width:20px;height:20px}
h1{font-size:20px;font-weight:600;letter-spacing:-.02em;margin:0 0 6px}
h1 span{background-image:linear-gradient(135deg,var(--grad-from),var(--grad-to));
-webkit-background-clip:text;background-clip:text;color:transparent}
@keyframes rise{from{opacity:0;transform:translateY(6px)}}
.lock{width:40px;height:40px;border-radius:12px;background:var(--ink);color:var(--bg);
display:grid;place-items:center;margin-bottom:20px}
.lock svg{width:18px;height:18px}
h1{font-size:22px;font-weight:600;letter-spacing:-.02em;margin:0 0 6px}
.sub{margin:0 0 20px;color:var(--muted);font-size:14px;line-height:1.5}
.sub b{color:var(--ink);font-weight:500;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;word-break:break-all}
.sub b{display:block;margin-top:2px;color:var(--ink);font-weight:600;overflow-wrap:anywhere}
.pins{display:flex;gap:6px}
@media(min-width:420px){.pins{gap:8px}}
.pins input{flex:1;min-width:0;height:52px;padding:0;text-align:center;font-size:20px;font-weight:600;
font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--ink);
background:var(--surface-2);border:1px solid var(--line);border-radius:12px;outline:none;
transition:border-color .15s,box-shadow .15s}
.pins input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
.pins input{flex:1;min-width:0;height:48px;padding:0;text-align:center;font-size:18px;font-weight:600;
color:var(--ink);background:var(--surface);border:1px solid var(--line);border-radius:12px;outline:none;
transition:border-color .15s ease,box-shadow .15s ease}
.pins input:focus{border-color:var(--ring);box-shadow:0 0 0 4px var(--soft)}
.err{color:var(--danger);font-size:13px;font-weight:500;margin:10px 0 0;min-height:19px}
form.bad .pins input{border-color:var(--danger)}
form.bad .pins{animation:shake .4s}
@keyframes shake{25%{transform:translateX(-5px)}50%{transform:translateX(5px)}75%{transform:translateX(-3px)}}
button{width:100%;height:48px;margin-top:14px;border:0;border-radius:12px;
background-image:linear-gradient(135deg,var(--grad-from),var(--grad-to));color:var(--grad-ink);
font-size:15px;font-weight:500;font-family:inherit;
cursor:pointer;transition:opacity .15s,transform .1s}
button:hover{opacity:.9}
button:active{transform:scale(.99)}
:focus-visible{outline:none;box-shadow:0 0 0 2px var(--bg),0 0 0 4px var(--accent)}
.foot{margin-top:18px;text-align:center;font-size:12px;color:var(--muted)}
@media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
form.bad .pins{animation:shake .35s var(--out)}
@keyframes shake{25%{transform:translateX(-4px)}50%{transform:translateX(4px)}75%{transform:translateX(-2px)}}
button{width:100%;height:48px;margin-top:12px;border:0;border-radius:12px;background:var(--ink);color:var(--bg);
font-size:15px;font-weight:500;font-family:inherit;cursor:pointer;
transition:opacity .15s ease,transform .16s var(--out)}
@media(hover:hover){button:hover{opacity:.85}}
button:active{transform:scale(.97)}
button:disabled{opacity:.6;cursor:default}
:focus-visible{outline:none;box-shadow:0 0 0 2px var(--bg),0 0 0 4px var(--ink)}
.foot{margin-top:20px;display:flex;align-items:center;justify-content:center;gap:6px;font-size:12px;color:var(--muted)}
.mark{width:16px;height:16px;border-radius:5px;background:var(--ink);color:var(--bg);display:grid;place-items:center}
.mark svg{width:10px;height:10px}
@media(prefers-reduced-motion:reduce){.card{animation-name:fade}form.bad .pins{animation:none}button:active{transform:none}
@keyframes fade{from{opacity:0}}}
</style>
</head>
<body>
<div class="card">
<div class="lock"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="11" width="16" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg></div>
<h1>This link is <span>protected</span></h1>
<p class="sub">Enter the 6-digit PIN to continue to <b>{{.ShortHost}}/{{.Code}}</b>.</p>
<h1>This link is protected</h1>
<p class="sub">Enter the 6-digit PIN to open <b class="mono">{{.ShortHost}}/{{.Code}}</b>.</p>
<form method="post" action="{{.ActionPath}}" id="f" autocomplete="off"{{if .HasError}} class="bad"{{end}}>
<div class="pins" id="pins">
<input inputmode="numeric" maxlength="1" aria-label="PIN digit 1" required>
<input inputmode="numeric" maxlength="1" aria-label="PIN digit 2" required>
<input inputmode="numeric" maxlength="1" aria-label="PIN digit 3" required>
<input inputmode="numeric" maxlength="1" aria-label="PIN digit 4" required>
<input inputmode="numeric" maxlength="1" aria-label="PIN digit 5" required>
<input inputmode="numeric" maxlength="1" aria-label="PIN digit 6" required>
<input class="mono" inputmode="numeric" maxlength="1" aria-label="PIN digit 1" autocomplete="one-time-code" required>
<input class="mono" inputmode="numeric" maxlength="1" aria-label="PIN digit 2" required>
<input class="mono" inputmode="numeric" maxlength="1" aria-label="PIN digit 3" required>
<input class="mono" inputmode="numeric" maxlength="1" aria-label="PIN digit 4" required>
<input class="mono" inputmode="numeric" maxlength="1" aria-label="PIN digit 5" required>
<input class="mono" inputmode="numeric" maxlength="1" aria-label="PIN digit 6" required>
</div>
<p class="err" role="alert">{{if .HasError}}That PIN didn't match. Try again.{{end}}</p>
<input type="hidden" name="pin" id="pin">
<button type="submit">Unlock</button>
<button type="submit" id="go">Unlock</button>
</form>
<div class="foot">{{.ShortHost}}</div>
<div class="foot"><span class="mark"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M20 4 8.12 15.88M14.47 14.48 20 20M8.12 8.12 12 12"/></svg></span>snip</div>
</div>
<script>
(function(){
@@ -110,7 +105,7 @@ button:active{transform:scale(.99)}
b.value=b.value.replace(/\D/g,'').slice(0,1);
if(b.value&&i<boxes.length-1)boxes[i+1].focus();
sync();
if(hidden.value.length===6)f.submit();
if(hidden.value.length===6)go();
});
b.addEventListener('focus',function(){b.select()});
b.addEventListener('keydown',function(e){if(e.key==='Backspace'&&!b.value&&i>0){boxes[i-1].focus();fresh()}});
@@ -118,10 +113,12 @@ button:active{transform:scale(.99)}
var d=(e.clipboardData.getData('text')||'').replace(/\D/g,'').slice(0,6);
if(!d)return;e.preventDefault();fresh();
d.split('').forEach(function(c,j){if(boxes[j])boxes[j].value=c});
boxes[Math.min(d.length,5)].focus();sync();if(d.length===6)f.submit();
boxes[Math.min(d.length,5)].focus();sync();if(d.length===6)go();
});
});
f.addEventListener('submit',sync);
// Auto-submit on the 6th digit; the button says what is happening meanwhile.
function go(){var b=document.getElementById('go');b.disabled=true;b.textContent='Unlocking…';f.submit()}
f.addEventListener('submit',function(){sync();var b=document.getElementById('go');b.disabled=true;b.textContent='Unlocking…'});
if(boxes[0])boxes[0].focus();
})();
</script>