OverviewFilesCommitsIssuesDocsReleasesPeers

favela-web commits / d45c49c

render: demote the composer's gates behind a disclosure, and stop the copy talking about hpp
The composer opened with four gate selects — scope, lang, quotes, replies — competing with the timeline before a reader had seen a single post, and a placeholder that read "post (relays to your hpp, which signs)… — a line with only --- starts a new post in a thread". That describes our architecture to someone who wants to type a sentence. placeholder -> "Say something…" gates -> behind a native <details> ("post options") threading tip -> inside that disclosure, where a power feature belongs hint line -> one line again, not two The post button and image picker stay OUTSIDE the disclosure deliberately. Making someone open a twisty before they can post would trade one bad default for a worse one, and with scripting off there is no other way to submit. MEASURED, because the honest number is smaller than the change feels: composer height 279px -> 254px first post at y 507 -> 482 25px, ~9%. Collapsing two rows of selects bought ~76px and the summary's own 44px touch target gave most of it back. The real gain is noise, not height: four dropdowns become one small link, which was the actual complaint. A genuine demotion means not rendering a composer on the timeline at all — a compose route with an entry point, the way mainstream clients do it. That is an interaction change rather than a style one and is not in this commit. Test note: the suite asserts the composer explains threading, and I had reworded that string while moving it. The wording is restored verbatim — the explanation moved, it did not change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
d45c49c3a54ca422f2c05071f78063b04ec59e8d
by plankatron <fleeky@gmail.com> · 2026-08-24T20:54:29.000Z

Changed files (2) +28 -4

lib/render.js +27 -3
……@@ -864,6 +864,28 @@ nav a[aria-current=page]{color:var(--fg);font-weight:600;border-bottom-color:var
864864/* Controls row. Small gap on purpose: each control is its own 44x44 box, so the whitespace between
865865 them is padding INSIDE a target, not dead space between two small ones. */
866866.meta{display:flex;align-items:center;gap:var(--s1);flex-wrap:wrap;margin-top:var(--s2);font-size:var(--f0);color:var(--fg-faint)}
867/* ── composer ──────────────────────────────────────────────────────────────────────────────────────
868 The composer was ~40% of the first screen on a phone, permanently expanded, with four gate selects
869 competing with the timeline before a reader had seen a single post. The gates move behind a native
870 <details> so the default is what people actually do — type, maybe attach, post.
871
872 The post button and image picker stay OUTSIDE the disclosure deliberately: making someone open a
873 twisty before they can post would trade one bad default for a worse one, and with scripting off
874 there is no other way to submit. */
875.copts{margin:var(--s2) 0 0}
876.copts>summary{display:inline-flex;align-items:center;min-height:44px;cursor:pointer;list-style:none;
877 font-size:var(--f0);color:var(--fg-dim);text-decoration:underline}
878.copts>summary::-webkit-details-marker{display:none}
879.copts>summary:hover{color:var(--fg)}
880.copts[open]>summary{margin-bottom:var(--s1)}
881/* The submit row. Post sits at the end on whatever line it lands on, as it did when it lived in
882 .controls. */
883.optshint{font-size:var(--f0);color:var(--fg-faint);margin:var(--s1) 0 0;flex-basis:100%}
884.cbar{display:flex;align-items:center;gap:var(--s2);flex-wrap:wrap;margin-top:var(--s2)}
885.cbar #post-btn{margin-left:auto;flex:none}
886@media(max-width:640px){
887 .cbar #post-btn{margin-left:0;flex:1 1 100%}
888}
867889/* ── two-column post row ───────────────────────────────────────────────────────────────────────────
868890 Avatar column, then everything else. 42px and a 10px gutter, matching bsky's PostFeedItem
869891 (size={42}, gap: 10). Applied to an inner .prow rather than to .post, because .post doubles as the
……@@ -1037,8 +1059,8 @@ function composer (canWrite, quoting = null, lists = [], viewer = null) {
10371059<div class="qpreview">❝ quoting <strong>${esc(quoting.who || shortDid(quoting.did || ''))}</strong>${quoting.text ? `: <span class="qptext">${esc(String(quoting.text).slice(0, 140))}</span>` : ''} <a class="qcancel" href="/">cancel</a></div>`
10381060 : ''
10391061 return `<form class="composer" method="POST" action="/api/post">${whoLine}${q}
1040<label class="sr-only" for="composer-text">what do you want to post?</label><textarea id="composer-text" name="text" rows="2" placeholder="post (relays to your hpp, which signs)… — a line with only --- starts a new post in a thread" style="width:100%;box-sizing:border-box;font:inherit;padding:.5rem" required></textarea>
1041<div class="controls"><label>scope <select name="scope">
1062<label class="sr-only" for="composer-text">what do you want to post?</label><textarea id="composer-text" name="text" rows="2" placeholder="Say something…" style="width:100%;box-sizing:border-box;font:inherit;padding:.5rem" required></textarea>
1063<details class="copts"><summary>post options</summary><div class="controls"><label>scope <select name="scope">
10421064<option value="public">public</option>
10431065<option value="local">local only</option>
10441066</select></label><label>lang <select name="lang">
……@@ -1052,7 +1074,9 @@ ${LANGS.map(([code, label]) => `<option value="${code}">${esc(label)}</option>`)
10521074<option value="mentioned">mentioned</option>
10531075<option value="nobody">nobody</option>
10541076${lists.map((l) => `<option value="${esc(l.uri)}">only: ${esc(l.name)}</option>`).join('')}
1055</select></label><span id="img-controls" hidden><label class="imgpick">📎 image<input type="file" id="post-image" accept="image/*" multiple hidden></label></span><span id="charcount" class="cc" aria-live="polite"></span><button id="post-btn">post</button></div>
1077</select></label>
1078<p class="optshint">a line with only --- starts a new post in a thread</p></div></details>
1079<div class="cbar"><span id="img-controls" hidden><label class="imgpick">📎 image<input type="file" id="post-image" accept="image/*" multiple hidden></label></span><span id="charcount" class="cc" aria-live="polite"></span><button id="post-btn">post</button></div>
10561080<p class="composerhint">public posts syndicate to bsky · local-only stays on favela</p>
10571081<div id="img-preview" class="imgprev"></div></form>`
10581082}
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "favela-web",
3 "version": "0.3.73",
3 "version": "0.3.74",
44 "description": "favela-web \u2014 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",