Public: anyone can read this; the crew can change it.
The live-update pill draws §5's badge, and .live-pill leaves the markup and its script #4715
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/4702-the-live-update-pill-draws-5-s-badge-and → main 2 commits fork
6 files
changed +36
−11
docs/DESIGN.md +1 −1
| @@ -391,7 +391,7 @@ onto a §5 component, or into the table when a page-local thing turns out to | ||
| 391 | 391 | be a component — and the check fails a name kept here after its last rule is |
| 392 | 392 | gone, so this list only shrinks. |
| 393 | 393 | |
| 394 | - **`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 | - **`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` `.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` | |
| 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-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` |
docs/design/areas/live.md added +3
| @@ -0,0 +1,3 @@ | ||
| 1 | # Live updates | |
| 2 | ||
| 3 | - `.live-news` — the fixed, bottom-centred, click-through wrapper of the live-update pill. The pill is a §5 badge (attn tone) inside it; the wrapper keeps it out of flow so showing it moves nothing, and the rule gives that badge a 36px target. A bare `.badge` cannot carry this: it is a small inline element that would insert itself into the content where the mount sits. |
src/web/live.ts +5 −3
| @@ -70,9 +70,11 @@ export function liveMount(config: LiveConfig): Html { | ||
| 70 | 70 | role="status" |
| 71 | 71 | aria-live="polite" |
| 72 | 72 | ></p> |
| 73 | <a id="live-pill" class="live-pill" href="" hidden | |
| 74 | >↻ New activity — refresh</a | |
| 75 | >`; | |
| 73 | <div class="live-news"> | |
| 74 | <a id="live-pill" class="badge" data-variant="attn" href="" hidden | |
| 75 | >↻ New activity — refresh</a | |
| 76 | > | |
| 77 | </div>`; | |
| 76 | 78 | } |
| 77 | 79 | |
| 78 | 80 | export const liveClientJs = /* js */ ` |
src/web/style/base.ts +1 −1
| @@ -222,7 +222,7 @@ export const utilities = /* css */ ` | ||
| 222 | 222 | } |
| 223 | 223 | |
| 224 | 224 | @media print { |
| 225 | .topbar, .foot, .live-pill, .sticky-action, .toast { display: none !important; } | |
| 225 | .topbar, .foot, .live-news, .sticky-action, .toast { display: none !important; } | |
| 226 | 226 | .card { box-shadow: none; } |
| 227 | 227 | } |
| 228 | 228 | } |
src/web/style/components.ts +7 −6
| @@ -893,14 +893,15 @@ details[open] > summary.row > .row-lead { transform: rotate(90deg); } | ||
| 893 | 893 | @media (min-width: 720px) { .sticky-action { position: static; background: none; padding: 0; } .sticky-action .btn { flex: none; } } |
| 894 | 894 | |
| 895 | 895 | /* ---- live updates ---- */ |
| 896 | .live-pill { | |
| 897 | position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%); z-index: 25; | |
| 898 | padding: 0 14px; min-height: 36px; display: inline-flex; align-items: center; gap: 6px; | |
| 899 | background: var(--primary); color: var(--primary-fg); border-radius: 999px; | |
| 900 | font-size: 13px; font-weight: 500; box-shadow: var(--shadow-2); | |
| 896 | .live-news { | |
| 897 | position: fixed; left: 0; right: 0; bottom: 20px; z-index: 25; | |
| 898 | display: flex; justify-content: center; pointer-events: none; | |
| 899 | } | |
| 900 | .live-news .badge { | |
| 901 | pointer-events: auto; min-height: 36px; padding: 0 14px; font-size: 13px; | |
| 902 | background: var(--card); box-shadow: var(--shadow-2); | |
| 901 | 903 | animation: pop 200ms var(--ease) both; |
| 902 | 904 | } |
| 903 | .live-pill:hover { color: var(--primary-fg); opacity: .92; } | |
| 904 | 905 | |
| 905 | 906 | } |
| 906 | 907 |
test/live-pages.test.ts +19
| @@ -291,4 +291,23 @@ describe("the live mount's markup", () => { | ||
| 291 | 291 | expect(out).toContain('id="live-pill"'); |
| 292 | 292 | expect(out).toContain("New activity — refresh"); |
| 293 | 293 | }); |
| 294 | ||
| 295 | test("the pill is §5's attn badge, hidden until news, and the script finds it by id alone", async () => { | |
| 296 | const { liveMount, liveClientJs } = await import("../src/web/live.ts"); | |
| 297 | const out = String(liveMount({ repo: "/a/b" })); | |
| 298 | expect(out).toMatch(/<a\s+id="live-pill"\s+class="badge"\s+data-variant="attn"\s+href=""\s+hidden/); | |
| 299 | expect(out).toMatch(/<div class="live-news">\s*<a id="live-pill"/); | |
| 300 | expect(out).not.toMatch(/class="[^"]*\blive-pill\b/); | |
| 301 | expect(liveClientJs).toContain("document.getElementById('live-pill')"); | |
| 302 | expect(liveClientJs).not.toMatch(/\.live-pill|querySelector\([^)]*live-pill/); | |
| 303 | }); | |
| 304 | }); | |
| 305 | ||
| 306 | describe("the live pill's wrapper", () => { | |
| 307 | test("is out of flow and the badge inside is a 36px target", async () => { | |
| 308 | const { components } = await import("../src/web/style/components.ts"); | |
| 309 | const rule = (sel: string) => components.slice(components.indexOf(sel + " {")).split("}")[0]; | |
| 310 | expect(rule(".live-news")).toMatch(/position: fixed/); | |
| 311 | expect(rule(".live-news .badge")).toMatch(/min-height: 36px/); | |
| 312 | }); | |
| 294 | 313 | }); |
new above
offline