Public: anyone can read this; the crew can change it.

The home page's apps rail draws §5's card, row and btn, and .home-rail, .home-apps, .home-app, .home-apps-verbs, .home-pick leave the markup #4284

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/4269-the-home-page-s-apps-rail-draws-5-s-card → main 4 commits fork

  • Merged. scout/4269-the-home-page-s-apps-rail-draws-5-s-card was merged into main as cf12b93.

Review

Sign in to review this change.
4 files changed +43 −42
docs/DESIGN.md +1 −2
@@ -402,8 +402,7 @@ gone, so this list only shrinks.
402402 - **`style/content.ts`, search results** — search results, the inbox's ask cards: `.ask-acts` `.ask-card` `.ask-group` `.ask-kind` `.ask-meta` `.ask-more-acts` `.ask-pick` `.ask-pick-hit` `.ask-short` `.ask-waiting` `.live-toggle` `.needs-you`
403403 - **`pages/board.ts`** — the board itself is §5's **board**; the spend tables `spend.ts` draws wear §5's `.table` and `.stack`, no class of their own
404404 - **`pages/map.ts`** — the map of what waits for what: `.map-ask` `.map-breath` `.map-canvas` `.map-card` `.map-counts` `.map-dot` `.map-edge` `.map-edge-crit` `.map-edge-done` `.map-empty` `.map-list` `.map-main` `.map-meta` `.map-node` `.map-pillars` `.map-rule` `.map-svg` `.map-title` `.map-tool` `.map-tools` `.map-who` `.map-who-i` `.map-who-n` `.map-word` `.map-wrap`
405 - **`studio/feed_style.ts`** — the home feed's cards, its composer and the apps rail: `.composer` `.composer-bar` `.fcard-form` `.fcard-reason` `.fcard-reply` `.fcard-text` `.fcard-unblock` `.fcard-who` `.fcard-why` `.feed` `.feed-head` `.home-app` `.home-apps` `.home-apps-verbs` `.home-pick` `.home-rail` `.home-tabs`
406
405 - **`studio/feed_style.ts`** — the home feed's cards and its composer (the apps rail is §5's card, row and btn; an admin's tick boxes and Archive are always shown, deliberately, with no Select toggle): `.composer` `.composer-bar` `.fcard-form` `.fcard-reason` `.fcard-reply` `.fcard-text` `.fcard-unblock` `.fcard-who` `.fcard-why` `.feed` `.feed-head` `.home-tabs`
407406 ---
408407
409408 ## 6. Keyboard
src/web/studio/feed_style.ts +4 −7
@@ -21,17 +21,14 @@ export const feedCss = /* css */ `
2121 .composer-bar select { min-width: 0; max-width: 60%; }
2222 /* Your apps: a narrow column on a desk, a tab away on a phone. */
2323 .home-tabs { display: none; margin-bottom: 12px; }
24 .home-app { gap: 8px; }
25 .home-pick { display: none; }
26 #apps-select:checked ~ * .home-pick { display: inline-flex; }
27 #apps-select:focus-visible ~ * .home-apps-verbs label { outline: 2px solid var(--accent, currentColor); }
28 .home-apps-verbs { flex-wrap: wrap; padding: 10px 14px; }
24 /* An admin's tick boxes are always shown (no Select toggle); 24 px to tap. */
25 #live-apps input[type="checkbox"] { width: 24px; height: 24px; }
2926 @media (max-width: 899px) {
3027 .home-tabs { display: inline-flex; }
3128 /* "What you asked for" is a tab on a phone: the first post comes next.
3229 !important, as [hidden] is: .bar sits in a later cascade layer. */
3330 .feed-head { display: none !important; }
34 .home-rail .home-apps { display: none; }
31 #live-apps { display: none; }
3532 }
3633 .fcard-form { display: grid; gap: 8px; margin-top: 6px; }
3734 .fcard-form textarea { width: 100%; min-height: 56px; resize: vertical; }
@@ -40,6 +37,6 @@ export const feedCss = /* css */ `
4037 .feed .disclosure > summary { min-height: 44px; display: flex; align-items: center; }
4138 }
4239 @media (forced-colors: active) {
43 .feed > .card:focus-visible, #apps-select:focus-visible ~ * .home-apps-verbs label { outline: 2px solid Highlight; }
40 .feed > .card:focus-visible { outline: 2px solid Highlight; }
4441 }
4542 `;
src/web/studio/home.ts +19 −33
@@ -90,29 +90,27 @@ function yourApps(ctx: Ctx, user: User): Html {
9090 record === undefined || runtime === undefined
9191 ? undefined
9292 : runtime.status(record).find((env) => env.env === "production")?.state;
93 return html`<li class="row home-app">
94 ${when(
95 user.isAdmin,
96 () =>
97 html`<input
98 class="home-pick"
99 type="checkbox"
100 name="repo"
101 value="${repo.id}"
102 aria-label="Select ${repo.name}"
103 />`,
104 )}
105 <a class="truncate" href="${repoBase(repo)}">${repo.ownerName}/${repo.name}</a>
106 <span class="spacer"></span>
107 ${state === undefined ? null : envStateBadge(state)}
108 </li>`;
93 return row({
94 ...(user.isAdmin
95 ? {
96 lead: html`<input
97 type="checkbox"
98 name="repo"
99 value="${repo.id}"
100 aria-label="Select ${repo.name}"
101 />`,
102 }
103 : {}),
104 title: html`<a class="truncate break" href="${repoBase(repo)}">${repo.ownerName}/${repo.name}</a>`,
105 ...(state === undefined ? {} : { trail: envStateBadge(state) }),
106 });
109107 });
110108 const list = html`${rows.length === 0
111109 ? html`<p class="muted small">No apps yet.</p>`
112110 : html`<ul class="list">
113111 ${rows}
114112 </ul>`}
115 <div class="bar home-apps-verbs">
113 <div class="bar">
116114 ${btn(`All apps${repos.length > RAIL_APPS ? ` (${repos.length})` : ""}`, { href: "/apps", size: "sm" })}
117115 ${btn("New app", { href: "/apps/new", size: "sm", icon: "plus" })}
118116 ${btn("Repositories", {
@@ -126,29 +124,17 @@ function yourApps(ctx: Ctx, user: User): Html {
126124 ${when(user.isAdmin, () =>
127125 btn("Front page", { href: "/front", size: "sm", icon: "globe" }),
128126 )}
129 ${when(
130 user.isAdmin && rows.length > 0,
131 () => html`<label class="btn" data-size="sm" for="apps-select">Select</label>
132 <span class="home-pick">${btn("Archive", { size: "sm" })}</span>`,
133 )}
127 ${when(user.isAdmin && rows.length > 0, () => btn("Archive", { size: "sm" }))}
134128 </div>`;
135129 return card(
136 // An admin's list is a form: ticks cost no space until Select shows them.
130 // An admin's list is a form: tick apps, then Archive.
137131 user.isAdmin && rows.length > 0
138 ? html`<form method="post" action="/home/archive">
139 <input
140 class="sr-only"
141 type="checkbox"
142 id="apps-select"
143 aria-label="Select apps"
144 />${list}
145 </form>`
132 ? html`<form method="post" action="/home/archive">${list}</form>`
146133 : list,
147134 {
148135 head: html`${icon("rocket")}
149136 <h2>Your apps</h2>`,
150137 pad: false,
151 extra: "home-apps",
152138 },
153139 );
154140 }
@@ -285,7 +271,7 @@ export async function studioHome(
285271 ${feedNote()}
286272 ${timePhase(ctx.request, "feed", () => feedRegion(user, ctx), () => feedCandidateCache(ctx.app.db))}
287273 </div>
288 <aside class="dash-rail stack home-rail">
274 <aside class="dash-rail stack">
289275 ${
290276 // The crew's missing key is the owner's to fix, not the first
291277 // thing above every post: beside the timeline on a desk, after it
test/home-archive.test.ts +19
@@ -6,6 +6,7 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
66 import { mkdtemp, rm } from "node:fs/promises";
77 import { tmpdir } from "node:os";
88 import { join } from "node:path";
9 import { feedCss } from "../src/web/studio/feed_style.ts";
910 import { createApp, type App } from "../src/app.ts";
1011 import { createRepo } from "../src/actions/repos.ts";
1112 import { createApp as insertApp } from "../src/core/apps.ts";
@@ -126,6 +127,24 @@ describe("home archive", () => {
126127 expect(await home(rootCookie)).toContain("keeper/gamma");
127128 });
128129
130 test("the apps rail is drawn with the design system's classes alone", async () => {
131 const page = await full(rootCookie);
132 for (const old of ["home-rail", "home-apps", "home-app", "home-apps-verbs", "home-pick"])
133 expect(page).not.toMatch(new RegExp(`class="[^"]*\\b${old}\\b`));
134 expect(page).toContain("Your apps");
135 expect(page).toContain('href="/apps/new"');
136 expect(page).toContain('href="/map"');
137 expect(page).toContain("keeper/gamma");
138 expect(page).toMatch(/<form method="post" action="\/home\/archive">[\s\S]*name="repo"[\s\S]*Archive/);
139 expect(page).toContain('class="truncate break"');
140 });
141
142 test("the stylesheet no longer names the removed classes", () => {
143 for (const old of ["home-rail", "home-apps", "home-app", "home-pick", "apps-select"])
144 expect(feedCss).not.toContain(old);
145 expect(feedCss).toContain("#live-apps");
146 });
147
129148 test("a non-admin sees no checkboxes and cannot post", async () => {
130149 const page = await full(bobCookie);
131150 expect(page).not.toContain('name="repo"');

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs