Public: anyone can read this; the crew can change it.
/explore's rows and filters draw §5's .row and .filters #4629
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/4583-explore-s-rows-and-filters-draw-5-s-row → main 2 commits fork
5 files
changed +28
−52
docs/DESIGN.md +1 −1
| @@ -395,7 +395,7 @@ gone, so this list only shrinks. | ||
| 395 | 395 | A `.field` directly inside a `.bar` drops its bottom margin and the bar aligns to the end, so a labelled input and its button share a baseline |
| 396 | 396 | - **`style/components.ts`** — parts of the §5 components that the table does not spell out — the palette's list, the menu's items, a dialog's head and foot, comments, the theme picker — and a few small helpers beside them: `.cat` `.clone` `.clone-text` `.comment` `.comment-body` `.comment-form` `.dash` `.dialog-body` `.dialog-foot` `.dialog-head` `.empty-title` `.error-note` `.field-row` `.form-actions` `.hex` `.ico` `.ico-loader` `.list` `.menu-item` `.menu-panel` `.menu-sep` `.menu-title` `.palette-empty` `.palette-foot` `.palette-group` `.palette-heading` `.palette-input` `.palette-item` `.palette-list` `.passkey` `.passkey-lead` `.passkey-note` `.passkey-off` `.passkey-on` `.preview` `.sep` `.split` `.sprite` `.swatch` `.table-scroll` `.tell-here` `.theme-apply` `.theme-option` `.tick` `.timeline` `.tl-dot` `.toast-body` `.topbar-search` |
| 397 | 397 | - **`style/components.ts`, settings rows** — a list whose rows are settings (limits, capacity): the `<ul class="list setting-list">` lets title and meta wrap instead of ellipsizing, and a row's controls take their own line under 640px. `.row-fields` is the form in that row's trail (input, Save, Default): always visible, never hover-gated, wrapping under the text before it would clip. Use both for any page whose rows show a name, a current value, and a control to change it; use `.row-actions` only for secondary actions (delete, wake, pin, revoke). Flags, telemetry, cloud, people, crew roles and subscription headroom/budget use `.row-fields`. Classes: `.row-fields` `.setting-list` |
| 398 | - **`style/content.ts`, page furniture** — what frames a page's content: page and repo heads, the merge box, the front page and its tiles, explore, backups, boards, the fleet chart's series colours, lineage: `.app-shelf` `.backups-drill` `.backups-error` `.backups-now` `.backups-restore` `.bform` `.bgrid` `.bmanage` `.bp-h2` `.bp-h3` `.bp-w2` `.bp-w3` `.break` `.bt-bar` `.bt-grid` `.bt-group` `.bt-hist` `.bt-name` `.bt-procs` `.bt-row` `.bt-val` `.commit-body` `.commit-head` `.domain-records` `.explore-filters` `.explore-line` `.explore-main` `.explore-name` `.explore-row` `.fg-s0` `.fg-s1` `.fg-s2` `.fg-s3` `.fg-s4` `.fg-s5` `.fg-s6` `.fg-s7` `.filters` `.flag-form` `.front-admin` `.front-controls` `.front-crew` `.front-featured` `.front-featured-body` `.front-featured-look` `.front-issues` `.front-live` `.front-more` `.front-pick` `.front-pin-any` `.front-weights` `.frozen` `.frozen-form` `.frozen-sentence` `.headroom` `.issue-head` `.issue-meta` `.merge-box` `.merge-form` `.merge-headline` `.merge-sub` `.mirror-form` `.number` `.origin-mark` `.origin-name` `.origin-row` `.owner` `.page-head` `.path-head` `.policy-line` `.qf` `.repo-actions` `.repo-audience` `.repo-desc` `.repo-layout` `.repo-lineage` `.repo-title` `.search` `.slash` `.stage-detail` `.stage-dot` `.steps` `.sub` `.sys-pair` `.table-wrap` `.tok` `.tree-row` `.tree-total` `.wall-cells` `.where-sep` | |
| 398 | - **`style/content.ts`, page furniture** — what frames a page's content: page and repo heads, the merge box, the front page and its tiles, explore, backups, boards, the fleet chart's series colours, lineage: `.app-shelf` `.backups-drill` `.backups-error` `.backups-now` `.backups-restore` `.bform` `.bgrid` `.bmanage` `.bp-h2` `.bp-h3` `.bp-w2` `.bp-w3` `.break` `.bt-bar` `.bt-grid` `.bt-group` `.bt-hist` `.bt-name` `.bt-procs` `.bt-row` `.bt-val` `.commit-body` `.commit-head` `.domain-records` `.fg-s0` `.fg-s1` `.fg-s2` `.fg-s3` `.fg-s4` `.fg-s5` `.fg-s6` `.fg-s7` `.filters` `.flag-form` `.front-admin` `.front-controls` `.front-crew` `.front-featured` `.front-featured-body` `.front-featured-look` `.front-issues` `.front-live` `.front-more` `.front-pick` `.front-pin-any` `.front-weights` `.frozen` `.frozen-form` `.frozen-sentence` `.headroom` `.issue-head` `.issue-meta` `.merge-box` `.merge-form` `.merge-headline` `.merge-sub` `.mirror-form` `.number` `.origin-mark` `.origin-name` `.origin-row` `.owner` `.page-head` `.path-head` `.policy-line` `.qf` `.repo-actions` `.repo-audience` `.repo-desc` `.repo-layout` `.repo-lineage` `.repo-title` `.search` `.slash` `.stage-detail` `.stage-dot` `.steps` `.sub` `.sys-pair` `.table-wrap` `.tok` `.tree-row` `.tree-total` `.wall-cells` `.where-sep` | |
| 399 | 399 | - **`style/content.ts`, code view** — the source view and the syntax-highlight token classes `highlight-html.ts` builds as `tok-${kind}`: `.blob-view` `.ln` `.tok-attribute` `.tok-comment` `.tok-constant` `.tok-decorator` `.tok-deleted` `.tok-emphasis` `.tok-function` `.tok-heading` `.tok-inserted` `.tok-keyword` `.tok-link` `.tok-meta` `.tok-number` `.tok-operator` `.tok-plain` `.tok-property` `.tok-punctuation` `.tok-regex` `.tok-string` `.tok-tag` `.tok-type` `.tok-variable` |
| 400 | 400 | - **`style/content.ts`, prose (markdown)** — table cell alignment, built by `markdown.ts` as `ta-${align}`: `.ta-c` `.ta-r` |
| 401 | 401 | - **`style/content.ts`, diff viewer** — the diff table: its gutters, line kinds, file head and inline review comments: `.add` `.del` `.dh` `.diff` `.diff-body` `.diff-head` `.diff-note` `.diff-path` `.diff-stat` `.diff-stub` `.diff-summary` `.diff-table` `.dl` `.dl-review` `.dn` `.dn-comment` `.nonl` `.rc-hidden` `.rc-resolve` `.rc-resolved` `.wd` |
src/web/studio/explore.ts +18 −24
| @@ -18,7 +18,7 @@ import type { Ctx } from "../../http/context.ts"; | ||
| 18 | 18 | import { pageHead } from "../chrome.ts"; |
| 19 | 19 | import { html, when, type Html } from "../html.ts"; |
| 20 | 20 | import { page } from "../layout.ts"; |
| 21 | import { btn, empty, listCard, nameMark, seg, timeAgo } from "../ui.ts"; | |
| 21 | import { btn, empty, listCard, nameMark, row as rowOf, seg, timeAgo } from "../ui.ts"; | |
| 22 | 22 | |
| 23 | 23 | const href = (query: ExploreQuery): string => { |
| 24 | 24 | const line = exploreLine(query); |
| @@ -40,26 +40,20 @@ function numbers(row: ExploreRow): string { | ||
| 40 | 40 | |
| 41 | 41 | function rowHtml(row: ExploreRow): Html { |
| 42 | 42 | const said = numbers(row); |
| 43 | return html`<li class="row explore-row"> | |
| 44 | <span class="explore-main"> | |
| 45 | <a class="explore-name" href="${row.url}">${row.name}</a | |
| 46 | >${nameMark(row.mark)} | |
| 47 | ${when( | |
| 48 | row.description, | |
| 49 | (d) => html`<span class="muted small truncate">${d}</span>`, | |
| 50 | )} | |
| 51 | ${when(said, (s) => html`<span class="muted small">${s}</span>`)} | |
| 52 | </span> | |
| 53 | ${when( | |
| 54 | row.copyable === true ? row.repo : undefined, | |
| 55 | (repo) => | |
| 56 | btn("Make it yours", { | |
| 57 | href: `/${repo.owner}/${repo.name}/make-it-yours`, | |
| 58 | size: "sm", | |
| 59 | icon: "fork", | |
| 60 | }), | |
| 61 | )} | |
| 62 | </li>`; | |
| 43 | const meta = [row.description, said].filter((p) => p !== "" && p !== undefined); | |
| 44 | return rowOf({ | |
| 45 | title: html`<a href="${row.url}">${row.name}</a>${nameMark(row.mark)}`, | |
| 46 | ...(meta.length === 0 ? {} : { meta: html`${meta.join(" · ")}` }), | |
| 47 | ...(row.copyable === true && row.repo !== undefined | |
| 48 | ? { | |
| 49 | trail: btn("Make it yours", { | |
| 50 | href: `/${row.repo.owner}/${row.repo.name}/make-it-yours`, | |
| 51 | size: "sm", | |
| 52 | icon: "fork", | |
| 53 | }), | |
| 54 | } | |
| 55 | : {}), | |
| 56 | }); | |
| 63 | 57 | } |
| 64 | 58 | |
| 65 | 59 | /** Nothing to list: a bare line means nothing is public yet, not a miss. */ |
| @@ -109,9 +103,9 @@ export async function explorePage(ctx: Ctx): Promise<Response> { | ||
| 109 | 103 | const body = html`${pageHead("Explore", { |
| 110 | 104 | sub: "Apps and their makers, ranked by forks, ships, use and how lately — the numbers on every row.", |
| 111 | 105 | })} |
| 112 | <form method="get" action="/explore" class="bar mb explore-line"> | |
| 106 | <form method="get" action="/explore" class="bar mb"> | |
| 113 | 107 | <input |
| 114 | class="input mono" | |
| 108 | class="input mono search" | |
| 115 | 109 | name="q" |
| 116 | 110 | value="${exploreLine(query)}" |
| 117 | 111 | placeholder="kind:app sort:forks garden" |
| @@ -121,7 +115,7 @@ export async function explorePage(ctx: Ctx): Promise<Response> { | ||
| 121 | 115 | /> |
| 122 | 116 | ${btn("Go", { size: "sm" })} |
| 123 | 117 | </form> |
| 124 | <div class="filters mb explore-filters"> | |
| 118 | <div class="filters mb"> | |
| 125 | 119 | ${seg( |
| 126 | 120 | kinds.map((k) => ({ |
| 127 | 121 | label: k.label, |
src/web/style/content.ts −26
| @@ -356,31 +356,6 @@ form.inline { display: inline; margin-left: auto; } | ||
| 356 | 356 | grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); |
| 357 | 357 | } |
| 358 | 358 | |
| 359 | /* Explore (#607): a row is a name, a line, its numbers, and one verb. */ | |
| 360 | .explore-row { gap: 12px; } | |
| 361 | .explore-main { display: grid; gap: 2px; min-width: 0; flex: 1; } | |
| 362 | .explore-name { font-weight: 600; } | |
| 363 | .explore-line .input { flex: 1; min-width: 0; } | |
| 364 | /* On a phone the first rows belong on the first screen: one row of chips | |
| 365 | that scrolls sideways, and a row's verb under its numbers. The chips fade | |
| 366 | at the right edge, so a cut one says there is more; where the browser | |
| 367 | ties an animation to scrolling, the fade lifts at the end. */ | |
| 368 | @property --chip-fade { syntax: "<length>"; inherits: false; initial-value: 32px; } | |
| 369 | @keyframes chip-fade { 90% { --chip-fade: 32px; } to { --chip-fade: 0px; } } | |
| 370 | @media (max-width: 600px) { | |
| 371 | .explore-filters { | |
| 372 | flex-wrap: nowrap; overflow-x: auto; scrollbar-width: none; | |
| 373 | mask-image: linear-gradient(to right, var(--fg) calc(100% - var(--chip-fade)), transparent); | |
| 374 | } | |
| 375 | @supports (animation-timeline: scroll()) { | |
| 376 | .explore-filters { animation: chip-fade linear both; animation-timeline: scroll(self inline); } | |
| 377 | } | |
| 378 | .explore-filters .seg { flex: none; } | |
| 379 | .explore-row { flex-wrap: wrap; row-gap: 6px; } | |
| 380 | .explore-row > .explore-main { flex-basis: 100%; } | |
| 381 | .explore-row > .btn { margin-left: 0; } | |
| 382 | } | |
| 383 | ||
| 384 | 359 | /* The featured repository: its picture and its crew at work, side by side |
| 385 | 360 | on a desk, stacked on a phone. */ |
| 386 | 361 | .front-featured { |
| @@ -1095,7 +1070,6 @@ textarea.editor:focus-visible { outline: none; } | ||
| 1095 | 1070 | @media (prefers-reduced-motion: reduce) { |
| 1096 | 1071 | .front-live, .run-log .ln-pulse i { animation: none; } |
| 1097 | 1072 | .run-log .ln-pulse i { opacity: .6; } |
| 1098 | .explore-filters { animation: none; } | |
| 1099 | 1073 | .ex-dir > summary > .ico, .code-layout > #code-main[aria-busy="true"], .code-layout > [data-split]::after { transition: none; } |
| 1100 | 1074 | } |
| 1101 | 1075 | `; |
test/explore.test.ts +8
| @@ -237,6 +237,14 @@ describe("explore", () => { | ||
| 237 | 237 | expect(names(told.items)).toEqual(names((await api("kind:app sort:ships", bobToken)).items)); |
| 238 | 238 | }); |
| 239 | 239 | |
| 240 | test("the page draws §5's row and filters, and no explore- class", async () => { | |
| 241 | const page = await (await fetch(`${base}/explore`)).text(); | |
| 242 | expect(page).toContain('class="row"'); | |
| 243 | expect(page).toContain('class="row-title"'); | |
| 244 | expect(page).toContain('class="filters mb"'); | |
| 245 | expect(page).not.toContain("explore-"); | |
| 246 | }); | |
| 247 | ||
| 240 | 248 | test("a suspended account's apps and the account itself are offered to nobody", async () => { |
| 241 | 249 | const sus = await user("sus"); |
| 242 | 250 | await appOf(sus, "susapp"); |
test/name-marks.test.ts +1 −1
| @@ -286,7 +286,7 @@ describe("a name always says who it is", () => { | ||
| 286 | 286 | const row = (name: string) => |
| 287 | 287 | html.match( |
| 288 | 288 | new RegExp( |
| 289 | `<li class="row explore-row">[\\s\\S]*?href="/${name}"[\\s\\S]*?</li>`, | |
| 289 | `<li>\\s*<div class="row"[\\s\\S]*?href="/${name}"[\\s\\S]*?</li>`, | |
| 290 | 290 | ), |
| 291 | 291 | )?.[0] ?? ""; |
| 292 | 292 | expect(marksIn(row("alice"))).toEqual(["owner"]); |
new above
offline