Public: anyone can read this; the crew can change it.
Your apps, one line each: the studio home's rows and its empty state #522
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/69-your-apps-one-line-each-the-studio-home → main 1 commit fork
4 files
changed +42
−6
src/web/studio/home.ts +8 −2
| @@ -64,7 +64,11 @@ async function shelf( | ||
| 64 | 64 | icon: "rocket", |
| 65 | 65 | title: "No apps yet", |
| 66 | 66 | note: "Make one, tell the crew what it is, and watch it come up.", |
| 67 | action: btn("New app", { href: "/apps/new", variant: "primary" }), | |
| 67 | action: html`${btn("New app", { | |
| 68 | href: "/apps/new", | |
| 69 | variant: "primary", | |
| 70 | })} | |
| 71 | ${btn("Repositories", { href: "/?mode=dev" })}`, | |
| 68 | 72 | }) |
| 69 | 73 | : listCard(appRows, { |
| 70 | 74 | head: html`${icon("rocket")} |
| @@ -239,7 +243,9 @@ export function studioDoor(ctx: Ctx): Response { | ||
| 239 | 243 | <span class="app-tile-body"> |
| 240 | 244 | <span class="app-tile-name">${repo.name}</span> |
| 241 | 245 | ${repo.description !== "" |
| 242 | ? html`<span class="app-tile-desc truncate" | |
| 246 | ? html`<span | |
| 247 | class="app-tile-desc truncate" | |
| 248 | title="${repo.description}" | |
| 243 | 249 | >${repo.description}</span |
| 244 | 250 | >` |
| 245 | 251 | : null} |
src/web/studio/shelf.ts +7 −3
| @@ -265,9 +265,13 @@ export async function shelfRows( | ||
| 265 | 265 | lead: dot(tone), |
| 266 | 266 | title, |
| 267 | 267 | titleAttr: repo.description || repo.name, |
| 268 | meta: html`${repo.description | |
| 269 | ? html`${repo.description} · ` | |
| 270 | : null}${happening}`, | |
| 268 | meta: html`<span class="shelf-meta" | |
| 269 | >${repo.description | |
| 270 | ? html`<span class="shelf-desc">${repo.description}</span>` | |
| 271 | : null}<span class="shelf-facts" | |
| 272 | >${repo.description ? "· " : null}${happening}</span | |
| 273 | ></span | |
| 274 | >`, | |
| 271 | 275 | trail: raw( |
| 272 | 276 | `\n <span class="badge" data-tone="${tone}">${escapeHtml(live.word)}</span>`, |
| 273 | 277 | ), |
src/web/studio/style.ts +5
| @@ -67,6 +67,9 @@ const rows = part(/* css */ ` | ||
| 67 | 67 | .studio-list li svg { vertical-align: -3px; } |
| 68 | 68 | .studio-who { font-weight: 600; } |
| 69 | 69 | .studio-word { font-family: var(--mono); font-size: 11.5px; opacity: .8; white-space: nowrap; } |
| 70 | .shelf-meta { display: flex; gap: 0 4px; min-width: 0; } | |
| 71 | .shelf-desc { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } | |
| 72 | .shelf-facts { flex: 0 0 auto; max-width: 100%; } | |
| 70 | 73 | .row-meta .studio-word { font-family: inherit; font-size: inherit; opacity: 1; font-weight: 560; } |
| 71 | 74 | .studio-list li[data-word="working"] .studio-word { color: var(--accent); opacity: 1; } |
| 72 | 75 | .studio-list li[data-word="look"] .studio-word { color: var(--merged); opacity: 1; } |
| @@ -106,6 +109,8 @@ const phone = part(/* css */ ` | ||
| 106 | 109 | .studio-list li > :nth-child(n+3) { grid-column: 2; } |
| 107 | 110 | .studio-word { white-space: normal; } |
| 108 | 111 | .studio .row-meta, .studio-list .row-meta { white-space: normal; overflow-wrap: anywhere; } |
| 112 | .shelf-meta { flex-wrap: wrap; } | |
| 113 | .shelf-desc { flex: 1 1 100%; } | |
| 109 | 114 | .studio-tell textarea, .studio-tell .input { font-size: 16px; } |
| 110 | 115 | } |
| 111 | 116 | `); |
test/studio.test.ts +22 −1
| @@ -213,10 +213,30 @@ describe("the studio", () => { | ||
| 213 | 213 | // The bytes the home rendered before the row moved into its own |
| 214 | 214 | // module, escaped so no formatter can reflow them. |
| 215 | 215 | expect(shelfRow).toBe( |
| 216 | '<li>\n <a href="/alice/notes" class="row"\n \n data-nav tabindex="-1"><span class="row-lead"><span\n class="dot"\n \n ></span></span>\n <span class="row-main">\n <span\n class="row-title"\n title="a place for notes"\n >notes</span\n >\n <span class="row-meta">a place for notes · <span class="muted">quiet — tell the crew something</span></span>\n </span>\n <span class="row-trail">\n <span class="badge" data-tone="default">not running</span></span></a>\n </li>', | |
| 216 | '<li>\n <a href="/alice/notes" class="row"\n \n data-nav tabindex="-1"><span class="row-lead"><span\n class="dot"\n \n ></span></span>\n <span class="row-main">\n <span\n class="row-title"\n title="a place for notes"\n >notes</span\n >\n <span class="row-meta"><span class="shelf-meta"\n ><span class="shelf-desc">a place for notes</span><span class="shelf-facts"\n >· <span class="muted">quiet — tell the crew something</span></span\n ></span\n ></span>\n </span>\n <span class="row-trail">\n <span class="badge" data-tone="default">not running</span></span></a>\n </li>', | |
| 217 | 217 | ); |
| 218 | 218 | }); |
| 219 | 219 | |
| 220 | test("a 400-character description is one clamped line on the row, whole in its title, and the facts keep their own span (#69)", async () => { | |
| 221 | const long = "paragraph ".repeat(40).trim(); | |
| 222 | expect(long.length).toBeGreaterThanOrEqual(399); | |
| 223 | app.db.run("UPDATE repo SET description = ?1 WHERE id = ?2", [long, repo.id]); | |
| 224 | try { | |
| 225 | const home = await (await get("/")).text(); | |
| 226 | const at = home.indexOf('href="/alice/notes"'); | |
| 227 | const shelfRow = home.slice(home.lastIndexOf("<li>", at), home.indexOf("</li>", at)); | |
| 228 | expect(shelfRow).toContain(`title="${long}"`); | |
| 229 | expect(shelfRow).toContain(`<span class="shelf-desc">${long}</span>`); | |
| 230 | expect(shelfRow).toContain('class="shelf-facts"'); | |
| 231 | // The facts sit outside the description, so it cannot push them out. | |
| 232 | expect(shelfRow.indexOf('class="shelf-facts"')).toBeGreaterThan( | |
| 233 | shelfRow.indexOf("</span>", shelfRow.indexOf('class="shelf-desc"')), | |
| 234 | ); | |
| 235 | } finally { | |
| 236 | app.db.run("UPDATE repo SET description = ?1 WHERE id = ?2", ["a place for notes", repo.id]); | |
| 237 | } | |
| 238 | }); | |
| 239 | ||
| 220 | 240 | test("an app's home is the app in the studio, and the code in dev", async () => { |
| 221 | 241 | const studio = await (await get("/alice/notes")).text(); |
| 222 | 242 | expect(studio).toContain("Tell the crew"); |
| @@ -1547,6 +1567,7 @@ describe("the studio", () => { | ||
| 1547 | 1567 | const erinHome = await (await erinGet("/")).text(); |
| 1548 | 1568 | expect(erinHome).toContain("No apps yet"); |
| 1549 | 1569 | expect(erinHome).toContain('href="/apps/new"'); |
| 1570 | expect(erinHome).toContain('href="/?mode=dev"'); | |
| 1550 | 1571 | expect(erinHome).not.toContain('href="/alice/lighthouse"'); |
| 1551 | 1572 | expect(erinHome).not.toContain("tell the crew something"); |
| 1552 | 1573 |
new above
offline