diff --git a/docs/design/artboards/d215/README.md b/docs/design/artboards/d215/README.md index 1f11e902f..7279963ca 100644 --- a/docs/design/artboards/d215/README.md +++ b/docs/design/artboards/d215/README.md @@ -6,23 +6,22 @@ The spec for D215 (artboard v2). Open `index.html` in a browser. The toggle at t - A second block under agent settings draws the states of that page (policy and machine, identity edit, connector grants, runtime with the model and effort selects, own API key) at 1440 and 390. The runtime and key frames follow V2RuntimeControls and V2AgentProviderKey, including the stored-model refusal and the key-save error. Those frames carry `data-state`, so `measure_after.js` skips them and `targets.json` does not move. - Two state-only surfaces: `task-record` (the task record page, loaded and not-available, under the reply trace) and `handoff` (the OAuth and Discord return pages, `data-state` working, done, error, on the auth card). Every frame carries `data-state`, so the scorer skips them and `targets.json` does not move. - A block under Settings draws the free-plan Plan row and the administrator-only Administration group (`data-state="admin-free"`, 1440 and 390, also skipped by the scorer). -- Under Activity and under Artifacts, a block redraws each page in the picked language (`#s-activity-signal`, `#s-artifacts-signal`): busy and day zero at 1440 and 390, built only from the seven picked surfaces' blocks. Activity's build scores its four frames (no `data-state`) and retired the traced `#s-activity` section. Artifacts' frames still carry `data-state="signal"`, so the scorer skips them until its build lands. +- Under Activity and under Artifacts, a block redraws each page in the picked language (`#s-activity-signal`, `#s-artifacts-signal`): busy and day zero at 1440 and 390, built only from the seven picked surfaces' blocks. Sam approved both on 11-03. Each page's build dropped the `data-state`, so the eight frames (`activity-busy`, `activity-dayzero`, `artifacts-busy`, `artifacts-dayzero`) are the scored ones, and retired the traced `#s-activity` and `#s-artifacts` sections and their CSS. - The theme switch (sun / moon, D224) sits in the rail above the avatar at 1440. At 390 there is no rail, so the same switch rides the Pods top bar, left of Search. It is a two-item segment: the item for the theme in effect is the white key. - `before/` holds prod captures from 2026-10-31, blurred so they show layout and density only. - `targets.json` holds the helper-word and scroll targets per frame. `cap/measure_after.js` writes it from `index.html`. - `cap/` holds the measuring scripts. See the header of `measure_after.js` for its env. -- Every frame without `data-state` must be listed in `measure_after.js`, either in `SURFACES` (scored) or in `SKIPPED` (not scored, with a reason). Activity, Artifacts and the two trace "before" frames are skipped. `node --test cap/measure_after.test.js` fails on any frame that is in neither list. +- Every frame without `data-state` must be listed in `measure_after.js`, either in `SURFACES` (scored) or in `SKIPPED` (not scored, with a reason). The two trace "before" frames, `run-trace-before` and `chat-trace-before`, draw the old pages and are skipped: they are never a target. On prod, `activity-busy` is measured at `/v2/activity` and `artifacts-busy` at `/v2/artifacts`; the two day-zero frames need an account with nothing in it, so they are artboard-only. The Artifacts prod check refuses the error line, the spinner and any list shorter than the frame's six rows. It then measures the first six rows, because prod returns up to 50 and the frame draws six. `node --test cap/measure_after.test.js` fails on any frame that is in neither list. ## Where each frame came from -Passing the prod check means a page matches its frame. It means the page matches the style Sam picked only when the frame came from his pick. Every frame is in one of four groups. A group's frames are scored only where the table says so, and prod scores are reported per group, never as one total. +Passing the prod check means a page matches its frame. It means the page matches the style Sam picked only when the frame came from his pick. Every frame is in one of three groups. A group's frames are scored only where the table says so, and prod scores are reported per group, never as one total. | Group | Frames | Source | |---|---|---| | **From Sam's pick** | sidebar, chat, board, agent-memory, agent-settings, settings, connectors | The seven surfaces picked on 10-31 (#3059). Later edits: the theme switch (#3175) and the open thread (#3178). #3288 (the 390 agent-settings and settings frames, brought up to the shipped pages) and #3289 (memory notes drawn as the API holds them) moved those frames toward the live page, so read them with that in mind. | | **Drawn after the pick** | dm, byo, byo-handoff, connect, team, firstrun-\*, signin, signup, signup-sent, verify-email, forgot-password, reset-password, cli-authorize, invite, invite-required, profile, agent-profile, run-trace-after, chat-trace, chat-agents-away, and the agent-settings and settings state blocks | New drawings in the picked language, from 11-01 on. Sam has not reviewed them as a set. | -| **Approved by Sam, 11-03** | activity-busy, activity-dayzero (`#s-activity-signal`, scored since the Activity build); artifacts-\* with `data-state="signal"`; task-record-\*; handoff-\* | Sam saw each one live-beside-proposed and answered "Build as drawn" for all four pages. Activity and Artifacts are built only from the picked surfaces' blocks. The scorer still skips these frames (they carry `data-state`). Each page's build PR makes its approved frame the scored one and retires the frame it replaces. | -| **Drawn from live, being retired** | artifacts-\* without `data-state` | Traced from the live page with small fixes (#3318). The scorer skips them (`SKIPPED` in `cap/measure_after.js`); scoring a page against them would only show it has not drifted from itself. The Artifacts build PR removes them; the Activity build removed its traced frames. The two before frames, `run-trace-before` and `chat-trace-before`, draw the old pages and are never a target. | +| **Approved by Sam, 11-03** | activity-busy, activity-dayzero (`#s-activity-signal`); artifacts-busy, artifacts-dayzero (`#s-artifacts-signal`); task-record-\*; handoff-\* | Sam saw each one live-beside-proposed and answered "Build as drawn" for all four pages. Activity and Artifacts are built only from the picked surfaces' blocks, and both are scored since their builds. Task record and handoff still carry `data-state`, so the scorer skips them. | ## Rules for D215 PRs diff --git a/docs/design/artboards/d215/cap/measure_after.js b/docs/design/artboards/d215/cap/measure_after.js index 85d4f75e7..3cbea9ee3 100644 --- a/docs/design/artboards/d215/cap/measure_after.js +++ b/docs/design/artboards/d215/cap/measure_after.js @@ -29,15 +29,12 @@ const SURFACES = [ 'settings', 'agent-settings', 'agent-memory', 'board', 'connectors', 'chat', 'sidebar', 'byo', 'byo-handoff', 'firstrun-welcome', 'firstrun-waiting', 'firstrun-connected', 'firstrun-error', 'connect', 'team', 'chat-agents-away', 'chat-trace', 'run-trace-after', 'profile', 'firstrun-welcome-nohost', - // Activity: the frames Sam approved 11-03 ("Build as drawn"), built in V2ActivityPage. - 'activity-busy', 'activity-dayzero', + // Activity and Artifacts: the frames Sam approved 11-03 ("Build as drawn"), built in + // V2ActivityPage and V2ArtifactsPage. + 'activity-busy', 'activity-dayzero', 'artifacts-busy', 'artifacts-dayzero', ]; // Base frames (no data-state) that are drawn but deliberately not scored. Each needs a reason. const SKIPPED = { - // Artifacts is being redesigned in another session; targets taken now would pin frames that - // are about to change. Score them once that redesign lands. - 'artifacts-busy': 'redesign in progress elsewhere', - 'artifacts-dayzero': 'redesign in progress elsewhere', // Drawings of the old page beside their "after" frames (390 only). They are what the fix moves // away from, so they are never a target. 'chat-trace-before': 'before shot, not a target', @@ -47,8 +44,9 @@ const SKIPPED = { // a real key. The first-run dialog is a latch-gated overlay, and the Scout welcome only exists in a // brand-new pod. chat-agents-away and chat-trace need a pod in that exact state, run-trace-after a // run id and profile a person id, none of which prod() takes yet, so they are artboard-only for now. -// activity-dayzero needs an account with no asks and no agents, so only the busy page is checked. -const PROD_SURFACES = ['settings', 'agent-settings', 'agent-memory', 'board', 'connectors', 'chat', 'sidebar', 'byo', 'connect', 'team', 'activity-busy']; +// activity-dayzero needs an account with no asks and no agents, and artifacts-dayzero one with no +// shared file, so only the two busy pages are checked on prod. +const PROD_SURFACES = ['settings', 'agent-settings', 'agent-memory', 'board', 'connectors', 'chat', 'sidebar', 'byo', 'connect', 'team', 'activity-busy', 'artifacts-busy']; const VIEWPORTS = ['1440', '390']; const EXPECTED = SURFACES.flatMap(s => VIEWPORTS.map(v => `${s}-${v}`)); @@ -192,6 +190,16 @@ async function checkSurface(p, [name, route, opt = {}], vp, targets) { if (!(await p.locator(opt.list390.list).first().isVisible())) throw new Error(`${name}-${vp}: pods list ${opt.list390.list} is not visible`); } assertLanded(url, p.url()); + if (opt.busy) { + const B = opt.busy; + if (await p.locator(B.refuse).first().isVisible()) throw new Error(`${name}-${vp}: the list did not load (${B.refuse} on screen in place of rows)`); + const n = await p.locator(B.rows).count(); + if (n < B.keep) throw new Error(`${name}-${vp}: ${n} ${B.rows} rows on the page; the busy frame draws ${B.keep}, so this is not the busy state`); + await p.evaluate(([rows, group, keep]) => { + document.querySelectorAll(rows).forEach((el, i) => { if (i >= keep) el.remove(); }); + document.querySelectorAll(group).forEach(g => { if (!g.querySelector(rows)) g.remove(); }); + }, [B.rows, B.group, B.keep]); + } const m = await p.evaluate(METRICS); const key = `${name}-${vp}`, t = targets[key]; return { key, m, t, ok: m.helperWords <= t.helperWords && m.innerRatio <= t.innerRatio }; @@ -206,6 +214,17 @@ const SIDEBAR_390 = { failed: '.v2-shell--list .v2-pods__empty:not(.v2-pods__list .v2-pods__empty)', }; +// Artifacts' busy frame draws six file rows; prod returns a page of up to 50. The scorer refuses +// the error line and the spinner, refuses a list shorter than the frame (an empty account or the +// day-zero page), and then measures the first six rows only, the way chat's scroll target is +// measured with its fold applied. Groups left with no rows go too. +const ARTIFACTS_BUSY = { + rows: '.v2a-file', + group: '.v2a-group', + refuse: '.v2a-state, .v2a-loading', + keep: 6, +}; + async function prod(only) { const { open } = require('./lib'); const pod = process.env.D215_POD, agent = process.env.D215_AGENT; @@ -222,6 +241,7 @@ async function prod(only) { ['connect', () => '/v2/connect'], ['team', () => '/v2/agents'], ['activity-busy', () => '/v2/activity'], + ['artifacts-busy', () => '/v2/artifacts', { busy: ARTIFACTS_BUSY }], ['chat', () => `/v2/pods/${pod}`], ['sidebar', vp => (vp === '1440' ? `/v2/pods/${side}` : '/v2'), { list390: SIDEBAR_390 }], ]; @@ -241,7 +261,7 @@ async function prod(only) { process.exit(miss ? 1 : 0); } -module.exports = { parseArgs, validateTargets, unknownFrameError, keyFrames, checkSurface, EXPECTED, SURFACES, SKIPPED, PROD_SURFACES, SIDEBAR_390 }; +module.exports = { ARTIFACTS_BUSY, parseArgs, validateTargets, unknownFrameError, keyFrames, checkSurface, EXPECTED, SURFACES, SKIPPED, PROD_SURFACES, SIDEBAR_390 }; if (require.main === module) { let args; diff --git a/docs/design/artboards/d215/cap/measure_after.test.js b/docs/design/artboards/d215/cap/measure_after.test.js index 8090d4229..d61219d50 100644 --- a/docs/design/artboards/d215/cap/measure_after.test.js +++ b/docs/design/artboards/d215/cap/measure_after.test.js @@ -3,7 +3,7 @@ const test = require('node:test'); const assert = require('node:assert'); const fs = require('fs'); const path = require('path'); -const { parseArgs, validateTargets, unknownFrameError, keyFrames, checkSurface, EXPECTED, SURFACES, SKIPPED, PROD_SURFACES, SIDEBAR_390 } = require('./measure_after'); +const { ARTIFACTS_BUSY, parseArgs, validateTargets, unknownFrameError, keyFrames, checkSurface, EXPECTED, SURFACES, SKIPPED, PROD_SURFACES, SIDEBAR_390 } = require('./measure_after'); const { assertLanded, BASE } = require('./lib'); const argv = (...a) => ['node', 'measure_after.js', ...a]; @@ -29,7 +29,7 @@ test('--only with an unknown surface throws', () => { test('the committed targets.json passes its own validation', () => { const t = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'targets.json'), 'utf8')); - assert.strictEqual(Object.keys(t).length, 44); + assert.strictEqual(Object.keys(t).length, 48); validateTargets(t); }); @@ -39,7 +39,7 @@ test('an empty or partial measurement is refused', () => { const partial = { ...t }; delete partial['board-390']; assert.throws(() => validateTargets(partial), /missing board-390/); assert.throws(() => validateTargets({ ...t, 'chat-1440': { helperWords: NaN, innerRatio: 2 } }), /invalid chat-1440/); - assert.strictEqual(EXPECTED.length, 44); + assert.strictEqual(EXPECTED.length, 48); }); test('a frame the scorer does not know is named, with the way to fix it', () => { @@ -193,3 +193,42 @@ test('only the /v2 pod auto-select counts as landing', () => { assert.throws(() => assertLanded('/v2/agents/a/default', BASE + '/v2'), /redirected to \/v2$/); assert.throws(() => assertLanded('/v2/settings', BASE + '/v2/pods/abc123'), /redirected/); }); + +// The Artifacts page on its route, with `rows` file rows and optionally the error line or the +// spinner on screen. `trimmed` records the trim the scorer asked for before measuring. +const fakeArtifacts = ({ rows, refused = false }) => { + let url = 'about:blank'; + const page = { + trimmed: null, + measured: 0, + goto: async () => { url = BASE + '/v2/artifacts'; return { ok: () => true, status: () => 200 }; }, + waitForTimeout: async () => {}, + url: () => url, + locator: sel => { + if (sel === ARTIFACTS_BUSY.refuse) return { first: () => ({ isVisible: async () => refused }) }; + if (sel === ARTIFACTS_BUSY.rows) return { count: async () => rows }; + throw new Error(`unexpected locator ${sel}`); + }, + evaluate: async (fn, arg) => { + if (arg) { page.trimmed = arg; return undefined; } + page.measured++; + return { helperWords: 0, innerRatio: 1 }; + }, + }; + return page; +}; + +test('artifacts-busy scores only a loaded list as long as the frame, measured at the frame\'s six rows', async () => { + const t = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'targets.json'), 'utf8')); + const surf = ['artifacts-busy', () => '/v2/artifacts', { busy: ARTIFACTS_BUSY }]; + const failed = fakeArtifacts({ rows: 0, refused: true }); + await assert.rejects(checkSurface(failed, surf, '1440', t), /did not load/); + assert.strictEqual(failed.measured, 0); + const short = fakeArtifacts({ rows: 2 }); + await assert.rejects(checkSurface(short, surf, '390', t), /2 \.v2a-file rows .* not the busy state/); + assert.strictEqual(short.measured, 0); + const busy = fakeArtifacts({ rows: 50 }); + assert.strictEqual((await checkSurface(busy, surf, '1440', t)).ok, true); + assert.deepStrictEqual(busy.trimmed, ['.v2a-file', '.v2a-group', 6]); + assert.strictEqual(busy.measured, 1); +}); diff --git a/docs/design/artboards/d215/index.html b/docs/design/artboards/d215/index.html index 3c9a2fc54..aad205ba4 100644 --- a/docs/design/artboards/d215/index.html +++ b/docs/design/artboards/d215/index.html @@ -871,56 +871,6 @@ .app.m .v2-activity .v2-activity__moved-line { grid-template-columns: minmax(0, 1fr) auto; } .app.m .v2-activity .v2-activity__moved-line span { grid-column: 1 / -1; grid-row: 2; } /* activity:end */ -/* artifacts: the live v2-artifacts__* names (V2ArtifactsPage) and V2PageHead's v2-page-head__*, - redrawn in the connectors family. Scoped to .v2-artifacts, the page's root. New names: __zero, __open. */ -.v2-artifacts { display: flex; flex-direction: column; gap: 20px; min-width: 0; } -.v2-artifacts button { all: unset; box-sizing: border-box; cursor: pointer; } -.v2-artifacts .info, .v2-artifacts__head .info { all: unset; cursor: help; position: relative; display: inline-grid; place-items: center; width: 18px; height: 18px; border-radius: 50%; color: var(--ph); } -.v2-artifacts .v2-page-head__identity { display: flex; align-items: center; gap: 10px; min-width: 0; } -.v2-artifacts .v2-artifacts__title { margin: 0; font: 800 30px/1.05 var(--f-display); letter-spacing: -0.03em; color: var(--ink); } -.v2-artifacts .v2-page-head__meta, .v2-artifacts__head .v2-page-head__meta { font: 500 12px/16px var(--f-mono); color: var(--mu); white-space: nowrap; } -.v2-artifacts .v2-artifacts__controls { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; } -.v2-artifacts .v2-artifacts__seg { display: flex; gap: 2px; padding: 3px; border-radius: 9px; background: var(--sf); box-shadow: inset 0 0 0 1px var(--d); min-width: 0; } -.v2-artifacts .v2-artifacts__seg-button { display: inline-flex; align-items: center; height: 28px; padding: 0 10px; border-radius: 7px; font: 600 11.5px/1 var(--f-mono); color: var(--se); white-space: nowrap; } -.v2-artifacts .v2-artifacts__seg-button--active { background: var(--t); color: var(--ink); } -.v2-artifacts .v2-artifacts__search { display: flex; align-items: center; gap: 8px; min-width: 220px; height: 36px; margin-left: auto; padding: 0 10px; border-radius: 9px; box-shadow: inset 0 0 0 1px var(--d); background: var(--sf); } -.v2-artifacts .v2-artifacts__search-input { flex: 1; font-size: 13px; color: var(--ph); } -.v2-artifacts .v2-artifacts__search-key { font: 500 11px/16px var(--f-mono); color: var(--mu); } -.v2-artifacts .v2-artifacts__table-wrap { border-radius: 10px; box-shadow: inset 0 0 0 1px var(--d); background: var(--sf); overflow: hidden; } -.v2-artifacts .v2-artifacts__table { width: 100%; border-collapse: collapse; font-size: 13px; line-height: 18px; color: var(--ink); } -.v2-artifacts .v2-artifacts__table th { padding: 8px 12px; border-bottom: 1px solid var(--d); font: 500 11px/16px var(--f-mono); color: var(--mu); text-align: left; text-transform: lowercase; white-space: nowrap; } -.v2-artifacts .v2-artifacts__table td { padding: 9px 12px; border-bottom: 1px solid var(--d); vertical-align: middle; } -.v2-artifacts .v2-artifacts__table tr:last-child td { border-bottom: 0; } -.v2-artifacts .v2-artifacts__file { display: flex; align-items: center; gap: 10px; min-width: 0; } -.v2-artifacts .v2-artifacts__ext { display: inline-flex; align-items: center; justify-content: center; min-width: 34px; height: 20px; padding: 0 5px; border-radius: 5px; box-shadow: inset 0 0 0 1px var(--d); font: 500 11px/14px var(--f-mono); color: var(--se); } -.v2-artifacts .v2-artifacts__name { font-weight: 600; color: var(--ink); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; } -.v2-artifacts .v2-artifacts__pod-line { display: none; font: 500 11px/16px var(--f-mono); color: var(--se); } -.v2-artifacts .v2-artifacts__mono { font: 500 11px/16px var(--f-mono); color: var(--se); white-space: nowrap; } -.v2-artifacts .v2-artifacts__foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; } -.v2-artifacts .v2-artifacts__more { font: 600 12px/16px var(--f-body); color: var(--c-text); } -.v2-artifacts .v2-artifacts__zero { width: 100%; max-width: 760px; padding: 14px; border-radius: 10px; background: var(--sf); box-shadow: inset 0 0 0 1px var(--d); } -.v2-artifacts .v2-artifacts__zero h2 { margin: 0; font: 600 14px/20px var(--f-body); letter-spacing: 0; color: var(--ink); } -.v2-artifacts .v2-artifacts__zero p { margin: 2px 0 0; font-size: 14px; line-height: 20px; color: var(--se); } -.v2-artifacts .v2-artifacts__open { display: inline-flex; align-items: center; justify-content: center; height: 32px; margin-top: 12px; padding: 0 13px; border-radius: 9px; font: 600 13px/1 var(--f-body); color: var(--on-fill); background: linear-gradient(180deg, var(--fill-a), var(--fill-b)); box-shadow: inset 0 0 0 1px var(--fill-edge); text-decoration: none; white-space: nowrap; } -/* 390: head is the top bar; filters stack; the pod folds under the name; the day-zero key is full width, 44px */ -.app.m .mtop.v2-artifacts__head { padding: 0 12px 0 16px; gap: 8px; } -.app.m .mtop.v2-artifacts__head h1 { font-size: 22px; } -.app.m .mtop.v2-artifacts__head .v2-page-head__meta { margin-left: auto; font-size: 11.5px; } -.app.m .v2-artifacts { gap: 12px; } -.app.m .v2-artifacts .v2-artifacts__controls { flex-direction: column; align-items: stretch; } -.app.m .v2-artifacts .v2-artifacts__seg-button { flex: 1 1 0; justify-content: center; height: 36px; } -.app.m .v2-artifacts .v2-artifacts__search { margin-left: 0; min-width: 0; height: 44px; } -.app.m .v2-artifacts .v2-artifacts__table { table-layout: fixed; } -.app.m .v2-artifacts .v2-artifacts__table th:nth-child(3), .app.m .v2-artifacts .v2-artifacts__table td:nth-child(3), -.app.m .v2-artifacts .v2-artifacts__table th:nth-child(4), .app.m .v2-artifacts .v2-artifacts__table td:nth-child(4), -.app.m .v2-artifacts .v2-artifacts__table th:nth-child(6), .app.m .v2-artifacts .v2-artifacts__table td:nth-child(6) { display: none; } -.app.m .v2-artifacts .v2-artifacts__table th:nth-child(2), .app.m .v2-artifacts .v2-artifacts__table td:nth-child(2) { width: 64px; } -.app.m .v2-artifacts .v2-artifacts__table th:nth-child(5), .app.m .v2-artifacts .v2-artifacts__table td:nth-child(5) { width: 52px; } -.app.m .v2-artifacts .v2-artifacts__file { flex-wrap: wrap; row-gap: 2px; } -.app.m .v2-artifacts .v2-artifacts__pod-line { display: block; flex: 1 1 100%; } -.app.m .v2-artifacts .v2-artifacts__name { white-space: normal; overflow-wrap: anywhere; } -.app.m .v2-artifacts .v2-artifacts__open { display: flex; width: 100%; height: 44px; font-size: 14px; } -/* artifacts:end */ /* run trace: the live v2-run-trace__* names (V2RunTrace.tsx), redrawn with the page's own rules. Scoped to .v2-run-trace, the page's root. New name: __foot. */ .v2-run-trace { max-width: 880px; min-width: 0; color: var(--ink); font-size: 14px; line-height: 20px; } @@ -1282,7 +1232,7 @@
Files you and your agents share in a pod land here: docs, images and pages.
Open My Workspace| File | Kind | Pod | Shared by | When | Size |
|---|---|---|---|---|---|
Northwind | doc | Northwind | Fern | 12m | 2.1 MB |
Northwind | image | Northwind | Juno Park | 1h | 412 KB |
Field notes | page | Field notes | Scout | 3h | — |
Field notes | doc | Field notes | Fern | 1d | 18 KB |
Northwind | doc | Northwind | Alex Chen | 2d | 96 KB |
Field notes | image | Field notes | Juno Park | 4d | 1.4 MB |
Files you and your agents share in a pod land here: docs, images and pages.
Open My Workspace| File | Kind | Pod | Shared by | When | Size |
|---|---|---|---|---|---|
Northwind | doc | Northwind | Fern | 12m | 2.1 MB |
Northwind | image | Northwind | Juno Park | 1h | 412 KB |
Field notes | page | Field notes | Scout | 3h | — |
Field notes | doc | Field notes | Fern | 1d | 18 KB |
Northwind | doc | Northwind | Alex Chen | 2d | 96 KB |
Field notes | image | Field notes | Juno Park | 4d | 1.4 MB |
| Day zero | Drawn at 1440 and 390. Total 0, no kind, pod or search set, not loading, no error: no controls, no count, the card. |
| Busy | Drawn at 1440 and 390. Prod's table and foot, unchanged. |
| Filtered, empty | Not drawn. Controls stay; prod's dashed emptyFiltered line under them. |
| No pods | Not drawn. The day-zero card without its key. |
| Loading, error | Not drawn. They keep prod's spinner and __error line. |