@azure-id/orc 1.9.1 → 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 +390 -0
- package/README-id.md +20 -36
- package/README.md +24 -34
- package/bin/build-agents.js +206 -109
- package/bin/cli.js +760 -62
- 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/onboarding-content.js +4 -4
- package/bin/pricing.json +207 -200
- package/bin/run-undo.js +398 -0
- package/bin/trace-write.js +657 -0
- package/bin/verify-contracts.js +527 -81
- package/bin/verify-package.js +58 -11
- 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 +249 -152
- package/bin/webui/css/06-responsive.css +203 -178
- package/bin/webui/css/panels/behaviour.css +205 -0
- package/bin/webui/css/panels/knowledge.css +563 -116
- package/bin/webui/fixtures/behaviour.js +532 -0
- package/bin/webui/fixtures/extra.js +2036 -2036
- package/bin/webui/fixtures/hookui.js +5 -5
- package/bin/webui/fixtures/index.js +34 -0
- package/bin/webui/fixtures/knowledge.js +7 -1
- package/bin/webui/fixtures/settings.js +305 -305
- package/bin/webui/fixtures/stats.js +16 -0
- package/bin/webui/i18n/en/behaviour.json +143 -0
- package/bin/webui/i18n/en/knowledge.json +117 -2
- 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/knowledge.json +117 -2
- 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/bin/webui/js/panels/knowledge.js +552 -7
- package/bin/webui/js/panels/overview.js +13 -4
- package/mock-run/INDEX.md +109 -107
- package/mock-run/context-combiner.md +100 -100
- package/mock-run/gotcha-import.md +118 -0
- package/mock-run/habits.md +129 -0
- package/mock-run/orc-budget.md +534 -534
- package/mock-run/orc-challenge-council.md +262 -262
- package/mock-run/orc-quick.md +6 -1
- package/mock-run/orc-ultra.md +103 -103
- package/package.json +1 -1
- package/templates/agents/MODEL-MAPPING.md +49 -49
- package/templates/agents/orc-advisor-opus-5-xhigh.md +50 -56
- package/templates/agents/orc-analyze-mini-opus-5-med.md +55 -60
- package/templates/agents/orc-analyze-mini-sonnet-5-high.md +55 -58
- package/templates/agents/orc-challenge-advisor-opus-5-med.md +75 -75
- package/templates/agents/orc-challenge-contrarian-opus-5-high.md +110 -110
- package/templates/agents/orc-challenge-executor-opus-5-med.md +114 -114
- package/templates/agents/orc-challenge-expansionist-opus-5-med.md +112 -112
- package/templates/agents/orc-challenge-judge-opus-5-high.md +132 -132
- package/templates/agents/orc-challenge-outsider-opus-5-low.md +109 -109
- package/templates/agents/orc-challenge-principles-opus-5-high.md +109 -109
- package/templates/agents/orc-challenge-reader-opus-5-low.md +90 -90
- package/templates/agents/orc-claude-writer-opus-4-8-high.md +48 -53
- package/templates/agents/orc-claude-writer-opus-5-med.md +48 -55
- package/templates/agents/orc-context-combiner-opus-5-high.md +78 -88
- package/templates/agents/orc-doc-checker-opus-5-low.md +108 -108
- package/templates/agents/orc-doc-writer-opus-5-med.md +134 -134
- 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 +15 -7
- package/templates/agents/orc-executor-opus-5-low.md +15 -7
- package/templates/agents/orc-executor-opus-5-med.md +15 -7
- 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 +81 -85
- package/templates/agents/orc-learn-writer-opus-5-low.md +67 -73
- package/templates/agents/orc-pattern-codifier-opus-5-med.md +58 -65
- package/templates/agents/orc-pattern-codifier-sonnet-5-high.md +58 -63
- package/templates/agents/orc-planner-mini-opus-5-med.md +4 -7
- package/templates/agents/orc-planner-mini-sonnet-5-high.md +2 -3
- package/templates/agents/orc-planner-opus-5-med.md +157 -160
- package/templates/agents/orc-recon-opus-5-low.md +3 -10
- package/templates/agents/orc-recon-sonnet-4-6-med.md +1 -8
- package/templates/agents/orc-retro-opus-5-med.md +8 -10
- package/templates/agents/orc-retro-sonnet-5-high.md +6 -7
- package/templates/agents/orc-reviewer-opus-5-med.md +96 -60
- package/templates/agents/orc-scout-opus-5-low.md +35 -40
- package/templates/agents/orc-scout-sonnet-4-6-high.md +35 -39
- package/templates/agents/orc-system-analyst-opus-5-high.md +115 -120
- package/templates/agents/orc-test-author-opus-5-med.md +70 -71
- package/templates/agents/orc-test-designer-opus-5-high.md +158 -158
- package/templates/agents/orc-test-interpreter-opus-5-low.md +130 -130
- package/templates/agents/orc-trace-writer-haiku-4-5.md +3 -7
- package/templates/agents/orc-verifier-opus-5-med.md +77 -69
- package/templates/agents/orc-wiki-scanner-opus-4-8-high.md +74 -79
- package/templates/agents/orc-wiki-scanner-opus-5-med.md +74 -81
- 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/commands/orc-ultra.md +17 -17
- package/templates/commands/orc-verify.md +11 -11
- package/templates/hooks/README.md +36 -2
- package/templates/hooks/orc-effort-guard.js +178 -178
- package/templates/hooks/orc-session-hook.js +264 -0
- package/templates/hooks/orc-statusline.js +9 -7
- package/templates/skills/_shared/README.md +9 -0
- package/templates/skills/_shared/code-graph.md +48 -56
- package/templates/skills/_shared/config-precedence.md +200 -198
- package/templates/skills/_shared/extra-dispatch.md +1331 -1346
- 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/opus5-only.md +8 -8
- package/templates/skills/_shared/phases/README.md +142 -83
- package/templates/skills/_shared/phases/analyst-gates.md +125 -136
- 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 +16 -45
- package/templates/skills/context-combiner/schemas/combined-report.md +78 -78
- package/templates/skills/orc/README.md +2 -2
- package/templates/skills/orc/SKILL.md +41 -69
- package/templates/skills/orc/config.md +9 -9
- package/templates/skills/orc/examples/full-run-mock.md +73 -73
- package/templates/skills/orc/references/effort-and-mode.md +222 -222
- 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 +10 -11
- package/templates/skills/orc/references/ultra-mode.md +11 -9
- 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-planner/SKILL.md +2 -2
- package/templates/skills/orc/subskills/orc-planner-mini/SKILL.md +121 -121
- package/templates/skills/orc/subskills/orc-review-verify/SKILL.md +35 -76
- 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 +34 -45
- 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 +32 -68
- package/templates/skills/orc-analyze/examples/analyze-mock.md +42 -42
- package/templates/skills/orc-analyze/schemas/report-audit.md +84 -83
- package/templates/skills/orc-analyze/schemas/report-prose.md +64 -63
- package/templates/skills/orc-analyze/schemas/report-requirement.md +78 -78
- package/templates/skills/orc-analyze-mini/SKILL.md +35 -70
- package/templates/skills/orc-challenge/README.md +142 -142
- package/templates/skills/orc-challenge/examples/council-full-roster.md +273 -273
- package/templates/skills/orc-challenge/references/council.md +315 -315
- package/templates/skills/orc-challenge/references/intake.md +171 -171
- package/templates/skills/orc-claude/SKILL.md +14 -14
- package/templates/skills/orc-diy/README.md +174 -143
- package/templates/skills/orc-diy/SKILL.md +23 -74
- package/templates/skills/orc-diy/references/blocks/pattern.md +18 -18
- package/templates/skills/orc-diy/references/flow-schema.md +1 -1
- package/templates/skills/orc-doc/README.md +229 -229
- package/templates/skills/orc-doc/examples/orc-doc-prd-run.md +325 -325
- package/templates/skills/orc-doc/references/chunking.md +527 -527
- package/templates/skills/orc-fast/SKILL.md +45 -73
- package/templates/skills/orc-judge/SKILL.md +77 -82
- package/templates/skills/orc-learn/SKILL.md +14 -14
- package/templates/skills/orc-learn/examples/learn-run-mock.md +61 -61
- package/templates/skills/orc-mini/SKILL.md +65 -113
- 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 +21 -39
- package/templates/skills/orc-verify/examples/verify-mock.md +33 -33
- package/templates/skills/orc-wait/SKILL.md +156 -163
- package/templates/skills/orc-wiki/references/extra.md +1 -1
- 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
|
+
}
|