fabrica

hanna/fabrica

fix(web): style url inputs and make profile links incremental

e46bfa4 · hanna committed on 2026-07-25

Add input[type=url] to the styled-input rule (the link slots were rendering
as bare inline inputs), and progressively reveal link slots: show the filled
links plus one empty, with an Add link button up to the max of five. Without
JS all five slots show.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed by hannaSSH key fingerprint: SHA256:4g9cWhkAAw8gwqhJUcVbSnGEvdGwklW+1Aa/fMUu59k
3 files changed · +48 −4UnifiedSplit
assets/base.css +8 −0
@@ -275,6 +275,7 @@ input[type="text"],
275275 input[type="password"],
276276 input[type="email"],
277277 input[type="search"],
278+input[type="url"],
278279 textarea,
279280 select {
280281 width: 100%;
@@ -303,6 +304,13 @@ select:focus {
303304 .link-slot {
304305 margin-bottom: 0.5rem;
305306 }
307+.link-add {
308+ margin-top: 0.25rem;
309+}
310+/* Without JS every slot shows; the enhancement hides empties behind Add link. */
311+[hidden] {
312+ display: none !important;
313+}
306314
307315 /* Centered auth card (sign in, invite activation). */
308316 .auth-wrap {
assets/fabrica.js +33 −0
@@ -56,6 +56,39 @@
5656 });
5757 });
5858
59+ // ---- Progressive link fields ----
60+ // Show the filled links plus one empty slot; reveal more (up to the max) via
61+ // the Add link button. Without JS every slot is visible.
62+ document.querySelectorAll("[data-link-fields]").forEach(function (wrap) {
63+ var slots = Array.prototype.slice.call(wrap.querySelectorAll(".link-slot"));
64+ var addBtn = wrap.querySelector("[data-link-add]");
65+ var firstEmptyShown;
66+ slots.forEach(function (s) {
67+ if (s.value.trim()) {
68+ s.hidden = false;
69+ } else if (!firstEmptyShown) {
70+ s.hidden = false;
71+ firstEmptyShown = true;
72+ } else {
73+ s.hidden = true;
74+ }
75+ });
76+ function anyHidden() {
77+ return slots.some(function (s) { return s.hidden; });
78+ }
79+ if (addBtn) {
80+ addBtn.hidden = !anyHidden();
81+ addBtn.addEventListener("click", function () {
82+ var next = slots.filter(function (s) { return s.hidden; })[0];
83+ if (next) {
84+ next.hidden = false;
85+ next.focus();
86+ }
87+ addBtn.hidden = !anyHidden();
88+ });
89+ }
90+ });
91+
5992 // ---- Clipboard buttons ----
6093 document.addEventListener("click", function (e) {
6194 var btn = e.target.closest("[data-clipboard]");
crates/web/src/pages.rs +7 −4
@@ -631,10 +631,13 @@ fn settings_body(user: &User, csrf: &str, notice: Option<&str>) -> Markup {
631631 input type="text" id="location" name="location" value=(val(&user.location));
632632 label for="link1" { "Links" }
633633 p class="muted field-hint" { "Up to five links (website, GitHub, Mastodon, …); icons are chosen from the address." }
634- @for i in 0..LINK_SLOTS {
635- input type="url" id=(format!("link{}", i + 1)) name=(format!("link{}", i + 1))
636- class="link-slot" value=(user.links.get(i).map_or("", String::as_str))
637- placeholder="https://example.com";
634+ div class="link-fields" data-link-fields {
635+ @for i in 0..LINK_SLOTS {
636+ input type="url" id=(format!("link{}", i + 1)) name=(format!("link{}", i + 1))
637+ class="link-slot" value=(user.links.get(i).map_or("", String::as_str))
638+ placeholder="https://example.com";
639+ }
640+ button class="btn link-add" type="button" data-link-add { "Add link" }
638641 }
639642 button class="btn btn-primary" type="submit" { "Save profile" }
640643 }