Public: anyone can read this; the crew can change it.

Design: a repository's crew grants table uses §5's table and card foot, and grants/grants-foot leave §5d #3932

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/3924-design-a-repository-s-crew-grants-table → main 4 commits fork

  • Merged. mason/3924-design-a-repository-s-crew-grants-table was merged into main as 5ab7a7d.

Review

Sign in to review this change.
6 files changed +59 −43
docs/DESIGN.md +1 −1
@@ -392,7 +392,7 @@ gone, so this list only shrinks.
392392 - **`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`
393393 - **`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`
394394 - **`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`
395 - **`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: `.about` `.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` `.grants` `.grants-foot` `.handoff` `.handoff-links` `.headroom` `.hero` `.hero-terminal` `.issue-head` `.issue-meta` `.kv` `.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` `.quiet-details` `.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-dot` `.where-note` `.where-sep`
395 - **`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: `.about` `.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` `.hero` `.hero-terminal` `.issue-head` `.issue-meta` `.kv` `.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` `.quiet-details` `.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-dot` `.where-note` `.where-sep`
396396 - **`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`
397397 - **`style/content.ts`, prose (markdown)** — table cell alignment, built by `markdown.ts` as `ta-${align}`: `.ta-c` `.ta-r`
398398 - **`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/subscriptions.ts +37 −33
@@ -556,42 +556,46 @@ export function grantsSection(
556556 ${pool.map(
557557 (s) =>
558558 html`<td>
559 <input
560 type="checkbox"
561 name="grant"
562 value="${agentId === EVERYONE ? "*" : agentId}:${s.id}"
563 aria-label="${s.name} for ${agentId === EVERYONE
564 ? "everyone"
565 : label}"
566 ${granted.has(`${agentId}:${s.id}`) ? html`checked` : null}
567 />
559 <label class="check"
560 ><input
561 type="checkbox"
562 name="grant"
563 value="${agentId === EVERYONE ? "*" : agentId}:${s.id}"
564 aria-label="${s.name} for ${agentId === EVERYONE
565 ? "everyone"
566 : label}"
567 ${granted.has(`${agentId}:${s.id}`) ? html`checked` : null}
568 /></label>
568569 </td>`,
569570 )}
570571 </tr>`;
571 return card(
572 html`<form method="post" action="${o.action}">
573 <table class="table grants">
574 <thead>
575 <tr>
576 <th></th>
577 ${pool.map((s) => html`<th scope="col">${s.name}</th>`)}
578 </tr>
579 </thead>
580 <tbody>
581 ${line(EVERYONE, html`Everyone on the crew`)}
582 ${o.agents.map((a) => line(a.user.id, html`${a.user.username}`))}
583 </tbody>
584 </table>
585 <div class="grants-foot">
586 ${btn("Save", { variant: "primary", size: "sm" })}
587 <span class="muted small"
588 >Two ticked on one row take turns. Nothing ticked means the wider
589 grant applies: ${o.wider}.</span
590 >
591 </div>
592 </form>`,
593 { head, pad: false },
594 );
572 return html`<form method="post" action="${o.action}">
573 ${card(
574 html`<div class="table-scroll">
575 <table class="table">
576 <thead>
577 <tr>
578 <th></th>
579 ${pool.map((s) => html`<th scope="col">${s.name}</th>`)}
580 </tr>
581 </thead>
582 <tbody>
583 ${line(EVERYONE, html`Everyone on the crew`)}
584 ${o.agents.map((a) => line(a.user.id, html`${a.user.username}`))}
585 </tbody>
586 </table>
587 </div>`,
588 {
589 head,
590 pad: false,
591 foot: html`${btn("Save", { variant: "primary", size: "sm" })}
592 <span class="muted small"
593 >Two ticked on one row take turns. Nothing ticked means the wider
594 grant applies: ${o.wider}.</span
595 >`,
596 },
597 )}
598 </form>`;
595599 }
596600
597601 /**
src/web/style/components.ts +7
@@ -114,6 +114,13 @@ select.input {
114114 font-weight: 400; font-size: 14px; cursor: pointer; margin: 0;
115115 }
116116 .check input { accent-color: var(--accent); margin-top: 2px; flex: none; }
117 /* A thumb needs a 44px target and a box it can see; in a table the bare
118 check centres in its cell's target, so a miss does not tick the next one. */
119 @media (pointer: coarse) {
120 .check { min-height: 44px; min-width: 44px; align-items: center; }
121 .check input { width: 24px; height: 24px; margin: 0; }
122 .table .check { justify-content: center; }
123 }
117124
118125 .field { display: block; margin: 0 0 16px; }
119126 .field > label {
src/web/style/content.ts −8
@@ -325,14 +325,6 @@ a.origin-chip:hover { border-color: var(--border-strong); color: var(--fg); }
325325 .frozen-form .field { margin: 0; min-width: 0; flex: 1 1 14rem; max-width: 22rem; }
326326
327327 .wall-cells { margin: 8px 0 0; padding-left: 18px; font-family: var(--mono); font-size: 12.5px; }
328 .grants th, .grants td { padding: 8px 12px; border-top: 1px solid var(--border); }
329 .grants thead th { border-top: 0; font-weight: 600; }
330 .grants th[scope="col"], .grants td { text-align: center; }
331 .grants th[scope="row"] { text-align: left; font-weight: 500; white-space: nowrap; }
332 .grants { --grant-box: 16px; }
333 .grants input[type="checkbox"] { width: var(--grant-box); height: var(--grant-box); accent-color: var(--accent); }
334 @media (pointer: coarse) { .grants { --grant-box: 24px; } }
335 .grants-foot { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border-top: 1px solid var(--border); }
336328 /* A subscription's headroom for people: a small number beside its windows. */
337329 .headroom { display: flex; align-items: center; gap: 6px; }
338330 .headroom .input { width: 5em; }
test/check-target.test.ts added +11
@@ -0,0 +1,11 @@
1 /** #3924: a checkbox is a 44px target on a coarse pointer, so a grants table on a phone ticks the cell it means. */
2 import { expect, test } from "bun:test";
3 import { components } from "../src/web/style/components.ts";
4
5 test(".check grows to a 44px target with a 24px box under (pointer: coarse)", () => {
6 const block = components.slice(components.indexOf("@media (pointer: coarse) {\n .check"));
7 const rules = block.slice(0, block.indexOf("\n}") + 2);
8 expect(rules).toContain(".check { min-height: 44px; min-width: 44px;");
9 expect(rules).toContain(".check input { width: 24px; height: 24px; margin: 0; }");
10 expect(rules).toContain(".table .check { justify-content: center; }");
11 });
test/subscriptions.test.ts +3 −1
@@ -549,7 +549,9 @@ describe("the pages", () => {
549549 test("the crew page grants it to the repository, and a run there picks it", async () => {
550550 const work = account();
551551 let html = await get(`/ada/hello/settings/crew`);
552 expect(html).toContain('class="table grants"');
552 expect(html).toContain('<table class="table">');
553 expect(html).not.toContain('class="table grants"');
554 expect(html).not.toContain("grants-foot");
553555 expect(html).toContain(`value="*:${work.id}"`);
554556 const saved = await post(
555557 `/ada/hello/settings/crew/subscriptions`,

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs