@azure-id/orc 1.9.2 → 2.0.2
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/CHANGELOG.md +321 -0
- package/README-id.md +21 -36
- package/README.md +25 -36
- package/bin/build-agents.js +117 -20
- package/bin/cli.js +725 -29
- package/bin/gotcha-import.js +1081 -0
- package/bin/gotcha.js +1286 -0
- package/bin/graph-query.js +1 -1
- package/bin/graph.js +717 -717
- package/bin/habit.js +1453 -0
- package/bin/mockrun-catalog.js +281 -276
- package/bin/run-undo.js +398 -0
- package/bin/trace-write.js +657 -0
- package/bin/verify-contracts.js +525 -79
- package/bin/verify-package.js +51 -4
- package/bin/webui/api.js +26 -0
- package/bin/webui/app.html +239 -232
- package/bin/webui/css/00-tokens.css +110 -92
- package/bin/webui/css/04-motion.css +87 -0
- package/bin/webui/css/06-responsive.css +203 -178
- package/bin/webui/css/panels/behaviour.css +205 -0
- package/bin/webui/fixtures/behaviour.js +532 -0
- package/bin/webui/fixtures/index.js +34 -0
- package/bin/webui/fixtures/knowledge.js +7 -1
- package/bin/webui/fixtures/stats.js +16 -0
- package/bin/webui/i18n/en/behaviour.json +143 -0
- package/bin/webui/i18n/en/nav.json +25 -24
- package/bin/webui/i18n/en/tour.json +37 -35
- package/bin/webui/i18n/id/behaviour.json +143 -0
- package/bin/webui/i18n/id/nav.json +25 -24
- package/bin/webui/i18n/id/tour.json +37 -35
- package/bin/webui/js/01-i18n.js +155 -154
- package/bin/webui/js/90-tour.js +498 -494
- package/bin/webui/js/91-shortcuts.js +126 -126
- package/bin/webui/js/99-boot.js +121 -118
- package/bin/webui/js/panels/behaviour.js +1022 -0
- package/mock-run/INDEX.md +109 -107
- package/mock-run/gotcha-import.md +118 -0
- package/mock-run/habits.md +129 -0
- package/mock-run/orc-quick.md +6 -1
- package/package.json +1 -1
- package/templates/agents/MODEL-MAPPING.md +7 -7
- package/templates/agents/orc-advisor-opus-5-xhigh.md +1 -7
- package/templates/agents/orc-analyze-mini-opus-5-med.md +1 -6
- package/templates/agents/orc-analyze-mini-sonnet-5-high.md +1 -4
- package/templates/agents/orc-claude-writer-opus-4-8-high.md +48 -53
- package/templates/agents/orc-claude-writer-opus-5-med.md +1 -8
- package/templates/agents/orc-context-combiner-opus-5-high.md +1 -11
- package/templates/agents/orc-executor-haiku-4-5.md +14 -6
- package/templates/agents/orc-executor-opus-4-7-high.md +14 -6
- package/templates/agents/orc-executor-opus-4-7-med.md +14 -6
- package/templates/agents/orc-executor-opus-4-8-high.md +14 -6
- package/templates/agents/orc-executor-opus-5-high.md +14 -6
- package/templates/agents/orc-executor-opus-5-low.md +14 -6
- package/templates/agents/orc-executor-opus-5-med.md +14 -6
- package/templates/agents/orc-executor-sonnet-4-6-high.md +14 -6
- package/templates/agents/orc-executor-sonnet-4-6-med.md +14 -6
- package/templates/agents/orc-executor-sonnet-5-high.md +14 -6
- package/templates/agents/orc-graph-noter-sonnet-4-6-med.md +1 -10
- package/templates/agents/orc-judge-opus-5-xhigh.md +5 -9
- package/templates/agents/orc-learn-writer-opus-5-low.md +1 -7
- package/templates/agents/orc-pattern-codifier-opus-5-med.md +1 -8
- package/templates/agents/orc-pattern-codifier-sonnet-5-high.md +58 -63
- package/templates/agents/orc-planner-mini-opus-5-med.md +1 -4
- package/templates/agents/orc-planner-mini-sonnet-5-high.md +1 -2
- package/templates/agents/orc-planner-opus-5-med.md +1 -4
- package/templates/agents/orc-recon-opus-5-low.md +1 -8
- package/templates/agents/orc-recon-sonnet-4-6-med.md +1 -8
- package/templates/agents/orc-retro-opus-5-med.md +6 -8
- package/templates/agents/orc-retro-sonnet-5-high.md +6 -7
- package/templates/agents/orc-reviewer-opus-5-med.md +52 -16
- package/templates/agents/orc-scout-opus-5-low.md +1 -6
- package/templates/agents/orc-scout-sonnet-4-6-high.md +35 -39
- package/templates/agents/orc-system-analyst-opus-5-high.md +1 -6
- package/templates/agents/orc-test-author-opus-5-med.md +4 -5
- package/templates/agents/orc-trace-writer-haiku-4-5.md +3 -7
- package/templates/agents/orc-verifier-opus-5-med.md +16 -8
- package/templates/agents/orc-wiki-scanner-opus-4-8-high.md +74 -79
- package/templates/agents/orc-wiki-scanner-opus-5-med.md +1 -8
- package/templates/agents/orc-wiki-scanner-sonnet-5-high.md +97 -106
- package/templates/commands/orc-analyze.md +13 -21
- package/templates/commands/orc-fast.md +10 -15
- package/templates/commands/orc-poly.md +12 -21
- package/templates/commands/orc-pr-driver.md +11 -30
- package/templates/commands/orc-pr-setup.md +10 -31
- package/templates/commands/orc-route.md +11 -41
- package/templates/commands/orc-test.md +5 -60
- package/templates/hooks/README.md +34 -0
- package/templates/hooks/orc-session-hook.js +264 -0
- package/templates/hooks/orc-statusline.js +3 -1
- package/templates/skills/_shared/README.md +9 -0
- package/templates/skills/_shared/code-graph.md +47 -55
- package/templates/skills/_shared/config-precedence.md +3 -1
- package/templates/skills/_shared/extra-dispatch.md +73 -88
- package/templates/skills/_shared/gotchas.md +228 -177
- package/templates/skills/_shared/habits.md +101 -0
- package/templates/skills/_shared/lane-contract.md +84 -0
- package/templates/skills/_shared/phases/README.md +142 -83
- package/templates/skills/_shared/phases/analyst-gates.md +10 -21
- package/templates/skills/_shared/phases/execution.md +8 -14
- package/templates/skills/_shared/phases/house-rules.md +27 -32
- package/templates/skills/_shared/phases/intake.md +127 -133
- package/templates/skills/_shared/phases/mock-example.md +46 -56
- package/templates/skills/_shared/phases/plan-handoff.md +91 -97
- package/templates/skills/_shared/phases/planning.md +7 -17
- package/templates/skills/_shared/phases/preflight.md +19 -42
- package/templates/skills/_shared/phases/review.md +23 -27
- package/templates/skills/_shared/phases/rules.md +18 -42
- package/templates/skills/_shared/phases/scoring.md +55 -65
- package/templates/skills/_shared/phases/security-checklist.md +46 -50
- package/templates/skills/_shared/phases/security.md +45 -55
- package/templates/skills/_shared/phases/ship.md +6 -15
- package/templates/skills/_shared/phases/stop-resume.md +2 -5
- package/templates/skills/_shared/phases/summary.md +73 -48
- package/templates/skills/_shared/phases/testgen.md +41 -51
- package/templates/skills/_shared/phases/trace-verbs.md +433 -0
- package/templates/skills/_shared/phases/trace.md +136 -367
- package/templates/skills/_shared/phases/verify.md +62 -70
- package/templates/skills/_shared/phases/wave-grouping.md +128 -133
- package/templates/skills/_shared/phases/wiki-consult.md +10 -6
- package/templates/skills/_shared/read-ladder.md +2 -55
- package/templates/skills/_shared/return-validation.md +17 -70
- package/templates/skills/_shared/review-slice.md +79 -0
- package/templates/skills/_shared/smoke-gate.md +46 -28
- package/templates/skills/context-combiner/SKILL.md +15 -44
- package/templates/skills/orc/SKILL.md +34 -62
- package/templates/skills/orc/references/pattern-gate.md +89 -89
- package/templates/skills/orc/references/phases/intake.md +41 -47
- package/templates/skills/orc/references/phases/integration.md +13 -19
- package/templates/skills/orc/references/preflight-report.md +8 -9
- package/templates/skills/orc/references/ultra-mode.md +8 -6
- package/templates/skills/orc/subskills/orc-execution/SKILL.md +27 -73
- package/templates/skills/orc/subskills/orc-execution/core.md +12 -99
- package/templates/skills/orc/subskills/orc-execution/subagent.md +14 -13
- package/templates/skills/orc/subskills/orc-review-verify/SKILL.md +11 -52
- package/templates/skills/orc/subskills/orc-review-verify/core.md +52 -135
- package/templates/skills/orc/subskills/orc-review-verify/subagent.md +7 -7
- package/templates/skills/orc/subskills/orc-testgen/SKILL.md +11 -22
- package/templates/skills/orc/subskills/orc-testgen/core.md +20 -59
- package/templates/skills/orc/subskills/orc-testgen/subagent.md +7 -7
- package/templates/skills/orc-advisor/SKILL.md +56 -60
- package/templates/skills/orc-analyze/SKILL.md +30 -66
- package/templates/skills/orc-analyze/schemas/report-audit.md +2 -1
- package/templates/skills/orc-analyze/schemas/report-prose.md +2 -1
- package/templates/skills/orc-analyze-mini/SKILL.md +35 -70
- package/templates/skills/orc-diy/README.md +31 -0
- package/templates/skills/orc-diy/SKILL.md +23 -74
- package/templates/skills/orc-diy/references/blocks/pattern.md +18 -18
- package/templates/skills/orc-fast/SKILL.md +44 -72
- package/templates/skills/orc-judge/SKILL.md +77 -82
- package/templates/skills/orc-mini/SKILL.md +61 -109
- package/templates/skills/orc-pattern/SKILL.md +27 -48
- package/templates/skills/orc-poly/SKILL.md +32 -61
- package/templates/skills/orc-pr-driver/SKILL.md +25 -51
- package/templates/skills/orc-pr-driver/references/green-gate.md +113 -105
- package/templates/skills/orc-pr-setup/SKILL.md +20 -45
- package/templates/skills/orc-quick/README.md +43 -2
- package/templates/skills/orc-quick/SKILL.md +76 -107
- package/templates/skills/orc-quick/references/dispatch-gate.md +16 -5
- package/templates/skills/orc-quick/references/gh-mode.md +48 -1
- package/templates/skills/orc-quick/references/look.md +3 -1
- package/templates/skills/orc-retro/SKILL.md +19 -18
- package/templates/skills/orc-retro/examples/retro-mock.md +1 -1
- package/templates/skills/orc-route/SKILL.md +25 -45
- package/templates/skills/orc-test/SKILL.md +16 -37
- package/templates/skills/orc-verify/SKILL.md +14 -32
- package/templates/skills/orc-wait/SKILL.md +156 -163
- package/templates/skills/orc-wiki/references/phases/phase-0.md +1 -6
- package/templates/skills/orc-wiki/references/phases/phase-1.md +1 -6
- package/templates/skills/orc-wiki/references/phases/phase-2.md +1 -6
- package/templates/skills/orc-wiki/references/phases/phase-3.md +1 -6
- package/templates/skills/orc-wiki/references/phases/phase-3c.md +1 -6
|
@@ -0,0 +1,1022 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* panels/behaviour.js — orc ui client
|
|
3
|
+
Behaviour (v2.0.0 W7): what the lanes learned about how YOU work, from your
|
|
4
|
+
own answers, in this project only. Five tabs — Habits, Rhythm, Gotchas,
|
|
5
|
+
Review quality, Answer log — above them a portrait and four tiles.
|
|
6
|
+
|
|
7
|
+
THE RULE OF THIS PANEL: it renders the CLI's lines and derives no habit.
|
|
8
|
+
Every sentence (`portrait[].line`, `rows[].line`, `effect`, `rule`), every
|
|
9
|
+
share, streak, state word, class word, `by=` value, band and command arrives
|
|
10
|
+
from `orc habit show|log --json` and `orc gotcha list|card|quality --json`.
|
|
11
|
+
The panel translates only its OWN prose (DE-19). It never branches on a
|
|
12
|
+
state word: the actions come from `commands{}`, the colours from a
|
|
13
|
+
`data-state` / `data-by` / `data-source` attribute that
|
|
14
|
+
css/panels/behaviour.css maps to a token. A `never` habit has NO button —
|
|
15
|
+
only the command a person types.
|
|
16
|
+
|
|
17
|
+
Loaded by app.html in the order its numeric prefix names. Classic
|
|
18
|
+
script, no import/export: an ES module import carries no query string,
|
|
19
|
+
and every static request here needs the per-launch session token. */
|
|
20
|
+
|
|
21
|
+
const BH_TABS = ["habits", "rhythm", "gotchas", "quality", "log"];
|
|
22
|
+
const BH_WINDOWS = ["30d", "90d", "all"];
|
|
23
|
+
const BH_SVG = "http://www.w3.org/2000/svg";
|
|
24
|
+
let bhTab = "habits";
|
|
25
|
+
let bhWindow = "90d";
|
|
26
|
+
// Set after the first paint. A repaint after a write adds `bh-quiet`, so no
|
|
27
|
+
// entrance plays twice — the v1.4.2 `hk-quiet` rule.
|
|
28
|
+
let bhPainted = false;
|
|
29
|
+
|
|
30
|
+
// Panel prose for the words the CLI publishes. Looked up BY the CLI's word; a
|
|
31
|
+
// word this table does not know renders bare, never invented.
|
|
32
|
+
const BH_STATE_HELP = () => ({
|
|
33
|
+
observed: t("behaviour.state.observed"),
|
|
34
|
+
proposed: t("behaviour.state.proposed"),
|
|
35
|
+
applied: t("behaviour.state.applied"),
|
|
36
|
+
declined: t("behaviour.state.declined"),
|
|
37
|
+
"never-ask": t("behaviour.state.neverAsk"),
|
|
38
|
+
stale: t("behaviour.state.stale"),
|
|
39
|
+
shadowed: t("behaviour.state.shadowed"),
|
|
40
|
+
never: t("behaviour.state.never"),
|
|
41
|
+
});
|
|
42
|
+
const BH_CLASS_HELP = () => ({
|
|
43
|
+
apply: t("behaviour.class.apply"),
|
|
44
|
+
suggest: t("behaviour.class.suggest"),
|
|
45
|
+
never: t("behaviour.class.never"),
|
|
46
|
+
});
|
|
47
|
+
const BH_BY_HELP = () => ({
|
|
48
|
+
user: t("behaviour.by.user"),
|
|
49
|
+
ledger: t("behaviour.by.ledger"),
|
|
50
|
+
learned: t("behaviour.by.learned"),
|
|
51
|
+
config: t("behaviour.by.config"),
|
|
52
|
+
default: t("behaviour.by.default"),
|
|
53
|
+
});
|
|
54
|
+
const BH_MODE_HELP = () => ({
|
|
55
|
+
off: t("behaviour.mode.off"),
|
|
56
|
+
observe: t("behaviour.mode.observe"),
|
|
57
|
+
propose: t("behaviour.mode.propose"),
|
|
58
|
+
});
|
|
59
|
+
const BH_TAB_LABEL = () => ({
|
|
60
|
+
habits: t("behaviour.tab.habits"),
|
|
61
|
+
rhythm: t("behaviour.tab.rhythm"),
|
|
62
|
+
gotchas: t("behaviour.tab.gotchas"),
|
|
63
|
+
quality: t("behaviour.tab.quality"),
|
|
64
|
+
log: t("behaviour.tab.log"),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
PANELS.behaviour = function (host) {
|
|
68
|
+
bhPainted = false;
|
|
69
|
+
const right = el("div", "bh-headright");
|
|
70
|
+
head(host, t("behaviour.title"), t("behaviour.sub"), right);
|
|
71
|
+
const live = el("div", "bh-sr");
|
|
72
|
+
live.id = "bh-live";
|
|
73
|
+
live.setAttribute("aria-live", "polite");
|
|
74
|
+
host.append(live);
|
|
75
|
+
const body = el("div", "stack");
|
|
76
|
+
host.append(body);
|
|
77
|
+
bhLoad(body, right);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// The window switch and the learning switch live in the page head. The window
|
|
81
|
+
// is a READ; the learning switch is a WRITE, through the one confirmation.
|
|
82
|
+
async function bhLoad(body, right, focusId) {
|
|
83
|
+
if (!bhPainted) body.replaceChildren(skeleton(5));
|
|
84
|
+
let hab;
|
|
85
|
+
try {
|
|
86
|
+
hab = await read("/api/habits?window=" + encodeURIComponent(bhWindow));
|
|
87
|
+
} catch (e) {
|
|
88
|
+
body.replaceChildren(failBox(e));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const d = hab.data || {};
|
|
92
|
+
right.replaceChildren(bhModeSwitch(d, body, right));
|
|
93
|
+
// `habits: off` (exit 3) — ONE card instead of the tabs.
|
|
94
|
+
if (d.ok === false && Array.isArray(d.on)) {
|
|
95
|
+
body.classList.toggle("bh-quiet", bhPainted);
|
|
96
|
+
body.replaceChildren(bhOffCard(d), bhFooter(null, true));
|
|
97
|
+
bhPainted = true;
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
right.append(bhWindowSwitch(body, right));
|
|
101
|
+
const [log, states, gotchas, quality, cands] = await Promise.all([
|
|
102
|
+
read("/api/habits/log").catch(() => ({ data: null })),
|
|
103
|
+
read("/api/habits/states").catch(() => ({ data: null })),
|
|
104
|
+
read("/api/gotchas").catch(() => ({ data: null })),
|
|
105
|
+
read("/api/gotcha/quality?window=" + encodeURIComponent(bhWindow)).catch(() => ({ data: null })),
|
|
106
|
+
read("/api/gotcha/candidates").catch(() => ({ data: null })),
|
|
107
|
+
]);
|
|
108
|
+
const ctx = { d, log: log.data, states: states.data, gotchas: gotchas.data, quality: quality.data, cands: cands.data, body, right };
|
|
109
|
+
body.classList.toggle("bh-quiet", bhPainted);
|
|
110
|
+
const out = frag();
|
|
111
|
+
out.append(bhHero(d));
|
|
112
|
+
const tabs = bhTabs(ctx);
|
|
113
|
+
out.append(tabs.bar);
|
|
114
|
+
out.append(tabs.panel);
|
|
115
|
+
out.append(bhFooter(d, false));
|
|
116
|
+
body.replaceChildren(out);
|
|
117
|
+
tabs.fill();
|
|
118
|
+
bhPainted = true;
|
|
119
|
+
bhRailDot(d);
|
|
120
|
+
if (focusId) {
|
|
121
|
+
const card = body.querySelector('[data-habit="' + focusId + '"]');
|
|
122
|
+
if (card) card.focus();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function bhModeSwitch(d, body, right) {
|
|
127
|
+
const seg = el("div", "bh-seg bh-seg-mode");
|
|
128
|
+
seg.setAttribute("role", "group");
|
|
129
|
+
seg.setAttribute("aria-label", t("behaviour.learning"));
|
|
130
|
+
seg.append(el("span", "bh-seg-label", t("behaviour.learning")));
|
|
131
|
+
const help = BH_MODE_HELP();
|
|
132
|
+
for (const m of d.modes || []) {
|
|
133
|
+
const b = el("button", null, m);
|
|
134
|
+
b.type = "button";
|
|
135
|
+
b.setAttribute("aria-pressed", String(m === d.mode));
|
|
136
|
+
b.addEventListener("click", () => {
|
|
137
|
+
if (m === d.mode) return;
|
|
138
|
+
const cmd = "orc config set habits " + m;
|
|
139
|
+
bhConfirm({
|
|
140
|
+
title: t("behaviour.modeTitle", { mode: m }),
|
|
141
|
+
what: help[m] || "",
|
|
142
|
+
command: cmd,
|
|
143
|
+
go: async () => {
|
|
144
|
+
const r = await post("/api/config/set", { key: "habits", value: m });
|
|
145
|
+
toast(r.ok ? t("behaviour.modeDone", { mode: m }) : t("common.writeFail"), r.ok ? "ok" : "bad", r.output);
|
|
146
|
+
bhSay(t("behaviour.modeDone", { mode: m }));
|
|
147
|
+
bhLoad(body, right);
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
seg.append(b);
|
|
152
|
+
}
|
|
153
|
+
return seg;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function bhWindowSwitch(body, right) {
|
|
157
|
+
const seg = el("div", "bh-seg");
|
|
158
|
+
seg.setAttribute("role", "group");
|
|
159
|
+
seg.setAttribute("aria-label", t("behaviour.window"));
|
|
160
|
+
for (const w of BH_WINDOWS) {
|
|
161
|
+
const b = el("button", null, w);
|
|
162
|
+
b.type = "button";
|
|
163
|
+
b.setAttribute("aria-pressed", String(w === bhWindow));
|
|
164
|
+
b.addEventListener("click", () => {
|
|
165
|
+
if (w === bhWindow) return;
|
|
166
|
+
bhWindow = w;
|
|
167
|
+
bhLoad(body, right);
|
|
168
|
+
});
|
|
169
|
+
seg.append(b);
|
|
170
|
+
}
|
|
171
|
+
return seg;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── the off card ────────────────────────────────────────────────────────────
|
|
175
|
+
function bhOffCard(d) {
|
|
176
|
+
const c = el("section", "bh-hero bh-off");
|
|
177
|
+
c.append(el("div", "bh-eyebrow", t("behaviour.off.eyebrow")));
|
|
178
|
+
c.append(el("h2", "bh-off-title", t("behaviour.off.title")));
|
|
179
|
+
const ul = el("ul", "bh-off-list");
|
|
180
|
+
const help = BH_MODE_HELP();
|
|
181
|
+
for (const m of d.modes || []) {
|
|
182
|
+
const li = el("li");
|
|
183
|
+
li.append(el("span", "mono", m + " "), document.createTextNode(help[m] || ""));
|
|
184
|
+
ul.append(li);
|
|
185
|
+
}
|
|
186
|
+
if (d.kept) ul.append(el("li", null, t("behaviour.off.kept", { n: d.kept })));
|
|
187
|
+
c.append(ul);
|
|
188
|
+
c.append(el("div", "note", t("behaviour.off.how")));
|
|
189
|
+
for (const cmd of d.on || []) c.append(bhCmd(cmd));
|
|
190
|
+
return c;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── the hero: portrait + four tiles ─────────────────────────────────────────
|
|
194
|
+
function bhHero(d) {
|
|
195
|
+
const h = el("section", "bh-hero");
|
|
196
|
+
h.append(bhOrbit());
|
|
197
|
+
const eb = el("div", "bh-eyebrow");
|
|
198
|
+
eb.append(document.createTextNode(t("behaviour.portrait", { window: d.window || bhWindow })));
|
|
199
|
+
const mc = chip("habits: " + d.mode);
|
|
200
|
+
mc.dataset.mode = d.mode;
|
|
201
|
+
eb.append(mc);
|
|
202
|
+
h.append(eb);
|
|
203
|
+
if (!d.proposal && !(d.counts || {}).proposed && (d.rows || []).length) h.append(el("div", "note", t("behaviour.noProposal")));
|
|
204
|
+
const ul = el("ul", "bh-portrait");
|
|
205
|
+
(d.portrait || []).forEach((p, i) => {
|
|
206
|
+
const li = el("li");
|
|
207
|
+
li.style.animationDelay = 120 + i * 110 + "ms";
|
|
208
|
+
li.append(el("span", "bh-k mono", p.k), el("span", null, p.line));
|
|
209
|
+
ul.append(li);
|
|
210
|
+
});
|
|
211
|
+
if (!(d.portrait || []).length) h.append(empty(t("behaviour.emptyTitle"), t("behaviour.emptyHint")));
|
|
212
|
+
else h.append(ul);
|
|
213
|
+
const T = d.tiles || {};
|
|
214
|
+
const tiles = el("div", "bh-tiles");
|
|
215
|
+
tiles.append(
|
|
216
|
+
bhTile(t("behaviour.tile.runs"), T.runs, null, t("behaviour.tile.runsNote", { n: d.traces || 0 })),
|
|
217
|
+
bhTile(t("behaviour.tile.answers"), T.answers, null, t("behaviour.tile.answersNote")),
|
|
218
|
+
bhTile(t("behaviour.tile.saved"), T.saved, T.saved_prev, t("behaviour.tile.savedNote")),
|
|
219
|
+
bhTile(t("behaviour.tile.qpr"), T.qpr, T.qpr_prev, t("behaviour.tile.qprNote"))
|
|
220
|
+
);
|
|
221
|
+
h.append(tiles);
|
|
222
|
+
return h;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// The orbit turns ONCE, slowly, and rests (WCAG 2.2.2). Reduced motion
|
|
226
|
+
// removes the satellites in 04-motion.css.
|
|
227
|
+
function bhOrbit() {
|
|
228
|
+
const s = bhSv("svg", { class: "bh-orbit", viewBox: "0 0 260 260", "aria-hidden": "true" });
|
|
229
|
+
for (const r of [50, 85, 120]) s.append(bhSv("circle", { cx: 130, cy: 130, r }));
|
|
230
|
+
s.append(bhSv("circle", { class: "sat", cx: 130, cy: 45, r: 4 }));
|
|
231
|
+
s.append(bhSv("circle", { class: "sat sat-b", cx: 215, cy: 130, r: 3 }));
|
|
232
|
+
return s;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function bhTile(label, value, prev, note) {
|
|
236
|
+
const tile = el("div", "bh-tile");
|
|
237
|
+
tile.append(el("div", "bh-tile-l", label));
|
|
238
|
+
const v = el("div", "bh-tile-v");
|
|
239
|
+
const num = el("span", null, "—");
|
|
240
|
+
v.append(num);
|
|
241
|
+
if (prev !== null && prev !== undefined && value !== null && value !== undefined)
|
|
242
|
+
v.append(el("small", null, t("behaviour.tile.prev", { n: prev })));
|
|
243
|
+
tile.append(v);
|
|
244
|
+
if (note) tile.append(el("div", "bh-tile-n", note));
|
|
245
|
+
bhCount(num, value);
|
|
246
|
+
return tile;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Numbers count up, finitely, and REST on the CLI's own value. Reduced motion
|
|
250
|
+
// and a repaint skip the count and write the value at once.
|
|
251
|
+
function bhCount(node, to) {
|
|
252
|
+
if (to === null || to === undefined) return;
|
|
253
|
+
const final = String(to);
|
|
254
|
+
if (bhPainted || bhStill() || typeof to !== "number") {
|
|
255
|
+
node.textContent = final;
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const dec = Number.isInteger(to) ? 0 : 1;
|
|
259
|
+
const t0 = performance.now();
|
|
260
|
+
const tick = (now) => {
|
|
261
|
+
const k = Math.min(1, (now - t0) / 900);
|
|
262
|
+
const e = 1 - Math.pow(1 - k, 3);
|
|
263
|
+
node.textContent = k < 1 ? (to * e).toFixed(dec) : final;
|
|
264
|
+
if (k < 1) requestAnimationFrame(tick);
|
|
265
|
+
};
|
|
266
|
+
requestAnimationFrame(tick);
|
|
267
|
+
}
|
|
268
|
+
function bhStill() {
|
|
269
|
+
return window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── the tabs ────────────────────────────────────────────────────────────────
|
|
273
|
+
function bhTabs(ctx) {
|
|
274
|
+
const d = ctx.d;
|
|
275
|
+
const bar = el("div", "bh-tabs");
|
|
276
|
+
bar.setAttribute("role", "tablist");
|
|
277
|
+
bar.setAttribute("aria-label", t("behaviour.tabsLabel"));
|
|
278
|
+
const ink = el("span", "bh-tab-ink");
|
|
279
|
+
const panel = el("div", "stack bh-tabpanel");
|
|
280
|
+
panel.setAttribute("role", "tabpanel");
|
|
281
|
+
const label = BH_TAB_LABEL();
|
|
282
|
+
const g = (ctx.gotchas && ctx.gotchas.panel) || null;
|
|
283
|
+
const counts = {
|
|
284
|
+
habits: (d.rows || []).length,
|
|
285
|
+
gotchas: g ? g.rows.length : null,
|
|
286
|
+
log: ctx.log && ctx.log.events ? ctx.log.events.length : null,
|
|
287
|
+
};
|
|
288
|
+
const btns = [];
|
|
289
|
+
for (const id of BH_TABS) {
|
|
290
|
+
const b = el("button", "bh-tab");
|
|
291
|
+
b.type = "button";
|
|
292
|
+
b.id = "bh-tab-" + id;
|
|
293
|
+
b.setAttribute("role", "tab");
|
|
294
|
+
b.setAttribute("aria-controls", "bh-tabpanel");
|
|
295
|
+
b.append(document.createTextNode(label[id]));
|
|
296
|
+
if (counts[id] !== null && counts[id] !== undefined) {
|
|
297
|
+
const n = el("span", "bh-count", String(counts[id]));
|
|
298
|
+
if (id === "habits" && (d.counts || {}).proposed > 0) n.classList.add("hot");
|
|
299
|
+
b.append(n);
|
|
300
|
+
}
|
|
301
|
+
b.addEventListener("click", () => select(id, false));
|
|
302
|
+
b.addEventListener("keydown", (e) => {
|
|
303
|
+
const i = BH_TABS.indexOf(id);
|
|
304
|
+
const to =
|
|
305
|
+
e.key === "ArrowRight" ? BH_TABS[(i + 1) % BH_TABS.length]
|
|
306
|
+
: e.key === "ArrowLeft" ? BH_TABS[(i - 1 + BH_TABS.length) % BH_TABS.length]
|
|
307
|
+
: e.key === "Home" ? BH_TABS[0]
|
|
308
|
+
: e.key === "End" ? BH_TABS[BH_TABS.length - 1]
|
|
309
|
+
: null;
|
|
310
|
+
if (!to) return;
|
|
311
|
+
e.preventDefault();
|
|
312
|
+
select(to, true);
|
|
313
|
+
});
|
|
314
|
+
btns.push(b);
|
|
315
|
+
bar.append(b);
|
|
316
|
+
}
|
|
317
|
+
bar.append(ink);
|
|
318
|
+
panel.id = "bh-tabpanel";
|
|
319
|
+
function moveInk() {
|
|
320
|
+
const a = bar.querySelector('[aria-selected="true"]');
|
|
321
|
+
if (!a) return;
|
|
322
|
+
ink.style.left = a.offsetLeft + "px";
|
|
323
|
+
ink.style.width = a.offsetWidth + "px";
|
|
324
|
+
}
|
|
325
|
+
function select(id, focus) {
|
|
326
|
+
bhTab = id;
|
|
327
|
+
for (const b of btns) {
|
|
328
|
+
const on = b.id === "bh-tab-" + id;
|
|
329
|
+
b.setAttribute("aria-selected", String(on));
|
|
330
|
+
b.tabIndex = on ? 0 : -1;
|
|
331
|
+
if (on && focus) b.focus();
|
|
332
|
+
}
|
|
333
|
+
panel.setAttribute("aria-labelledby", "bh-tab-" + id);
|
|
334
|
+
panel.replaceChildren();
|
|
335
|
+
({ habits: bhHabits, rhythm: bhRhythm, gotchas: bhGotchas, quality: bhQuality, log: bhLog })[id](panel, ctx);
|
|
336
|
+
requestAnimationFrame(moveInk);
|
|
337
|
+
}
|
|
338
|
+
return { bar, panel, fill: () => select(BH_TABS.includes(bhTab) ? bhTab : "habits", false) };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ── Habits ──────────────────────────────────────────────────────────────────
|
|
342
|
+
function bhHabits(slot, ctx) {
|
|
343
|
+
const d = ctx.d;
|
|
344
|
+
const rows = d.rows || [];
|
|
345
|
+
if ((d.next_run || []).length) {
|
|
346
|
+
const nr = card(t("behaviour.nextRun"), el("span", "note", t("behaviour.nextRunNote")));
|
|
347
|
+
const list = el("ul", "bh-next");
|
|
348
|
+
for (const n of d.next_run) {
|
|
349
|
+
const li = el("li");
|
|
350
|
+
li.append(el("span", "bh-lane mono", n.lane), el("span", null, n.line));
|
|
351
|
+
list.append(li);
|
|
352
|
+
}
|
|
353
|
+
nr.append(list);
|
|
354
|
+
slot.append(nr);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const legend = card(t("behaviour.legendTitle"));
|
|
358
|
+
const lg = el("div", "bh-legend");
|
|
359
|
+
const sh = BH_STATE_HELP();
|
|
360
|
+
for (const s of d.state_words || []) {
|
|
361
|
+
const w = el("span", "note");
|
|
362
|
+
w.append(bhStateChip(s), document.createTextNode(" " + (sh[s] || "")));
|
|
363
|
+
lg.append(w);
|
|
364
|
+
}
|
|
365
|
+
legend.append(lg);
|
|
366
|
+
const cl = el("div", "bh-legend");
|
|
367
|
+
const ch = BH_CLASS_HELP();
|
|
368
|
+
for (const c of d.classes || []) {
|
|
369
|
+
const w = el("span", "note");
|
|
370
|
+
const tag = el("span", "bh-cls", c);
|
|
371
|
+
tag.dataset.cls = c;
|
|
372
|
+
w.append(tag, document.createTextNode(" " + (ch[c] || "")));
|
|
373
|
+
cl.append(w);
|
|
374
|
+
}
|
|
375
|
+
legend.append(cl);
|
|
376
|
+
slot.append(legend);
|
|
377
|
+
|
|
378
|
+
if (!rows.length) {
|
|
379
|
+
const e = card(null);
|
|
380
|
+
e.append(empty(t("behaviour.emptyTitle"), t("behaviour.emptyHint")));
|
|
381
|
+
slot.append(e);
|
|
382
|
+
} else {
|
|
383
|
+
// The CLI sends the rows in the order that needs you first; the panel does
|
|
384
|
+
// not sort them.
|
|
385
|
+
const grid = el("div", "bh-habits");
|
|
386
|
+
rows.forEach((r, i) => {
|
|
387
|
+
const c = bhHabitCard(r, ctx);
|
|
388
|
+
c.style.animationDelay = Math.min(i, 12) * 55 + "ms";
|
|
389
|
+
grid.append(c);
|
|
390
|
+
});
|
|
391
|
+
slot.append(grid);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (ctx.states && (ctx.states.history || []).length) {
|
|
395
|
+
const list = el("div", "bh-history");
|
|
396
|
+
for (const h of ctx.states.history) {
|
|
397
|
+
const row = el("div", "bh-hist-row");
|
|
398
|
+
row.append(el("span", "mono bh-faint", new Date(h.at).toLocaleString()), el("span", "mono", h.id), el("span", null, `${h.from} → ${h.to}`));
|
|
399
|
+
const by = chip(h.by);
|
|
400
|
+
by.dataset.by = h.by;
|
|
401
|
+
row.append(by);
|
|
402
|
+
if (h.inverse) row.append(bhCmd(h.inverse));
|
|
403
|
+
list.append(row);
|
|
404
|
+
}
|
|
405
|
+
slot.append(collapsible({ title: t("behaviour.history"), count: String(ctx.states.history.length), collapsed: true, desc: t("behaviour.historyNote"), content: list }));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function bhStateChip(s) {
|
|
410
|
+
const c = chip(s);
|
|
411
|
+
c.dataset.state = s;
|
|
412
|
+
return c;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function bhHabitCard(r, ctx) {
|
|
416
|
+
const c = el("article", "bh-habit");
|
|
417
|
+
c.dataset.state = r.state;
|
|
418
|
+
c.dataset.habit = r.id;
|
|
419
|
+
c.tabIndex = -1;
|
|
420
|
+
const top = el("div", "bh-habit-top");
|
|
421
|
+
for (const l of r.lanes || []) top.append(el("span", "bh-lane mono", l));
|
|
422
|
+
top.append(el("span", "bh-qid mono", r.qid));
|
|
423
|
+
const cls = el("span", "bh-cls", r.effective_class);
|
|
424
|
+
cls.dataset.cls = r.effective_class;
|
|
425
|
+
top.append(cls, bhStateChip(r.state));
|
|
426
|
+
c.append(top);
|
|
427
|
+
|
|
428
|
+
const bodyRow = el("div", "bh-habit-body");
|
|
429
|
+
bodyRow.append(bhRing(r.share, r.state));
|
|
430
|
+
const txt = el("div");
|
|
431
|
+
txt.append(el("h3", null, r.title));
|
|
432
|
+
if (r.line) txt.append(el("div", "bh-line", r.line));
|
|
433
|
+
bodyRow.append(txt);
|
|
434
|
+
c.append(bodyRow);
|
|
435
|
+
|
|
436
|
+
const opts = r.options || [];
|
|
437
|
+
const total = opts.reduce((s, o) => s + o.n, 0);
|
|
438
|
+
if (total) {
|
|
439
|
+
const dist = el("div", "bh-dist");
|
|
440
|
+
dist.setAttribute("role", "img");
|
|
441
|
+
dist.setAttribute("aria-label", opts.map((o) => `${o.label} ${o.n}`).join(", "));
|
|
442
|
+
const leg = el("div", "bh-dist-legend");
|
|
443
|
+
opts.forEach((o, i) => {
|
|
444
|
+
if (o.n) {
|
|
445
|
+
const s = el("span", "bh-grow-x" + (i === 0 ? " is-top" : ""));
|
|
446
|
+
s.style.flex = String(o.n);
|
|
447
|
+
s.style.animationDelay = 250 + i * 120 + "ms";
|
|
448
|
+
dist.append(s);
|
|
449
|
+
}
|
|
450
|
+
const l = el("span");
|
|
451
|
+
l.append(el("i", i === 0 ? "is-top" : null), document.createTextNode(o.label + " "), el("span", "mono", String(o.n)));
|
|
452
|
+
leg.append(l);
|
|
453
|
+
});
|
|
454
|
+
c.append(dist, leg);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const meta = el("div", "bh-habit-meta");
|
|
458
|
+
if (r.rule) meta.append(bhMeta(t("behaviour.field.rule"), r.rule));
|
|
459
|
+
meta.append(bhMeta(t("behaviour.field.context"), r.context || t("behaviour.anyContext")));
|
|
460
|
+
if ((r.streak || []).length) meta.append(bhStreak(r.streak));
|
|
461
|
+
c.append(meta);
|
|
462
|
+
if (r.why) c.append(el("div", "note", r.why));
|
|
463
|
+
if (r.effect) c.append(el("div", "bh-why", r.effect));
|
|
464
|
+
c.append(bhActions(r, ctx));
|
|
465
|
+
return c;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function bhMeta(label, value) {
|
|
469
|
+
const s = el("span");
|
|
470
|
+
s.append(document.createTextNode(label + " "), el("b", null, value));
|
|
471
|
+
return s;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// The 10-answer streak uses SHAPES, never colour alone (WCAG 1.4.1):
|
|
475
|
+
// ● the top choice · ○ another choice · ◐ Enter on a pre-selected option.
|
|
476
|
+
function bhStreak(streak) {
|
|
477
|
+
const st = el("span", "bh-streak");
|
|
478
|
+
const top = streak.filter((x) => x === 1).length;
|
|
479
|
+
const pre = streak.filter((x) => x > 0 && x < 1).length;
|
|
480
|
+
st.setAttribute("role", "img");
|
|
481
|
+
st.setAttribute("aria-label", t("behaviour.streakLabel", { n: streak.length, top, pre }));
|
|
482
|
+
for (const x of streak) st.append(el("i", x ? "on" : null, x === 1 ? "●" : x > 0 ? "◐" : "○"));
|
|
483
|
+
return st;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function bhRing(share, state) {
|
|
487
|
+
const R = 26;
|
|
488
|
+
const C = 2 * Math.PI * R;
|
|
489
|
+
const pct = Math.round((share || 0) * 100);
|
|
490
|
+
const s = bhSv("svg", { class: "bh-ring", viewBox: "0 0 64 64", role: "img", "aria-label": t("behaviour.ringLabel", { pct }) });
|
|
491
|
+
s.dataset.state = state;
|
|
492
|
+
s.append(bhSv("circle", { class: "bg", cx: 32, cy: 32, r: R }));
|
|
493
|
+
const fg = bhSv("circle", { class: "fg", cx: 32, cy: 32, r: R, "stroke-dasharray": C, "stroke-dashoffset": bhPainted ? C * (1 - (share || 0)) : C });
|
|
494
|
+
s.append(fg);
|
|
495
|
+
const tx = bhSv("text", { x: 32, y: 30 });
|
|
496
|
+
tx.textContent = pct + "%";
|
|
497
|
+
const t2 = bhSv("text", { x: 32, y: 44, class: "s" });
|
|
498
|
+
t2.textContent = t("behaviour.ringTop");
|
|
499
|
+
s.append(tx, t2);
|
|
500
|
+
// A transition in 04-motion.css, so the reduced-motion cap makes it instant.
|
|
501
|
+
requestAnimationFrame(() => requestAnimationFrame(() => fg.setAttribute("stroke-dashoffset", String(C * (1 - (share || 0))))));
|
|
502
|
+
return s;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// The actions come from `commands{}` — the CLI decides which exist. The
|
|
506
|
+
// `manual` command (a `never` habit) gets NO button, on purpose: a safety
|
|
507
|
+
// check is switched off by a person typing it.
|
|
508
|
+
function bhActions(r, ctx) {
|
|
509
|
+
const cm = r.commands || {};
|
|
510
|
+
const wrap = el("div", "bh-actions-wrap");
|
|
511
|
+
const act = el("div", "row-actions");
|
|
512
|
+
const B = (labelText, cls, route, body, command, title, what) => {
|
|
513
|
+
const b = el("button", "btn btn-sm " + cls, labelText);
|
|
514
|
+
b.type = "button";
|
|
515
|
+
b.addEventListener("click", () =>
|
|
516
|
+
bhConfirm({ title, what, command, go: () => bhWrite(route, body, r.id, ctx, t("behaviour.done", { id: r.id, command })) })
|
|
517
|
+
);
|
|
518
|
+
return b;
|
|
519
|
+
};
|
|
520
|
+
if (cm.accept) {
|
|
521
|
+
act.append(
|
|
522
|
+
B(t("behaviour.act.accept"), "btn-primary", "/api/habit/accept", { id: r.id }, cm.accept, t("behaviour.act.acceptTitle", { id: r.id }), t("behaviour.act.acceptWhat")),
|
|
523
|
+
B(t("behaviour.act.decline"), "btn-ghost", "/api/habit/decline", { id: r.id }, cm.decline, t("behaviour.act.declineTitle"), t("behaviour.act.declineWhat")),
|
|
524
|
+
B(t("behaviour.act.never"), "btn-ghost", "/api/habit/decline", { id: r.id, never: true }, cm.never, t("behaviour.act.neverTitle"), t("behaviour.act.neverWhat"))
|
|
525
|
+
);
|
|
526
|
+
wrap.append(act, bhCmd(cm.accept));
|
|
527
|
+
} else if (cm.forget) {
|
|
528
|
+
act.append(B(t("behaviour.act.forget"), "", "/api/habit/forget", { id: r.id }, cm.forget, t("behaviour.act.forgetTitle", { id: r.id }), t("behaviour.act.forgetWhat")));
|
|
529
|
+
wrap.append(act, bhCmd(cm.forget));
|
|
530
|
+
} else if (cm.reset) {
|
|
531
|
+
act.append(B(t("behaviour.act.reset"), "", "/api/habit/reset", { id: r.id }, cm.reset, t("behaviour.act.resetTitle"), t("behaviour.act.resetWhat")));
|
|
532
|
+
wrap.append(act, bhCmd(cm.reset));
|
|
533
|
+
} else if (cm.manual) {
|
|
534
|
+
wrap.append(el("div", "note", t("behaviour.act.manual")), bhCmd(cm.manual));
|
|
535
|
+
}
|
|
536
|
+
return wrap;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// THE ONE CONFIRMATION every write goes through: it names the command, says
|
|
540
|
+
// what changes, and has Cancel / Run it. `modal()` traps nothing it does not
|
|
541
|
+
// own, closes on Escape, and `closeModal` is its single exit.
|
|
542
|
+
function bhConfirm({ title, what, command, go }) {
|
|
543
|
+
const b = frag();
|
|
544
|
+
if (what) b.append(el("p", null, what));
|
|
545
|
+
b.append(el("div", "note", t("behaviour.confirmNote")));
|
|
546
|
+
b.append(el("pre", "cmd", command));
|
|
547
|
+
const opener = document.activeElement;
|
|
548
|
+
modal({
|
|
549
|
+
title,
|
|
550
|
+
body: b,
|
|
551
|
+
actions: [
|
|
552
|
+
{ label: t("common.cancel"), cls: "btn-ghost", onClick: (close) => { close(); if (opener && opener.focus) opener.focus(); } },
|
|
553
|
+
{ label: t("behaviour.runIt"), cls: "btn-primary", onClick: (close) => { close(); go(); } },
|
|
554
|
+
],
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// A write re-fetches `/api/habits` and repaints QUIETLY: nothing arrives again,
|
|
559
|
+
// and focus moves to the card that changed. The undo is on that card, never
|
|
560
|
+
// in a toast that disappears (WCAG 2.2.1).
|
|
561
|
+
async function bhWrite(route, body, id, ctx, said) {
|
|
562
|
+
let r;
|
|
563
|
+
try {
|
|
564
|
+
r = await post(route, body);
|
|
565
|
+
} catch (e) {
|
|
566
|
+
toast(t("common.writeFail"), "bad", String(e.message || e));
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
toast(r.ok ? said : t("common.writeFail"), r.ok ? "ok" : "bad", r.output);
|
|
570
|
+
if (r.ok) bhSay(said + " " + t("behaviour.undoWhere"));
|
|
571
|
+
bhLoad(ctx.body, ctx.right, id);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function bhSay(text) {
|
|
575
|
+
const live = $("#bh-live");
|
|
576
|
+
if (live) live.textContent = text;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// ── Rhythm ──────────────────────────────────────────────────────────────────
|
|
580
|
+
function bhRhythm(slot, ctx) {
|
|
581
|
+
const rh = ctx.d.rhythm || {};
|
|
582
|
+
const g = el("div", "grid grid-2");
|
|
583
|
+
|
|
584
|
+
const hc = card(t("behaviour.rhythm.when"), el("span", "note", t("behaviour.rhythm.whenNote")));
|
|
585
|
+
const heat = rh.heat || [];
|
|
586
|
+
const flat = heat.flat();
|
|
587
|
+
const max = Math.max(0, ...flat);
|
|
588
|
+
const days = [t("behaviour.day.mon"), t("behaviour.day.tue"), t("behaviour.day.wed"), t("behaviour.day.thu"), t("behaviour.day.fri"), t("behaviour.day.sat"), t("behaviour.day.sun")];
|
|
589
|
+
const grid = el("div", "bh-heat");
|
|
590
|
+
grid.setAttribute("role", "img");
|
|
591
|
+
grid.setAttribute("aria-label", t("behaviour.rhythm.heatLabel", { n: flat.reduce((a, b) => a + b, 0) }));
|
|
592
|
+
grid.append(el("span"));
|
|
593
|
+
for (let h = 0; h < 24; h++) grid.append(el("span", "h", h % 3 === 0 ? String(h).padStart(2, "0") : ""));
|
|
594
|
+
heat.forEach((row, di) => {
|
|
595
|
+
grid.append(el("span", "d", days[di] || ""));
|
|
596
|
+
row.forEach((v, h) => {
|
|
597
|
+
const cell = el("span", "c");
|
|
598
|
+
cell.title = t("behaviour.rhythm.cell", { day: days[di] || "", hour: String(h).padStart(2, "0"), n: v });
|
|
599
|
+
// k = the CLI's cell value over the max; the colour is a token mix.
|
|
600
|
+
if (v && max) cell.style.background = `color-mix(in srgb, var(--accent) ${Math.round(18 + (82 * v) / max)}%, var(--surface-3))`;
|
|
601
|
+
cell.style.animationDelay = Math.min(h * 18 + di * 26, 700) + "ms";
|
|
602
|
+
grid.append(cell);
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
hc.append(grid);
|
|
606
|
+
g.append(hc);
|
|
607
|
+
|
|
608
|
+
const lanes = rh.lanes || [];
|
|
609
|
+
const tot = lanes.reduce((s, l) => s + l.n, 0);
|
|
610
|
+
const mc = card(t("behaviour.rhythm.lanes"), el("span", "note", tn(tot, "behaviour.rhythm.runs")));
|
|
611
|
+
if (tot) {
|
|
612
|
+
const mix = el("div", "bh-mix");
|
|
613
|
+
mix.setAttribute("role", "img");
|
|
614
|
+
mix.setAttribute("aria-label", lanes.map((l) => `${l.lane} ${l.n}`).join(", "));
|
|
615
|
+
const ml = el("div", "bh-mix-legend");
|
|
616
|
+
lanes.forEach((l, i) => {
|
|
617
|
+
const s = el("span", "bh-grow-x", l.n / tot > 0.09 ? Math.round((100 * l.n) / tot) + "%" : "");
|
|
618
|
+
s.style.flex = String(l.n);
|
|
619
|
+
s.style.background = `var(--s${(i % 7) + 1})`;
|
|
620
|
+
s.style.animationDelay = i * 90 + "ms";
|
|
621
|
+
mix.append(s);
|
|
622
|
+
const row = el("div");
|
|
623
|
+
const sw = el("i");
|
|
624
|
+
sw.style.background = `var(--s${(i % 7) + 1})`;
|
|
625
|
+
row.append(sw, el("span", "mono", l.lane), el("span", "mono bh-dim", String(l.n)));
|
|
626
|
+
ml.append(row);
|
|
627
|
+
});
|
|
628
|
+
mc.append(mix, ml);
|
|
629
|
+
} else mc.append(empty(t("behaviour.emptyTitle"), t("behaviour.emptyHint")));
|
|
630
|
+
g.append(mc);
|
|
631
|
+
slot.append(g);
|
|
632
|
+
|
|
633
|
+
const g2 = el("div", "grid grid-2");
|
|
634
|
+
const qpr = rh.qpr || [];
|
|
635
|
+
const qc = card(t("behaviour.rhythm.qpr"));
|
|
636
|
+
const known = qpr.filter((v) => v !== null && v !== undefined);
|
|
637
|
+
if (known.length >= 2) {
|
|
638
|
+
qc.append(bhLine(qpr, { kind: "accent", fmt: (v) => String(v), label: t("behaviour.rhythm.qprLabel", { from: known[0], to: known[known.length - 1], n: qpr.length }) }));
|
|
639
|
+
qc.append(el("div", "note", t("behaviour.rhythm.qprNote")));
|
|
640
|
+
} else qc.append(empty(t("behaviour.emptyTitle"), t("behaviour.emptyHint")));
|
|
641
|
+
g2.append(qc);
|
|
642
|
+
|
|
643
|
+
const wc = card(t("behaviour.rhythm.how"));
|
|
644
|
+
const how = rh.how || [];
|
|
645
|
+
const keys = rh.by || [];
|
|
646
|
+
const colMax = Math.max(0, ...how.map((w) => keys.reduce((s, k) => s + (w[k] || 0), 0)));
|
|
647
|
+
if (colMax) {
|
|
648
|
+
const bars = el("div", "bh-how");
|
|
649
|
+
bars.setAttribute("role", "img");
|
|
650
|
+
bars.setAttribute("aria-label", t("behaviour.rhythm.howLabel", { n: how.length }));
|
|
651
|
+
how.forEach((w, i) => {
|
|
652
|
+
const col = el("div", "bh-how-col");
|
|
653
|
+
col.title = keys.map((k) => `${k} ${w[k] || 0}`).join(" · ");
|
|
654
|
+
keys.forEach((k, j) => {
|
|
655
|
+
if (!w[k]) return;
|
|
656
|
+
const s = el("span", "bh-grow-y");
|
|
657
|
+
s.dataset.by = k;
|
|
658
|
+
s.style.height = (100 * w[k]) / colMax + "%";
|
|
659
|
+
s.style.animationDelay = i * 45 + j * 30 + "ms";
|
|
660
|
+
col.append(s);
|
|
661
|
+
});
|
|
662
|
+
bars.append(col);
|
|
663
|
+
});
|
|
664
|
+
wc.append(bars);
|
|
665
|
+
const lg = el("div", "bh-dist-legend");
|
|
666
|
+
const bh = BH_BY_HELP();
|
|
667
|
+
for (const k of keys) {
|
|
668
|
+
const l = el("span");
|
|
669
|
+
const sw = el("i");
|
|
670
|
+
sw.dataset.by = k;
|
|
671
|
+
l.append(sw, el("span", "mono", k), document.createTextNode(" — " + (bh[k] || "")));
|
|
672
|
+
lg.append(l);
|
|
673
|
+
}
|
|
674
|
+
wc.append(lg);
|
|
675
|
+
} else wc.append(empty(t("behaviour.emptyTitle"), t("behaviour.emptyHint")));
|
|
676
|
+
g2.append(wc);
|
|
677
|
+
slot.append(g2);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// A line chart that draws itself (stroke-dash, finite). The first and last
|
|
681
|
+
// values are labelled; a null week keeps its slot and draws no point.
|
|
682
|
+
function bhLine(vals, o) {
|
|
683
|
+
const W = 520;
|
|
684
|
+
const H = 150;
|
|
685
|
+
const P = 26;
|
|
686
|
+
const pts = vals.map((v, i) => [i, v]).filter((p) => p[1] !== null && p[1] !== undefined);
|
|
687
|
+
const ys = pts.map((p) => p[1]).concat(o.target !== undefined ? [o.target] : []);
|
|
688
|
+
// The scale is drawing, not data: it frames the CLI's numbers with a margin.
|
|
689
|
+
const lo = Math.min(...ys);
|
|
690
|
+
const hi = Math.max(...ys);
|
|
691
|
+
const pad = (hi - lo) * 0.3 || hi * 0.2 || 1;
|
|
692
|
+
const min = Math.max(0, lo - pad);
|
|
693
|
+
const max = hi + pad;
|
|
694
|
+
const x = (i) => P + ((W - 2 * P) * i) / Math.max(1, vals.length - 1);
|
|
695
|
+
const y = (v) => H - P + 4 - ((H - 2 * P) * (v - min)) / (max - min || 1);
|
|
696
|
+
const s = bhSv("svg", { class: "bh-chart", viewBox: `0 0 ${W} ${H}`, role: "img", "aria-label": o.label });
|
|
697
|
+
s.dataset.kind = o.kind;
|
|
698
|
+
for (let k = 0; k <= 4; k++) {
|
|
699
|
+
const yy = P - 4 + ((H - 2 * P) * k) / 4;
|
|
700
|
+
s.append(bhSv("line", { class: "grid-l", x1: P, x2: W - P, y1: yy, y2: yy }));
|
|
701
|
+
}
|
|
702
|
+
const dPath = pts.map((p, i) => (i ? "L" : "M") + x(p[0]).toFixed(1) + " " + y(p[1]).toFixed(1)).join(" ");
|
|
703
|
+
const ln = bhSv("path", { class: "ln" + (bhPainted ? "" : " bh-draw"), d: dPath });
|
|
704
|
+
s.append(ln);
|
|
705
|
+
requestAnimationFrame(() => {
|
|
706
|
+
try {
|
|
707
|
+
ln.style.setProperty("--len", String(ln.getTotalLength()));
|
|
708
|
+
} catch (_) {}
|
|
709
|
+
});
|
|
710
|
+
[pts[0], pts[pts.length - 1]].forEach((p, i) => {
|
|
711
|
+
s.append(bhSv("circle", { class: "pt bh-fade-late", cx: x(p[0]), cy: y(p[1]), r: 4 }));
|
|
712
|
+
const tx = bhSv("text", { class: "bh-fade-late", x: x(p[0]) + (i ? -6 : 6), y: y(p[1]) - 9, "text-anchor": i ? "end" : "start" });
|
|
713
|
+
tx.textContent = o.fmt(p[1]);
|
|
714
|
+
s.append(tx);
|
|
715
|
+
});
|
|
716
|
+
if (o.target !== undefined && o.target !== null) {
|
|
717
|
+
const ty = y(o.target);
|
|
718
|
+
s.append(bhSv("line", { class: "target", x1: P, x2: W - P, y1: ty, y2: ty }));
|
|
719
|
+
const tt = bhSv("text", { x: W - P, y: ty - 5, "text-anchor": "end" });
|
|
720
|
+
tt.textContent = t("behaviour.target", { v: o.fmt(o.target) });
|
|
721
|
+
s.append(tt);
|
|
722
|
+
}
|
|
723
|
+
return s;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// ── Gotchas ─────────────────────────────────────────────────────────────────
|
|
727
|
+
function bhGotchas(slot, ctx) {
|
|
728
|
+
const G = ctx.gotchas;
|
|
729
|
+
const P = G && G.panel;
|
|
730
|
+
if (!P) {
|
|
731
|
+
const c = card(t("behaviour.g.title"));
|
|
732
|
+
c.append(empty(t("behaviour.g.none"), t("behaviour.g.noneHint")));
|
|
733
|
+
slot.append(c);
|
|
734
|
+
bhCardPreview(slot);
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const sync = el("div", "row-actions");
|
|
738
|
+
sync.append(el("span", "note", P.sync && P.sync.last_sync ? t("behaviour.g.lastSync", { at: new Date(P.sync.last_sync).toLocaleString() }) : t("behaviour.g.noSync")));
|
|
739
|
+
for (const s of (P.sync && P.sync.sources) || []) {
|
|
740
|
+
const c = chip(`${s.name} ${s.state}`);
|
|
741
|
+
c.dataset.sync = s.state;
|
|
742
|
+
sync.append(c);
|
|
743
|
+
}
|
|
744
|
+
const sc = card(t("behaviour.g.sources"), el("span", "note", t("behaviour.g.alwaysOn")));
|
|
745
|
+
const tot = (P.by_source || []).reduce((s, x) => s + x.n, 0);
|
|
746
|
+
if (tot) {
|
|
747
|
+
const bar = el("div", "bh-src-bar");
|
|
748
|
+
bar.setAttribute("role", "img");
|
|
749
|
+
bar.setAttribute("aria-label", P.by_source.map((s) => `${s.source} ${s.n}`).join(", "));
|
|
750
|
+
P.by_source.forEach((s, i) => {
|
|
751
|
+
const b = el("span", "bh-grow-x", s.n / tot > 0.08 ? `${s.source} ${s.n}` : "");
|
|
752
|
+
b.dataset.source = s.source;
|
|
753
|
+
b.style.flex = String(s.n);
|
|
754
|
+
b.style.animationDelay = i * 80 + "ms";
|
|
755
|
+
b.title = `${s.source}: ${s.n}`;
|
|
756
|
+
bar.append(b);
|
|
757
|
+
});
|
|
758
|
+
sc.append(bar);
|
|
759
|
+
}
|
|
760
|
+
const chips = el("div", "bh-legend");
|
|
761
|
+
for (const s of P.statuses || []) {
|
|
762
|
+
const c = chip(`${(P.status_counts || {})[s] || 0} ${s}`);
|
|
763
|
+
c.dataset.status = s;
|
|
764
|
+
chips.append(c);
|
|
765
|
+
}
|
|
766
|
+
sc.append(chips, el("div", "note", t("behaviour.g.statusNote")), sync);
|
|
767
|
+
slot.append(sc);
|
|
768
|
+
|
|
769
|
+
const g = el("div", "grid grid-2");
|
|
770
|
+
const lc = card(t("behaviour.g.list"), el("span", "note", t("behaviour.g.listNote")));
|
|
771
|
+
(P.rows || []).forEach((r, i) => {
|
|
772
|
+
const row = el("div", "bh-lesson");
|
|
773
|
+
row.style.animationDelay = Math.min(i, 12) * 45 + "ms";
|
|
774
|
+
row.append(el("div", "bh-lesson-id mono", r.id));
|
|
775
|
+
const mid = el("div");
|
|
776
|
+
mid.append(el("div", "bh-lesson-rule", r.fix && r.fix.charAt(0) !== "(" ? `${r.symptom} → ${r.fix}` : r.symptom || ""));
|
|
777
|
+
const sub = el("div", "bh-lesson-sub");
|
|
778
|
+
const src = el("span", "bh-src mono", r.source);
|
|
779
|
+
src.dataset.source = r.source;
|
|
780
|
+
sub.append(src);
|
|
781
|
+
if (r.rule) sub.append(el("span", "mono", r.rule));
|
|
782
|
+
if (r.scope) sub.append(el("span", "bh-scope mono", r.scope));
|
|
783
|
+
if (r.status_reason) sub.append(el("span", null, r.status_reason));
|
|
784
|
+
mid.append(sub);
|
|
785
|
+
row.append(mid);
|
|
786
|
+
const right = el("div", "bh-lesson-right");
|
|
787
|
+
const st = chip(r.status);
|
|
788
|
+
st.dataset.status = r.status;
|
|
789
|
+
right.append(st, bhSpark(r.weeks || []));
|
|
790
|
+
row.append(right);
|
|
791
|
+
lc.append(row);
|
|
792
|
+
});
|
|
793
|
+
g.append(lc);
|
|
794
|
+
slot.append(g);
|
|
795
|
+
bhCardPreview(g);
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function bhSpark(weeks) {
|
|
799
|
+
const sp = el("div", "bh-spark");
|
|
800
|
+
const mx = Math.max(1, ...weeks);
|
|
801
|
+
sp.setAttribute("role", "img");
|
|
802
|
+
sp.setAttribute("aria-label", t("behaviour.g.spark", { n: weeks.reduce((a, b) => a + b, 0), w: weeks.length }));
|
|
803
|
+
weeks.forEach((h, i) => {
|
|
804
|
+
const b = el("i", "bh-grow-y" + (h ? "" : " zero"));
|
|
805
|
+
b.style.height = (h ? 20 + (80 * h) / mx : 8) + "%";
|
|
806
|
+
b.style.animationDelay = 200 + i * 40 + "ms";
|
|
807
|
+
sp.append(b);
|
|
808
|
+
});
|
|
809
|
+
return sp;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// What the reviewer will see: the EXACT card `orc gotcha card` returns. It
|
|
813
|
+
// re-reads on input, debounced 300 ms. Zero matches is the CLI's own answer.
|
|
814
|
+
function bhCardPreview(host) {
|
|
815
|
+
const pc = card(t("behaviour.g.preview"), el("span", "note", t("behaviour.g.previewFree")));
|
|
816
|
+
pc.append(el("div", "note", t("behaviour.g.previewHint")));
|
|
817
|
+
const inp = el("input", "bh-input");
|
|
818
|
+
inp.type = "text";
|
|
819
|
+
inp.placeholder = "src/routes/export.js, client/src/i18n/en.json";
|
|
820
|
+
inp.setAttribute("aria-label", t("behaviour.g.previewLabel"));
|
|
821
|
+
const cmdBox = el("pre", "cmd", "orc gotcha card --files <paths> --json");
|
|
822
|
+
const out = el("pre", "bh-card-out", "");
|
|
823
|
+
out.setAttribute("aria-live", "polite");
|
|
824
|
+
let timer = null;
|
|
825
|
+
let seq = 0;
|
|
826
|
+
const paint = async () => {
|
|
827
|
+
const files = inp.value.split(",").map((s) => s.trim()).filter(Boolean).join(",");
|
|
828
|
+
if (!files) {
|
|
829
|
+
cmdBox.textContent = "orc gotcha card --files <paths> --json";
|
|
830
|
+
out.textContent = t("behaviour.g.previewEmpty");
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
cmdBox.textContent = `orc gotcha card --files ${files} --json`;
|
|
834
|
+
const mine = ++seq;
|
|
835
|
+
let r;
|
|
836
|
+
try {
|
|
837
|
+
r = await read("/api/gotcha/card?files=" + encodeURIComponent(files));
|
|
838
|
+
} catch (e) {
|
|
839
|
+
if (mine === seq) out.textContent = String(e.message || e);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
if (mine !== seq) return;
|
|
843
|
+
const c = r.data || {};
|
|
844
|
+
out.textContent = c.matched ? c.text : t("behaviour.g.noCard", { n: c.known || 0 });
|
|
845
|
+
};
|
|
846
|
+
inp.addEventListener("input", () => {
|
|
847
|
+
clearTimeout(timer);
|
|
848
|
+
timer = setTimeout(paint, 300);
|
|
849
|
+
});
|
|
850
|
+
pc.append(inp, cmdBox, out);
|
|
851
|
+
paint();
|
|
852
|
+
host.append(pc);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// ── Review quality ──────────────────────────────────────────────────────────
|
|
856
|
+
function bhQuality(slot, ctx) {
|
|
857
|
+
const Q = ctx.quality;
|
|
858
|
+
if (!Q) {
|
|
859
|
+
const c = card(t("behaviour.q.cats"));
|
|
860
|
+
c.append(empty(t("behaviour.q.none")));
|
|
861
|
+
slot.append(c);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
// Below the floor, every chart KEEPS its slot and shows the CLI's sentence.
|
|
865
|
+
// Not knowing is an answer.
|
|
866
|
+
const floor = (c) => c.append(el("div", "bh-floor", Q.floor_line || ""));
|
|
867
|
+
const g = el("div", "grid grid-2");
|
|
868
|
+
const cc = card(t("behaviour.q.cats"), el("span", "note", tn(Q.reviews || 0, "behaviour.q.reviews")));
|
|
869
|
+
if (Q.below_floor) floor(cc);
|
|
870
|
+
else {
|
|
871
|
+
const bands = Q.bands || {};
|
|
872
|
+
(Q.categories || []).forEach((c, i) => {
|
|
873
|
+
const row = el("div", "bh-cat");
|
|
874
|
+
const acc = c.acceptance;
|
|
875
|
+
const bar = el("div", "bh-cat-bar");
|
|
876
|
+
const s = el("span", "bh-grow-x");
|
|
877
|
+
if (acc !== null && acc !== undefined) {
|
|
878
|
+
s.style.width = Math.round(acc * 100) + "%";
|
|
879
|
+
// The band thresholds are the CLI's numbers (`bands{}`), not the panel's.
|
|
880
|
+
s.dataset.band = acc >= bands.ok ? "ok" : acc >= bands.warn ? "warn" : "bad";
|
|
881
|
+
}
|
|
882
|
+
s.style.animationDelay = i * 90 + "ms";
|
|
883
|
+
bar.append(s);
|
|
884
|
+
if (Q.target !== undefined) {
|
|
885
|
+
const tick = el("i", "tick");
|
|
886
|
+
tick.style.left = Q.target * 100 + "%";
|
|
887
|
+
tick.title = t("behaviour.target", { v: Math.round(Q.target * 100) + "%" });
|
|
888
|
+
bar.append(tick);
|
|
889
|
+
}
|
|
890
|
+
row.append(el("div", "bh-cat-name mono", c.category), bar, el("div", "bh-cat-num mono", `${acc === null ? "—" : Math.round(acc * 100) + "%"} · n ${c.findings}`));
|
|
891
|
+
cc.append(row);
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
g.append(cc);
|
|
895
|
+
|
|
896
|
+
const series = Q.series || [];
|
|
897
|
+
const tc = card(t("behaviour.q.trend"));
|
|
898
|
+
const acc = series.map((p) => p.acceptance);
|
|
899
|
+
if (Q.below_floor) floor(tc);
|
|
900
|
+
else if (acc.filter((v) => v !== null).length >= 2)
|
|
901
|
+
tc.append(bhLine(acc, { kind: "ok", fmt: (v) => Math.round(v * 100) + "%", target: Q.target, label: t("behaviour.q.trendLabel", { n: series.length }) }));
|
|
902
|
+
else tc.append(empty(t("behaviour.q.none")));
|
|
903
|
+
g.append(tc);
|
|
904
|
+
slot.append(g);
|
|
905
|
+
|
|
906
|
+
const g2 = el("div", "grid grid-2");
|
|
907
|
+
const nc = card(t("behaviour.q.p3"));
|
|
908
|
+
const p3 = series.map((p) => p.p3);
|
|
909
|
+
if (Q.below_floor) floor(nc);
|
|
910
|
+
else if (p3.length >= 2) {
|
|
911
|
+
nc.append(bhLine(p3, { kind: "warn", fmt: (v) => String(v), label: t("behaviour.q.p3Label", { n: series.length }) }));
|
|
912
|
+
nc.append(el("div", "note", t("behaviour.q.p3Note")));
|
|
913
|
+
} else nc.append(empty(t("behaviour.q.none")));
|
|
914
|
+
g2.append(nc);
|
|
915
|
+
|
|
916
|
+
const sups = (ctx.cands && ctx.cands.suppressions) || [];
|
|
917
|
+
const sp = card(t("behaviour.q.sups"), el("span", "note", tn(sups.length, "behaviour.q.waiting")));
|
|
918
|
+
if (!sups.length) sp.append(empty(t("behaviour.q.noSups")));
|
|
919
|
+
for (const c of sups) {
|
|
920
|
+
const row = el("div", "bh-lesson");
|
|
921
|
+
row.append(el("div", "bh-lesson-id mono", c.id));
|
|
922
|
+
const mid = el("div");
|
|
923
|
+
mid.append(el("div", "bh-lesson-rule", c.sig || c.rule || ""));
|
|
924
|
+
const sub = el("div", "bh-lesson-sub");
|
|
925
|
+
if (c.category) sub.append(el("span", "mono", c.category));
|
|
926
|
+
if (c.scope) sub.append(el("span", "bh-scope mono", c.scope));
|
|
927
|
+
sub.append(el("span", null, tn(c.disputed, "behaviour.q.disputed")));
|
|
928
|
+
mid.append(sub);
|
|
929
|
+
row.append(mid);
|
|
930
|
+
const cmd = "orc gotcha accept " + c.id;
|
|
931
|
+
const b = el("button", "btn btn-sm", t("behaviour.q.accept"));
|
|
932
|
+
b.type = "button";
|
|
933
|
+
b.addEventListener("click", () =>
|
|
934
|
+
bhConfirm({
|
|
935
|
+
title: t("behaviour.q.acceptTitle", { id: c.id }),
|
|
936
|
+
what: t("behaviour.q.acceptWhat"),
|
|
937
|
+
command: cmd,
|
|
938
|
+
go: async () => {
|
|
939
|
+
const r = await post("/api/gotcha/accept", { id: c.id });
|
|
940
|
+
toast(r.ok ? t("behaviour.done", { id: c.id, command: cmd }) : t("common.writeFail"), r.ok ? "ok" : "bad", r.output);
|
|
941
|
+
if (r.ok) bhSay(t("behaviour.done", { id: c.id, command: cmd }));
|
|
942
|
+
bhLoad(ctx.body, ctx.right);
|
|
943
|
+
},
|
|
944
|
+
})
|
|
945
|
+
);
|
|
946
|
+
row.append(b);
|
|
947
|
+
sp.append(row);
|
|
948
|
+
}
|
|
949
|
+
g2.append(sp);
|
|
950
|
+
slot.append(g2);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// ── Answer log ──────────────────────────────────────────────────────────────
|
|
954
|
+
function bhLog(slot, ctx) {
|
|
955
|
+
const c = card(t("behaviour.log.title"), el("span", "note", t("behaviour.log.from")));
|
|
956
|
+
const events = (ctx.log && ctx.log.events) || [];
|
|
957
|
+
if (!events.length) c.append(empty(t("behaviour.emptyTitle"), t("behaviour.emptyHint")));
|
|
958
|
+
const list = el("div", "bh-log");
|
|
959
|
+
events.forEach((e, i) => {
|
|
960
|
+
const row = el("div", "bh-log-row");
|
|
961
|
+
row.style.animationDelay = Math.min(i, 12) * 70 + "ms";
|
|
962
|
+
const q = el("span", "bh-log-q");
|
|
963
|
+
q.append(el("span", "mono", e.qid), document.createTextNode(" → "), el("b", null, e.chose));
|
|
964
|
+
const by = chip(e.by);
|
|
965
|
+
by.dataset.by = e.by;
|
|
966
|
+
row.append(el("span", "t mono", new Date(e.at).toLocaleString()), el("span", "ln mono", e.lane), q, by);
|
|
967
|
+
list.append(row);
|
|
968
|
+
});
|
|
969
|
+
c.append(list);
|
|
970
|
+
c.append(el("div", "note", t("behaviour.log.note")));
|
|
971
|
+
slot.append(c);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// ── Footer (every tab) ──────────────────────────────────────────────────────
|
|
975
|
+
function bhFooter(d, off) {
|
|
976
|
+
const f = el("div", "bh-foot");
|
|
977
|
+
f.append(el("span", "bh-lock", "🔒 " + t("behaviour.localOnly")));
|
|
978
|
+
for (const p of (d && d.files) || []) f.append(el("span", "mono", p));
|
|
979
|
+
const tail = el("span");
|
|
980
|
+
if (off) tail.append(document.createTextNode(t("behaviour.offCosts") + " "), el("span", "mono", "orc config set habits observe"));
|
|
981
|
+
else tail.append(document.createTextNode(t("behaviour.turnOff") + " "), el("span", "mono", "orc config set habits off"));
|
|
982
|
+
f.append(tail);
|
|
983
|
+
return f;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// ── the rail dot: the ONLY place outside this panel that shows habit state ──
|
|
987
|
+
// A proposal waiting (`counts.proposed > 0`) puts a dot on the rail link that
|
|
988
|
+
// pulses three times and stops.
|
|
989
|
+
function bhRailDot(d) {
|
|
990
|
+
const a = document.querySelector('#nav a[data-panel="behaviour"]');
|
|
991
|
+
if (!a) return;
|
|
992
|
+
const has = !!(d && d.counts && d.counts.proposed > 0);
|
|
993
|
+
let dot = a.querySelector(".bh-dot");
|
|
994
|
+
if (has && !dot) {
|
|
995
|
+
dot = el("span", "bh-dot");
|
|
996
|
+
dot.setAttribute("aria-label", t("behaviour.railDot"));
|
|
997
|
+
dot.setAttribute("role", "img");
|
|
998
|
+
a.append(dot);
|
|
999
|
+
} else if (!has && dot) dot.remove();
|
|
1000
|
+
}
|
|
1001
|
+
async function behaviourRailDot() {
|
|
1002
|
+
try {
|
|
1003
|
+
const r = await read("/api/habits");
|
|
1004
|
+
bhRailDot(r.data);
|
|
1005
|
+
} catch (_) {}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
function bhCmd(text) {
|
|
1009
|
+
const b = el("div", "bh-cmd");
|
|
1010
|
+
b.append(el("span", "mono", "$ " + text));
|
|
1011
|
+
const cp = el("button", "copy-btn", t("common.copy").toLowerCase());
|
|
1012
|
+
cp.type = "button";
|
|
1013
|
+
cp.addEventListener("click", () => copy(text, text));
|
|
1014
|
+
b.append(cp);
|
|
1015
|
+
return b;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function bhSv(tag, attrs) {
|
|
1019
|
+
const n = document.createElementNS(BH_SVG, tag);
|
|
1020
|
+
for (const k in attrs || {}) n.setAttribute(k, attrs[k]);
|
|
1021
|
+
return n;
|
|
1022
|
+
}
|