Public: anyone can read this; the crew can change it.
An app's domain DNS records draw §5's table, and .domain-records leaves the stylesheet #4676
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/4613-an-app-s-domain-dns-records-draw-5-s-tab → main 17 commits fork 2 threads · 2 unresolved
7 files
changed +51
−22
docs/DESIGN.md −1
| @@ -433,7 +433,6 @@ the same line (#4888). | ||
| 433 | 433 | - `.break` |
| 434 | 434 | - `.commit-body` |
| 435 | 435 | - `.commit-head` |
| 436 | - `.domain-records` | |
| 437 | 436 | - `.filters` |
| 438 | 437 | - `.front-admin` |
| 439 | 438 | - `.front-crew` |
docs/design/areas/backups.md added +3
| @@ -0,0 +1,3 @@ | ||
| 1 | # Backups | |
| 2 | ||
| 3 | - `.restore-command` — the one-line restore command on /settings/backups: a `<pre>` that wraps, scrolls with the keyboard (`tabindex="0"`) and takes its height from its text, so a one-liner is not a log pane. |
src/web/pages/backups.ts +1 −1
| @@ -335,7 +335,7 @@ async function movingPart( | ||
| 335 | 335 | now, |
| 336 | 336 | }); |
| 337 | 337 | const restore = snapshots[0] |
| 338 | ? html`<pre class="domain-records" tabindex="0" aria-label="Restore command">bun run src/snapshot/restore.ts ${snapshots[0].name} /path/to/empty/data</pre>` | |
| 338 | ? html`<pre class="restore-command" tabindex="0" aria-label="Restore command">bun run src/snapshot/restore.ts ${snapshots[0].name} /path/to/empty/data</pre>` | |
| 339 | 339 | : null; |
| 340 | 340 | return html`<div id="backups-live" data-live="backup.*" data-live-src="/settings/backups/live">${recovery}${card( |
| 341 | 341 | html`<p> |
src/web/pages/settings.ts +38 −18
| @@ -923,7 +923,7 @@ function appDomainsView( | ||
| 923 | 923 | html`<li> |
| 924 | 924 | <div class="row"> |
| 925 | 925 | <span class="row-lead">${icon("globe")}</span> |
| 926 | <span class="row-main"> | |
| 926 | <div class="row-main"> | |
| 927 | 927 | <span class="row-title" |
| 928 | 928 | ><span class="mono">${d.host}</span> ${d.verified |
| 929 | 929 | ? badge("live", { tone: "success" }) |
| @@ -935,8 +935,8 @@ function appDomainsView( | ||
| 935 | 935 | ? null |
| 936 | 936 | : html` · checked ${time(d.checkedAt)}`}</span |
| 937 | 937 | > |
| 938 | ${when(!d.verified, () => recordsToPublish(d.records))} | |
| 939 | </span> | |
| 938 | ${when(!d.verified, () => recordsToPublish(d.host, d.records))} | |
| 939 | </div> | |
| 940 | 940 | <span class="row-trail"> |
| 941 | 941 | <form method="post" action="${at(d.host)}/check"> |
| 942 | 942 | ${btn("Check now", { size: "sm", icon: "globe" })} |
| @@ -1005,21 +1005,41 @@ function appDomainsView( | ||
| 1005 | 1005 | } |
| 1006 | 1006 | |
| 1007 | 1007 | /** The records as a person copies them into a zone: type, name, value. */ |
outdated
Add a caption or aria-label naming the host. Wrap the scroll container as a labelled, focusable region (tabindex=0) so the overflow is reachable by keyboard at 360px. |
||
| 1008 | function recordsToPublish(records: DnsRecord[]): Html { | |
| 1009 | return html`${records.map( | |
| 1010 | (r) => | |
| 1011 | html`<span class="row-meta" | |
| 1012 | >${badge(r.type, { variant: "outline" })} | |
| 1013 | <span class="mono">${r.name}</span> | |
| 1014 | <span aria-hidden="true">→</span> | |
| 1015 | <span class="mono record-value" title="${r.value}" | |
| 1016 | >${r.value}</span | |
| 1017 | ></span | |
| 1018 | >`, | |
| 1019 | )} | |
| 1020 | ${records | |
| 1021 | .filter((r) => r.note !== undefined) | |
| 1022 | .map((r) => html`<span class="row-meta">${r.note}</span>`)}`; | |
| 1008 | function recordsToPublish(host: string, records: DnsRecord[]): Html { | |
| 1009 | return html`<div | |
| 1010 | class="table-scroll" | |
| 1011 | role="region" | |
| 1012 | tabindex="0" | |
| 1013 | aria-label="DNS records for ${host}" | |
| 1014 | > | |
| 1015 | <table class="table" data-dense> | |
| 1016 | <caption class="sr-only"> | |
| 1017 | DNS records for ${host} | |
| 1018 | </caption> | |
| 1019 | <thead> | |
| 1020 | <tr> | |
| 1021 | <th>Type</th> | |
| 1022 | <th>Name</th> | |
| 1023 | <th>Value</th> | |
| 1024 | </tr> | |
| 1025 | </thead> | |
| 1026 | <tbody> | |
| 1027 | ${records.map( | |
| 1028 | (r) => | |
| 1029 | html`<tr> | |
| 1030 | <td>${badge(r.type, { variant: "outline" })}</td> | |
| 1031 | <td class="mono">${r.name}</td> | |
| 1032 | <td class="mono record-value" title="${r.value}"> | |
| 1033 | ${r.value} | |
| 1034 | </td> | |
| 1035 | </tr>`, | |
| 1036 | )} | |
| 1037 | </tbody> | |
| 1038 | </table> | |
| 1039 | </div> | |
| 1040 | ${records | |
| 1041 | .filter((r) => r.note !== undefined) | |
| 1042 | .map((r) => html`<span class="row-meta">${r.note}</span>`)}`; | |
| 1023 | 1043 | } |
| 1024 | 1044 | |
| 1025 | 1045 | const appValues = (repo: Repo, record: AppRecord | undefined): AppValues => ({ |
src/web/style/content.ts +1 −2
| @@ -219,8 +219,7 @@ a.origin-chip:hover { border-color: var(--border-strong); color: var(--fg); } | ||
| 219 | 219 | .fg-s5 { fill: var(--series-5); color: var(--series-5); } |
| 220 | 220 | .fg-s6 { fill: var(--series-6); color: var(--series-6); } |
| 221 | 221 | .fg-s7 { fill: var(--series-7); color: var(--series-7); } |
| 222 | /* The records a domain waits on: type, name, value — as a zone file reads. */ | |
| 223 | .domain-records { margin: 6px 0 2px; padding: 8px 10px; font-size: 12.5px; line-height: 1.6; white-space: pre; overflow-x: auto; scrollbar-width: thin; background: var(--muted); border-radius: var(--radius); } | |
| 222 | .restore-command { margin: 8px 0 0; padding: 8px 10px; font-size: 12.5px; white-space: pre-wrap; word-break: break-all; background: var(--muted); border-radius: var(--radius); } | |
| 224 | 223 | .policy-line { |
| 225 | 224 | display: flex; align-items: center; gap: 8px; flex-wrap: wrap; |
| 226 | 225 | margin: 12px 0 0; font-size: 13px; color: var(--muted-fg); |
test/app-domains.test.ts +6
| @@ -596,6 +596,12 @@ describe("an app gets its own domain", () => { | ||
| 596 | 596 | ); |
| 597 | 597 | expect(page).toContain("ALIAS"); |
| 598 | 598 | expect(page).not.toContain("domain-records"); |
| 599 | // The table is named, and its scroller is a focusable labelled region. | |
| 600 | expect(page).toContain("<caption"); | |
| 601 | expect(page).toMatch( | |
| 602 | /<div\s+class="table-scroll"\s+role="region"\s+tabindex="0"\s+aria-label="DNS records for example\.com"/, | |
| 603 | ); | |
| 604 | expect(page).toMatch(/<table class="table" data-dense>[\s\S]*?<th>Type<\/th>/); | |
| 599 | 605 | // The records wrap instead of truncating (.row-meta only wraps inside |
| 600 | 606 | // .setting-list), carry the type badge, and the whole value is selectable. |
| 601 | 607 | const token = getDomain(app.db, "example.com")!.token; |
test/backups-parts.test.ts +2
| @@ -76,6 +76,8 @@ test("the page carries none of the old classes, and keeps its words, links and f | ||
| 76 | 76 | test("the restore command is a scrollable pre in the card body, not in the foot", async () => { |
| 77 | 77 | const page = await get(); |
| 78 | 78 | const pre = /<pre[^>]*>[^]*?<\/pre>/.exec(page)?.[0] ?? ""; |
| 79 | expect(pre).toContain('class="restore-command"'); | |
| 80 | expect(page).not.toContain("domain-records"); | |
| 79 | 81 | expect(pre).toContain('tabindex="0"'); |
| 80 | 82 | expect(pre).toContain("aria-label="); |
| 81 | 83 | expect(pre).toContain("bun run src/snapshot/restore.ts"); |
new above
offline
The class rename has no test that would fail without it. Assert restore-command is present and domain-records is absent in the backups page test.