done-done-axi 0.6.10 → 0.6.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/dist/cli.js +22 -3
- package/dist/config.js +22 -0
- package/dist/design.js +45 -1
- package/docs/reference.md +19 -0
- package/docs/releasing.md +2 -2
- package/package.json +1 -1
- package/skills/done-done/SKILL.md +7 -0
- package/skills/done-done/interaction-checklist.json +8 -0
package/README.md
CHANGED
|
@@ -194,6 +194,10 @@ That first run is a trial on uncommitted files, not proof. Read the generated sc
|
|
|
194
194
|
|
|
195
195
|
Every answer is an exit code: **0** pass, **1** fail, **2** usage or config error (including no agent or judge set, a check that fails before its judge, a capture the judge cannot be sent, or a judge that did not answer: never a result), **3** can't tell either way, **4** a `verify`, `maintain` or `rerun` run is still running, or a `verify` run waits at its gate.
|
|
196
196
|
|
|
197
|
+
### Interaction polish needs proof too
|
|
198
|
+
|
|
199
|
+
Opt in with `interaction_checklist` in `.done-done.yaml`: detail next/previous plus swipe, instant send/answer state, destructive undo, scroll position on back, long-list search/filter, and useful empty states. Each item needs judged real-product frames or an explicit not-applicable reason; missing proof is flagged, never silently passed. The [shipped checklist and wiring](docs/reference.md#interaction-checklist-opt-in) extend `check`, `receipt` and `verify`; existing approved-design checks stay unchanged without opt-in.
|
|
200
|
+
|
|
197
201
|
## Alone, and with the other tools
|
|
198
202
|
|
|
199
203
|
done-done works on its own: the CLI and the skill need nothing else. It is one of three tools that each do one job and connect through small written contracts ([Vision](VISION.md)):
|
package/dist/cli.js
CHANGED
|
@@ -10,12 +10,12 @@ import { parseArgs } from 'node:util';
|
|
|
10
10
|
import { encode } from '@toon-format/toon';
|
|
11
11
|
import { Document, parseDocument } from 'yaml';
|
|
12
12
|
import { ACPX_INSTALL, agentChoices, agentPath, agentRun, askAgent, findBin, JUDGES, modelFor, noAgent, noJudge, notStartable, routeOf, selectAgent, selectFallback, selectJudge, toollessReason } from './agents.js';
|
|
13
|
-
import { AGENTS, CHEAP, globalConfigText, home, inputPattern, isAgent, isJudgeRoute, loadConfig, parseDuration, tilde, UsageError } from './config.js';
|
|
13
|
+
import { AGENTS, CHEAP, INTERACTIONS, globalConfigText, home, inputPattern, isAgent, isJudgeRoute, loadConfig, parseDuration, tilde, UsageError } from './config.js';
|
|
14
14
|
import { backend, judge, judgeable, onPath, quote, screen } from './judge.js';
|
|
15
15
|
import { startInstance, stopInstance } from './instance.js';
|
|
16
16
|
import { attemptProblem, changedSince, commitFix, dirtyTracked, FIX_SCHEMA, inWorktree, maintainSteps, journeyProposal, named, pathspec, readerProblem, repoState, triage } from './maintain.js';
|
|
17
17
|
import { cellEnv, cellName, cellSuffix, hasMatrix, matrixManifest, splitMatrix } from './matrix.js';
|
|
18
|
-
import { design, KINDS } from './design.js';
|
|
18
|
+
import { design, interactions, KINDS } from './design.js';
|
|
19
19
|
import { checkMedia } from './media.js';
|
|
20
20
|
import { domIssues, domRecord, isDomRecord, lookIssues, SQUASHED_TEXT } from './dom.js';
|
|
21
21
|
import { lint } from './lint.js';
|
|
@@ -53,7 +53,11 @@ const CHECKS_TEXT = `Checks block (--from): the newest "## Checks" heading, then
|
|
|
53
53
|
proof: <the command or path that drove the real product; name the repo's done-done-verify-<app> skill, or say "no verify skill">
|
|
54
54
|
evidence: <the evidence file paths, or "not proven: <cause>">
|
|
55
55
|
approved_design (.done-done.yaml): a non-empty manifest of {screen, size: <width>x<height>, theme, mock: <repo PNG/JPEG>, capture: <unique filename>};
|
|
56
|
-
every claim needs every listed live screen, paired with its mock and a passing design verdict; missing or unsure matches fail by screen/size/theme
|
|
56
|
+
every claim needs every listed live screen, paired with its mock and a passing design verdict; missing or unsure matches fail by screen/size/theme
|
|
57
|
+
interaction_checklist (.done-done.yaml, opt-in): map the six ids in skills/done-done/interaction-checklist.json to {captures: [<live PNG/JPEG filename>, ...]}
|
|
58
|
+
or {not_applicable: <reason>}; detail-navigation, instant-state, destructive-undo, scroll-restoration, list-search, empty-next-step.
|
|
59
|
+
check/receipt/verify report every item: pass only with judged real-product frames, not_applicable with a reason, otherwise flagged (exit 1).
|
|
60
|
+
Omitted checklist: unchanged behavior; interaction_checklist: {} flags all six. Frames must be cited from sealed runs; static controls alone do not prove an interaction`;
|
|
57
61
|
const RUNS_TEXT = `runs: each verify or maintain run lives in $DONE_DONE_HOME/runs/<app>/<run-id> (run.json, events.jsonl, logs/<step>.log,
|
|
58
62
|
agents/<n>-<name>.json for each agent call) and goes on when the command that started it ends;
|
|
59
63
|
verify steps: resolve, drive, check, judge (always; skipped only after a failed check), triage and fix (only with auto_fix.verify above 0), gate, receipt, report;
|
|
@@ -1024,7 +1028,17 @@ async function runCheck(argv, run) {
|
|
|
1024
1028
|
if (judgeBlock)
|
|
1025
1029
|
judgeBlock.approved_design = matches;
|
|
1026
1030
|
}
|
|
1031
|
+
const touches = await interactions(cfg, choice, captures, run ? join(run.dir, 'agents') : join(home(), 'agents'));
|
|
1032
|
+
for (const r of touches) {
|
|
1033
|
+
r.captures = r.captures.map(c => ({ ...c, path: receiptPath(repo, cfg, cite(c.path)) }));
|
|
1034
|
+
if (r.status === 'flagged')
|
|
1035
|
+
findings.push({ level: 'fail', why: `interaction checklist: ${r.id}: flagged: ${r.reason}` });
|
|
1036
|
+
}
|
|
1037
|
+
if (judgeBlock && touches.length)
|
|
1038
|
+
judgeBlock.interaction_checklist = touches;
|
|
1027
1039
|
const rep = report(findings, files, { cfg, repo, claim: claim.outcome, source: values.from, citationBase: values['citation-base'], judge: judged, judgeLevel, warnings });
|
|
1040
|
+
if (touches.length)
|
|
1041
|
+
rep.result.interaction_checklist = touches;
|
|
1028
1042
|
if (matches.length)
|
|
1029
1043
|
rep.result.design = matches;
|
|
1030
1044
|
if (rep.code === PASS) {
|
|
@@ -1070,6 +1084,11 @@ function receiptFault(rec, short) {
|
|
|
1070
1084
|
return `receipt for ${short} has a malformed judge block`;
|
|
1071
1085
|
if (j.approved_design !== undefined && (!Array.isArray(j.approved_design) || !j.approved_design.length || !j.approved_design.every(v => v && v.verdict === 'pass' && [v.screen, v.size, v.theme].every(s => typeof s === 'string' && s.trim()) && rec.files.some(f => f.path === v.path && f.sha256 === v.sha256) && rec.files.some(f => f.path === v.mock && f.sha256 === v.mock_sha256))))
|
|
1072
1086
|
return `receipt for ${short} has a malformed approved-design verdict`;
|
|
1087
|
+
if (j.interaction_checklist !== undefined) {
|
|
1088
|
+
const rows = j.interaction_checklist;
|
|
1089
|
+
if (!Array.isArray(rows) || rows.length !== Object.keys(INTERACTIONS).length || new Set(rows.map(r => r?.id)).size !== rows.length || !rows.every(r => r && Object.hasOwn(INTERACTIONS, r.id) && typeof r.reason === 'string' && r.reason.trim() && Array.isArray(r.captures) && (r.status === 'not_applicable' ? !r.captures.length : r.status === 'pass' && r.captures.length && r.captures.every(c => c && rec.files.some(f => f.path === c.path && f.sha256 === c.sha256)))))
|
|
1090
|
+
return `receipt for ${short} has a malformed interaction-checklist verdict`;
|
|
1091
|
+
}
|
|
1073
1092
|
const code = runsFault(rec.runs);
|
|
1074
1093
|
if (code)
|
|
1075
1094
|
return `receipt for ${short} is no real-product proof: ${code}`;
|
package/dist/config.js
CHANGED
|
@@ -12,6 +12,7 @@ export class UsageError extends Error {
|
|
|
12
12
|
this.help = help;
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
+
export const INTERACTIONS = JSON.parse(readFileSync(new URL('../skills/done-done/interaction-checklist.json', import.meta.url), 'utf8'));
|
|
15
16
|
// The coding agents a user can set as agent.
|
|
16
17
|
export const AGENTS = ['claude', 'codex', 'opencode', 'pi', 'gemini'];
|
|
17
18
|
// The agents whose CLI can read an attached image with every tool off, so they judge through the answerer route;
|
|
@@ -68,6 +69,7 @@ const KEYS = {
|
|
|
68
69
|
dom: { scope: 'repo', type: 'dom' },
|
|
69
70
|
look: { scope: 'repo', type: 'look' },
|
|
70
71
|
approved_design: { scope: 'repo', type: 'approved_design' },
|
|
72
|
+
interaction_checklist: { scope: 'repo', type: 'interaction_checklist' },
|
|
71
73
|
base_ref: { scope: 'repo', type: 'string' },
|
|
72
74
|
'evidence.root': { scope: 'repo', type: 'string' },
|
|
73
75
|
'evidence.allow_dirty': { scope: 'repo', type: 'boolean' },
|
|
@@ -214,6 +216,25 @@ function apply(target, text, file, scope) {
|
|
|
214
216
|
target[key] = { command: v.command, ready: has(r.url) ? { url: r.url } : { command: r.command }, ready_timeout: v.ready_timeout ?? '30s' };
|
|
215
217
|
continue;
|
|
216
218
|
}
|
|
219
|
+
if (key === 'interaction_checklist') {
|
|
220
|
+
const fail = () => { throw new UsageError(`${file}: interaction_checklist needs known checklist ids, each with only captures: [<PNG/JPEG filename>, ...] (1-8), or not_applicable: <non-empty reason>`); };
|
|
221
|
+
if (!isMap(value))
|
|
222
|
+
fail();
|
|
223
|
+
for (const [id, row] of Object.entries(value)) {
|
|
224
|
+
if (!Object.hasOwn(INTERACTIONS, id) || !isMap(row) || Object.keys(row).length !== 1)
|
|
225
|
+
fail();
|
|
226
|
+
const r = row;
|
|
227
|
+
if (Object.hasOwn(r, 'not_applicable')) {
|
|
228
|
+
if (typeof r.not_applicable !== 'string' || !r.not_applicable.trim())
|
|
229
|
+
fail();
|
|
230
|
+
}
|
|
231
|
+
else if (!Array.isArray(r.captures) || !r.captures.length || r.captures.length > 8 || new Set(r.captures).size !== r.captures.length || !r.captures.every(p => typeof p === 'string' && !/[\\/\\\\]/.test(p) && p !== '..' && /\.(png|jpe?g)$/i.test(p)))
|
|
232
|
+
fail();
|
|
233
|
+
}
|
|
234
|
+
target.interaction_checklist = value;
|
|
235
|
+
target.from[key] = scope;
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
217
238
|
if (key === 'approved_design') {
|
|
218
239
|
const fail = () => { throw new UsageError(`${file}: approved_design must be a non-empty list of unique {screen, size: <width>x<height> in pixels, theme, mock: <repo-relative PNG/JPEG>, capture: <unique PNG/JPEG filename>}`); };
|
|
219
240
|
const relativeImage = (p) => typeof p === 'string' && !isAbsolute(p) && !p.split(/[\\/]/).includes('..') && /\.(png|jpe?g)$/i.test(p);
|
|
@@ -314,6 +335,7 @@ function defaultConfig(repo) {
|
|
|
314
335
|
dom: null,
|
|
315
336
|
look: null,
|
|
316
337
|
approved_design: [],
|
|
338
|
+
interaction_checklist: null,
|
|
317
339
|
base_ref: '',
|
|
318
340
|
evidence: { local_root: join(home(), 'evidence'), root: '', allow_dirty: false, ignore_paths: [] },
|
|
319
341
|
secrets: { patterns: {}, allow: [] },
|
package/dist/design.js
CHANGED
|
@@ -1,8 +1,52 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { home, UsageError } from './config.js';
|
|
3
|
+
import { home, INTERACTIONS, UsageError } from './config.js';
|
|
4
4
|
import { askJudge, backend, prepare, readCache, screen, VERSION, writeCache } from './judge.js';
|
|
5
5
|
import { decodePixels } from './media.js';
|
|
6
|
+
// Only sealed, cited product frames reach here. Missing evidence is a finding, never a model-inferred pass.
|
|
7
|
+
export async function interactions(cfg, choice, captures, records) {
|
|
8
|
+
if (cfg.interaction_checklist === null)
|
|
9
|
+
return [];
|
|
10
|
+
const out = [];
|
|
11
|
+
for (const [id, requirement] of Object.entries(INTERACTIONS)) {
|
|
12
|
+
const entry = cfg.interaction_checklist[id];
|
|
13
|
+
const row = { id, status: 'flagged', reason: 'no real-product frame cited for this item', captures: [] };
|
|
14
|
+
if (entry?.not_applicable) {
|
|
15
|
+
row.status = 'not_applicable';
|
|
16
|
+
row.reason = entry.not_applicable;
|
|
17
|
+
}
|
|
18
|
+
else if (entry?.captures) {
|
|
19
|
+
const frames = entry.captures.map(name => captures.filter(c => c.name.split(/[\\/\\\\]/).at(-1) === name && screen(c)));
|
|
20
|
+
if (frames.some(f => f.length !== 1))
|
|
21
|
+
row.reason = 'each configured frame must name exactly one cited, run-bound PNG/JPEG: ' + entry.captures.join(', ');
|
|
22
|
+
else {
|
|
23
|
+
const shots = frames.map(f => f[0]);
|
|
24
|
+
row.captures = shots.map(c => ({ path: c.name, sha256: c.sha256 }));
|
|
25
|
+
const images = Object.fromEntries(shots.map((c, i) => {
|
|
26
|
+
const img = image(c);
|
|
27
|
+
if (typeof img === 'string')
|
|
28
|
+
throw new UsageError(`interaction checklist: ${img}`);
|
|
29
|
+
return [`frame_${i}`, img];
|
|
30
|
+
}));
|
|
31
|
+
const q = { interaction: { kind: 'choice', instructions: `Check this interaction on the real product: ${requirement} Frames are in action order. Describe visible evidence for each part before answering. Do not infer working behavior, timing, swipe or scroll restoration from a control's presence or a final state. Missing or insufficient evidence is missing, never shown.`, options: { shown: 'Every required part is demonstrated by these real-product frames.', missing: 'A required part is absent, broken or cannot be established from these frames.' }, images: Object.keys(images), floor: cfg.judge.floor, personReason: true } };
|
|
32
|
+
const key = createHash('sha256').update(JSON.stringify([VERSION, q, row.captures, choice.route, choice.model])).digest('hex');
|
|
33
|
+
const dir = join(home(), 'runs', cfg.app, 'interactions');
|
|
34
|
+
let hit = readCache(dir, key);
|
|
35
|
+
if (!hit || !['pass', 'flagged'].includes(hit.status) || typeof hit.reason !== 'string') {
|
|
36
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
37
|
+
const b = await backend(cfg, choice, n => `${records}/interaction-${stamp}-${id}-${n}.agent.json`, q, ASK_ATTEMPTS);
|
|
38
|
+
const a = (await askJudge(cfg, b, choice, { item: id, frames: entry.captures }, q, images)).interaction;
|
|
39
|
+
hit = { status: !a.abstained && a.answer === 'shown' ? 'pass' : 'flagged', reason: a.personReason || a.rationale || 'interaction not demonstrated' };
|
|
40
|
+
writeCache(dir, key, hit);
|
|
41
|
+
}
|
|
42
|
+
row.status = hit.status;
|
|
43
|
+
row.reason = hit.reason;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
out.push(row);
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
6
50
|
// The defect kinds, each its own question. Each asks for something the judge can see and measure, not whether it was
|
|
7
51
|
// meant that way: a cheap model talks itself out of a judgment ("an intentional style", "the viewer's border"), not out of
|
|
8
52
|
// a measurement. look is what the judge writes in its rationale before it answers.
|
package/docs/reference.md
CHANGED
|
@@ -544,6 +544,24 @@ After those checks pass, each live image is sent beside its approved mock throug
|
|
|
544
544
|
|
|
545
545
|
With no manifest, claims behave as before. `design` and `look --mock` remain ad-hoc measurements of supplied screens, not coverage claims: use `check`, `receipt` or `verify` to prove the complete approved inventory. This checks the declared inventory; it cannot discover an approved shot omitted from the manifest or establish capture authenticity beyond the existing run record.
|
|
546
546
|
|
|
547
|
+
#### Interaction checklist (opt-in)
|
|
548
|
+
|
|
549
|
+
Visual comparisons do not prove interaction polish. The package ships six requirements in `skills/done-done/interaction-checklist.json`: `detail-navigation` (next/previous **and swipe**), `instant-state` (immediate feedback on every send/answer), `destructive-undo` (undo restores content), `scroll-restoration` (back keeps list position), `list-search` (search/filter on long lists), and `empty-next-step` (empty states explain what to do next).
|
|
550
|
+
|
|
551
|
+
Wire the checklist in the repo's committed `.done-done.yaml`; it works with or without `approved_design`:
|
|
552
|
+
|
|
553
|
+
```yaml
|
|
554
|
+
interaction_checklist:
|
|
555
|
+
empty-next-step: { captures: [empty.png] }
|
|
556
|
+
list-search: { captures: [list.png, filtered.png] }
|
|
557
|
+
detail-navigation: { not_applicable: "This product has no detail views" }
|
|
558
|
+
# Map the other three items too; omitted items are flagged, not passed.
|
|
559
|
+
```
|
|
560
|
+
|
|
561
|
+
`check`, `receipt` and `verify` report all six under `interaction_checklist`, with `id`, `status`, `reason` and bound capture paths/hashes. Each entry accepts **only** `captures` (1–8 unique PNG/JPEG filenames, in action order) or `not_applicable` (a non-empty surface-specific reason). Capture real product frames through the project's recipe and cite them from sealed runs. Missing, ambiguous or uncited frames, an omitted item, a judge rejection or an unsure answer produce **flagged findings (exit 1)**. A pass requires the judge to tie every required part to those frames; a static control or final reply alone cannot prove swipe, timing, undo or scroll restoration. Include before/action/after frames where needed; insufficient visual evidence stays flagged. There is no automatic video-frame extraction.
|
|
562
|
+
|
|
563
|
+
A surface that does not exist must be explicitly `not_applicable` with its reason, never silently passed. Applicability is a declared inventory, not automatic surface discovery: reviewers must check those reasons and enumerate all relevant actions. Receipts preserve all six results under `judge.interaction_checklist`; passing frame hashes are bound to cited evidence, and receipt/gate validation rejects flagged or malformed entries. Without this config key, existing approved-design behavior is unchanged. `interaction_checklist: {}` opts in and flags all six; ad-hoc `design` is still only a visual check.
|
|
564
|
+
|
|
547
565
|
### Record a receipt
|
|
548
566
|
|
|
549
567
|
A receipt ties one passing check to one commit and to the exact bytes it cited, so a merge gate (or `gate` in CI) can re-verify the same proof later.
|
|
@@ -759,6 +777,7 @@ An `agent_config` entry merges with the defaults field by field, so `claude: { m
|
|
|
759
777
|
| `matrix` | none, so each journey runs once; `theme`, `width`, `form` and `skip` set the [capture matrix](#capture-matrix) |
|
|
760
778
|
| `dom` | none; page-check records per capture, see [DOM assertions](#dom-assertions) |
|
|
761
779
|
| `look` | none; the look step's approved design reference (`tokens`, `max_control`), see [Look check](#look-check) |
|
|
780
|
+
| `interaction_checklist` | none; opt-in six-item interaction evidence map; omitted/unproven items flagged, absent surfaces need reasons; see [Interaction checklist](#interaction-checklist-opt-in) |
|
|
762
781
|
| `approved_design` | none; committed screen × pixel size × theme manifest of mocks and required live filenames; gates all claims, see [Approved-design coverage](#approved-design-coverage) |
|
|
763
782
|
| `inputs` | `[]`; repo-relative paths or globs the drive must leave alone, see [Input guard](#input-guard) |
|
|
764
783
|
| `base_ref` | none; a ref `verify` also drives, pairing its captures with the head's, see [Base-ref before](#base-ref-before) |
|
package/docs/releasing.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
# 0.6.
|
|
1
|
+
# 0.6.11 patch release
|
|
2
2
|
|
|
3
3
|
This release shows the running version in CLI help headers, labels prereleases without changing the bare `--version` line, and clarifies the signoff posting hint.
|
|
4
4
|
|
|
5
|
-
Do not publish an unmerged candidate. The existing `release` workflow on main tests and publishes an unpublished package version using npm trusted publishing. After merge, confirm `npm view done-done-axi@0.6.
|
|
5
|
+
Do not publish an unmerged candidate. The existing `release` workflow on main tests and publishes an unpublished package version using npm trusted publishing. After merge, confirm `npm view done-done-axi@0.6.11 version dist.integrity` and smoke-test a clean `npx` install with `done-done --version`. Do not run `npm publish` manually unless the workflow fails and the release is blocked; if so, stop and report.
|
package/package.json
CHANGED
|
@@ -70,6 +70,13 @@ journeys:
|
|
|
70
70
|
command: done-done look http://127.0.0.1:3000/inbox --out "$DONE_DONE_EVIDENCE_ROOT/inbox.png"
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
Interaction coverage is opt-in via `interaction_checklist` in the repo config. Read the shipped `skills/done-done/interaction-checklist.json`:
|
|
74
|
+
next/previous plus swipe in details, instant send/answer feedback, destructive undo, list scroll restoration, long-list search/filter,
|
|
75
|
+
and empty-state next steps. Map every item to real-product frame filenames (`captures`, in action order) or `not_applicable`
|
|
76
|
+
with a surface-specific reason. `check`, `receipt` and `verify` flag omitted or unproven items; only frames the judge ties to the item
|
|
77
|
+
can pass. Static controls do not prove motion or timing. Capture the interaction, never substitute a mock. Without the key, approved-design behavior is unchanged;
|
|
78
|
+
`done-done check --help` and `docs/reference.md` explain wiring.
|
|
79
|
+
|
|
73
80
|
To gate a worker's claim cheaply, `done-done lint <PR URL | report file> <evidence folder>` reads the newest `## Checks` block
|
|
74
81
|
with no judge or model and prints `PASS` or `FAIL: <reason>` (exit 1 on a fail): missing lines, evidence that is absent,
|
|
75
82
|
empty, stale, duplicated or already used by another PR, a n/a repro on a fix, or a PR of 1,000+ added lines all fail.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"detail-navigation": "Detail views offer next and previous navigation plus swipe between items. Demonstrate both controls and the swipe transition, not just a static detail screen.",
|
|
3
|
+
"instant-state": "Every send or answer immediately shows a pending, sent, answered or error state. Demonstrate the action and its immediate feedback; a final reply alone does not prove timing.",
|
|
4
|
+
"destructive-undo": "Destructive actions offer undo. Demonstrate the action, visible undo and restored content after undo.",
|
|
5
|
+
"scroll-restoration": "Lists preserve scroll position on returning from a detail view. Demonstrate the scrolled list, opening a detail and returning to the same position.",
|
|
6
|
+
"list-search": "Long lists have usable search or filtering. Demonstrate the long list and the changed results after searching or filtering.",
|
|
7
|
+
"empty-next-step": "Empty states explain the next step a user can take. Show the real empty state with its actionable explanation."
|
|
8
|
+
}
|