Skip to content

Commit 0e07309

Browse files
committed
Restore the arrow keys to their original triage meanings
Applying the UI spec's keyboard model reassigned the arrows: they stopped deciding and became navigation, so the long-standing right-keep/left-trash/ down-undo mapping silently changed under the operator. That mapping is muscle memory and it mirrors the swipe directions, so it wins. Arrows go back to keep/trash/undo/skip and Backspace back to undo (mixing Backspace up with trash costs a file move). The spec's letter keys are kept as additions rather than replacements: K/Enter keep, X/Delete trash, Z undo, F/S star, hold Space to zoom. e2e now covers the arrows and the letters as separate cases, plus that ArrowUp skips without POSTing an action, so the mapping can't drift again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EHGNXqgRVUa2qVY6SYofg9
1 parent 7972531 commit 0e07309

3 files changed

Lines changed: 66 additions & 24 deletions

File tree

README.md

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -468,10 +468,23 @@ UI_SPEC.md Dark Studio UI spec the frontend implements
468468
```
469469
470470
The frontend follows the Dark Studio spec in `UI_SPEC.md`: two theme tiers
471-
(Charcoal default, Black for OLED — switchable in ⚙ → Appearance), decision
472-
colors reserved for keep/reject only, and a desktop keyboard model —
473-
`K`/`Enter` keep, `X`/`Delete`/`Backspace` reject, `F` star, `Z`/`←` undo,
474-
`→` skip without deciding, hold `Space` to zoom 2×. On desktop up to six
475-
up-next previews stack in their own column on the right of the viewer
476-
(next image at the bottom); on phones up to five sit side by side over the
477-
bottom of the stage.
471+
(Charcoal default, Black for OLED — switchable in ⚙ → Appearance) and
472+
decision colors reserved for keep/reject only. On desktop up to six up-next
473+
previews stack in their own column on the right of the viewer (next image at
474+
the bottom); on phones up to five sit side by side over the bottom of the
475+
stage.
476+
477+
**Desktop keys.** The arrows match the swipe directions, and the spec's
478+
letter keys are layered on top rather than replacing them:
479+
480+
| Action | Keys |
481+
|---|---|
482+
| keep | `→` `K` `Enter` |
483+
| trash | `←` `X` `Delete` |
484+
| undo | `↓` `Z` `Backspace` |
485+
| skip to end of stack | `↑` |
486+
| star | `F` `S` |
487+
| zoom 2× | hold `Space` |
488+
489+
`→`/`←` run through `swipe_map`, so remapping a swipe direction in config
490+
remaps its arrow with it.

e2e/tests/triage.spec.js

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,33 @@ test("search filters the stack by prompt text", async ({ page }) => {
102102
.toContain("charlie");
103103
});
104104

105-
test("keyboard: K keeps, Z undoes", async ({ page }) => {
105+
// Each case decides once and undoes it, so the shared seeded library is left
106+
// as it was found.
107+
for (const [name, keep, undo] of [
108+
["arrows (the original mapping)", "ArrowRight", "ArrowDown"],
109+
["letters", "k", "z"],
110+
]) {
111+
test(`keyboard: ${name}`, async ({ page }) => {
112+
const first = await topCardId(page);
113+
const actioned = page.waitForRequest(
114+
(r) => r.url().includes("/action") && r.method() === "POST");
115+
await page.keyboard.press(keep);
116+
await actioned;
117+
await expect.poll(async () => topCardId(page)).not.toBe(first);
118+
await page.keyboard.press(undo);
119+
await expect.poll(async () => topCardId(page)).toBe(first);
120+
});
121+
}
122+
123+
test("keyboard: ArrowUp skips without deciding", async ({ page }) => {
106124
const first = await topCardId(page);
107-
const actioned = page.waitForRequest(
108-
(r) => r.url().includes("/action") && r.method() === "POST");
109-
await page.keyboard.press("k");
110-
await actioned;
125+
let actions = 0;
126+
page.on("request", (r) => {
127+
if (r.url().includes("/action") && r.method() === "POST") actions++;
128+
});
129+
await page.keyboard.press("ArrowUp");
111130
await expect.poll(async () => topCardId(page)).not.toBe(first);
112-
await page.keyboard.press("z"); // net zero decisions for later tests
113-
await expect.poll(async () => topCardId(page)).toBe(first);
131+
expect(actions).toBe(0); // skip defers the card, it does not triage it
114132
});
115133

116134
test("star toggles the button state", async ({ page }) => {

frontend/app.js

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1843,10 +1843,19 @@ els.search.addEventListener("keydown", (e) => {
18431843
else if (e.key === "Escape") { clearSearch(); els.search.blur(); }
18441844
});
18451845

1846-
// Desktop keyboard (spec §10): K/Enter = keep, X/Delete/Backspace = reject,
1847-
// F = flag (star; no advance), Z = undo, ←/↓ = back (undo), →/↑ = skip the
1848-
// card to the end of the stack without deciding, hold Space = 2x zoom.
1849-
// Arrows deliberately never decide — only the explicit decision keys do.
1846+
// Desktop keyboard. The arrows keep their original meanings — → keep, ← trash,
1847+
// ↓ undo, ↑ skip to the end of the stack — because that mapping is muscle
1848+
// memory and it matches the swipe directions (→/← go through swipe_map, so a
1849+
// remapped swipe remaps the arrow with it). The spec's letter keys are layered
1850+
// on top rather than replacing them:
1851+
//
1852+
// keep K, Enter, → trash X, Delete, ←
1853+
// undo Z, Backspace, ↓ skip ↑
1854+
// star F, S zoom hold Space (2x)
1855+
//
1856+
// Backspace stays on undo, not trash: mixing those up costs you a file move,
1857+
// and undo is what it has always done here.
1858+
//
18501859
// Skipped while typing in search so keys still edit text. If the metadata
18511860
// sheet is open, Escape closes it and everything else is ignored to avoid
18521861
// triaging the card behind it.
@@ -1884,16 +1893,18 @@ window.addEventListener("keydown", (e) => {
18841893
if (e.code === "Space") { e.preventDefault(); setZoom(true); return; }
18851894
if (e.repeat) return; // auto-repeat only drives the hold-zoom above
18861895
const k = e.key.length === 1 ? e.key.toLowerCase() : e.key;
1887-
if (k === "k" || k === "Enter") { e.preventDefault(); buttonAction("right"); }
1888-
else if (k === "x" || k === "Delete" || k === "Backspace") {
1889-
e.preventDefault(); buttonAction("left");
1896+
if (k === "k" || k === "Enter" || k === "ArrowRight") {
1897+
e.preventDefault(); buttonAction("right"); // keep
1898+
}
1899+
else if (k === "x" || k === "Delete" || k === "ArrowLeft") {
1900+
e.preventDefault(); buttonAction("left"); // trash
18901901
}
18911902
else if (k === "f" || k === "s") { e.preventDefault(); toggleStar(); }
1892-
else if (k === "z" || k === "ArrowLeft" || k === "ArrowDown") {
1903+
else if (k === "z" || k === "Backspace" || k === "ArrowDown") {
18931904
e.preventDefault(); undo(); // back
18941905
}
1895-
else if (k === "ArrowRight" || k === "ArrowUp") {
1896-
e.preventDefault(); skipCurrent(); // next without deciding
1906+
else if (k === "ArrowUp") {
1907+
e.preventDefault(); skipCurrent(); // skip to end of stack
18971908
}
18981909
});
18991910

0 commit comments

Comments
 (0)