@gavin-hu/my-pi-agent 0.21.0 → 0.22.0
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 +46 -31
- package/README.zh-CN.md +40 -26
- package/assets/screens/board.png +0 -0
- package/assets/screens/chrome.png +0 -0
- package/assets/screens/file-browse.png +0 -0
- package/assets/screens/plan-review.png +0 -0
- package/assets/screens/plan.png +0 -0
- package/docs/extensions/README.md +22 -24
- package/docs/extensions/board.md +361 -147
- package/docs/extensions/bytefly.md +152 -111
- package/docs/extensions/check.md +15 -17
- package/docs/extensions/context.md +35 -15
- package/docs/extensions/file-browse.md +30 -31
- package/docs/extensions/file-open.md +9 -11
- package/docs/extensions/file-read.md +12 -14
- package/docs/extensions/goal.md +15 -17
- package/docs/extensions/job.md +54 -42
- package/docs/extensions/memory.md +17 -19
- package/docs/extensions/plan.md +166 -126
- package/docs/extensions/question.md +16 -18
- package/docs/extensions/rewind.md +17 -19
- package/docs/extensions/serve.md +34 -18
- package/docs/extensions/session-bus.md +38 -16
- package/docs/extensions/subagent.md +23 -25
- package/docs/extensions/team.md +72 -33
- package/docs/extensions/todo.md +18 -18
- package/docs/extensions/watch.md +31 -31
- package/docs/extensions/web-access.md +2 -4
- package/docs/extensions/wechat.md +34 -35
- package/docs/extensions/worktree.md +56 -50
- package/extensions/board/commands.ts +56 -15
- package/extensions/board/format.ts +43 -24
- package/extensions/board/index.ts +5 -4
- package/extensions/board/input.ts +4 -9
- package/extensions/board/join.ts +4 -40
- package/extensions/board/lazy-page.ts +40 -0
- package/extensions/board/page.ts +13 -11
- package/extensions/board/result.ts +30 -16
- package/extensions/board/rows.ts +62 -25
- package/extensions/board/schema.ts +42 -10
- package/extensions/board/snapshot.ts +2 -1
- package/extensions/board/tone.ts +22 -0
- package/extensions/board/tools.ts +131 -44
- package/extensions/board/types.ts +36 -32
- package/extensions/board/view/card.ts +229 -0
- package/extensions/board/view/client.ts +33 -15
- package/extensions/board/view/dialog.ts +169 -6
- package/extensions/board/view/empty.ts +47 -0
- package/extensions/board/view/keys.ts +13 -2
- package/extensions/board/view/live.ts +89 -21
- package/extensions/board/view/names.ts +13 -4
- package/extensions/board/view/page.ts +23 -9
- package/extensions/board/view/render.ts +208 -271
- package/extensions/board/view/styles.ts +123 -31
- package/extensions/board/view/team.ts +297 -76
- package/extensions/bytefly/block/fallback.ts +1 -1
- package/extensions/bytefly/doctor/runtime.ts +5 -2
- package/extensions/bytefly/footer/collect/label.ts +60 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +8 -1
- package/extensions/bytefly/footer/config.ts +4 -2
- package/extensions/bytefly/footer/register.ts +33 -4
- package/extensions/bytefly/footer/render/lines.ts +7 -4
- package/extensions/bytefly/footer/render/segments.ts +37 -10
- package/extensions/bytefly/footer/render/status.ts +0 -11
- package/extensions/bytefly/footer/types.ts +7 -0
- package/extensions/bytefly/header/config.ts +1 -1
- package/extensions/bytefly/index.ts +32 -6
- package/extensions/bytefly/mode/editor.ts +115 -0
- package/extensions/bytefly/panel/commands.ts +210 -120
- package/extensions/bytefly/panel/format.ts +4 -4
- package/extensions/bytefly/panel/settings.ts +73 -6
- package/extensions/bytefly/panel/tui/keys.ts +2 -6
- package/extensions/bytefly/update/cache.ts +1 -1
- package/extensions/bytefly/update/register.ts +4 -4
- package/extensions/bytefly/working/config.ts +5 -7
- package/extensions/bytefly/working/format.ts +10 -13
- package/extensions/bytefly/working/register.ts +22 -27
- package/extensions/check/commands.ts +3 -2
- package/extensions/check/{tui.ts → view/check-screens.ts} +14 -19
- package/extensions/context/commands.ts +2 -2
- package/extensions/context/{tui.ts → view/context-report.ts} +51 -19
- package/extensions/file-browse/index.ts +67 -36
- package/extensions/file-read/render.ts +4 -4
- package/extensions/goal/plan-route.ts +2 -2
- package/extensions/job/child.ts +527 -0
- package/extensions/job/clock.ts +142 -0
- package/extensions/job/commands.ts +4 -2
- package/extensions/job/config.ts +8 -1
- package/extensions/job/format.ts +22 -1
- package/extensions/job/log-writer.ts +13 -2
- package/extensions/job/registry.ts +63 -7
- package/extensions/job/render.ts +34 -0
- package/extensions/job/runtime.ts +296 -655
- package/extensions/job/session.ts +132 -19
- package/extensions/job/store.ts +7 -7
- package/extensions/job/tools.ts +3 -3
- package/extensions/job/types.ts +47 -0
- package/extensions/job/{tui.ts → view/jobs-screen.ts} +19 -73
- package/extensions/job/waiters.ts +44 -1
- package/extensions/memory/commands.ts +4 -2
- package/extensions/memory/{tui.ts → view/memory-picker.ts} +12 -12
- package/extensions/plan/commands.ts +12 -9
- package/extensions/plan/format.ts +8 -2
- package/extensions/plan/git-bash.ts +115 -16
- package/extensions/plan/index.ts +53 -52
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plans.ts +5 -54
- package/extensions/plan/policy.ts +8 -10
- package/extensions/plan/review-lazy.ts +85 -0
- package/extensions/plan/review.ts +2 -2
- package/extensions/plan/runtime.ts +49 -39
- package/extensions/plan/sections.ts +1 -1
- package/extensions/plan/steps.ts +3 -3
- package/extensions/plan/todos.ts +1 -1
- package/extensions/plan/tools/cancel.ts +27 -62
- package/extensions/plan/tools/enter.ts +55 -81
- package/extensions/plan/tools/exit.ts +221 -238
- package/extensions/plan/tools/refusal.ts +18 -0
- package/extensions/plan/tools/steps.ts +75 -92
- package/extensions/plan/tools/write.ts +77 -120
- package/extensions/plan/tools.ts +188 -30
- package/extensions/plan/tui.ts +5 -5
- package/extensions/plan/types.ts +48 -5
- package/extensions/plan/web-client.ts +3 -0
- package/extensions/plan/web-lazy.ts +53 -0
- package/extensions/plan/web-markdown.ts +9 -67
- package/extensions/plan/web-payload.ts +3 -3
- package/extensions/plan/web-router.ts +18 -9
- package/extensions/plan/web.ts +1 -12
- package/extensions/question/tools.ts +6 -2
- package/extensions/question/{tui.ts → view/question-screen.ts} +11 -11
- package/extensions/rewind/commands/commands.ts +6 -3
- package/extensions/rewind/state/runtime.ts +1 -1
- package/extensions/rewind/{view/status.ts → status.ts} +4 -4
- package/extensions/rewind/view/review-tui.ts +4 -4
- package/extensions/serve/config.ts +10 -8
- package/extensions/serve/index.ts +17 -3
- package/extensions/serve/registry.ts +26 -17
- package/extensions/session-bus/commands.ts +11 -6
- package/extensions/session-bus/join-request.ts +2 -1
- package/extensions/session-bus/mail-watch.ts +26 -17
- package/extensions/session-bus/protocol.ts +15 -3
- package/extensions/session-bus/resolve.ts +5 -7
- package/extensions/session-bus/runtime.ts +40 -12
- package/extensions/session-bus/store.ts +101 -19
- package/extensions/session-bus/types.ts +19 -0
- package/extensions/session-bus/{tui.ts → view/session-list.ts} +16 -16
- package/extensions/subagent/agents.ts +1 -1
- package/extensions/subagent/commands.ts +4 -2
- package/extensions/subagent/format.ts +15 -15
- package/extensions/subagent/index.ts +1 -1
- package/extensions/subagent/render.ts +3 -3
- package/extensions/subagent/{background-tui.ts → view/agents-board.ts} +11 -11
- package/extensions/team/commands.ts +24 -7
- package/extensions/team/dispatch.ts +18 -4
- package/extensions/team/format.ts +17 -2
- package/extensions/team/index.ts +9 -8
- package/extensions/team/schema.ts +6 -5
- package/extensions/team/tools.ts +107 -45
- package/extensions/team/types.ts +3 -2
- package/extensions/todo/commands.ts +4 -2
- package/extensions/todo/format.ts +11 -0
- package/extensions/todo/render.ts +57 -0
- package/extensions/todo/runtime.ts +1 -1
- package/extensions/todo/tools.ts +1 -1
- package/extensions/todo/view/todo-screen.ts +122 -0
- package/extensions/todo/widget.ts +47 -0
- package/extensions/watch/commands.ts +4 -2
- package/extensions/watch/format.ts +36 -7
- package/extensions/watch/render.ts +57 -0
- package/extensions/watch/runtime.ts +27 -22
- package/extensions/watch/tools.ts +4 -4
- package/extensions/watch/{tui.ts → view/watch-screen.ts} +18 -80
- package/extensions/web-access/fetch/tool.ts +12 -4
- package/extensions/wechat/commands/commands.ts +18 -14
- package/extensions/wechat/index.ts +72 -37
- package/extensions/wechat/state/credentials.ts +1 -1
- package/extensions/wechat/state/state.ts +1 -1
- package/extensions/wechat/text/status.ts +112 -0
- package/extensions/wechat/types.ts +2 -2
- package/extensions/wechat/view/status-screen.ts +4 -103
- package/extensions/worktree/commands.ts +11 -9
- package/extensions/worktree/enter.ts +2 -2
- package/extensions/worktree/exit.ts +2 -2
- package/extensions/worktree/index.ts +5 -5
- package/extensions/worktree/screen.ts +3 -3
- package/extensions/worktree/status.ts +2 -2
- package/extensions/worktree/tools.ts +113 -116
- package/lib/README.md +27 -13
- package/lib/board/actions.ts +111 -0
- package/lib/board/artifact.ts +149 -0
- package/lib/board/card.ts +163 -52
- package/lib/board/config.ts +58 -0
- package/lib/board/contract.ts +113 -0
- package/lib/board/join.ts +38 -0
- package/lib/board/lanes.ts +182 -0
- package/lib/board/lead.ts +130 -0
- package/lib/board/limits.ts +78 -0
- package/lib/board/links.ts +31 -10
- package/lib/board/page-pointer.ts +1 -1
- package/lib/board/read.ts +83 -0
- package/lib/board/reason.ts +102 -0
- package/lib/board/round-text.ts +68 -6
- package/lib/board/round.ts +31 -15
- package/lib/board/rules.ts +151 -307
- package/lib/board/seat.ts +35 -0
- package/lib/board/session.ts +136 -54
- package/lib/board/skipped.ts +22 -0
- package/lib/board/steps.ts +487 -0
- package/lib/board/store.ts +87 -30
- package/lib/board/tone.ts +92 -0
- package/lib/board/view.ts +70 -58
- package/{extensions/watch → lib}/child.ts +40 -21
- package/lib/env.ts +65 -7
- package/lib/format.ts +13 -0
- package/lib/http/index.ts +14 -12
- package/lib/http/port.ts +15 -10
- package/lib/lazy.ts +45 -0
- package/lib/plan-entry.ts +39 -10
- package/lib/plans/paths.ts +82 -0
- package/lib/policy.ts +9 -6
- package/lib/report.ts +3 -2
- package/lib/requirements.ts +8 -8
- package/lib/session-label.ts +111 -5
- package/lib/session-roster.ts +43 -1
- package/lib/session-tool.ts +5 -4
- package/lib/stray-fields.ts +7 -2
- package/lib/tool-names.ts +5 -10
- package/lib/tui/README.md +23 -8
- package/lib/tui/chrome.ts +16 -15
- package/lib/tui/cursor.ts +18 -3
- package/lib/tui/glyphs.ts +9 -7
- package/lib/tui/tokens.ts +33 -2
- package/lib/tui/tool.ts +3 -3
- package/lib/web/README.md +21 -0
- package/lib/web/components.ts +5 -1
- package/lib/web/index.ts +1 -0
- package/lib/web/markdown.ts +83 -0
- package/lib/web/markup.ts +2 -1
- package/lib/web/runtime.ts +9 -10
- package/package.json +6 -1
- package/skills/init/SKILL.md +5 -5
- package/skills/next/SKILL.md +3 -5
- package/extensions/board/seat.ts +0 -28
- package/extensions/todo/tui.ts +0 -212
- package/lib/tui/index.ts +0 -34
- package/skills/project-docs/SKILL.md +0 -133
- package/skills/project-docs/references/audit.md +0 -131
- package/skills/project-docs/references/changelog.md +0 -105
- package/skills/project-docs/references/discovery.md +0 -127
- package/skills/project-docs/references/readme.md +0 -80
- package/skills/project-docs/references/release.md +0 -84
- package/skills/project-docs/references/roadmap.md +0 -93
- package/skills/project-docs/scripts/state.ts +0 -316
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One card's face: the title, the facts a reader acts on, and the two marks that say
|
|
3
|
+
* where they have been.
|
|
4
|
+
*
|
|
5
|
+
* It is a fragment of the client bundle like `team.ts` and `dialog.ts`, and it reads the
|
|
6
|
+
* helpers the renderer publishes — the element builder, the chip, the age, the count's
|
|
7
|
+
* wording, and the reason rule spliced in from `lib/board/reason.ts` — so no rule about a
|
|
8
|
+
* card is stated twice.
|
|
9
|
+
*
|
|
10
|
+
* Two of the face's facts are longer than it can draw: the hints past its third path, the
|
|
11
|
+
* reason sentence, and the cards it is holding up. Each is on a hover title for the
|
|
12
|
+
* pointer, and each is named by the title button's `aria-describedby` for the keyboard,
|
|
13
|
+
* because `j`/`k` walks cards by focusing that button and never hovers anything.
|
|
14
|
+
*
|
|
15
|
+
* Every value arrives as `textContent`, never as markup: a title is model-written and a
|
|
16
|
+
* note is anything a person typed.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export const BOARD_CARD_JS = String.raw`(function () {
|
|
20
|
+
"use strict";
|
|
21
|
+
var board = window.bfBoard || (window.bfBoard = {});
|
|
22
|
+
var el = board.el;
|
|
23
|
+
var chip = board.chip;
|
|
24
|
+
var ago = board.ago;
|
|
25
|
+
var plural = board.plural;
|
|
26
|
+
var reasonFor = board.reasonFor;
|
|
27
|
+
var reasonTone = board.reasonTone;
|
|
28
|
+
|
|
29
|
+
/** Path chips a lane card shows before it counts the rest. */
|
|
30
|
+
var PATH_CHIPS = 3;
|
|
31
|
+
/**
|
|
32
|
+
* The class that draws each tone, in the page kit's own words.
|
|
33
|
+
*
|
|
34
|
+
* A tone is a colour and the class is named after it, so the reason line for a missing
|
|
35
|
+
* contract, an approval nobody gave, and a spot-check nobody did are three kinds drawn in
|
|
36
|
+
* one tone — instead of three kinds sharing a class named after the first of them.
|
|
37
|
+
*/
|
|
38
|
+
var TONE_CLASS = {
|
|
39
|
+
muted: N.cardReasonMuted,
|
|
40
|
+
dim: N.cardReasonDim,
|
|
41
|
+
warn: N.cardReasonWarn,
|
|
42
|
+
accent: N.cardReasonAccent,
|
|
43
|
+
danger: N.cardReasonDanger,
|
|
44
|
+
success: N.cardReasonSuccess,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** "3h", "just now" — the age a stamp is drawn with, read against the reader's clock. */
|
|
48
|
+
function sentence(wording, age) {
|
|
49
|
+
return wording.replace("{age}", age === "now" ? "just now" : age + " ago");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* An age the ticker can keep honest: the stamp it was painted from, and the sentence
|
|
54
|
+
* saying which fact it is, which the ticker fills from the same stamp a second later.
|
|
55
|
+
*
|
|
56
|
+
* The wording is the stamp's rather than the ticker's because a card now carries two
|
|
57
|
+
* ages — when it was written to, and when its last note was filed — and a ticker that
|
|
58
|
+
* assumed "updated" would relabel one of them within a minute.
|
|
59
|
+
*/
|
|
60
|
+
function stamp(at, wording) {
|
|
61
|
+
var node = el("span", "", ago(at));
|
|
62
|
+
node.setAttribute("data-at", String(at));
|
|
63
|
+
node.setAttribute("data-at-title", wording);
|
|
64
|
+
node.title = sentence(wording, ago(at));
|
|
65
|
+
return node;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The one line under a card's meta: why it sits where it does. */
|
|
69
|
+
function reasonOf(card) {
|
|
70
|
+
// An order is the strongest reason a card is not being worked on, and the one a
|
|
71
|
+
// reader cannot see from the lane. The sentence is the snapshot's, so this line and
|
|
72
|
+
// the tool's refusal name one list.
|
|
73
|
+
if (card.waits !== undefined) return { text: "waits on " + card.waits, kind: "waiting" };
|
|
74
|
+
return reasonFor(card, ago);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* A path hint, or the +N chip that points at the rest.
|
|
79
|
+
*
|
|
80
|
+
* The hints past the third are the ones a reader needs before opening anything, so they
|
|
81
|
+
* are not dropped: the chip carries them on its hover one per line, and the list holds
|
|
82
|
+
* them again as text a screen reader reads, which is the whole of what the chip counts.
|
|
83
|
+
*/
|
|
84
|
+
function paths(card, limit) {
|
|
85
|
+
var list = el("ul", N.cardPaths);
|
|
86
|
+
list.id = "card-paths-" + card.id;
|
|
87
|
+
var shown = card.paths.slice(0, limit);
|
|
88
|
+
for (var i = 0; i < shown.length; i++) {
|
|
89
|
+
var item = el("li", "");
|
|
90
|
+
var hint = chip(shown[i]);
|
|
91
|
+
// A path longer than the card elides, and the whole of it is on the hover title: the
|
|
92
|
+
// card is often the only place its hints are read before opening the record.
|
|
93
|
+
hint.title = shown[i];
|
|
94
|
+
item.appendChild(hint);
|
|
95
|
+
list.appendChild(item);
|
|
96
|
+
}
|
|
97
|
+
if (card.paths.length > limit) {
|
|
98
|
+
var beyond = card.paths.slice(limit);
|
|
99
|
+
var rest = el("li", "");
|
|
100
|
+
var more = chip("+" + beyond.length);
|
|
101
|
+
more.title = beyond.join("\n");
|
|
102
|
+
// The count is a shape, not a word: what it counts is in the list below it, and a
|
|
103
|
+
// reader is owed the hints rather than "+3".
|
|
104
|
+
more.setAttribute("aria-hidden", "true");
|
|
105
|
+
rest.appendChild(more);
|
|
106
|
+
list.appendChild(rest);
|
|
107
|
+
for (var j = 0; j < beyond.length; j++) list.appendChild(el("li", "sr-only", beyond[j]));
|
|
108
|
+
}
|
|
109
|
+
return list;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Whether the reader is dragging a selection, which a click at the end of it would clear. */
|
|
113
|
+
function textSelected() {
|
|
114
|
+
var selection = window.getSelection ? window.getSelection() : null;
|
|
115
|
+
return selection !== null && String(selection) !== "";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** One card in its lane. */
|
|
119
|
+
function cardNode(card) {
|
|
120
|
+
var item = el("li", N.card);
|
|
121
|
+
item.setAttribute("data-card", card.id);
|
|
122
|
+
item.setAttribute("data-lane", card.lane);
|
|
123
|
+
item.setAttribute("data-rev", String(card.rev));
|
|
124
|
+
|
|
125
|
+
var head = el("p", N.cardHead);
|
|
126
|
+
head.appendChild(el("span", N.cardId, "#" + card.id));
|
|
127
|
+
// The title is the page's only focusable thing inside a card, so Enter and the focus
|
|
128
|
+
// ring have one home; the card around it is the larger target for a pointer. A native
|
|
129
|
+
// button's Enter and Space arrive as a click that bubbles to the card, so the card is
|
|
130
|
+
// the one handler and the button needs none.
|
|
131
|
+
var title = el("button", N.cardTitle, card.title);
|
|
132
|
+
title.type = "button";
|
|
133
|
+
head.appendChild(title);
|
|
134
|
+
item.appendChild(head);
|
|
135
|
+
item.addEventListener("click", function () {
|
|
136
|
+
// A drag that ends on a card is a reader selecting a title to copy, not opening it.
|
|
137
|
+
if (textSelected()) return;
|
|
138
|
+
board.openCard(card.id);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
/** The face's clipped text, by id: what the title button describes. */
|
|
142
|
+
var described = [];
|
|
143
|
+
|
|
144
|
+
var meta = el("p", N.cardMeta);
|
|
145
|
+
meta.appendChild(card.owner ? chip(card.owner.label, "accent") : el("span", "", "unassigned"));
|
|
146
|
+
// The order inside a lane is the priority, so an urgent card says so: an order
|
|
147
|
+
// nobody can see reads as an accident. The lane's colour stays the card's own.
|
|
148
|
+
if (card.priority !== "normal") meta.appendChild(chip(card.priority, "outline"));
|
|
149
|
+
// The stamp, so the clock can keep the age honest between frames: a board can be
|
|
150
|
+
// quiet for ten minutes while the page still says "live".
|
|
151
|
+
meta.appendChild(stamp(card.updatedAt, "updated {age}"));
|
|
152
|
+
// A card with ten notes and a card with none drew the same face, and the notes are the
|
|
153
|
+
// card's history: the count says how much is there. The last note's own age earns a place
|
|
154
|
+
// only when it reads differently from the stamp beside it — a note is itself a write, so
|
|
155
|
+
// usually the two are one moment and the meta line printed the same bare number twice.
|
|
156
|
+
if (card.notes.length > 0) {
|
|
157
|
+
var last = card.notes[card.notes.length - 1];
|
|
158
|
+
var notes = el("span", N.cardNotes, plural(card.notes.length, "note"));
|
|
159
|
+
if (ago(last.at) !== ago(card.updatedAt)) {
|
|
160
|
+
notes.appendChild(document.createTextNode(" \u00b7 "));
|
|
161
|
+
notes.appendChild(stamp(last.at, "last note {age} by " + last.by));
|
|
162
|
+
}
|
|
163
|
+
meta.appendChild(notes);
|
|
164
|
+
}
|
|
165
|
+
item.appendChild(meta);
|
|
166
|
+
|
|
167
|
+
if (card.paths.length > 0) {
|
|
168
|
+
item.appendChild(paths(card, PATH_CHIPS));
|
|
169
|
+
// Only a truncated list is described: the hints a reader can already see are read in
|
|
170
|
+
// order with the card, and the ones behind the chip have nowhere else to be read.
|
|
171
|
+
if (card.paths.length > PATH_CHIPS) described.push("card-paths-" + card.id);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
var reason = reasonOf(card);
|
|
175
|
+
if (reason) {
|
|
176
|
+
// The tone is a lookup, not a suffix: a name assembled from a prefix is a name no
|
|
177
|
+
// test can compare with the stylesheet.
|
|
178
|
+
var line = el("p", N.cardReason + " " + TONE_CLASS[reasonTone(reason.kind)], reason.text);
|
|
179
|
+
line.id = "card-reason-" + card.id;
|
|
180
|
+
// It clamps on the card, so the full sentence is the tooltip.
|
|
181
|
+
line.title = reason.text;
|
|
182
|
+
item.appendChild(line);
|
|
183
|
+
described.push(line.id);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// The other side of a link, on the face: what leaving this card undone holds up. It was
|
|
187
|
+
// dialog-only, so a card three others were waiting on looked like any other card.
|
|
188
|
+
if (card.dependents !== undefined) {
|
|
189
|
+
var holds = el("p", N.cardHolds, "holds up " + card.dependents);
|
|
190
|
+
holds.id = "card-holds-" + card.id;
|
|
191
|
+
holds.title = "holds up " + card.dependents;
|
|
192
|
+
item.appendChild(holds);
|
|
193
|
+
described.push(holds.id);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// The reason and the hints are drawn below the title, and a keyboard reader never
|
|
197
|
+
// hovers: describing the button with them is what makes a clamped line readable from
|
|
198
|
+
// j/k and from a screen reader alike.
|
|
199
|
+
if (described.length > 0) title.setAttribute("aria-describedby", described.join(" "));
|
|
200
|
+
|
|
201
|
+
return item;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
board.cardNode = cardNode;
|
|
205
|
+
board.sentence = sentence;
|
|
206
|
+
|
|
207
|
+
/** Set a mark on a card, or drop it, so one that lost it does not keep it. */
|
|
208
|
+
function marked(node, name, on) {
|
|
209
|
+
if (on) node.setAttribute(name, "true");
|
|
210
|
+
else node.removeAttribute(name);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Paint the card the reader is on and the cards they have already read.
|
|
215
|
+
*
|
|
216
|
+
* j/k walks the drawn cards and wraps, so walking a lane and coming back looked
|
|
217
|
+
* exactly like arriving for the first time. The two ids are the client's own — never
|
|
218
|
+
* sent anywhere — and this runs after every frame as well as on a focus and an open,
|
|
219
|
+
* because a lane is rebuilt whole when its cards move.
|
|
220
|
+
*/
|
|
221
|
+
board.markCards = function () {
|
|
222
|
+
var nodes = document.querySelectorAll(".board-card");
|
|
223
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
224
|
+
var id = nodes[i].getAttribute("data-card");
|
|
225
|
+
marked(nodes[i], "data-current", board.current !== undefined && String(board.current) === id);
|
|
226
|
+
marked(nodes[i], "data-visited", board.visited !== undefined && board.visited[id] === true);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
})();`;
|
|
@@ -6,37 +6,55 @@
|
|
|
6
6
|
* Served as one string, because a page that opens six requests before it can paint
|
|
7
7
|
* is a page that looks broken for a moment. Nothing here is typed — no compiler sees
|
|
8
8
|
* a template string — so the split is by job and the class-contract test reads the
|
|
9
|
-
* result: `render.ts` paints, `
|
|
10
|
-
* `keys.ts` binds, and `live.ts` owns the
|
|
9
|
+
* result: `render.ts` paints a whole frame, `card.ts` draws one card's face, `team.ts`
|
|
10
|
+
* draws the round, `dialog.ts` opens one card, `keys.ts` binds, and `live.ts` owns the
|
|
11
|
+
* tabs and the stream.
|
|
11
12
|
*
|
|
12
|
-
* Load order is a dependency: `team`
|
|
13
|
-
* last, after every `board.*` handler it wires exists.
|
|
13
|
+
* Load order is a dependency: `card` and `team` take helpers from `render`, and `live`
|
|
14
|
+
* starts last, after every `board.*` handler it wires exists.
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
import { MATCHES_QUERY_JS } from "../../../lib/board/card.ts";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
18
|
+
import { REASON_FOR_JS } from "../../../lib/board/reason.ts";
|
|
19
|
+
import { DECISION_RANK_JS, DECISION_TONE_JS, REASON_TONE_JS } from "../../../lib/board/tone.ts";
|
|
20
|
+
import { LANES } from "../../../lib/board/lanes.ts";
|
|
21
|
+
import {
|
|
22
|
+
CLIENT_BASE_JS,
|
|
23
|
+
CLIENT_RUNTIME_JS,
|
|
24
|
+
dialogScript,
|
|
25
|
+
INLINE_MARKDOWN_JS,
|
|
26
|
+
keysDialogScript,
|
|
27
|
+
namesScript,
|
|
28
|
+
TAB_TITLE_JS,
|
|
29
|
+
THEME_TOGGLE_JS,
|
|
30
|
+
} from "../../../lib/web/index.ts";
|
|
31
|
+
import { PLURAL_JS, HISTORY_LINE_JS, HISTORY_OWNER_JS } from "../../../lib/board/round-text.ts";
|
|
32
|
+
import { skippedText } from "../../../lib/board/skipped.ts";
|
|
19
33
|
import { FORMAT_AGE_JS } from "../../../lib/format.ts";
|
|
20
|
-
import {
|
|
34
|
+
import { BOARD_CARD_JS } from "./card.ts";
|
|
21
35
|
import { BOARD_CLASS } from "./names.ts";
|
|
22
|
-
import {
|
|
36
|
+
import { BOARD_EMPTY_JS } from "./empty.ts";
|
|
37
|
+
import { dialogJs } from "./dialog.ts";
|
|
23
38
|
import { BOARD_KEYS_JS } from "./keys.ts";
|
|
24
39
|
import { BOARD_LIVE_JS } from "./live.ts";
|
|
25
40
|
import { renderJs } from "./render.ts";
|
|
26
|
-
import {
|
|
41
|
+
import { teamJs } from "./team.ts";
|
|
27
42
|
|
|
28
43
|
/**
|
|
29
44
|
* The whole client, in load order: paint, round, the card dialog, keys, wiring, start.
|
|
30
45
|
*
|
|
31
|
-
*
|
|
32
|
-
* shape, what a `query` matches,
|
|
33
|
-
*
|
|
46
|
+
* Seven rules cross the boundary as source — the count's wording, the tab title's
|
|
47
|
+
* shape, what a `query` matches, why a card sits where it does, how an age reads, which
|
|
48
|
+
* owners a round may state once, and how that one line reads: the browser has no import
|
|
49
|
+
* graph, and a second copy of any of them is exactly what this bundle is not allowed to hold.
|
|
34
50
|
*/
|
|
35
51
|
export const BOARD_CLIENT_JS = `${CLIENT_BASE_JS}
|
|
36
52
|
${namesScript(BOARD_CLASS)}
|
|
37
|
-
${renderJs({ plural: PLURAL_JS, tabTitle: TAB_TITLE_JS, matches: MATCHES_QUERY_JS, age: FORMAT_AGE_JS, runtime: CLIENT_RUNTIME_JS })}
|
|
38
|
-
${
|
|
39
|
-
${
|
|
53
|
+
${renderJs({ plural: PLURAL_JS, tabTitle: TAB_TITLE_JS, matches: MATCHES_QUERY_JS, reason: REASON_FOR_JS, age: FORMAT_AGE_JS, lanes: String(LANES.length), skipped: String(skippedText), decisions: DECISION_RANK_JS, decisionTones: DECISION_TONE_JS, reasonTones: REASON_TONE_JS, runtime: CLIENT_RUNTIME_JS })}
|
|
54
|
+
${teamJs({ historyOwner: HISTORY_OWNER_JS, historyLine: HISTORY_LINE_JS })}
|
|
55
|
+
${BOARD_CARD_JS}
|
|
56
|
+
${BOARD_EMPTY_JS}
|
|
57
|
+
${dialogJs({ inlineMarkdown: INLINE_MARKDOWN_JS })}
|
|
40
58
|
${BOARD_KEYS_JS}
|
|
41
59
|
${BOARD_LIVE_JS}
|
|
42
60
|
${THEME_TOGGLE_JS}
|
|
@@ -1,19 +1,177 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The card dialog: one card, whole, as a read view.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Filling it, opening it, and forgetting which card is open is the whole of it. `render.ts`
|
|
5
|
+
* paints the board and hands each frame to {@link syncDialog}, which keeps an open record
|
|
6
|
+
* current and closes it when its card has left the board. The page cannot change a card, so
|
|
7
|
+
* this module has no request, no confirmation, no banner and no footer actions — the dialogs
|
|
8
|
+
* it used to fill are gone from the document, and the only way a card changes is a tool.
|
|
9
|
+
*
|
|
10
|
+
* A note is the one field that is not a `textContent` write: its prose is Markdown, read by
|
|
11
|
+
* the kit's one inline renderer (`lib/web/markdown.ts`), which appends text nodes and the
|
|
12
|
+
* four inline atoms and never parses markup. That renderer is spliced into this fragment
|
|
13
|
+
* rather than the bundle's top level — it is only ever read here, and inside this closure it
|
|
14
|
+
* resolves `el` to `board.el` instead of making the page's builder a `window` global.
|
|
15
|
+
* Everything else here is set as text, like the rest of the page.
|
|
8
16
|
*
|
|
9
17
|
* The one thing it must not do is leave a stale panel behind: a dialog the kit closed —
|
|
10
18
|
* `Esc`, a backdrop click, its own close button — forgets `openId`, so the next frame
|
|
11
19
|
* stops filling something nobody can see.
|
|
12
20
|
*/
|
|
13
21
|
|
|
14
|
-
|
|
22
|
+
/**
|
|
23
|
+
* The dialog fragment, with the inline Markdown renderer a note is read through spliced in.
|
|
24
|
+
*
|
|
25
|
+
* The source crosses the boundary as text, because `lib/web/markdown.ts` is JavaScript and a
|
|
26
|
+
* page's bundle has no import graph; `__INLINE_MARKDOWN__` is a rule a served script must
|
|
27
|
+
* never carry, and `pages.test` fails an unfilled one.
|
|
28
|
+
*/
|
|
29
|
+
export function dialogJs(rules: { inlineMarkdown: string }): string {
|
|
30
|
+
return dialogTemplate.replace("__INLINE_MARKDOWN__", rules.inlineMarkdown);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const dialogTemplate = String.raw`(function () {
|
|
15
34
|
"use strict";
|
|
16
35
|
var board = window.bfBoard || (window.bfBoard = {});
|
|
36
|
+
var el = board.el;
|
|
37
|
+
var chip = board.chip;
|
|
38
|
+
var ago = board.ago;
|
|
39
|
+
|
|
40
|
+
__INLINE_MARKDOWN__
|
|
41
|
+
|
|
42
|
+
/** Fill the dialog's contents from a card, without opening or closing it. */
|
|
43
|
+
function fillDialog(card, view) {
|
|
44
|
+
document.getElementById("card-dialog-title").textContent = card.title;
|
|
45
|
+
document.getElementById("card-dialog-id").textContent = "#" + card.id;
|
|
46
|
+
// The lane is a status of its own, not a word inside a sentence: the same dot and
|
|
47
|
+
// label a lane head wears, so the dialog and the board speak the same vocabulary.
|
|
48
|
+
var state = document.getElementById("card-dialog-state");
|
|
49
|
+
var lane = board.laneOf(view, card.lane);
|
|
50
|
+
state.setAttribute("data-lane", card.lane);
|
|
51
|
+
state.textContent = lane === undefined ? card.lane : lane.label;
|
|
52
|
+
|
|
53
|
+
// One fact per row of the ladder, so a reader can pick out a value instead of
|
|
54
|
+
// hunting for it in a sentence. A row with nothing to say takes its label with it:
|
|
55
|
+
// an empty cell under a heading reads as a missing value, not as an absence.
|
|
56
|
+
var pair = function (name, shown) {
|
|
57
|
+
var dt = document.getElementById("card-dialog-" + name + "-label");
|
|
58
|
+
var dd = document.getElementById("card-dialog-" + name);
|
|
59
|
+
if (dt) dt.hidden = !shown;
|
|
60
|
+
if (dd) dd.hidden = !shown;
|
|
61
|
+
return dd;
|
|
62
|
+
};
|
|
63
|
+
var setRow = function (name, shown, value, full) {
|
|
64
|
+
var dd = pair(name, shown);
|
|
65
|
+
dd.textContent = shown ? value : "";
|
|
66
|
+
if (full === undefined) dd.removeAttribute("title");
|
|
67
|
+
else dd.setAttribute("title", full);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
var owner = document.getElementById("card-dialog-owner");
|
|
71
|
+
owner.textContent = "";
|
|
72
|
+
owner.appendChild(card.owner ? chip(card.owner.label, "accent") : document.createTextNode("unassigned"));
|
|
73
|
+
pair("owner", true);
|
|
74
|
+
|
|
75
|
+
var priority = pair("priority", card.priority !== "normal");
|
|
76
|
+
priority.textContent = "";
|
|
77
|
+
if (card.priority !== "normal") priority.appendChild(chip(card.priority, "outline"));
|
|
78
|
+
|
|
79
|
+
setRow("updated", true, ago(card.updatedAt));
|
|
80
|
+
// How long the card has been in the lane it is in now, which updatedAt cannot say:
|
|
81
|
+
// a note filed against a blocked card moves that stamp without moving the card. Absent
|
|
82
|
+
// for a card written before the log existed, so the row takes its label with it.
|
|
83
|
+
setRow("lane", card.since !== undefined, card.lane + " \u00b7 " + ago(card.since));
|
|
84
|
+
setRow("by", true, card.by);
|
|
85
|
+
setRow("landed", card.landed !== undefined, card.landed ? card.landed.commit : "", card.landed ? card.landed.commit : undefined);
|
|
86
|
+
|
|
87
|
+
// The dialog and the card face say one thing, from one field: the ids it was linked
|
|
88
|
+
// to are the storage, and a reader is owed the lanes that make them a decision.
|
|
89
|
+
setRow("depends", card.waits !== undefined, card.waits || "", card.waits);
|
|
90
|
+
// The other side of the same link: what leaving this card undone is holding up, which
|
|
91
|
+
// is the one thing a card's own row cannot say.
|
|
92
|
+
setRow("dependents", card.dependents !== undefined, card.dependents || "", card.dependents);
|
|
93
|
+
|
|
94
|
+
// A long plan path elides on one line, and the whole of it is on the hover title and
|
|
95
|
+
// in the text, so it stays copyable: the reason to open a card is often its plan.
|
|
96
|
+
setRow("plan", Boolean(card.planPath), card.planPath || "", card.planPath || undefined);
|
|
97
|
+
|
|
98
|
+
// The contract is three lines and a signature, and each is its own rung: a reader
|
|
99
|
+
// comparing what was promised with what was done reads them one at a time, and the
|
|
100
|
+
// approval's age is the fact that says whether anybody has looked.
|
|
101
|
+
var contract = card.contract;
|
|
102
|
+
setRow("goal", Boolean(contract), contract ? contract.goal : "", contract ? contract.goal : undefined);
|
|
103
|
+
setRow("acceptance", Boolean(contract), contract ? contract.acceptance : "", contract ? contract.acceptance : undefined);
|
|
104
|
+
setRow("scope-out", Boolean(contract), contract ? contract.scopeOut : "", contract ? contract.scopeOut : undefined);
|
|
105
|
+
var approved = contract && contract.approved;
|
|
106
|
+
setRow("approved", Boolean(approved), approved ? approved.by + " \u00b7 " + ago(approved.at) : "");
|
|
107
|
+
setRow("reviewed", card.review !== undefined, card.review ? card.review.by + " \u00b7 " + ago(card.review.at) : "");
|
|
108
|
+
|
|
109
|
+
pair("paths", card.paths.length > 0);
|
|
110
|
+
var pathList = document.getElementById("card-dialog-path-list");
|
|
111
|
+
pathList.textContent = "";
|
|
112
|
+
for (var i = 0; i < card.paths.length; i++) {
|
|
113
|
+
var item = el("li", "");
|
|
114
|
+
var hint = chip(card.paths[i]);
|
|
115
|
+
hint.className += " " + N.dialogPath;
|
|
116
|
+
hint.title = card.paths[i];
|
|
117
|
+
item.appendChild(hint);
|
|
118
|
+
pathList.appendChild(item);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
var notes = document.getElementById("card-dialog-notes");
|
|
122
|
+
notes.textContent = "";
|
|
123
|
+
for (var j = 0; j < card.notes.length; j++) {
|
|
124
|
+
var note = card.notes[j];
|
|
125
|
+
// The prose is the record and reads at body size; who wrote it and when is its
|
|
126
|
+
// footnote, so it follows the sentence instead of standing over it.
|
|
127
|
+
var row = el("li", N.dialogNote);
|
|
128
|
+
// A note is Markdown, read by the kit's one inline renderer: a backtick, a bold word,
|
|
129
|
+
// or an ordinary URL reads as what it says, a target the policy refuses stays text,
|
|
130
|
+
// and nothing a person typed can become markup.
|
|
131
|
+
var text = el("p", N.dialogNoteText);
|
|
132
|
+
renderInline(text, note.text);
|
|
133
|
+
row.appendChild(text);
|
|
134
|
+
var foot = el("p", N.dialogNoteFoot);
|
|
135
|
+
foot.appendChild(el("span", N.dialogNoteBy, note.by));
|
|
136
|
+
foot.appendChild(el("span", N.dialogNoteAt, ago(note.at)));
|
|
137
|
+
row.appendChild(foot);
|
|
138
|
+
notes.appendChild(row);
|
|
139
|
+
}
|
|
140
|
+
var count = document.getElementById("card-dialog-note-count");
|
|
141
|
+
if (count) count.textContent = card.notes.length === 0 ? " \u00b7 none" : " \u00b7 " + card.notes.length;
|
|
142
|
+
var earlier = document.getElementById("card-dialog-earlier");
|
|
143
|
+
earlier.hidden = card.notesDropped === 0;
|
|
144
|
+
earlier.textContent = card.notesDropped === 0 ? "" : card.notesDropped + " earlier notes dropped";
|
|
145
|
+
|
|
146
|
+
// A modal whose title changes is not announced, so walking with j/k says where it
|
|
147
|
+
// landed: the position is counted over the cards as they are drawn, not as stored, so
|
|
148
|
+
// a filter or a folded lane cannot make the number disagree with the screen.
|
|
149
|
+
var where = document.getElementById("card-dialog-where");
|
|
150
|
+
if (where) {
|
|
151
|
+
var drawn = board.drawnCards();
|
|
152
|
+
var at = 0;
|
|
153
|
+
for (var k = 0; k < drawn.length; k++) {
|
|
154
|
+
if (drawn[k].getAttribute("data-card") === String(card.id)) at = k + 1;
|
|
155
|
+
}
|
|
156
|
+
// Opened from the Team tab there is no drawn board to count along, so the line
|
|
157
|
+
// names the card instead of claiming a position it cannot know.
|
|
158
|
+
where.textContent = drawn.length === 0 ? card.title : "card " + at + " of " + drawn.length + ": " + card.title;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Keep an open record current: fill it from the frame, or close it when its card left.
|
|
164
|
+
*
|
|
165
|
+
* The frame owns this rather than the open handler: a peer's note moves the card's
|
|
166
|
+
* revision, and a panel left showing the record it was opened with is a page that lies
|
|
167
|
+
* for as long as the tab stays open.
|
|
168
|
+
*/
|
|
169
|
+
board.syncDialog = function (view) {
|
|
170
|
+
if (board.openId === undefined) return;
|
|
171
|
+
var card = board.findCard(view, board.openId);
|
|
172
|
+
if (card) fillDialog(card, view);
|
|
173
|
+
else board.closeCard();
|
|
174
|
+
};
|
|
17
175
|
|
|
18
176
|
/** Fill and open the card dialog. */
|
|
19
177
|
board.openCard = function (id) {
|
|
@@ -22,7 +180,12 @@ export const BOARD_DIALOG_JS = String.raw`(function () {
|
|
|
22
180
|
var card = board.findCard(view, id);
|
|
23
181
|
if (!card || !window.bfCard) return;
|
|
24
182
|
board.openId = id;
|
|
25
|
-
|
|
183
|
+
// A card a reader opened is one they have read, and it is where they are: the board
|
|
184
|
+
// paints both marks from these two fields, so closing the record leaves the place.
|
|
185
|
+
board.visited[id] = true;
|
|
186
|
+
board.current = id;
|
|
187
|
+
fillDialog(card, view);
|
|
188
|
+
board.markCards();
|
|
26
189
|
window.bfCard.open();
|
|
27
190
|
};
|
|
28
191
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The board's two empty states: no cards at all, and a filter that matches none.
|
|
3
|
+
*
|
|
4
|
+
* Both reuse the one block the document already carries (`#board-no-cards`), because the
|
|
5
|
+
* difference between them is a sentence and one button, not a second layout. They live in
|
|
6
|
+
* their own fragment so the renderer stays the painter of cards and lanes: an empty board is
|
|
7
|
+
* a statement about the whole board, and it is the one part of the page that says a thing
|
|
8
|
+
* instead of drawing one.
|
|
9
|
+
*
|
|
10
|
+
* Same rule as the rest of the client: every value arrives as `textContent`, and the query
|
|
11
|
+
* the reader typed is one of them.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const BOARD_EMPTY_JS = String.raw`(function () {
|
|
15
|
+
"use strict";
|
|
16
|
+
var board = window.bfBoard || (window.bfBoard = {});
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* What the board says when there is nothing to read.
|
|
20
|
+
*
|
|
21
|
+
* Two different emptinesses. No cards at all is the board's own state, and it teaches the
|
|
22
|
+
* verb that fills it. A filter that matches none of them is the reader's own query: the
|
|
23
|
+
* lanes go, because five trays saying "no match" read as a board with no work, and the way
|
|
24
|
+
* back is one button rather than a key a reader has to remember.
|
|
25
|
+
*/
|
|
26
|
+
board.renderEmpty = function (view, query, shownTotal) {
|
|
27
|
+
var block = document.getElementById("board-no-cards");
|
|
28
|
+
if (!block) return;
|
|
29
|
+
var lanes = document.getElementById("board-lanes");
|
|
30
|
+
var noCards = view.total === 0;
|
|
31
|
+
var noMatch = !noCards && query !== "" && shownTotal === 0;
|
|
32
|
+
block.hidden = !noCards && !noMatch;
|
|
33
|
+
if (lanes) lanes.hidden = noCards || noMatch;
|
|
34
|
+
var title = document.getElementById("board-no-cards-title");
|
|
35
|
+
var sub = document.getElementById("board-no-cards-sub");
|
|
36
|
+
var hint = document.getElementById("board-no-cards-hint");
|
|
37
|
+
var clear = document.getElementById("board-no-match-clear");
|
|
38
|
+
if (title) title.textContent = noMatch ? "No card matches \u201c" + query + "\u201d" : "No cards yet";
|
|
39
|
+
if (sub) {
|
|
40
|
+
sub.textContent = noMatch
|
|
41
|
+
? "The filter reads a title, its owner, a path, a plan, and the notes."
|
|
42
|
+
: "One card per feature: what it delivers, who holds it, and where to start.";
|
|
43
|
+
}
|
|
44
|
+
if (hint) hint.hidden = noMatch;
|
|
45
|
+
if (clear) clear.hidden = !noMatch;
|
|
46
|
+
};
|
|
47
|
+
})();`;
|
|
@@ -65,7 +65,12 @@ export const BOARD_KEYS_JS = String.raw`(function () {
|
|
|
65
65
|
if (walk(delta)) return;
|
|
66
66
|
var drawn = cards();
|
|
67
67
|
if (drawn.length === 0) return;
|
|
68
|
-
|
|
68
|
+
// Focus sits on a card's title button, not on the card, so the step reads the card that
|
|
69
|
+
// holds it: an index of -1 for every card made j from one land on the first card in the
|
|
70
|
+
// lane instead of the next — which is the place a reader lost when they came back.
|
|
71
|
+
var here = document.activeElement;
|
|
72
|
+
var card = here && here.closest ? here.closest(".board-card") : null;
|
|
73
|
+
var at = card === null ? -1 : drawn.indexOf(card);
|
|
69
74
|
var next = at < 0 ? (delta > 0 ? 0 : drawn.length - 1) : (at + delta + drawn.length) % drawn.length;
|
|
70
75
|
var button = drawn[next].querySelector(".board-card__title");
|
|
71
76
|
if (button) button.focus();
|
|
@@ -75,7 +80,11 @@ export const BOARD_KEYS_JS = String.raw`(function () {
|
|
|
75
80
|
doc.addEventListener("keydown", function (event) {
|
|
76
81
|
if (event.metaKey || event.ctrlKey || event.altKey) return;
|
|
77
82
|
var target = event.target;
|
|
78
|
-
|
|
83
|
+
// A dialog is modal for the pointer and for the keyboard alike: "/", "t", and "?"
|
|
84
|
+
// reach the page behind the scrim from nowhere else, so a modal keeps them and the
|
|
85
|
+
// keys that move inside it -- j, k, Enter, Esc -- are the ones still read.
|
|
86
|
+
var modal = (board.dialogOpen && board.dialogOpen()) || (board.keysOpen && board.keysOpen());
|
|
87
|
+
if (event.key === "?" && !typing(target) && !modal) {
|
|
79
88
|
event.preventDefault();
|
|
80
89
|
board.openKeys();
|
|
81
90
|
return;
|
|
@@ -108,6 +117,7 @@ export const BOARD_KEYS_JS = String.raw`(function () {
|
|
|
108
117
|
return;
|
|
109
118
|
}
|
|
110
119
|
if (event.key === "/") {
|
|
120
|
+
if (modal) return;
|
|
111
121
|
event.preventDefault();
|
|
112
122
|
board.focusFilter();
|
|
113
123
|
} else if (event.key === "j") {
|
|
@@ -117,6 +127,7 @@ export const BOARD_KEYS_JS = String.raw`(function () {
|
|
|
117
127
|
event.preventDefault();
|
|
118
128
|
step(-1);
|
|
119
129
|
} else if (event.key === "t") {
|
|
130
|
+
if (modal) return;
|
|
120
131
|
event.preventDefault();
|
|
121
132
|
board.showView(board.viewName() === "team" ? "board" : "team");
|
|
122
133
|
}
|