Public: anyone can read this; the crew can change it.
The family and chain pages' lineage list draw §5's row, and .lineage, .lineage-children, .lineage-here leave the markup #4213
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/4134-the-family-and-chain-pages-lineage-list → main 6 commits fork 1 thread · 1 unresolved
8 files
changed +80
−53
docs/DESIGN.md +2 −2
| @@ -158,7 +158,7 @@ specification. | ||
| 158 | 158 | | **meter** `meter()` | `meter` | `data-tone=attn` when the share is a limit reached | native `<meter min=0 max=100>`, a whole percentage, an `aria-label` saying what it is a share of; no width rule per value, so no inline style | `▓▓▓░░ 62%` | |
| 159 | 159 | | **kbd** `kbd()` | `kbd` | — | — | a reverse-video key name | |
| 160 | 160 | | **card** `card()` / `listCard()` | `.card` (`.card-head`, `.card-links`, `.card-body`, `.card-foot`) | `data-tone=success\|danger\|merged\|attn` | — | a box with a title bar | |
| 161 | | **list row** `row()` | `.row` (`.row-lead`, `.row-main`, `.row-title`, `.row-meta`, `.row-trail`, `.row-actions`; settings: `.setting-list`, `.row-fields`) | `data-state=open\|closed\|merged\|draft` — colours the **lead glyph**, so a list filtered by state carries no badge repeating the filter | hover, focus, `data-nav`; `fold` makes the row a `<details>`'s `<summary>`, its lead the caret turned when open (/map's lanes); `.row-meta` is one honest line: nowrap, ellipsis, `title` — except in a `.setting-list`, where it wraps | one line: glyph, title, right-aligned meta | | |
| 161 | | **list row** `row()` | `.row` (`.row-lead`, `.row-main`, `.row-title`, `.row-meta`, `.row-trail`, `.row-actions`, `.row-children`; settings: `.setting-list`, `.row-fields`) | `data-state=open\|closed\|merged\|draft` — colours the **lead glyph**, so a list filtered by state carries no badge repeating the filter | hover, focus, `data-nav`; `fold` makes the row a `<details>`'s `<summary>`, its lead the caret turned when open (/map's lanes); `children` nests the rows under a row in a `<ul class="row-children">` inside its `<li>` — a tree (/family), a step in under a rule, real list nesting for assistive tech; `aria-current="true"` on the `<li>` (`current`) tints the row; `.row-meta` is one honest line: nowrap, ellipsis, `title` — except in a `.setting-list`, where it wraps | one line: glyph, title, right-aligned meta | | |
| 162 | 162 | | **edit row** | `.edit-row` | — | one line on a desktop, stacked below 720px | a form line | |
| 163 | 163 | | **colour field** | `.color-field` | — | native `<input type=color>` plus its hex; works with no script | a hex entry | |
| 164 | 164 | | **table** | `.table` | `data-dense`; `.num` cells right-aligned | — | aligned columns | |
| @@ -393,7 +393,7 @@ gone, so this list only shrinks. | ||
| 393 | 393 | - **`style/base.ts`** — the page shell (topbar, main, footer) and the layout and text utilities every page leans on: `.bar` `.bare` `.foot` `.grow` `.hide-lg` `.hide-sm` `.js-only` `.live-pill` `.mark` `.mb` `.mt` `.muted` `.narrow` `.narrow-wide` `.num` `.reduced` `.shell` `.shell-wide` `.skip` `.small` `.spacer` `.sr-only` `.stack` `.stack-tight` `.topbar` `.topbar-tools` `.truncate` `.wordmark` |
| 394 | 394 | - **`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` `.live-status` `.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` |
| 395 | 395 | - **`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` |
| 396 | - **`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, following, lineage: `.act` `.app-shelf` `.app-tile` `.app-tile-body` `.app-tile-desc` `.app-tile-look` `.app-tile-name` `.backups-drill` `.backups-error` `.backups-now` `.backups-restore` `.bform` `.bgrid` `.big` `.bmanage` `.boards-result` `.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` `.chart` `.chart-box` `.commit-body` `.commit-head` `.connect-more` `.connect-path` `.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` `.following` `.following-actions` `.following-eyebrow` `.following-foot` `.following-head` `.following-sentence` `.fork-bar` `.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` `.handoff` `.handoff-links` `.headroom` `.issue-head` `.issue-meta` `.lineage` `.lineage-children` `.lineage-here` `.merge-box` `.merge-form` `.merge-headline` `.merge-sub` `.mirror-form` `.number` `.origin-link` `.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` `.terminal-line` `.tok` `.tree-row` `.tree-total` `.wall-cells` `.where-sep` | |
| 396 | - **`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, following, lineage: `.act` `.app-shelf` `.app-tile` `.app-tile-body` `.app-tile-desc` `.app-tile-look` `.app-tile-name` `.backups-drill` `.backups-error` `.backups-now` `.backups-restore` `.bform` `.bgrid` `.big` `.bmanage` `.boards-result` `.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` `.chart` `.chart-box` `.commit-body` `.commit-head` `.connect-more` `.connect-path` `.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` `.following` `.following-actions` `.following-eyebrow` `.following-foot` `.following-head` `.following-sentence` `.fork-bar` `.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` `.handoff` `.handoff-links` `.headroom` `.issue-head` `.issue-meta` `.merge-box` `.merge-form` `.merge-headline` `.merge-sub` `.mirror-form` `.number` `.origin-link` `.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` `.terminal-line` `.tok` `.tree-row` `.tree-total` `.wall-cells` `.where-sep` | |
| 397 | 397 | - **`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` |
| 398 | 398 | - **`style/content.ts`, prose (markdown)** — table cell alignment, built by `markdown.ts` as `ta-${align}`: `.ta-c` `.ta-r` |
| 399 | 399 | - **`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/pages/chain.ts +8 −12
| @@ -17,7 +17,7 @@ import { loadRepo, notFoundPage, pageHead, repoBase, signInFirst } from "../chro | ||
| 17 | 17 | import { field, readForm } from "../form.ts"; |
| 18 | 18 | import { html, type Html } from "../html.ts"; |
| 19 | 19 | import { flashOrJson, page, redirect } from "../layout.ts"; |
| 20 | import { btn, card } from "../ui.ts"; | |
| 20 | import { badge, btn, card, row } from "../ui.ts"; | |
| 21 | 21 | |
| 22 | 22 | /** The events that change what the chain page says. */ |
| 23 | 23 | export const CHAIN_LIVE = "forge.severed,forge.proposal,forge.follow_mode,forge.upstream,forge.updates_taken"; |
| @@ -58,16 +58,12 @@ function lineRow(l: ChainLine, upstream?: string): Html { | ||
| 58 | 58 | : "the root: follows nothing" |
| 59 | 59 | : MODE_WORDS[l.mode]; |
| 60 | 60 | const ahead = l.ahead === null ? "" : l.ahead === 0 ? " · nothing of its own" : ` · ${l.ahead} of its own`; |
| 61 | return html`<li> | |
| 62 | <div class="row"> | |
| 63 | <span class="row-main"> | |
| 64 | <span class="row-title" | |
| 65 | >${l.repo}${where}${l.here ? html` <span class="lineage-here">you are here</span>` : null}</span | |
| 66 | > | |
| 67 | <span class="row-meta">${how}${ahead}${upstream === undefined ? null : ` · ${upstream}`}</span> | |
| 68 | </span> | |
| 69 | </div> | |
| 70 | </li>`; | |
| 61 | return row({ | |
| 62 | title: `${l.repo}${where}`, | |
| 63 | meta: html`${how}${ahead}${upstream === undefined ? null : ` · ${upstream}`}`, | |
| 64 | ...(l.here ? { trail: badge("you are here", { variant: "outline" }) } : {}), | |
| 65 | current: l.here, | |
| 66 | }); | |
| 71 | 67 | } |
| 72 | 68 | |
| 73 | 69 | /** Whether this repository is the forge code a spawned forge carries. */ |
| @@ -138,7 +134,7 @@ export async function chainPage(ctx: Ctx): Promise<Response> { | ||
| 138 | 134 | data-live-repo="${repo.ownerName}/${repo.name}" |
| 139 | 135 | > |
| 140 | 136 | ${card( |
| 141 | html`<ul class="list lineage" aria-label="Forges, root first"> | |
| 137 | html`<ul class="list" aria-label="Forges, root first"> | |
| 142 | 138 | ${view.lines.map((l) => lineRow(l, l.here && l.mode !== null ? upstreamWords(state) : undefined))} |
| 143 | 139 | </ul>`, |
| 144 | 140 | { pad: false }, |
src/web/pages/family.ts +19 −25
| @@ -16,7 +16,7 @@ import { MAX_DEPTH } from "../../spawn/heartbeat.ts"; | ||
| 16 | 16 | import { pageHead } from "../chrome.ts"; |
| 17 | 17 | import { html, type Html } from "../html.ts"; |
| 18 | 18 | import { page } from "../layout.ts"; |
| 19 | import { card, time } from "../ui.ts"; | |
| 19 | import { badge, card, row, time } from "../ui.ts"; | |
| 20 | 20 | |
| 21 | 21 | interface Node { |
| 22 | 22 | readonly label: string; |
| @@ -63,28 +63,22 @@ function standingNode(c: FamilyChild): Node { | ||
| 63 | 63 | }; |
| 64 | 64 | } |
| 65 | 65 | |
| 66 | function row(n: Node, depth: number): Html { | |
| 67 | const inner = html`<span class="row-main"> | |
| 68 | <span class="row-title" | |
| 69 | >${n.label}${n.here ? html` <span class="lineage-here">you are here</span>` : null}</span | |
| 70 | > | |
| 71 | ${n.meta.length === 0 | |
| 72 | ? null | |
| 73 | : html`<span class="row-meta" | |
| 74 | >${n.meta.map((m, i) => html`${i > 0 ? " · " : ""}${m}`)}</span | |
| 75 | >`} | |
| 76 | </span> | |
| 77 | ${n.badge === null ? null : html`<span class="row-trail">${n.badge}</span>`}`; | |
| 78 | return html`<li> | |
| 79 | ${n.href === null | |
| 80 | ? html`<div class="row">${inner}</div>` | |
| 81 | : html`<a class="row" href="${n.href}" rel="noopener noreferrer">${inner}</a>`} | |
| 82 | ${n.children.length === 0 || depth + 1 >= MAX_DEPTH | |
| 83 | ? null | |
| 84 | : html`<ul class="lineage-children"> | |
| 85 | ${n.children.map((c) => row(c, depth + 1))} | |
| 86 | </ul>`} | |
| 87 | </li>`; | |
| 66 | /** The tree as §5 rows: each forge's row holds the rows of the forges it sent. */ | |
| 67 | function treeRow(n: Node, depth: number): Html { | |
| 68 | const trail = [n.here ? badge("you are here", { variant: "outline" }) : null, n.badge].filter( | |
| 69 | (t): t is Html => t !== null, | |
| 70 | ); | |
| 71 | const kids = depth + 1 >= MAX_DEPTH ? [] : n.children.map((c) => treeRow(c, depth + 1)); | |
| 72 | return row({ | |
| 73 | title: n.label, | |
| 74 | ...(n.href === null ? {} : { href: n.href }), | |
| 75 | ...(n.meta.length === 0 | |
| 76 | ? {} | |
| 77 | : { meta: html`${n.meta.map((m, i) => html`${i > 0 ? " · " : ""}${m}`)}` }), | |
| 78 | ...(trail.length === 0 ? {} : { trail: html`${trail}` }), | |
| 79 | current: n.here === true, | |
| 80 | children: kids, | |
| 81 | }); | |
| 88 | 82 | } |
| 89 | 83 | |
| 90 | 84 | // GET /family |
| @@ -132,8 +126,8 @@ export async function familyPage( | ||
| 132 | 126 | sub: "The forge this one came from, and the forges it sent. Children report in; nothing here reaches into one.", |
| 133 | 127 | })} |
| 134 | 128 | ${card( |
| 135 | html`<ul class="list lineage" aria-label="Forges, by who sent whom"> | |
| 136 | ${row(root, 0)} | |
| 129 | html`<ul class="list" aria-label="Forges, by who sent whom"> | |
| 130 | ${treeRow(root, 0)} | |
| 137 | 131 | </ul> |
| 138 | 132 | ${kids.more > 0 |
| 139 | 133 | ? html`<p class="muted small card-body"> |
src/web/style/components.ts +5
| @@ -399,6 +399,11 @@ a.sha:hover { color: var(--accent); } | ||
| 399 | 399 | } |
| 400 | 400 | .row:focus-visible { outline-offset: -2px; } |
| 401 | 401 | a.row:hover, .row[data-nav]:hover { background: var(--muted); color: inherit; } |
| 402 | .list li[aria-current="true"] > .row { background: var(--muted); } | |
| 403 | /* A tree: the rows under a row hang from a rule, a step in. */ | |
| 404 | .row-children { list-style: none; margin: 0 0 0 22px; padding: 0; border-left: 1px solid var(--border); } | |
| 405 | .row-children > li { border-top: 1px solid var(--border); } | |
| 406 | @media (max-width: 719px) { .row-children { margin-left: 12px; } } | |
| 402 | 407 | a.row:hover .row-title { color: var(--accent); } |
| 403 | 408 | .row-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; } |
| 404 | 409 | .row-title { |
src/web/style/content.ts −13
| @@ -393,19 +393,6 @@ a.origin-chip:hover { border-color: var(--border-strong); color: var(--fg); } | ||
| 393 | 393 | .wall-cells li + li { margin-top: 3px; } |
| 394 | 394 | form.inline { display: inline; margin-left: auto; } |
| 395 | 395 | |
| 396 | /* The family tree: nesting is indentation and a rule, never a scrollbar. */ | |
| 397 | .lineage-children { | |
| 398 | list-style: none; margin: 0 0 0 22px; padding: 0; | |
| 399 | border-left: 1px solid var(--border); | |
| 400 | } | |
| 401 | .lineage-children .row { padding-left: 12px; } | |
| 402 | .lineage-here { | |
| 403 | margin-left: 6px; font-family: var(--mono); font-size: var(--fs-min); font-weight: 500; | |
| 404 | letter-spacing: 0.04em; color: var(--accent); | |
| 405 | } | |
| 406 | .lineage .row[aria-current="page"] { background: var(--muted); } | |
| 407 | @media (max-width: 719px) { .lineage-children { margin-left: 12px; } } | |
| 408 | ||
| 409 | 396 | .terminal-line { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 14px; } |
| 410 | 397 | |
| 411 | 398 | /* The studio's front door: the public apps, live first, as pictures. */ |
src/web/ui.ts +6 −1
| @@ -879,6 +879,9 @@ export interface RowOptions { | ||
| 879 | 879 | data?: Readonly<Record<string, string>>; |
| 880 | 880 | /** The row is the one chosen (`aria-current="true"`). */ |
| 881 | 881 | current?: boolean; |
| 882 | /** The rows under this one, already drawn (`row()`s): a nested `<ul>` inside | |
| 883 | * the `<li>`, so a tree stays a tree to assistive tech and with no script. */ | |
| 884 | children?: readonly Html[]; | |
| 882 | 885 | /** The row folds: it is the `<summary>` of a `<details>` holding `body`, |
| 883 | 886 | * open when `open` says so. Its `lead` is the caret. */ |
| 884 | 887 | fold?: { id: string; open: boolean; body: Html }; |
| @@ -916,7 +919,9 @@ export function row(o: RowOptions): Html { | ||
| 916 | 919 | </details>` |
| 917 | 920 | : o.href !== undefined |
| 918 | 921 | ? html`<a href="${o.href}" ${attrs}>${inner}</a>` |
| 919 | : html`<div ${attrs}>${inner}</div>`} | |
| 922 | : html`<div ${attrs}>${inner}</div>`}${o.children === undefined || o.children.length === 0 | |
| 923 | ? null | |
| 924 | : html`<ul class="row-children">${[...o.children]}</ul>`} | |
| 920 | 925 | </li>`; |
| 921 | 926 | } |
| 922 | 927 |
test/family-heartbeat.test.ts +32
| @@ -31,6 +31,7 @@ import { familyScreen } from "../src/tui/screens/family.ts"; | ||
| 31 | 31 | import { headlessTerminal } from "../src/tui/term/terminal.ts"; |
| 32 | 32 | import type { Ctx } from "../src/http/context.ts"; |
| 33 | 33 | import { familyPage } from "../src/web/pages/family.ts"; |
| 34 | import { row } from "../src/web/ui.ts"; | |
| 34 | 35 | import { settleApp } from "./helpers/tui.ts"; |
| 35 | 36 | import { quietLogs } from "./helpers/globals.ts"; |
| 36 | 37 | |
| @@ -283,6 +284,37 @@ describe("family heartbeats", () => { | ||
| 283 | 284 | expect(await text(ctxOf(true), () => ({ total: 0, running: 0, notRunning: [] }))).not.toContain("data-apps"); |
| 284 | 285 | }); |
| 285 | 286 | |
| 287 | test("the family page draws §5's row: the old lineage names are gone, the words and links stay", async () => { | |
| 288 | const ctx: Ctx = { | |
| 289 | app: parent, | |
| 290 | request: new Request("http://localhost/family"), | |
| 291 | url: new URL("http://localhost/family"), | |
| 292 | params: {}, | |
| 293 | auth: { user: { id: 1, username: "alice", isAdmin: true } as never, scope: undefined, via: "session" }, | |
| 294 | theme: undefined, | |
| 295 | }; | |
| 296 | const page = await (await familyPage(ctx, two)).text(); | |
| 297 | expect(page).not.toMatch(/lineage(-children|-here)?\b/); | |
| 298 | expect(page).toContain("you are here"); | |
| 299 | expect(page).toContain('aria-label="Forges, by who sent whom"'); | |
| 300 | expect(page).toContain('class="row"'); | |
| 301 | expect(page).toContain("version "); | |
| 302 | // The tree stays a tree: what this forge sent is a list inside this forge's row. | |
| 303 | expect(page).toContain('<ul class="row-children">'); | |
| 304 | expect(page.indexOf('aria-current="true"')).toBeLessThan(page.indexOf("row-children")); | |
| 305 | }); | |
| 306 | ||
| 307 | test("row() nests children's rows inside its own li, after the row", () => { | |
| 308 | const grand = row({ title: "grand" }); | |
| 309 | const child = row({ title: "child", children: [grand] }); | |
| 310 | const top = row({ title: "top", children: [child] }).text; | |
| 311 | expect(top.indexOf("top")).toBeLessThan(top.indexOf("child")); | |
| 312 | expect(top.indexOf("child")).toBeLessThan(top.indexOf("grand")); | |
| 313 | expect(top.match(/<ul class="row-children">/g)?.length).toBe(2); | |
| 314 | expect(row({ title: "x" }).text).not.toContain("row-children"); | |
| 315 | expect(row({ title: "x", children: [] }).text).not.toContain("row-children"); | |
| 316 | }); | |
| 317 | ||
| 286 | 318 | test("appsLine counts what runs and says why the rest does not", () => { |
| 287 | 319 | expect(appsLine({ total: 2, running: 0, notRunning: [{ slug: "a", why: "not started" }, { slug: "b", why: "boom: x" }] })) |
| 288 | 320 | .toBe("0 of 2 apps running — a not deployed; b failing"); |
test/following.test.ts +8
| @@ -1095,6 +1095,10 @@ describe("an open chain page hears a sever", () => { | ||
| 1095 | 1095 | expect(page).toContain('data-live="forge.severed,forge.proposal,forge.follow_mode,forge.upstream,forge.updates_taken"'); |
| 1096 | 1096 | expect(page).toContain('data-live-repo="kid/forge"'); |
| 1097 | 1097 | expect(page).not.toContain("follows nothing since"); |
| 1098 | expect(page).not.toMatch(/lineage(-children|-here)?\b/); | |
| 1099 | expect(page).toContain('aria-label="Forges, root first"'); | |
| 1100 | expect(page).toContain("you are here"); | |
| 1101 | expect(page).toContain('action="/kid/forge/following/mode"'); | |
| 1098 | 1102 | }); |
| 1099 | 1103 | |
| 1100 | 1104 | test("a sever reaches the viewer's stream with the repo the region filters on, and a refetch shows it", async () => { |
| @@ -1124,5 +1128,9 @@ describe("an open chain page hears a sever", () => { | ||
| 1124 | 1128 | const page = await chainPage(); |
| 1125 | 1129 | expect(page).toContain("follows nothing since"); |
| 1126 | 1130 | expect(page).toContain('id="live-chain"'); |
| 1131 | expect(page).not.toMatch(/lineage(-children|-here)?\b/); | |
| 1132 | expect(page).toContain('aria-label="Forges, root first"'); | |
| 1133 | expect(page).toContain("you are here"); | |
| 1134 | expect(page).toContain('id="severed"'); | |
| 1127 | 1135 | }); |
| 1128 | 1136 | }); |
new above
offline
The old family link carried rel="noopener noreferrer" for child-forge URLs; this anchor does not. Add an optional rel (or external) option to RowOptions and pass it from family.ts.