OverviewFilesCommitsIssuesDocsReleasesPeers

favela-web commits / 1694f6e

render: migrate the base layer's raw literals onto the existing tokens
The sheet had two eras stacked: rules written with hardcoded hex, and a later "design pass" that introduced tokens and only reached the selectors it re-listed. That is why there is no single place to change the look, and why dark mode needed a hand-written patch block per era. 63 literals across 56 lines now read from the tokens that were already defined. Scope: light rules only (484-719). The dark patch at 720-724 is deliberately left for the next step, which deletes it rather than migrating it. Not a no-op, and the diff says so exactly (computed-style fingerprint, 1350 properties, before vs after): light 40 changed 35x #888 -> --fg-faint, 4x #ccc -> --line, 1x -> --bg-soft dark 25 changed 15x --fg-faint, 5x .skip inverting, 4x --line, 1x --bg-soft The 35 are the same accessibility fix as the previous commit finishing its job: #888 measures 3.54:1 on white and fails AA; the token is 4.74:1. The dark deltas are values that were hardcoded LIGHT and so never responded to the theme at all. One trap caught by reviewing the diff rather than trusting the substitution: `color:#fff` is not always the page background. On .delbtn and .blockbtn it means "foreground on a fixed red", and mapping it to var(--bg) put #111 on #c92a2a in dark — 3.46:1, a regression I introduced. Reverted both to #fff. The three sites on --accent keep var(--bg) because there it is genuinely correct: #fff on #1a56db in light (6.18:1), #111 on #9ec5ff in dark (10.69:1). Deliberately NOT migrated, because they have no token to migrate to and inventing one is a design decision, not a refactor: the danger family (#c92a2a/#b91c1c/ #e03131, 16 uses), #f59f00 bookmark, #2b8a3e ok, #b8860b warn, and the lens family (#cbd5e1/#dbe4ff/#3b5bdb) which per audit #45 encodes "named foreign source" and is load-bearing rather than decorative. Phase 1a of the visual-parity order. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1694f6ee2c77ebf9ebb49d05198d2d17034bf529
by plankatron <fleeky@gmail.com> · 2026-08-24T13:53:04.000Z

Changed files (2) +55 -55

