feat(ui,store): live today clock, rail as lane filter, b toggle, quick look on space

This commit is contained in:
hh
2026-08-23 16:44:57 +02:00
parent 83ca32d98e
commit 0a7742eb5e
19 changed files with 1236 additions and 192 deletions
+124 -6
View File
@@ -228,8 +228,7 @@ if (button) {
// The whole point of the composer: pick the day and the lane without ever
// touching the sidebar, and write the line straight into that lane.
store.view = "calendar";
store.sphereFilter = null;
store.projectFilter = null;
store.filterAll();
flushSync();
await settle();
@@ -338,22 +337,74 @@ flushSync();
check("u again handed it back", store.zone === "day", store.zone);
check("and closed the tray", store.undatedCollapsed === true);
// `b` does the same for the spheres, and Enter filters by the row.
// `b` opens the spheres and steps in; `b` again shuts them and steps out,
// whether or not they were open before.
store.undatedCollapsed = false;
store.railCollapsed = false;
key(viewRoot, { key: "b", code: "KeyB" });
flushSync();
check("b took the cursor into the rail", store.zone === "rail", store.zone);
check("the rail stayed open", store.railCollapsed === false);
key(viewRoot, { key: "b", code: "KeyB" });
flushSync();
check("b again stepped out", store.zone !== "rail", store.zone);
check("and shut the rail even though it was open before", store.railCollapsed === true);
key(viewRoot, { key: "b", code: "KeyB" });
flushSync();
check("b reopened the shut rail and stepped in", store.zone === "rail" && !store.railCollapsed);
key(viewRoot, { key: "Escape", code: "Escape" });
flushSync();
check("escape stepped out and left the rail open", store.zone !== "rail" && !store.railCollapsed, store.zone);
// The rail is a gate over lanes: Enter picks a row out, space flips one.
key(viewRoot, { key: "b", code: "KeyB" });
flushSync();
const everything = store.visible.length;
key(viewRoot, { key: "j", code: "KeyJ" });
key(viewRoot, { key: "Enter", code: "Enter" });
flushSync();
check("enter filtered by the row", store.sphereFilter !== null, `${store.sphereFilter}`);
check("and stepped back out", store.zone !== "rail", store.zone);
check("enter on a sphere shows only that sphere", store.railState === "some" && store.visible.length < everything, `${store.visible.length} of ${everything}`);
check("the cursor stayed in the rail", store.zone === "rail", store.zone);
check("the first sphere is wholly on", store.sphereState(store.spheres[0].path) === "all");
check("the second is off", store.sphereState(store.spheres[1].path) === "none");
check("the rail says how much is through", text().includes("lanes") && text().includes("Show all"));
key(viewRoot, { key: "Enter", code: "Enter" });
flushSync();
check("enter again on the same lone sphere opens the gate", store.railState === "all" && store.visible.length === everything);
key(viewRoot, { key: "j", code: "KeyJ" });
key(viewRoot, { key: " ", code: "Space" });
flushSync();
check("space flips one sphere off, the rest stays", store.sphereState(store.spheres[1].path) === "none" && store.sphereState(store.spheres[0].path) === "all");
key(viewRoot, { key: " ", code: "Space" });
flushSync();
check("space again flips it back on, and a full set is the open gate", store.railState === "all");
key(viewRoot, { key: "k", code: "KeyK" });
key(viewRoot, { key: "k", code: "KeyK" });
key(viewRoot, { key: "Enter", code: "Enter" });
flushSync();
check("enter on the top row with everything on shuts the gate", store.railState === "none" && store.visible.length === 0);
check("the list says why it is empty", text().includes("Nothing let through") || store.view === "calendar");
key(viewRoot, { key: "j", code: "KeyJ" });
key(viewRoot, { key: "l", code: "KeyL" });
key(viewRoot, { key: "j", code: "KeyJ" });
key(viewRoot, { key: " ", code: "Space" });
flushSync();
check("a lane can be let through on its own", store.railState === "some" && store.visible.length > 0 && store.visible.every((task) => task.path === store.spheres[0].path), `${store.visible.length}`);
check("its sphere reads as partly on", store.sphereState(store.spheres[0].path) === "some" || store.spheres[0].projects.length === 1);
key(viewRoot, { key: "k", code: "KeyK" });
key(viewRoot, { key: "k", code: "KeyK" });
key(viewRoot, { key: "Enter", code: "Enter" });
flushSync();
check("enter on the top row with some on opens the gate", store.railState === "all");
key(viewRoot, { key: "b", code: "KeyB" });
flushSync();
// The list has no day grid, so its cursor starts on a task from the first key.
store.view = "list";
store.railCollapsed = false;
store.leaveToMain();
store.cursorTask = null;
store.sphereFilter = null;
store.filterAll();
flushSync();
await settle();
key(viewRoot, { key: "j", code: "KeyJ" });
@@ -491,6 +542,73 @@ for (const [name, init] of [
check("the task was written with the date those words meant", written?.includes(stamp), written);
}
// Space over a task is the quick look: the whole text, and the actions.
{
store.view = "list";
store.leaveToMain();
store.cursorTask = null;
flushSync();
key(viewRoot, { key: "j", code: "KeyJ" });
flushSync();
const under = store.taskById(store.cursorTask);
key(viewRoot, { key: " ", code: "Space" });
flushSync();
await settle();
const lens = dom.window.document.querySelector("[aria-label='Quick look']");
check("space opened the quick look", store.peeking && lens !== null);
check("it shows the whole task", lens?.textContent.includes(under.text.slice(0, 20)));
check("it shows the actions", lens?.textContent.includes("edit") && lens?.textContent.includes("priority"));
key(viewRoot, { key: "j", code: "KeyJ" });
flushSync();
check("the lens follows the cursor", store.peeking && store.cursorTask !== under.id);
const toggled = store.cursorTask;
const was = store.taskById(toggled).status;
key(viewRoot, { key: "x", code: "KeyX" });
await settle();
flushSync();
check("keys still reach the task underneath", store.taskById(toggled)?.status !== was || store.taskById(toggled) === null);
key(viewRoot, { key: " ", code: "Space" });
flushSync();
check("space again puts the lens away", !store.peeking);
key(viewRoot, { key: " ", code: "Space" });
flushSync();
key(viewRoot, { key: "Escape", code: "Escape" });
flushSync();
check("escape puts it away too, without leaving the task", !store.peeking && store.zone === "task");
key(viewRoot, { key: " ", code: "Space" });
flushSync();
key(viewRoot, { key: "e", code: "KeyE" });
flushSync();
check("edit closes the lens and opens the editor", !store.peeking && store.editing !== null);
panelOf()?.dispatchEvent(new dom.window.KeyboardEvent("keydown", { bubbles: true, key: "Escape", code: "Escape" }));
flushSync();
}
// The clock is a signal: the editor offers the store's today, not the day
// the view was opened on.
{
store.today = "2031-03-09";
store.leaveToMain();
flushSync();
key(viewRoot, { key: "n", code: "KeyN" });
flushSync();
check("opening the editor re-reads the clock", store.today !== "2031-03-09");
store.today = "2031-03-09";
flushSync();
const panel = panelOf();
const tab = () => {
(dom.window.document.activeElement ?? panel).dispatchEvent(
new dom.window.KeyboardEvent("keydown", { bubbles: true, key: "Tab", code: "Tab" }),
);
flushSync();
};
tab(); tab(); tab();
check("the date field offers the clock's tomorrow", panel.textContent.includes("Mar 10"), panel.textContent.slice(0, 200));
panel.dispatchEvent(new dom.window.KeyboardEvent("keydown", { bubbles: true, key: "Escape", code: "Escape" }));
flushSync();
store.tick();
}
// A request whose task vanished must not leave the view deaf.
store.request = "priority";
store.cursorTask = "nothing/at/all#0";