feat: add self-contained link composer
Build and Push Docker Container / build-and-push (push) Successful in 1m36s

- Add structured URL codec with compact alphabets and dictionary support.
- Add public decode, redirect, and QR endpoints for composed links.
- Add authenticated encode API and link composer UI.
- Generate PNG QR codes via qrcode with Pillow support.
- Register the new blueprint and navigation entry.
- Cover public decode, auth gating, redirect URL parsing, and QR output.
- Bump NanoShare to 1.27.0 and lock new dependencies.
This commit is contained in:
2026-08-14 08:56:42 +02:00
parent d3e5e11f8d
commit ed62837f6d
18 changed files with 1139 additions and 163 deletions
+44 -3
View File
@@ -48,9 +48,14 @@
}
/* Globale Stile */
html, body {
height: -webkit-fill-available;
height: 100%;
html {
min-height: 100%;
background: var(--bg);
}
body {
min-height: 100vh;
min-height: 100dvh;
display: flex;
flex-direction: column;
font-family: Arial, sans-serif;
@@ -300,6 +305,8 @@ body{
radial-gradient(1200px 600px at 10% -10%, rgba(124,158,255,.18), transparent 60%),
radial-gradient(900px 500px at 110% 10%, rgba(142,246,255,.18), transparent 60%),
var(--bg);
background-attachment: fixed;
background-repeat: no-repeat;
font:500 14px/1.5 system-ui,-apple-system,Segoe UI,Inter,Roboto,"Helvetica Neue",Arial,"Noto Sans","Apple Color Emoji","Segoe UI Emoji";
}
@@ -528,6 +535,40 @@ input[type="file"]{display:none}
.toast.show{display:flex;animation:pop .18s ease-out}
@keyframes pop{from{transform:translateY(8px);opacity:0}to{transform:translateY(0);opacity:1}}
/* ====== Link Composer ====== */
.link-composer{max-width:1400px;width:min(1400px, calc(100% - 32px));padding:clamp(18px,2.6vw,34px)}
.link-composer .hero-card{margin-bottom:22px;padding:clamp(24px,3vw,36px)}
.link-composer .composer-card,.link-composer .result-panel{padding:clamp(24px,3vw,36px)}
.composer-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:24px;margin-bottom:24px}
.composer-grid.is-single{grid-template-columns:1fr}
@media (max-width:860px){.composer-grid{grid-template-columns:1fr}}
.composer-card textarea{
width:100%;height:190px;min-height:190px;resize:vertical;border-radius:14px;border:1px solid var(--border);
background:color-mix(in srgb,var(--panel) 88%, transparent);color:var(--text);padding:16px 18px;
}
.composer-card textarea:focus{outline:none;border-color:color-mix(in srgb,var(--accent) 50%, var(--border));box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 25%, transparent)}
.composer-card textarea:disabled,.composer-card button:disabled{opacity:.55;cursor:not-allowed}
.card-heading{display:flex;justify-content:space-between;gap:12px;align-items:center;margin-bottom:14px}
.card-heading h2{margin:0;font-size:1.25rem}
.badge-warning{color:#ffd37a;background:rgba(255,211,122,.12);border-color:rgba(255,211,122,.35)}
.notice{display:grid;gap:4px;margin:10px 0 16px;padding:12px;border:1px solid rgba(255,211,122,.35);border-radius:12px;background:rgba(255,211,122,.08);color:var(--muted)}
.switch-row{display:flex;align-items:center;gap:10px;margin:12px 0;color:var(--muted);font-weight:700}
.switch-row-spacer{visibility:hidden;pointer-events:none}
.switch-row input{accent-color:var(--accent)}
.actions-row{display:flex;gap:12px;flex-wrap:wrap;margin-top:18px}.actions-row.centered{justify-content:center}
.form-error{display:none;margin-top:12px;padding:12px;border-radius:12px;border:1px solid rgba(255,122,122,.45);background:rgba(255,122,122,.10);color:#ffd5d5}.form-error.visible{display:block}
.result-panel{display:none}.result-panel.visible{display:block}
.result-grid{display:grid;grid-template-columns:minmax(0,1fr) 320px;gap:24px;align-items:stretch}
@media (max-width:820px){.result-grid{grid-template-columns:1fr}}
.output-block{margin-top:12px}.output-label{display:block;color:var(--muted);font-size:.78rem;font-weight:800;text-transform:uppercase;letter-spacing:.08em;margin-bottom:7px}
.copy-field{display:flex;gap:10px;align-items:center;justify-content:space-between;border:1px solid var(--border);border-radius:12px;background:rgba(0,0,0,.16);padding:10px 12px;overflow-wrap:anywhere}
.copy-field code{font:700 .93rem ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;color:var(--text)}
.btn.mini{padding:.4rem .6rem;font-size:.82rem;flex:0 0 auto}
.stats-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-top:14px}
@media (max-width:620px){.stats-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}
.stat-card{border:1px solid var(--border);border-radius:12px;background:rgba(0,0,0,.16);padding:12px}.stat-card strong{display:block;font-size:1.15rem}.stat-card span{color:var(--muted);font-size:.82rem}
.qr-card{display:flex;flex-direction:column;gap:12px;justify-content:space-between;align-items:center;border:1px solid var(--border);border-radius:14px;background:rgba(0,0,0,.12);padding:14px}.qr-card img{max-width:230px;width:100%;background:#fff;border-radius:12px;padding:10px}
/* ====== Utilities ====== */
.sr-only{
position:absolute !important;width:1px;height:1px;padding:0;margin:-1px;