lib/render.js +54 -54
……@@ -481,10 +481,10 @@ export function countThread (replies) {
481481
482482// ── page shell ────────────────────────────────────────────────
483483const STYLE = `
484body{font-family:system-ui,sans-serif;max-width:620px;margin:2rem auto;padding:0 1rem;color:#111}
485a{color:#1a56db}h1{font-size:1.4rem}h1 small{color:#888;font-weight:400}
484body{font-family:system-ui,sans-serif;max-width:620px;margin:2rem auto;padding:0 1rem;color:var(--fg)}
485a{color:var(--accent)}h1{font-size:1.4rem}h1 small{color:var(--fg-faint);font-weight:400}
486486nav{margin:.4rem 0 1rem;display:flex;gap:1rem;font-size:.9rem}
487.post{position:relative;border-bottom:1px solid #e5e5e5;padding:.8rem 0}.post .text{white-space:pre-wrap}
487.post{position:relative;border-bottom:1px solid var(--line);padding:.8rem 0}.post .text{white-space:pre-wrap}
488488/* The whole-entry link emitted by renderPost. It covers the card and sits UNDER everything, so the
489489 card is one big click target for "open this thread". */
490490.pstretch{position:absolute;inset:0;z-index:0}
……@@ -499,19 +499,19 @@ nav{margin:.4rem 0 1rem;display:flex;gap:1rem;font-size:.9rem}
499499.post .byline,.post .inreply a,.post .text a,.post .meta a,.post .meta form,.post .media a,.post .quote,.post .card,.post .delw,details.post>summary{position:relative;z-index:1}
500500/* A click target with no hover state is invisible until you happen to click it. Kept faint: it must
501501 read as "this row is live", not as a selected state. */
502.post:hover{background:#fafafa}
502.post:hover{background:var(--bg-soft)}
503503/* No hover where there is no link to follow: the focused post of a thread, and a collapsed post while
504504 it is still closed (its overlay lives inside the hidden content, so there is nothing to click yet). */
505505.focus .post:hover,details.post.collapsed:not([open]):hover{background:none}
506.post .media{margin-top:.5rem}.post .media img{max-width:100%;height:auto;border-radius:.5rem;display:block;margin:.3rem 0;background:#f3f4f6}
507details.post>summary{cursor:pointer;color:#888;font-style:italic;list-style:revert}
508.meta{color:#888;font-size:.8rem;margin-top:.35rem}.meta .did{color:#888}
506.post .media{margin-top:.5rem}.post .media img{max-width:100%;height:auto;border-radius:.5rem;display:block;margin:.3rem 0;background:var(--bg-soft)}
507details.post>summary{cursor:pointer;color:var(--fg-faint);font-style:italic;list-style:revert}
508.meta{color:var(--fg-faint);font-size:.8rem;margin-top:.35rem}.meta .did{color:var(--fg-faint)}
509509.controls{display:flex;gap:.5rem;align-items:center;margin-top:.4rem}.controls select,.controls button{font:inherit;padding:.3rem}
510.badge{display:inline-block;font-size:.7rem;font-weight:600;padding:.05rem .4rem;border-radius:.6rem;margin-left:.4rem;background:#e8f0fe;color:#1a56db}
510.badge{display:inline-block;font-size:.7rem;font-weight:600;padding:.05rem .4rem;border-radius:.6rem;margin-left:.4rem;background:#e8f0fe;color:var(--accent)}
511511.lbl{display:inline-block;font-size:.7rem;font-weight:600;padding:.05rem .4rem;border-radius:.6rem;margin-left:.4rem;background:#fde8e8;color:#b91c1c}
512512.tag{text-decoration:none}.tag:hover{text-decoration:underline}
513.viewhdr{margin:.6rem 0;font-weight:600}.empty{color:#888;font-style:italic}
514.who{font-size:.85rem;color:#888;margin-left:auto}
513.viewhdr{margin:.6rem 0;font-weight:600}.empty{color:var(--fg-faint);font-style:italic}
514.who{font-size:.85rem;color:var(--fg-faint);margin-left:auto}
515515.phead{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;margin-bottom:.35rem}
516516.byline{display:flex;align-items:center;gap:.5rem;text-decoration:none;color:inherit;min-width:0}
517517.byline:hover .name{text-decoration:underline}
……@@ -519,28 +519,28 @@ details.post>summary{cursor:pointer;color:#888;font-style:italic;list-style:reve
519519.pfp{width:36px;height:36px;border-radius:50%;object-fit:cover;background:#e5e7eb;flex:none}
520520.pfp.none{display:inline-block;background:#e5e7eb}
521521.pfp.big{width:72px;height:72px}
522.meta .permalink{color:#888;text-decoration:none}.meta .permalink:hover{text-decoration:underline}
522.meta .permalink{color:var(--fg-faint);text-decoration:none}.meta .permalink:hover{text-decoration:underline}
523523.mention{text-decoration:none;font-weight:500}.mention:hover{text-decoration:underline}
524524.profhdr{display:flex;gap:.9rem;align-items:flex-start;margin:.6rem 0 1rem}
525525.profname{font-size:1.15rem;font-weight:700}
526.profdid code{font-size:.75rem;color:#888;word-break:break-all}
526.profdid code{font-size:.75rem;color:var(--fg-faint);word-break:break-all}
527527.profbio{margin:.4rem 0 0;white-space:pre-wrap}
528528.aturi code{font-size:.75rem;word-break:break-all}
529.replyc{color:#888;text-decoration:none}.replyc:hover{text-decoration:underline}
529.replyc{color:var(--fg-faint);text-decoration:none}.replyc:hover{text-decoration:underline}
530530.rpf{display:inline;margin-left:.15rem}
531.rpbtn{font:inherit;font-size:.9rem;line-height:1;border:none;background:none;color:#888;cursor:pointer;padding:0 .15rem}
532.rpbtn.on{color:#17bf63}.rpbtn:hover{color:#17bf63}
533.rps{color:#888;font-size:.8rem}
534.inreply{font-size:.8rem;color:#888;margin-bottom:.25rem}
535.replyf{margin:.8rem 0;padding:.6rem;border:1px solid #e5e5e5;border-radius:.5rem}
536.replyf textarea{width:100%;box-sizing:border-box;font:inherit;padding:.4rem;border:1px solid #ccc;border-radius:.3rem}
531.rpbtn{font:inherit;font-size:.9rem;line-height:1;border:none;background:none;color:var(--fg-faint);cursor:pointer;padding:0 .15rem}
532.rpbtn.on{color:var(--repost)}.rpbtn:hover{color:var(--repost)}
533.rps{color:var(--fg-faint);font-size:.8rem}
534.inreply{font-size:.8rem;color:var(--fg-faint);margin-bottom:.25rem}
535.replyf{margin:.8rem 0;padding:.6rem;border:1px solid var(--line);border-radius:.5rem}
536.replyf textarea{width:100%;box-sizing:border-box;font:inherit;padding:.4rem;border:1px solid var(--line);border-radius:.3rem}
537537.replyf p{display:flex;align-items:center;gap:.5rem;margin:.4rem 0 0}
538538.replyf button{font:inherit;padding:.3rem .9rem;cursor:pointer}
539.thread{margin-top:1rem;border-top:2px solid #e5e5e5;padding-top:.5rem}
540.thread h3{font-size:.9rem;color:#666;margin:.3rem 0}
539.thread{margin-top:1rem;border-top:2px solid var(--line);padding-top:.5rem}
540.thread h3{font-size:.9rem;color:var(--fg-dim);margin:.3rem 0}
541541/* Nested replies: children live INSIDE the parent node, so this one rule gives the accumulating indent
542542 and the thread line at every depth. Capped padding keeps a deep chain from marching off-screen. */
543.tnode .tnode{margin-left:.5rem;padding-left:.7rem;border-left:2px solid #eee}
543.tnode .tnode{margin-left:.5rem;padding-left:.7rem;border-left:2px solid var(--line-soft)}
544544.focus{border-left:3px solid #444;padding-left:.6rem;margin:.4rem 0}
545545/* A lens is a NAMED FOREIGN source. Styled visibly apart from our own posts — a reader must never have
546546 to guess whether a reply is one this node holds or one it is relaying from someone else's AppView. */
……@@ -548,17 +548,17 @@ details.post>summary{cursor:pointer;color:#888;font-style:italic;list-style:reve
548548/* The ancestor fill sits ABOVE the focused post, so its rule belongs on the bottom edge — otherwise the
549549 section reads as separating itself from the post it precedes. */
550550.lensup{margin:.3rem 0 .6rem;border-top:0;border-bottom:1px dashed #cbd5e1;padding:0 0 .4rem}
551.lens h3{font-size:.85rem;color:#666;font-weight:600;margin:.3rem 0}
551.lens h3{font-size:.85rem;color:var(--fg-dim);font-weight:600;margin:.3rem 0}
552552.lensname{color:#3b5bdb}
553553.lpost{border-left:2px solid #dbe4ff;padding:.3rem 0 .3rem .7rem;margin:.4rem 0}
554554.lwho{font-size:.9rem;font-weight:600}
555.lhandle{font-weight:400;color:#888}
555.lhandle{font-weight:400;color:var(--fg-faint)}
556556.ltext{white-space:pre-wrap;margin:.15rem 0}
557557.lensoff{font-size:.78rem;margin-top:1rem}
558.lensnote{font-size:.8rem;color:#666;margin:.4rem 0 0}
559.qbtn{color:#888;text-decoration:none}.qbtn:hover{color:#1a56db}
558.lensnote{font-size:.8rem;color:var(--fg-dim);margin:.4rem 0 0}
559.qbtn{color:var(--fg-faint);text-decoration:none}.qbtn:hover{color:var(--accent)}
560560.delw{display:inline}
561.delw>summary{display:inline;list-style:none;cursor:pointer;color:#888}
561.delw>summary{display:inline;list-style:none;cursor:pointer;color:var(--fg-faint)}
562562.delw>summary::-webkit-details-marker{display:none}
563563.delw>summary:hover{color:#c92a2a}
564564.delf{display:inline;margin-left:.3rem}
……@@ -579,7 +579,7 @@ details.post>summary{cursor:pointer;color:#888;font-style:italic;list-style:reve
579579.navsearch input:focus{outline:2px solid var(--accent,#1a56db);outline-offset:1px;width:11rem}
580580.searchbox{display:flex;gap:.5rem;margin:.6rem 0 1rem}
581581.searchbox input{flex:1;font:inherit;padding:.45rem .7rem;border:1px solid var(--line,#e5e5e5);border-radius:999px;background:var(--bg,#fff);color:inherit}
582.searchbox button{font:inherit;padding:.4rem 1rem;border:none;border-radius:999px;background:var(--accent,#1a56db);color:#fff;font-weight:600;cursor:pointer}
582.searchbox button{font:inherit;padding:.4rem 1rem;border:none;border-radius:999px;background:var(--accent,#1a56db);color:var(--bg);font-weight:600;cursor:pointer}
583583.actorrow{padding:.4rem 0;border-bottom:1px solid var(--line-soft,#f0f1f3)}
584584.gatenote{font-size:.85rem;color:var(--fg-dim,#666);margin:.6rem 0 0}
585585.gateoff{font-size:.85rem;margin:.6rem 0 0}
……@@ -629,13 +629,13 @@ details.post>summary{cursor:pointer;color:#888;font-style:italic;list-style:reve
629629.profbanner img{display:block;width:100%;aspect-ratio:3/1;object-fit:cover}
630630.profbanner.small img{aspect-ratio:4/1}
631631.bmf{display:inline}
632.bmbtn{font:inherit;font-size:.9rem;line-height:1;border:none;background:none;color:#888;cursor:pointer;padding:.3rem .45rem;margin:-.3rem -.1rem;transition:transform .08s ease,color .12s ease;touch-action:manipulation}
632.bmbtn{font:inherit;font-size:.9rem;line-height:1;border:none;background:none;color:var(--fg-faint);cursor:pointer;padding:.3rem .45rem;margin:-.3rem -.1rem;transition:transform .08s ease,color .12s ease;touch-action:manipulation}
633633.bmbtn:hover{color:#f59f00}.bmbtn.on{color:#f59f00}
634634.bmbtn:active{transform:scale(.88)}
635.bmbtn:focus-visible{outline:2px solid #1a56db;outline-offset:2px;border-radius:.3rem}
635.bmbtn:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:.3rem}
636636/* a11y. A skip link is the difference between reaching the content and tabbing the whole nav on every
637637 page; it is visually hidden until focused, which is the one time it matters. */
638.skip{position:absolute;left:-9999px;top:0;z-index:200;background:var(--accent,#1a56db);color:#fff;padding:.5rem .9rem;border-radius:0 0 .4rem 0}
638.skip{position:absolute;left:-9999px;top:0;z-index:200;background:var(--accent,#1a56db);color:var(--bg);padding:.5rem .9rem;border-radius:0 0 .4rem 0}
639639.skip:focus{left:0}
640640main:focus{outline:none}
641641/* Visually hidden but ANNOUNCED — for labels that would be redundant on screen and essential aloud.
……@@ -661,27 +661,27 @@ a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,
661661.qpreview{font-size:.85rem;color:var(--fg-dim,#666);border-left:3px solid var(--accent,#1a56db);padding:.2rem .5rem;margin-bottom:.5rem}
662662.qptext{font-style:italic}
663663.qcancel{font-size:.8rem}
664.stath{font-size:.85rem;color:#666;margin:1rem 0 .3rem;text-transform:uppercase;letter-spacing:.03em}
665.statblk{border:1px solid #e5e5e5;border-radius:4px;padding:.5rem .6rem;margin:.4rem 0}
664.stath{font-size:.85rem;color:var(--fg-dim);margin:1rem 0 .3rem;text-transform:uppercase;letter-spacing:.03em}
665.statblk{border:1px solid var(--line);border-radius:4px;padding:.5rem .6rem;margin:.4rem 0}
666666.statblk.alarm{border-color:#e03131;background:#fff5f5}
667.statdid{font-size:.8rem;color:#666}
667.statdid{font-size:.8rem;color:var(--fg-dim)}
668668.statsum{font-size:.9rem;margin:.2rem 0 .4rem}
669669.stattbl{border-collapse:collapse;font-size:.8rem;width:100%}
670.stattbl th{text-align:left;color:#888;font-weight:600;padding:.15rem .4rem .15rem 0}
670.stattbl th{text-align:left;color:var(--fg-faint);font-weight:600;padding:.15rem .4rem .15rem 0}
671671.stattbl td{padding:.15rem .4rem .15rem 0;border-top:1px solid #f1f1f1}
672672.stattbl tr.chosen td{font-weight:600;background:#f8f9ff}
673673.alarmnote{color:#c92a2a;font-size:.82rem;margin:.4rem 0 0}
674674.ok{color:#2b8a3e}.bad{color:#c92a2a}
675.ptabs{display:flex;gap:.15rem;border-bottom:1px solid #e5e5e5;margin:.6rem 0 .3rem;flex-wrap:wrap}
676.ptab{padding:.35rem .7rem;font-size:.88rem;color:#666;text-decoration:none;border-bottom:2px solid transparent}
677.ptab:hover{color:#111}
678.ptab.on{color:#111;font-weight:600;border-bottom-color:#444}
675.ptabs{display:flex;gap:.15rem;border-bottom:1px solid var(--line);margin:.6rem 0 .3rem;flex-wrap:wrap}
676.ptab{padding:.35rem .7rem;font-size:.88rem;color:var(--fg-dim);text-decoration:none;border-bottom:2px solid transparent}
677.ptab:hover{color:var(--fg)}
678.ptab.on{color:var(--fg);font-weight:600;border-bottom-color:#444}
679679.ancestor{opacity:.72}
680.missing{font-size:.8rem;border-left:2px dashed #ccc;padding-left:.6rem}
681.likes{color:#888;font-size:.8rem}
680.missing{font-size:.8rem;border-left:2px dashed var(--line);padding-left:.6rem}
681.likes{color:var(--fg-faint);font-size:.8rem}
682682.likef{display:inline;margin-left:.5rem}
683.likebtn{font:inherit;font-size:.9rem;line-height:1;border:none;background:none;color:#888;cursor:pointer;padding:0 .15rem}
684.likebtn.on{color:#e0245e}.likebtn:hover{color:#e0245e}
683.likebtn{font:inherit;font-size:.9rem;line-height:1;border:none;background:none;color:var(--fg-faint);cursor:pointer;padding:0 .15rem}
684.likebtn.on{color:var(--like)}.likebtn:hover{color:var(--like)}
685685/* Responsive controls. These used to be plain form POSTs that reloaded the whole page to toggle a heart —
686686 correct, and it felt broken. /static/actions.js now posts JSON and updates in place; this is the part
687687 that makes the press FEEL like it landed:
……@@ -696,25 +696,25 @@ a:focus-visible,button:focus-visible,input:focus-visible,textarea:focus-visible,
696696.likebtn.failed,.rpbtn.failed,.followbtn.failed{color:#c92a2a;animation:shake .25s}
697697@keyframes shake{25%{transform:translateX(-2px)}75%{transform:translateX(2px)}}
698698/* Keyboard users get the same affordance the mouse does — these are real buttons, so make focus visible. */
699.likebtn:focus-visible,.rpbtn:focus-visible,.followbtn:focus-visible,.replyc:focus-visible{outline:2px solid #1a56db;outline-offset:2px;border-radius:.3rem}
699.likebtn:focus-visible,.rpbtn:focus-visible,.followbtn:focus-visible,.replyc:focus-visible{outline:2px solid var(--accent);outline-offset:2px;border-radius:.3rem}
700700@media(prefers-reduced-motion:reduce){.likebtn,.rpbtn,.followbtn{transition:none}.likebtn.failed,.rpbtn.failed,.followbtn.failed{animation:none}}
701701.followf{margin:.5rem 0 0}
702.followbtn{font:inherit;border:1px solid #1a56db;background:#1a56db;color:#fff;border-radius:.4rem;padding:.3rem .9rem;cursor:pointer}
703.followbtn.on{background:none;color:#1a56db}
704.scopenote{color:#888;font-weight:400}
702.followbtn{font:inherit;border:1px solid var(--accent);background:var(--accent);color:var(--bg);border-radius:.4rem;padding:.3rem .9rem;cursor:pointer}
703.followbtn.on{background:none;color:var(--accent)}
704.scopenote{color:var(--fg-faint);font-weight:400}
705705.profnet{margin:.3rem 0;font-size:.92rem}
706706.fol{display:inline-block;margin:.1rem .3rem .1rem 0;color:#356}
707.folmore{color:#888}
708.profgraph{margin:.4rem 0 0;font-size:.9rem;color:#666}
709.cc{font-size:.8rem;color:#888;margin-left:auto}.cc.over{color:#b91c1c;font-weight:600}
710.proform label{font-size:.85rem;color:#666}
711.proform input,.proform textarea{width:100%;box-sizing:border-box;font:inherit;padding:.4rem;border:1px solid #ccc;border-radius:.3rem}
707.folmore{color:var(--fg-faint)}
708.profgraph{margin:.4rem 0 0;font-size:.9rem;color:var(--fg-dim)}
709.cc{font-size:.8rem;color:var(--fg-faint);margin-left:auto}.cc.over{color:#b91c1c;font-weight:600}
710.proform label{font-size:.85rem;color:var(--fg-dim)}
711.proform input,.proform textarea{width:100%;box-sizing:border-box;font:inherit;padding:.4rem;border:1px solid var(--line);border-radius:.3rem}
712712.proform button{font:inherit;padding:.35rem .9rem;cursor:pointer}
713713.proform .cc{margin-left:0;display:inline-block;margin-top:.15rem}
714.imgpick{display:inline-block;cursor:pointer;border:1px solid #ccc;border-radius:.3rem;padding:.25rem .6rem;font-size:.85rem}
714.imgpick{display:inline-block;cursor:pointer;border:1px solid var(--line);border-radius:.3rem;padding:.25rem .6rem;font-size:.85rem}
715715.imgprev{display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.5rem}
716716.imgprev .thumb{display:flex;flex-direction:column;gap:.2rem;width:140px}
717.imgprev .thumb img{width:140px;height:100px;object-fit:cover;border-radius:.3rem;background:#f3f4f6}
717.imgprev .thumb img{width:140px;height:100px;object-fit:cover;border-radius:.3rem;background:var(--bg-soft)}
718718.imgprev .thumb input,.imgprev .thumb button{font:inherit;font-size:.75rem;padding:.2rem;width:100%;box-sizing:border-box}
719719.imgprev .thumb button{cursor:pointer}
720720@media(prefers-color-scheme:dark){body{background:#111;color:#eee}a,.tag{color:#9ec5ff}.post{border-color:#333}
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "favela-web",
3 "version": "0.3.65",
3 "version": "0.3.66",
44 "description": "favela-web — the web/SSR surface of Favela, the native P2P-local, community-owned social client + appview over the self-custody atproto stack (hyper-proto-peer). One of the Favela surfaces (alongside favela-cli and favela-native). bsky is an optional POSSE export, not the home. See DESIGN.md.",
55 "type": "module",
66 "main": "bin/favela-web.js",