@celsoft/coordinator 0.0.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +107 -0
- package/THIRD_PARTY_NOTICES.md +2005 -0
- package/agents/console.md +20 -0
- package/agents/free.md +110 -0
- package/build/0-JP6XL6LB.js +17 -0
- package/build/1-A52YZWNS.js +17 -0
- package/build/2-CLH2NM6F.js +32 -0
- package/build/3-4N467IB6.js +45 -0
- package/build/4-WL5H4LUW.js +29 -0
- package/build/_layout.svelte-KPUSX7LQ.js +27 -0
- package/build/_page.svelte-22H56UR6.js +7972 -0
- package/build/_page.svelte-CVWUNRQC.js +135 -0
- package/build/_page.svelte-LTZXBHMT.js +57 -0
- package/build/_server.ts-3KVBYN6P.js +20 -0
- package/build/_server.ts-3POPKVEM.js +47 -0
- package/build/_server.ts-5735T2V5.js +31 -0
- package/build/_server.ts-5VXGNWV2.js +38 -0
- package/build/_server.ts-645KP7WP.js +42 -0
- package/build/_server.ts-A3AA24Z2.js +28 -0
- package/build/_server.ts-CITUOEM6.js +58 -0
- package/build/_server.ts-I7HMTPOA.js +25 -0
- package/build/_server.ts-M5BQ4LLX.js +30 -0
- package/build/_server.ts-OB4O27HF.js +683 -0
- package/build/_server.ts-OJT7HE7M.js +15 -0
- package/build/_server.ts-OYGWVLZM.js +12 -0
- package/build/_server.ts-T75UVMQP.js +38 -0
- package/build/_server.ts-WYZYNHUZ.js +28 -0
- package/build/_server.ts-Z26EB47B.js +486 -0
- package/build/chunk-4CIG53L5.js +33 -0
- package/build/chunk-BJTJS3XY.js +338 -0
- package/build/chunk-F2CNWJZM.js +14 -0
- package/build/chunk-HGYE3UTU.js +416 -0
- package/build/chunk-KAYW2W7H.js +1311 -0
- package/build/chunk-LFP3UPCM.js +10 -0
- package/build/chunk-M44JEFEH.js +62 -0
- package/build/chunk-MC5AGLMY.js +39 -0
- package/build/chunk-NCZ77J5E.js +19798 -0
- package/build/chunk-NVNLDPUJ.js +2122 -0
- package/build/chunk-PDCON4KS.js +494 -0
- package/build/chunk-Q4OQNY5M.js +25 -0
- package/build/chunk-QCUOR2U5.js +75 -0
- package/build/chunk-R6RYZZWA.js +46 -0
- package/build/chunk-SOJTASZC.js +51 -0
- package/build/chunk-SU26PJVX.js +410 -0
- package/build/chunk-U5BSBG4R.js +506 -0
- package/build/chunk-UEKOZGRO.js +37 -0
- package/build/chunk-W6EFUWJ2.js +9 -0
- package/build/chunk-WOPREK6Q.js +117 -0
- package/build/chunk-WWJ3F4QS.js +127 -0
- package/build/chunk-ZGZJEEE3.js +1091 -0
- package/build/client/_app/immutable/assets/0.BGa5ZNDM.css +1 -0
- package/build/client/_app/immutable/chunks/BUrrLYDt.js +2 -0
- package/build/client/_app/immutable/chunks/BgFr_EyZ.js +4 -0
- package/build/client/_app/immutable/chunks/Bjy-W4x2.js +82 -0
- package/build/client/_app/immutable/chunks/C8qZab0y.js +56 -0
- package/build/client/_app/immutable/chunks/C90fLsA5.js +4 -0
- package/build/client/_app/immutable/chunks/CRzLMJPS.js +1 -0
- package/build/client/_app/immutable/chunks/DLT1iIlz.js +2 -0
- package/build/client/_app/immutable/chunks/pIrVVOVY.js +2 -0
- package/build/client/_app/immutable/entry/app.0DERehLW.js +3 -0
- package/build/client/_app/immutable/entry/payload.DSmR2FwN.js +2 -0
- package/build/client/_app/immutable/entry/start.B7srFiqh.js +3 -0
- package/build/client/_app/immutable/nodes/0.aD9vJoxR.js +2 -0
- package/build/client/_app/immutable/nodes/1.Br4FL91i.js +2 -0
- package/build/client/_app/immutable/nodes/2.vl6OYfu3.js +8 -0
- package/build/client/_app/immutable/nodes/3.VYqJOLAF.js +2 -0
- package/build/client/_app/immutable/nodes/4.jF9UVlFN.js +2 -0
- package/build/client/_app/version.json +1 -0
- package/build/client/apple-touch-icon.png +0 -0
- package/build/client/favicon.ico +0 -0
- package/build/client/favicon.svg +48 -0
- package/build/client/icon-192.png +0 -0
- package/build/client/icon-512.png +0 -0
- package/build/client/icon-maskable-512.png +0 -0
- package/build/client/icon.svg +44 -0
- package/build/client/manifest.webmanifest +15 -0
- package/build/error.svelte-JXZDY65F.js +51 -0
- package/build/handler.js +716 -0
- package/build/hooks.server-34IMHRKC.js +43 -0
- package/build/instance-NP3FJIWW.js +2967 -0
- package/client/client.mjs +28 -0
- package/client/hooks.mjs +7 -0
- package/client/register.mjs +5 -0
- package/dist/cli.js +62826 -0
- package/dist/port.js +36691 -0
- package/package.json +20 -1
- package/skills/agents/.claude-plugin/plugin.json +5 -0
- package/skills/agents/hooks/hooks.json +1 -0
- package/skills/agents/hooks/register.ts +57 -0
- package/skills/agents/skills/bus-work/SKILL.md +66 -0
- package/skills/agents/skills/plan/SKILL.md +98 -0
- package/skills/console/.claude-plugin/plugin.json +5 -0
- package/skills/console/hooks/hooks.json +1 -0
- package/skills/console/hooks/register.ts +10 -0
- package/skills/console/skills/bus/SKILL.md +61 -0
- package/skills/default/.claude-plugin/plugin.json +5 -0
- package/skills/default/skills/git-workflow/SKILL.md +294 -0
- package/skills/default/skills/git-workflow/mise-tasks/_lib.sh +107 -0
- package/skills/default/skills/git-workflow/mise-tasks/clone +36 -0
- package/skills/default/skills/git-workflow/mise-tasks/repo-list +8 -0
- package/skills/default/skills/git-workflow/mise-tasks/unpushed +26 -0
- package/skills/default/skills/git-workflow/mise-tasks/worktree-add +60 -0
- package/skills/default/skills/github/SKILL.md +208 -0
- package/skills/default/skills/github/scripts/gh-org +196 -0
- package/skills/default/skills/plan-page/SKILL.md +300 -0
- package/skills/default/skills/plan-page/assets/index.html +854 -0
- package/skills/default/skills/plan-page/scripts/inline +46 -0
- package/skills/default/skills/plan-page/scripts/stage +26 -0
- package/skills/default/skills/secrets/SKILL.md +134 -0
- package/skills/default/skills/secrets/scripts/secrets-check +87 -0
- package/skills/default/skills/writing-style/SKILL.md +45 -0
- package/skills/extra/.claude-plugin/plugin.json +5 -0
- package/skills/extra/skills/artifacts/SKILL.md +52 -0
- package/skills/extra/skills/github-actions/SKILL.md +78 -0
- package/skills/extra/skills/github-actions/scan +32 -0
- package/skills/extra/skills/markdown-review/SKILL.md +70 -0
- package/skills/extra/skills/markdown-review/assets/index.html +201 -0
- package/skills/extra/skills/markdown-review/scripts/stage +36 -0
- package/skills/extra/skills/mise-usage/SKILL.md +155 -0
- package/skills/extra/skills/plugin-authoring/SKILL.md +317 -0
- package/skills/extra/skills/project-conventions/SKILL.md +64 -0
- package/skills/extra/skills/questions/SKILL.md +156 -0
- package/skills/extra/skills/questions/assets/index.html +376 -0
- package/skills/extra/skills/questions/scripts/stage +26 -0
- package/skills/extra/skills/serve/SKILL.md +165 -0
- package/skills/extra/skills/serve/scripts/serve +268 -0
- package/skills/extra/skills/serve/scripts/server.py +345 -0
- package/skills/extra/skills/skill-dependencies/SKILL.md +65 -0
- package/skills/extra/skills/skill-maintenance/SKILL.md +101 -0
- package/skills/extra/skills/ticketing/SKILL.md +125 -0
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
<title>Plan</title>
|
|
2
|
+
<meta name="claude:viewport-fit-sides" content="cover">
|
|
3
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
4
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600&display=swap">
|
|
5
|
+
<style>
|
|
6
|
+
/* Layout: one reading column; each section a heading and its summary, its lines indented beneath, each line led by
|
|
7
|
+
its mark's icon or a bullet, then its bold label where it has one. */
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #0f1216; --surface: #161a20; --raised: #1c212a; --line: #262c35; --fg: #e3e7ec; --muted: #8b95a3;
|
|
10
|
+
--accent: #6aa8ff; --accent-ink: #0b1a30; --warn: #e0b453; --hover: rgba(106, 168, 255, .08);
|
|
11
|
+
--ok: #5cc28a; --bad: #ef7a7a; --info: #6aa8ff; --soft: #c5ccd5;
|
|
12
|
+
--sans: "IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
13
|
+
--mono: "IBM Plex Mono", ui-monospace, Menlo, monospace;
|
|
14
|
+
color-scheme: dark;
|
|
15
|
+
}
|
|
16
|
+
@media (prefers-color-scheme: light) { :root:not([data-theme="dark"]) {
|
|
17
|
+
--bg: #f6f7f9; --surface: #ffffff; --raised: #eef1f5; --line: #dde1e7; --fg: #1a1f26; --muted: #5d6673;
|
|
18
|
+
--accent: #2563c9; --accent-ink: #ffffff; --warn: #9a6a00; --hover: rgba(37, 99, 201, .07);
|
|
19
|
+
--ok: #1f8a52; --bad: #c23b3b; --info: #2563c9; --soft: #343c47; color-scheme: light; } }
|
|
20
|
+
:root[data-theme="light"] {
|
|
21
|
+
--bg: #f6f7f9; --surface: #ffffff; --raised: #eef1f5; --line: #dde1e7; --fg: #1a1f26; --muted: #5d6673;
|
|
22
|
+
--accent: #2563c9; --accent-ink: #ffffff; --warn: #9a6a00; --hover: rgba(37, 99, 201, .07);
|
|
23
|
+
--ok: #1f8a52; --bad: #c23b3b; --info: #2563c9; --soft: #343c47; color-scheme: light; }
|
|
24
|
+
body { background: var(--bg); color: var(--fg); font: 15px/1.6 var(--sans); margin: 0; }
|
|
25
|
+
/* Spacing scale 4 8 12 16 24 32 48. Edges: E0 heading, summary, lead; E1 = E0 + 28 label; E2 = E1 + label column + 16 text. */
|
|
26
|
+
:root { --lead: 16px; --lead-gap: 12px; --col-gap: 16px; --lh: 22px; }
|
|
27
|
+
.wrap { max-width: 760px; margin: 0 auto; padding-block: 32px 96px; display: grid; gap: 32px;
|
|
28
|
+
padding-left: max(16px, env(safe-area-inset-left, 0px)); padding-right: max(16px, env(safe-area-inset-right, 0px)); }
|
|
29
|
+
header { display: grid; gap: 8px; border-bottom: 1px solid var(--line); padding-bottom: 24px; }
|
|
30
|
+
.eyebrow { font: 500 12px/1.3 var(--mono); color: var(--muted); letter-spacing: .03em; overflow-wrap: anywhere; }
|
|
31
|
+
h1 { margin: 0 0 4px; font-size: 26px; font-weight: 600; line-height: 1.25; text-wrap: balance; }
|
|
32
|
+
.bar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; color: var(--muted); font-size: 13px; }
|
|
33
|
+
.bar .grow { flex: 1; }
|
|
34
|
+
.status { color: var(--fg); font-size: 14px; font-weight: 500; }
|
|
35
|
+
.status a { color: var(--info); }
|
|
36
|
+
.hint { font-size: 12px; }
|
|
37
|
+
button { font: inherit; color: inherit; }
|
|
38
|
+
.done { border: 1px solid var(--accent); background: var(--accent); color: var(--accent-ink); border-radius: 4px; padding: 5px 12px; font-size: 13px; font-weight: 500; cursor: pointer; }
|
|
39
|
+
.done:disabled { opacity: .6; cursor: default; }
|
|
40
|
+
.notice { border: 1px solid var(--warn); color: var(--warn); border-radius: 4px; padding: 8px 12px; font-size: 13px; }
|
|
41
|
+
main { display: grid; min-width: 0; }
|
|
42
|
+
section { display: grid; min-width: 0; scroll-margin-top: 16px; }
|
|
43
|
+
section + section { border-top: 1px solid var(--line); margin-top: 32px; padding-top: 32px; }
|
|
44
|
+
h2 { margin: 0; font-size: 20px; font-weight: 600; line-height: 1.3; text-wrap: balance; }
|
|
45
|
+
.summary { margin: 6px 0 0; max-width: 64ch; font-size: 16px; line-height: 1.5; color: var(--fg); }
|
|
46
|
+
.legend { display: flex; flex-wrap: wrap; gap: 4px 16px; margin-top: 12px; color: var(--muted); font-size: 13px; line-height: 20px; }
|
|
47
|
+
.legend span { display: inline-flex; align-items: center; gap: 6px; }
|
|
48
|
+
.legend .mark { margin-top: 0; }
|
|
49
|
+
section > .lines { margin-top: 16px; }
|
|
50
|
+
.lines { display: grid; gap: 10px; min-width: 0; font-size: 14.5px; line-height: var(--lh); }
|
|
51
|
+
.lines .text { color: var(--soft); }
|
|
52
|
+
.lines .lines { margin-top: 8px; gap: 6px; padding-left: 12px; border-left: 1px solid var(--line); }
|
|
53
|
+
.row { display: grid; grid-template-columns: var(--lead) minmax(0, 1fr) 1rem; column-gap: var(--lead-gap); row-gap: 4px; align-items: start; border-radius: 4px; min-width: 0; }
|
|
54
|
+
:root { --pad: calc(var(--col-gap) - var(--lead-gap)); }
|
|
55
|
+
.row.named, .row.term-row { grid-template-columns: var(--lead) min(calc(var(--labelw, 8rem) + var(--pad)), 40%) minmax(0, 1fr) 1rem; }
|
|
56
|
+
.lines .lines > .row.named:not(:has(> .mark)) { grid-template-columns: min(var(--subw, 6rem), 40%) minmax(0, 1fr) 1rem; column-gap: var(--col-gap); }
|
|
57
|
+
.lines .lines > .row.named:not(:has(> .mark)) > :first-child { display: none; }
|
|
58
|
+
.row.file-row { grid-template-columns: var(--lead) min(calc(var(--filew, 16rem) + var(--pad)), 55%) minmax(0, 1fr) 1rem; }
|
|
59
|
+
.row > .body { min-width: 0; }
|
|
60
|
+
.commenting .row:hover { background: var(--hover); cursor: pointer; }
|
|
61
|
+
.text { margin: 0; overflow-wrap: anywhere; }
|
|
62
|
+
.text strong { font-weight: 600; }
|
|
63
|
+
/* Bold is for terms alone: a term row's term and **term** in text; a label is set apart by color, not weight. */
|
|
64
|
+
.term { font-weight: 600; overflow-wrap: anywhere; }
|
|
65
|
+
.label { font-weight: 500; color: var(--fg); overflow-wrap: anywhere; }
|
|
66
|
+
.text em, .d-node em, .d-label em { font-style: italic; }
|
|
67
|
+
.mark { width: 16px; height: 16px; margin-top: calc((var(--lh) - 16px) / 2); }
|
|
68
|
+
.mark svg { display: block; width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
|
69
|
+
.c-ok { color: var(--ok); } .c-bad { color: var(--bad); } .c-warn { color: var(--warn); } .c-info { color: var(--info); } .c-muted { color: var(--muted); }
|
|
70
|
+
code { font: 13px var(--mono); color: var(--info); }
|
|
71
|
+
.files { display: grid; justify-items: start; min-width: 0; }
|
|
72
|
+
.files code { color: var(--fg); line-height: var(--lh); white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
|
|
73
|
+
.bullet { justify-self: center; width: 5px; height: 5px; border-radius: 50%; background: var(--muted); margin-top: calc((var(--lh) - 5px) / 2); }
|
|
74
|
+
.pin { width: 1px; height: var(--lh); justify-self: end; }
|
|
75
|
+
nav.toc { display: none; }
|
|
76
|
+
@media (min-width: 1180px) {
|
|
77
|
+
.wrap { max-width: 1000px; grid-template-columns: 176px minmax(0, 760px); column-gap: 64px; justify-content: center; }
|
|
78
|
+
.wrap > header, .wrap > main { grid-column: 2; }
|
|
79
|
+
nav.toc { display: grid; grid-column: 1; grid-row: 1 / span 2; align-self: start; position: sticky; top: 32px; gap: 2px; font-size: 13px; line-height: 20px; padding-top: 2px; }
|
|
80
|
+
nav.toc a { color: var(--muted); text-decoration: none; padding: 4px 0 4px 12px; border-left: 2px solid transparent; display: flex; justify-content: space-between; gap: 8px; }
|
|
81
|
+
nav.toc a:hover { color: var(--fg); }
|
|
82
|
+
nav.toc a.on { color: var(--fg); border-left-color: var(--accent); }
|
|
83
|
+
nav.toc a span { font: 12px var(--mono); color: var(--muted); }
|
|
84
|
+
}
|
|
85
|
+
@media (max-width: 600px) {
|
|
86
|
+
.row.file-row { grid-template-columns: var(--lead) minmax(0, 1fr) 1rem; }
|
|
87
|
+
.row.file-row > .text { grid-column: 2; }
|
|
88
|
+
}
|
|
89
|
+
@media (max-width: 480px) {
|
|
90
|
+
:root { --lead-gap: 8px; }
|
|
91
|
+
.row.term-row, .row.named { grid-template-columns: var(--lead) minmax(0, 1fr) 1rem; }
|
|
92
|
+
.row.term-row > .text, .row.named > .body { grid-column: 2; }
|
|
93
|
+
.lines .lines > .row.named:not(:has(> .mark)) { grid-template-columns: minmax(0, 1fr) 1rem; }
|
|
94
|
+
.lines .lines > .row.named:not(:has(> .mark)) > .body { grid-column: 1; }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* Journey: the happy path as a swimlane, one column per place, time running down; each step a card in its
|
|
98
|
+
place's column, the hop to the next step labelled; edge cases fold under the step they leave from. */
|
|
99
|
+
.journey { display: grid; gap: 0; margin-top: 16px; --lanes: 3; }
|
|
100
|
+
.j-lanes, .j-row { display: grid; grid-template-columns: repeat(var(--lanes), minmax(0, 1fr)); column-gap: 12px; }
|
|
101
|
+
.j-lanes { position: sticky; top: env(safe-area-inset-top, 0px); z-index: 2; background: var(--bg); padding-block: 6px 10px; }
|
|
102
|
+
.j-lane { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--lane); border-bottom: 2px solid var(--lane); padding-bottom: 6px; }
|
|
103
|
+
.j-lane svg, .j-chip svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; flex: none; }
|
|
104
|
+
.j-hop { display: grid; grid-template-columns: repeat(var(--lanes), minmax(0, 1fr)); column-gap: 12px; min-height: 30px; align-items: center; position: relative; }
|
|
105
|
+
.j-hop > div { display: flex; align-items: center; gap: 6px; font-size: 12.5px; color: var(--muted); padding-left: 14px; }
|
|
106
|
+
.j-hop svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2; flex: none; }
|
|
107
|
+
.step { position: relative; background: var(--surface); border: 1px solid var(--line); border-left: 3px solid var(--lane); border-radius: 6px; padding: 10px 12px; display: grid; gap: 4px; min-width: 0; }
|
|
108
|
+
.step .j-n { font: 600 11px/1 var(--mono); color: var(--lane); }
|
|
109
|
+
.step h3 { margin: 0; font-size: 15px; line-height: 1.3; font-weight: 600; }
|
|
110
|
+
.step p { margin: 0; font-size: 13.5px; line-height: 1.45; color: var(--soft); overflow-wrap: anywhere; }
|
|
111
|
+
.step .who { color: var(--muted); font-size: 12px; }
|
|
112
|
+
.step .sees { color: var(--fg); }
|
|
113
|
+
.step .sees::before { content: "Sees: "; color: var(--muted); }
|
|
114
|
+
.j-chip { display: none; align-items: center; gap: 5px; font-size: 11.5px; font-weight: 600; color: var(--lane); }
|
|
115
|
+
.step details { margin-top: 4px; }
|
|
116
|
+
.step summary { cursor: pointer; font-size: 12px; color: var(--warn); list-style: none; display: inline-flex; gap: 6px; align-items: center; }
|
|
117
|
+
.step summary::-webkit-details-marker { display: none; }
|
|
118
|
+
.step summary::before { content: "+"; font: 600 12px var(--mono); }
|
|
119
|
+
.step details[open] summary::before { content: "\2212"; }
|
|
120
|
+
.edges { display: grid; gap: 6px; margin-top: 6px; }
|
|
121
|
+
.edge { border: 1px dashed var(--line); border-radius: 5px; padding: 6px 8px; display: grid; gap: 2px; }
|
|
122
|
+
.edge b { font-size: 13px; }
|
|
123
|
+
.edge p { font-size: 13px; }
|
|
124
|
+
.edge .to { font-size: 11.5px; color: var(--muted); }
|
|
125
|
+
.step > .pin { position: absolute; top: 10px; right: 8px; width: 1px; height: 1px; }
|
|
126
|
+
.commenting .step:hover { border-color: var(--info); cursor: pointer; }
|
|
127
|
+
.l-0 { --lane: var(--info); } .l-1 { --lane: var(--ok); } .l-2 { --lane: var(--warn); } .l-3 { --lane: var(--bad); }
|
|
128
|
+
@media (max-width: 640px) {
|
|
129
|
+
.j-lanes { display: none; }
|
|
130
|
+
.j-row, .j-hop { grid-template-columns: minmax(0, 1fr); }
|
|
131
|
+
.j-row > .step { grid-column: 1 !important; }
|
|
132
|
+
.j-hop > div { grid-column: 1 !important; }
|
|
133
|
+
.j-chip { display: inline-flex; }
|
|
134
|
+
}
|
|
135
|
+
/* Screens: a line's screenshots as captioned figures, before and after side by side, stacked on a phone. */
|
|
136
|
+
.screens { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr)); gap: 12px; margin-top: 10px; }
|
|
137
|
+
.shot { position: relative; margin: 0; display: grid; gap: 6px; align-content: start; border-radius: 6px; padding: 6px; border: 1px solid var(--line); background: var(--surface); min-width: 0; }
|
|
138
|
+
.shot img { display: block; width: 100%; height: auto; border-radius: 4px; background: var(--raised); cursor: zoom-in; }
|
|
139
|
+
.shot figcaption { font-size: 13px; line-height: 1.45; color: var(--soft); overflow-wrap: anywhere; }
|
|
140
|
+
.shot .kind { font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; margin-right: 6px; }
|
|
141
|
+
.shot .kind.before { color: var(--muted); } .shot .kind.after { color: var(--ok); }
|
|
142
|
+
.shot > .pin { position: absolute; top: 10px; right: 8px; width: 1px; height: 1px; }
|
|
143
|
+
.commenting .shot:hover { border-color: var(--info); cursor: pointer; }
|
|
144
|
+
.commenting .shot img { cursor: pointer; }
|
|
145
|
+
dialog.zoom { border: 0; padding: 0; background: transparent; max-width: 100vw; max-height: 100vh; }
|
|
146
|
+
dialog.zoom::backdrop { background: rgba(0, 0, 0, .8); }
|
|
147
|
+
dialog.zoom img { display: block; max-width: 96vw; max-height: 92vh; cursor: zoom-out; }
|
|
148
|
+
/* Mockups: a line's live mockups, the real UI in a frame at a phone's or a desktop's width, a desktop scaled to fit. */
|
|
149
|
+
.live { margin-top: 10px; }
|
|
150
|
+
/* A set of mockups: a settings-style list of its screens beside the one shown, a row of choices above it on a phone. */
|
|
151
|
+
.mset { display: grid; grid-template-columns: minmax(120px, 160px) minmax(0, 1fr); gap: 12px; margin-top: 10px; align-items: start; }
|
|
152
|
+
.mset .live { margin-top: 0; }
|
|
153
|
+
.mset-nav { display: grid; gap: 2px; position: sticky; top: 8px; }
|
|
154
|
+
.mset-nav button { text-align: left; background: none; border: 0; border-left: 2px solid transparent; padding: 6px 10px; border-radius: 0 4px 4px 0; cursor: pointer; display: grid; gap: 2px; }
|
|
155
|
+
.mset-nav button:hover { background: var(--hover); }
|
|
156
|
+
.mset-nav button.on { border-left-color: var(--accent); background: var(--raised); }
|
|
157
|
+
.mset-nav b { font-size: 13.5px; font-weight: 500; color: var(--fg); }
|
|
158
|
+
.mset-nav span { font-size: 12px; line-height: 1.4; color: var(--muted); }
|
|
159
|
+
@media (max-width: 700px) {
|
|
160
|
+
.mset { grid-template-columns: minmax(0, 1fr); }
|
|
161
|
+
.mset-nav { position: static; display: flex; flex-wrap: wrap; gap: 6px; }
|
|
162
|
+
.mset-nav button { border: 1px solid var(--line); border-radius: 4px; padding: 4px 10px; }
|
|
163
|
+
.mset-nav button.on { border-color: var(--accent); }
|
|
164
|
+
.mset-nav span { display: none; }
|
|
165
|
+
}
|
|
166
|
+
.live .bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
167
|
+
.live .bar figcaption { flex: 1; min-width: 0; }
|
|
168
|
+
:is(.live, dialog.pop) .widths { display: inline-flex; border: 1px solid var(--line); border-radius: 6px; overflow: hidden; }
|
|
169
|
+
:is(.live, dialog.pop) .widths button, .live .open { font: 500 12px/1 var(--sans); color: var(--soft); background: transparent; border: 0; padding: 6px 10px; cursor: pointer; }
|
|
170
|
+
:is(.live, dialog.pop) .widths button.on { background: var(--raised); color: var(--fg); }
|
|
171
|
+
.live .open { border: 1px solid var(--line); border-radius: 6px; }
|
|
172
|
+
.live .stage { position: relative; overflow: hidden; border-radius: 4px; background: var(--raised); margin-inline: auto; }
|
|
173
|
+
.live iframe { position: absolute; top: 0; left: 0; border: 0; transform-origin: 0 0; background: var(--bg); }
|
|
174
|
+
.live .shield { position: absolute; inset: 0; display: none; }
|
|
175
|
+
.commenting .live .shield { display: block; cursor: pointer; }
|
|
176
|
+
dialog.full { border: 0; padding: 0; margin: auto; width: 96vw; height: 94vh; max-width: 96vw; max-height: 94vh; display: flex; flex-direction: column; gap: 8px; background: transparent; }
|
|
177
|
+
dialog.pop { border: 0; padding: 12px 16px 16px; margin: 0; width: 100vw; height: 100vh; max-width: 100vw; max-height: 100vh; box-sizing: border-box; background: var(--bg); color: var(--fg); display: grid; grid-template-rows: auto minmax(0, 1fr); gap: 12px; }
|
|
178
|
+
dialog.pop:focus { outline: none; }
|
|
179
|
+
dialog.pop .head { display: flex; align-items: center; gap: 12px; font-size: 14px; }
|
|
180
|
+
dialog.pop .head b { flex: 1; min-width: 0; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
181
|
+
dialog.pop .body { display: grid; grid-template-columns: 200px minmax(0, 1fr); gap: 16px; min-height: 0; }
|
|
182
|
+
dialog.pop .mset-nav { position: static; align-content: start; overflow: auto; }
|
|
183
|
+
dialog.pop .pstage { display: flex; justify-content: center; min-height: 0; }
|
|
184
|
+
dialog.pop .pstage iframe { height: 100%; border: 1px solid var(--line); border-radius: 6px; background: var(--bg); }
|
|
185
|
+
dialog.pop .close { font: 500 13px/1 var(--sans); border-radius: 6px; padding: 8px 12px; cursor: pointer; }
|
|
186
|
+
dialog.pop .close { color: var(--accent-ink); background: var(--accent); border: 0; }
|
|
187
|
+
@media (max-width: 700px) { dialog.pop .body { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); } dialog.pop .mset-nav { display: flex; flex-wrap: wrap; gap: 6px; } }
|
|
188
|
+
dialog.full::backdrop { background: rgba(0, 0, 0, .8); }
|
|
189
|
+
dialog.full .head { display: flex; align-items: center; gap: 12px; color: #e3e7ec; font-size: 14px; }
|
|
190
|
+
dialog.full .head span { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
191
|
+
dialog.full .close { font: 600 14px/1 var(--sans); color: var(--accent-ink); background: var(--accent); border: 0; border-radius: 6px; padding: 10px 16px; cursor: pointer; }
|
|
192
|
+
dialog.full iframe { flex: 1; width: 100%; border: 0; border-radius: 6px; background: var(--bg); }
|
|
193
|
+
/* Layers: the gist is the whole plan at its shallowest, drawn large; a folded section shows its heading and summary,
|
|
194
|
+
a true picture on its own, with its lines one tap deeper. */
|
|
195
|
+
.gist { margin: 4px 0 0; max-width: 60ch; font-size: 19px; line-height: 1.5; color: var(--fg); text-wrap: pretty; }
|
|
196
|
+
.gist p { margin: 0; } .gist p + p { margin-top: 8px; }
|
|
197
|
+
details.fold { margin-top: 12px; }
|
|
198
|
+
details.fold > summary { cursor: pointer; list-style: none; display: inline-flex; align-items: center; gap: 6px; color: var(--info); font-size: 13px; font-weight: 500; }
|
|
199
|
+
details.fold > summary::-webkit-details-marker { display: none; }
|
|
200
|
+
details.fold > summary::before { content: "+"; font: 600 13px var(--mono); }
|
|
201
|
+
details.fold[open] > summary::before { content: "\2212"; }
|
|
202
|
+
details.fold > .lines, details.fold > .journey { margin-top: 12px; }
|
|
203
|
+
.why { margin: 4px 0 0; font-size: 13.5px; line-height: 1.5; color: var(--muted); max-width: 68ch; }
|
|
204
|
+
.why > span:first-child { font-size: 11px; font-weight: 600; letter-spacing: .06em; text-transform: uppercase; margin-right: 6px; color: var(--muted); }
|
|
205
|
+
/* Diagram: boxes in rows, top to bottom, with labelled arrows drawn over them; a dashed arrow or box is an exception. */
|
|
206
|
+
.diagram { position: relative; display: grid; gap: 72px; margin-top: 12px; padding: 16px 8px; }
|
|
207
|
+
.d-row { display: flex; flex-wrap: wrap; justify-content: center; gap: 16px 32px; }
|
|
208
|
+
.d-node { position: relative; z-index: 1; background: var(--surface); border: 1px solid var(--line); border-radius: 6px; padding: 8px 12px; max-width: 220px; display: grid; gap: 2px; text-align: center; }
|
|
209
|
+
.d-node b { font-size: 14px; font-weight: 600; }
|
|
210
|
+
.d-node span { font-size: 12.5px; line-height: 1.4; color: var(--muted); }
|
|
211
|
+
.d-node.exception { border-style: dashed; border-color: var(--warn); }
|
|
212
|
+
.d-node.new { border-color: var(--ok); }
|
|
213
|
+
.diagram svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }
|
|
214
|
+
.diagram path { fill: none; stroke: var(--muted); stroke-width: 1.5; }
|
|
215
|
+
.diagram path.exception { stroke: var(--warn); stroke-dasharray: 5 4; }
|
|
216
|
+
.d-label { position: absolute; z-index: 2; transform: translate(-50%, -50%); background: var(--bg); color: var(--soft); font-size: 12px; line-height: 1.3; padding: 1px 6px; border-radius: 4px; white-space: nowrap; }
|
|
217
|
+
.d-label.exception { color: var(--warn); }
|
|
218
|
+
</style>
|
|
219
|
+
|
|
220
|
+
<div class="wrap">
|
|
221
|
+
<nav class="toc" id="toc" aria-label="Sections"></nav>
|
|
222
|
+
<header>
|
|
223
|
+
<div class="eyebrow" id="eyebrow"></div>
|
|
224
|
+
<h1 id="title">Plan</h1>
|
|
225
|
+
<div class="gist" id="gist" hidden></div>
|
|
226
|
+
<div class="bar">
|
|
227
|
+
<span id="status" class="status"></span><span class="grow"></span><span id="hint" class="hint">To comment, turn on comment mode and tap a line</span>
|
|
228
|
+
<button class="done" id="done" type="button" hidden>Done, tell Claude</button>
|
|
229
|
+
</div>
|
|
230
|
+
<div class="notice" id="notice" hidden></div>
|
|
231
|
+
</header>
|
|
232
|
+
<main id="main"></main>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<script>
|
|
236
|
+
// The plan skill's renderer: plan.json is Claude's data. A line with a label anchors its comment threads to its
|
|
237
|
+
// section and label ("line:<section>/<label>"), so they survive reordering; any other line anchors by its place.
|
|
238
|
+
// The shell draws every thread.
|
|
239
|
+
let PLAN = { sections: [] };
|
|
240
|
+
let api = null;
|
|
241
|
+
let anchors = null;
|
|
242
|
+
let threads = [];
|
|
243
|
+
|
|
244
|
+
const el = (tag, cls, text) => { const e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; };
|
|
245
|
+
|
|
246
|
+
// The icons a mark may name, Lucide's paths at 24px; plan.json's marks pick one and a color for each kind of line.
|
|
247
|
+
const ICONS = {
|
|
248
|
+
check: '<path d="M20 6 9 17l-5-5"/>',
|
|
249
|
+
x: '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
|
|
250
|
+
shield: '<path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/>',
|
|
251
|
+
alert: '<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/>',
|
|
252
|
+
question: '<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/>',
|
|
253
|
+
dot: '<circle cx="12" cy="12" r="4"/>',
|
|
254
|
+
plus: '<path d="M5 12h14"/><path d="M12 5v14"/>',
|
|
255
|
+
sparkle: '<path d="M9.94 15.5A2 2 0 0 0 8.5 14.06l-6.13-1.58a.5.5 0 0 1 0-.96L8.5 9.94A2 2 0 0 0 9.94 8.5l1.58-6.13a.5.5 0 0 1 .96 0L14.06 8.5A2 2 0 0 0 15.5 9.94l6.13 1.58a.5.5 0 0 1 0 .96L15.5 14.06a2 2 0 0 0-1.44 1.44l-1.58 6.13a.5.5 0 0 1-.96 0z"/>',
|
|
256
|
+
book: '<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"/>',
|
|
257
|
+
pencil: '<path d="M21.17 6.81a1 1 0 0 0-3.99-3.99L3.84 16.17a2 2 0 0 0-.5.83l-1.32 4.35a.5.5 0 0 0 .62.62l4.35-1.32a2 2 0 0 0 .83-.5z"/>',
|
|
258
|
+
arrow: '<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>',
|
|
259
|
+
globe: '<circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/>',
|
|
260
|
+
phone: '<rect width="14" height="20" x="5" y="2" rx="2"/><path d="M12 18h.01"/>',
|
|
261
|
+
terminal: '<path d="m4 17 6-6-6-6"/><path d="M12 19h8"/>',
|
|
262
|
+
chat: '<path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/>',
|
|
263
|
+
down: '<path d="M12 5v14"/><path d="m19 12-7 7-7-7"/>',
|
|
264
|
+
link: '<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>',
|
|
265
|
+
file: '<path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/>',
|
|
266
|
+
layers: '<path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/>',
|
|
267
|
+
};
|
|
268
|
+
const COLORS = new Set(["ok", "bad", "warn", "info", "muted"]);
|
|
269
|
+
|
|
270
|
+
function inline(target, text) {
|
|
271
|
+
for (const part of String(text ?? "").split(/(\*\*[^*]+\*\*|\*[^*]+\*|`[^`]+`)/)) {
|
|
272
|
+
if (!part) continue;
|
|
273
|
+
if (part.startsWith("**") && part.endsWith("**")) target.append(el("strong", null, part.slice(2, -2)));
|
|
274
|
+
else if (part.length > 2 && part.startsWith("*") && part.endsWith("*")) target.append(el("em", null, part.slice(1, -1)));
|
|
275
|
+
else if (part.startsWith("`") && part.endsWith("`")) target.append(el("code", null, part.slice(1, -1)));
|
|
276
|
+
else target.append(part);
|
|
277
|
+
}
|
|
278
|
+
return target;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const slug = (t) => String(t).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
282
|
+
|
|
283
|
+
function notice(text) {
|
|
284
|
+
const n = document.getElementById("notice");
|
|
285
|
+
n.textContent = text;
|
|
286
|
+
n.hidden = false;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function allLines(lines, out = []) {
|
|
290
|
+
for (const l of lines || []) { out.push(l); allLines(l.lines, out); }
|
|
291
|
+
return out;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function problems() {
|
|
295
|
+
const lines = PLAN.sections.flatMap((s) => allLines(s.lines));
|
|
296
|
+
const out = [];
|
|
297
|
+
const unknown = [...new Set(lines.map((l) => l.mark).filter((m) => m && !PLAN.marks?.[m]))];
|
|
298
|
+
if (unknown.length) out.push("These marks are not defined in marks: " + unknown.join(", ") + ".");
|
|
299
|
+
return out.join(" ");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function markIcon(name) {
|
|
303
|
+
const m = PLAN.marks?.[name];
|
|
304
|
+
if (!m) return null;
|
|
305
|
+
const span = el("span", "mark c-" + (COLORS.has(m.color) ? m.color : "muted"));
|
|
306
|
+
span.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true">${ICONS[m.icon] ?? ICONS.dot}</svg>`;
|
|
307
|
+
span.title = m.label || name;
|
|
308
|
+
span.setAttribute("role", "img");
|
|
309
|
+
span.setAttribute("aria-label", m.label || name);
|
|
310
|
+
return span;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function lead(l) {
|
|
314
|
+
const icon = l.mark && markIcon(l.mark);
|
|
315
|
+
if (icon) return icon;
|
|
316
|
+
if (l.label) return el("span");
|
|
317
|
+
const b = el("span", "bullet");
|
|
318
|
+
b.setAttribute("aria-hidden", "true");
|
|
319
|
+
return b;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function legend(lines) {
|
|
323
|
+
const used = [...new Set(allLines(lines).map((l) => l.mark).filter((m) => PLAN.marks?.[m]))];
|
|
324
|
+
if (!used.length) return null;
|
|
325
|
+
const box = el("div", "legend");
|
|
326
|
+
for (const name of used) { const item = el("span"); item.append(markIcon(name), PLAN.marks[name].label || name); box.append(item); }
|
|
327
|
+
return box;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function lineRows(lines, scope) {
|
|
331
|
+
const box = el("div", "lines");
|
|
332
|
+
for (const l of lines || []) {
|
|
333
|
+
const r = el("div", l.files ? "row file-row" : l.term ? "row term-row" : "row");
|
|
334
|
+
if (l.label) r.dataset.anchor = `line:${scope}/${l.label}`;
|
|
335
|
+
if (l.label && !l.term && !l.files) r.classList.add("named");
|
|
336
|
+
r.dataset.label = l.label || l.term || l.files?.join(", ") || String(l.text ?? "").slice(0, 60);
|
|
337
|
+
r.addEventListener("click", (ev) => { if (ev.target.closest(".row") === r && document.body.classList.contains("commenting")) comment(r, ev); });
|
|
338
|
+
if (l.files) {
|
|
339
|
+
const cell = el("div", "files");
|
|
340
|
+
for (const f of l.files) cell.append(el("code", null, f));
|
|
341
|
+
r.append(el("span"), cell, inline(el("p", "text"), l.text), el("span", "pin"));
|
|
342
|
+
} else if (l.term) {
|
|
343
|
+
r.append(lead(l), inline(el("span", "term"), l.term), inline(el("p", "text"), l.text), el("span", "pin"));
|
|
344
|
+
} else {
|
|
345
|
+
const body = el("div", "body");
|
|
346
|
+
body.append(inline(el("p", "text"), l.text));
|
|
347
|
+
if (l.why) { const w = el("p", "why"); w.append(el("span", null, "Why ")); inline(w, l.why); body.append(w); }
|
|
348
|
+
if (l.not) { const n = el("p", "why"); n.append(el("span", null, "Not ")); inline(n, l.not); body.append(n); }
|
|
349
|
+
if (l.screens?.length) body.append(screens(l.screens));
|
|
350
|
+
if (l.diagram) body.append(diagram(l.diagram));
|
|
351
|
+
if (l.mockups?.length > 1) body.append(mockupSet(l.mockups));
|
|
352
|
+
else for (const m of l.mockups || []) body.append(mockup(m));
|
|
353
|
+
if (l.lines?.length) body.append(lineRows(l.lines, `${scope}/${l.label ?? ""}`));
|
|
354
|
+
if (l.label) r.append(lead(l), el("span", "label", l.label), body, el("span", "pin"));
|
|
355
|
+
else r.append(lead(l), body, el("span", "pin"));
|
|
356
|
+
}
|
|
357
|
+
box.append(r);
|
|
358
|
+
}
|
|
359
|
+
return box;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// A line's screenshots, published beside plan.json; each anchors its own threads by its file, and opens full size.
|
|
363
|
+
function screens(list) {
|
|
364
|
+
const box = el("div", "screens");
|
|
365
|
+
for (const s of list) {
|
|
366
|
+
const fig = el("figure", "shot");
|
|
367
|
+
fig.dataset.anchor = `screen:${s.src}`;
|
|
368
|
+
fig.dataset.label = [s.kind, s.caption || s.src].filter(Boolean).join(", ");
|
|
369
|
+
const img = el("img");
|
|
370
|
+
img.src = s.src;
|
|
371
|
+
img.alt = s.caption || s.src;
|
|
372
|
+
img.loading = "lazy";
|
|
373
|
+
img.addEventListener("load", report);
|
|
374
|
+
const cap = el("figcaption");
|
|
375
|
+
if (s.kind) cap.append(el("span", `kind ${s.kind}`, s.kind));
|
|
376
|
+
inline(cap, s.caption);
|
|
377
|
+
fig.append(img, cap, el("span", "pin"));
|
|
378
|
+
fig.addEventListener("click", (ev) => {
|
|
379
|
+
ev.stopPropagation();
|
|
380
|
+
if (document.body.classList.contains("commenting")) comment(fig);
|
|
381
|
+
else zoom(s);
|
|
382
|
+
});
|
|
383
|
+
box.append(fig);
|
|
384
|
+
}
|
|
385
|
+
return box;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// A live mockup, a self-contained page published beside plan.json: the real UI, clickable, at a phone's width or a
|
|
389
|
+
// desktop's scaled to the column; its threads anchor to its file, and in comment mode a tap on it comments.
|
|
390
|
+
const WIDTHS = { phone: [390, 760], desktop: [1280, 800] };
|
|
391
|
+
// A diagram of how things relate: nodes in rows (each node's row, else one below whatever points to it), and arrows
|
|
392
|
+
// from one to another, each labelled; exception: true draws a node or arrow dashed, after the rules it bends.
|
|
393
|
+
const DIAGRAMS = [];
|
|
394
|
+
function diagram(d) {
|
|
395
|
+
const box = el("div", "diagram");
|
|
396
|
+
const rowOf = {};
|
|
397
|
+
const depth = (id, seen = new Set()) => {
|
|
398
|
+
const n = d.nodes.find((x) => x.id === id);
|
|
399
|
+
if (n?.row != null) return n.row;
|
|
400
|
+
if (seen.has(id)) return 0;
|
|
401
|
+
seen.add(id);
|
|
402
|
+
const ins = d.edges.filter((e) => e.to === id && !e.exception);
|
|
403
|
+
return ins.length ? Math.max(...ins.map((e) => depth(e.from, seen) + 1)) : 0;
|
|
404
|
+
};
|
|
405
|
+
for (const n of d.nodes) rowOf[n.id] = depth(n.id);
|
|
406
|
+
const nodes = {};
|
|
407
|
+
for (const r of [...new Set(Object.values(rowOf))].sort((a, b) => a - b)) {
|
|
408
|
+
const row = el("div", "d-row");
|
|
409
|
+
for (const n of d.nodes.filter((x) => rowOf[x.id] === r)) {
|
|
410
|
+
const card = el("div", "d-node" + (n.exception ? " exception" : "") + (n.new ? " new" : ""));
|
|
411
|
+
card.append(inline(el("b"), n.label));
|
|
412
|
+
if (n.note) card.append(inline(el("span"), n.note));
|
|
413
|
+
nodes[n.id] = card;
|
|
414
|
+
row.append(card);
|
|
415
|
+
}
|
|
416
|
+
box.append(row);
|
|
417
|
+
}
|
|
418
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
419
|
+
svg.innerHTML = '<defs><marker id="d-arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0 10 5 0 10z" style="fill:var(--muted);stroke:none"/></marker></defs>';
|
|
420
|
+
box.prepend(svg);
|
|
421
|
+
const draw = () => {
|
|
422
|
+
for (const x of box.querySelectorAll(".d-label")) x.remove();
|
|
423
|
+
for (const x of svg.querySelectorAll("path.edge")) x.remove();
|
|
424
|
+
const o = box.getBoundingClientRect();
|
|
425
|
+
const rect = (id) => nodes[id]?.getBoundingClientRect();
|
|
426
|
+
// Arrows leaving or entering one side of a box spread along it, in the order of the boxes at their other ends.
|
|
427
|
+
const spread = (id, end, other, e) => {
|
|
428
|
+
const r = rect(id), below = (x) => rect(x[other]).top > r.top + 4, above = (x) => rect(x[other]).top < r.top - 4;
|
|
429
|
+
const same = below(e) ? below : above;
|
|
430
|
+
const side = d.edges.filter((x) => x[end] === id && rect(x[other]) && same(x)).sort((x, y) => rect(x[other]).left - rect(y[other]).left);
|
|
431
|
+
return r.left - o.left + (r.width * (side.indexOf(e) + 1)) / (side.length + 1);
|
|
432
|
+
};
|
|
433
|
+
for (const e of d.edges) {
|
|
434
|
+
const a = rect(e.from), b = rect(e.to);
|
|
435
|
+
if (!a || !b) continue;
|
|
436
|
+
const ax = spread(e.from, "from", "to", e), bx = spread(e.to, "to", "from", e);
|
|
437
|
+
let p, mid;
|
|
438
|
+
if (Math.abs(a.top - b.top) < 4) {
|
|
439
|
+
const right = bx > ax, y = a.top + a.height / 2 - o.top;
|
|
440
|
+
const x1 = right ? a.right - o.left : a.left - o.left, x2 = right ? b.left - o.left : b.right - o.left;
|
|
441
|
+
p = `M${x1} ${y} L${x2} ${y}`; mid = [(x1 + x2) / 2, y];
|
|
442
|
+
} else {
|
|
443
|
+
const down = b.top > a.top;
|
|
444
|
+
const y1 = (down ? a.bottom : a.top) - o.top, y2 = (down ? b.top : b.bottom) - o.top, ym = (y1 + y2) / 2;
|
|
445
|
+
p = `M${ax} ${y1} C${ax} ${ym} ${bx} ${ym} ${bx} ${y2}`;
|
|
446
|
+
const t = 0.7, u = 1 - t, at = (p0, p1, p2, p3) => u * u * u * p0 + 3 * u * u * t * p1 + 3 * u * t * t * p2 + t * t * t * p3;
|
|
447
|
+
mid = [at(ax, ax, bx, bx), at(y1, ym, ym, y2)];
|
|
448
|
+
}
|
|
449
|
+
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
450
|
+
path.setAttribute("d", p);
|
|
451
|
+
path.setAttribute("class", "edge" + (e.exception ? " exception" : ""));
|
|
452
|
+
path.setAttribute("marker-end", "url(#d-arrow)");
|
|
453
|
+
svg.append(path);
|
|
454
|
+
if (e.label) {
|
|
455
|
+
const t = inline(el("span", "d-label" + (e.exception ? " exception" : "")), e.label);
|
|
456
|
+
t.style.left = mid[0] + "px";
|
|
457
|
+
t.style.top = mid[1] + "px";
|
|
458
|
+
box.append(t);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
DIAGRAMS.push(draw);
|
|
463
|
+
requestAnimationFrame(draw);
|
|
464
|
+
return box;
|
|
465
|
+
}
|
|
466
|
+
window.addEventListener("resize", () => DIAGRAMS.forEach((f) => f()));
|
|
467
|
+
document.addEventListener("toggle", () => DIAGRAMS.forEach((f) => f()), true);
|
|
468
|
+
|
|
469
|
+
// Several mockups of one change on one line: a list of its screens, each its label and text, and the one chosen shown beside
|
|
470
|
+
// it, keeping the width last picked as the screens change.
|
|
471
|
+
function mockupSet(list) {
|
|
472
|
+
const box = el("div", "mset");
|
|
473
|
+
const nav = el("div", "mset-nav");
|
|
474
|
+
const view = el("div");
|
|
475
|
+
let width = null, shown = -1;
|
|
476
|
+
const show = (i) => {
|
|
477
|
+
if (i === shown) return;
|
|
478
|
+
shown = i;
|
|
479
|
+
nav.querySelectorAll("button.screen").forEach((b, j) => b.classList.toggle("on", j === i));
|
|
480
|
+
view.replaceChildren(mockup(width ? { ...list[i], width } : list[i], (w) => (width = w), () => popSet(list, shown, width)));
|
|
481
|
+
report();
|
|
482
|
+
};
|
|
483
|
+
list.forEach((m, i) => nav.append(screenButton(m, () => show(i))));
|
|
484
|
+
box.append(nav, view);
|
|
485
|
+
show(0);
|
|
486
|
+
return box;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function screenButton(m, pick) {
|
|
490
|
+
const b = el("button", "screen");
|
|
491
|
+
b.type = "button";
|
|
492
|
+
b.append(inline(el("b"), m.label || m.caption || m.src));
|
|
493
|
+
if (m.text) b.append(inline(el("span"), m.text));
|
|
494
|
+
b.addEventListener("click", (ev) => { ev.stopPropagation(); pick(); });
|
|
495
|
+
return b;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// A set at full size, from a screen's Full size, filling the window: the same list of screens, and the chosen one at its real size, Desktop as wide as the
|
|
499
|
+
// window allows and Phone 390px, opening on the screen and width shown inline.
|
|
500
|
+
function popSet(list, start, width) {
|
|
501
|
+
const d = el("dialog", "pop");
|
|
502
|
+
const head = el("div", "head");
|
|
503
|
+
const title = el("b");
|
|
504
|
+
const widths = el("div", "widths");
|
|
505
|
+
const close = el("button", "close", "Close");
|
|
506
|
+
close.type = "button";
|
|
507
|
+
close.addEventListener("click", () => d.close());
|
|
508
|
+
const nav = el("div", "mset-nav");
|
|
509
|
+
const stage = el("div", "pstage");
|
|
510
|
+
const frame = el("iframe");
|
|
511
|
+
stage.append(frame);
|
|
512
|
+
let w = width || list[start]?.width || "desktop", at = -1;
|
|
513
|
+
const size = () => {
|
|
514
|
+
frame.style.width = w === "phone" ? "390px" : "100%";
|
|
515
|
+
for (const b of widths.children) b.classList.toggle("on", b.dataset.w === w);
|
|
516
|
+
};
|
|
517
|
+
const show = (i) => {
|
|
518
|
+
if (i === at) return;
|
|
519
|
+
at = i;
|
|
520
|
+
nav.querySelectorAll("button.screen").forEach((b, j) => b.classList.toggle("on", j === i));
|
|
521
|
+
title.replaceChildren(inline(el("span"), list[i].label || list[i].caption || list[i].src));
|
|
522
|
+
frame.src = list[i].src;
|
|
523
|
+
frame.title = list[i].caption || list[i].src;
|
|
524
|
+
};
|
|
525
|
+
for (const k of Object.keys(WIDTHS)) {
|
|
526
|
+
const b = el("button", null, k === "phone" ? "Phone" : "Desktop");
|
|
527
|
+
b.type = "button"; b.dataset.w = k;
|
|
528
|
+
b.addEventListener("click", () => { w = k; size(); });
|
|
529
|
+
widths.append(b);
|
|
530
|
+
}
|
|
531
|
+
list.forEach((m, i) => nav.append(screenButton(m, () => show(i))));
|
|
532
|
+
head.append(title, widths, close);
|
|
533
|
+
const body = el("div", "body");
|
|
534
|
+
body.append(nav, stage);
|
|
535
|
+
d.append(head, body);
|
|
536
|
+
d.addEventListener("close", () => d.remove());
|
|
537
|
+
document.body.append(d);
|
|
538
|
+
size();
|
|
539
|
+
show(start < 0 ? 0 : start);
|
|
540
|
+
// The dialog takes focus itself, so no button wears a focus ring until the keyboard moves to one.
|
|
541
|
+
d.tabIndex = -1;
|
|
542
|
+
d.showModal();
|
|
543
|
+
d.focus();
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function mockup(m, onWidth, onFull) {
|
|
547
|
+
const fig = el("figure", "shot live");
|
|
548
|
+
fig.dataset.anchor = `mockup:${m.src}`;
|
|
549
|
+
fig.dataset.label = [m.kind, m.caption || m.src].filter(Boolean).join(", ");
|
|
550
|
+
const bar = el("div", "bar");
|
|
551
|
+
const cap = el("figcaption");
|
|
552
|
+
if (m.kind) cap.append(el("span", `kind ${m.kind}`, m.kind));
|
|
553
|
+
inline(cap, m.caption);
|
|
554
|
+
const widths = el("div", "widths");
|
|
555
|
+
const stage = el("div", "stage");
|
|
556
|
+
const frame = el("iframe");
|
|
557
|
+
frame.src = m.src;
|
|
558
|
+
frame.title = m.caption || m.src;
|
|
559
|
+
frame.loading = "lazy";
|
|
560
|
+
frame.addEventListener("load", report);
|
|
561
|
+
const shield = el("div", "shield");
|
|
562
|
+
stage.append(frame, shield);
|
|
563
|
+
let width = m.width === "desktop" ? "desktop" : "phone";
|
|
564
|
+
const fit = () => {
|
|
565
|
+
const [w, h] = WIDTHS[width];
|
|
566
|
+
const room = fig.clientWidth - 14;
|
|
567
|
+
const k = Math.min(1, room / w);
|
|
568
|
+
frame.style.width = w + "px"; frame.style.height = h + "px"; frame.style.transform = `scale(${k})`;
|
|
569
|
+
stage.style.width = w * k + "px"; stage.style.height = h * k + "px";
|
|
570
|
+
for (const b of widths.children) b.classList.toggle("on", b.dataset.w === width);
|
|
571
|
+
};
|
|
572
|
+
for (const w of Object.keys(WIDTHS)) {
|
|
573
|
+
const b = el("button", null, w === "phone" ? "Phone" : "Desktop");
|
|
574
|
+
b.type = "button"; b.dataset.w = w;
|
|
575
|
+
b.addEventListener("click", (ev) => { ev.stopPropagation(); if (document.body.classList.contains("commenting")) return comment(fig); width = w; onWidth?.(w); fit(); report(); });
|
|
576
|
+
widths.append(b);
|
|
577
|
+
}
|
|
578
|
+
const open = el("button", "open", "Full size");
|
|
579
|
+
open.type = "button";
|
|
580
|
+
open.addEventListener("click", (ev) => { ev.stopPropagation(); if (document.body.classList.contains("commenting")) return comment(fig); onFull ? onFull() : full(m); });
|
|
581
|
+
bar.append(cap, widths, open);
|
|
582
|
+
fig.append(bar, stage, el("span", "pin"));
|
|
583
|
+
fig.addEventListener("click", (ev) => { if (document.body.classList.contains("commenting")) { ev.stopPropagation(); comment(fig); } });
|
|
584
|
+
new ResizeObserver(fit).observe(fig);
|
|
585
|
+
return fig;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function full(m) {
|
|
589
|
+
const d = el("dialog", "full");
|
|
590
|
+
const head = el("div", "head");
|
|
591
|
+
const close = el("button", "close", "Close");
|
|
592
|
+
close.type = "button";
|
|
593
|
+
close.addEventListener("click", () => d.close());
|
|
594
|
+
head.append(el("span", null, m.caption || m.src), close);
|
|
595
|
+
const f = el("iframe");
|
|
596
|
+
f.src = m.src;
|
|
597
|
+
f.title = m.caption || m.src;
|
|
598
|
+
d.append(head, f);
|
|
599
|
+
d.addEventListener("click", (ev) => { if (ev.target === d) d.close(); });
|
|
600
|
+
d.addEventListener("close", () => d.remove());
|
|
601
|
+
document.body.append(d);
|
|
602
|
+
d.showModal();
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function zoom(s) {
|
|
606
|
+
const d = el("dialog", "zoom");
|
|
607
|
+
const img = el("img");
|
|
608
|
+
img.src = s.src;
|
|
609
|
+
img.alt = s.caption || s.src;
|
|
610
|
+
d.append(img);
|
|
611
|
+
d.addEventListener("click", () => d.close());
|
|
612
|
+
d.addEventListener("close", () => d.remove());
|
|
613
|
+
document.body.append(d);
|
|
614
|
+
d.showModal();
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const svg = (name) => `<svg viewBox="0 0 24 24" aria-hidden="true">${ICONS[name] ?? ICONS.dot}</svg>`;
|
|
618
|
+
|
|
619
|
+
// The happy path: places are lanes, steps run down in order, each in its place's lane; a step's edge cases fold
|
|
620
|
+
// beneath it, each saying where it rejoins the path or that it ends.
|
|
621
|
+
function journey(j) {
|
|
622
|
+
const places = j.places || [];
|
|
623
|
+
const lane = (id) => Math.max(0, places.findIndex((p) => p.id === id));
|
|
624
|
+
const box = el("div", "journey");
|
|
625
|
+
box.style.setProperty("--lanes", Math.max(1, places.length));
|
|
626
|
+
const head = el("div", "j-lanes");
|
|
627
|
+
places.forEach((p, i) => { const h = el("div", `j-lane l-${i % 4}`); h.innerHTML = svg(p.icon); h.append(p.name); head.append(h); });
|
|
628
|
+
box.append(head);
|
|
629
|
+
const titles = new Map((j.steps || []).map((st, i) => [st.id ?? st.title, `${i + 1}. ${st.title}`]));
|
|
630
|
+
(j.steps || []).forEach((st, i) => {
|
|
631
|
+
const li = lane(st.place);
|
|
632
|
+
if (i > 0) {
|
|
633
|
+
const hop = el("div", "j-hop");
|
|
634
|
+
const label = el("div");
|
|
635
|
+
label.style.gridColumn = String(li + 1);
|
|
636
|
+
label.innerHTML = svg("down");
|
|
637
|
+
if (st.via) inline(label, st.via);
|
|
638
|
+
hop.append(label);
|
|
639
|
+
box.append(hop);
|
|
640
|
+
}
|
|
641
|
+
const row = el("div", "j-row");
|
|
642
|
+
const card = el("div", `step l-${li % 4}`);
|
|
643
|
+
card.style.gridColumn = String(li + 1);
|
|
644
|
+
card.dataset.anchor = `step:${st.id ?? st.title}`;
|
|
645
|
+
card.dataset.label = `Step ${i + 1}, ${st.title}`;
|
|
646
|
+
card.addEventListener("click", (ev) => { if (!ev.target.closest("summary") && document.body.classList.contains("commenting")) comment(card); });
|
|
647
|
+
const chip = el("span", "j-chip");
|
|
648
|
+
chip.innerHTML = svg(places[li]?.icon);
|
|
649
|
+
chip.append(places[li]?.name ?? "");
|
|
650
|
+
card.append(el("span", "j-n", String(i + 1)), chip, inline(el("h3"), st.title));
|
|
651
|
+
if (st.who) card.append(el("span", "who", st.who));
|
|
652
|
+
if (st.does) card.append(inline(el("p"), st.does));
|
|
653
|
+
if (st.sees) card.append(inline(el("p", "sees"), st.sees));
|
|
654
|
+
if (st.edges?.length) {
|
|
655
|
+
const d = el("details");
|
|
656
|
+
d.append(el("summary", null, `${st.edges.length} edge case${st.edges.length === 1 ? "" : "s"}`));
|
|
657
|
+
const list = el("div", "edges");
|
|
658
|
+
for (const e of st.edges) {
|
|
659
|
+
const item = el("div", "edge");
|
|
660
|
+
item.append(el("b", null, e.title), inline(el("p"), e.text));
|
|
661
|
+
item.append(el("span", "to", e.rejoin ? `Rejoins at ${titles.get(e.rejoin) ?? e.rejoin}` : "Ends here"));
|
|
662
|
+
list.append(item);
|
|
663
|
+
}
|
|
664
|
+
d.append(list);
|
|
665
|
+
card.append(d);
|
|
666
|
+
}
|
|
667
|
+
card.append(el("span", "pin"));
|
|
668
|
+
row.append(card);
|
|
669
|
+
box.append(row);
|
|
670
|
+
});
|
|
671
|
+
return box;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function render() {
|
|
675
|
+
const main = document.getElementById("main");
|
|
676
|
+
main.replaceChildren();
|
|
677
|
+
for (const s of PLAN.sections) {
|
|
678
|
+
const sec = el("section");
|
|
679
|
+
sec.id = slug(s.title);
|
|
680
|
+
sec.append(inline(el("h2"), s.title));
|
|
681
|
+
if (s.summary) sec.append(inline(el("p", "summary"), s.summary));
|
|
682
|
+
const key = legend(s.lines);
|
|
683
|
+
if (key) sec.append(key);
|
|
684
|
+
let into = sec;
|
|
685
|
+
if (s.fold && (s.journey || s.lines?.length)) {
|
|
686
|
+
into = el("details", "fold");
|
|
687
|
+
const n = (s.journey?.steps?.length ?? 0) + allLines(s.lines).length;
|
|
688
|
+
const more = s.more || `Show ${n} more`;
|
|
689
|
+
const toggle = el("summary", null, more);
|
|
690
|
+
into.append(toggle);
|
|
691
|
+
into.addEventListener("toggle", () => { toggle.textContent = into.open ? "Show less" : more; fitTerms(); report(); });
|
|
692
|
+
sec.append(into);
|
|
693
|
+
}
|
|
694
|
+
if (s.journey) into.append(journey(s.journey));
|
|
695
|
+
if (s.lines?.length) into.append(lineRows(s.lines, slug(s.title)));
|
|
696
|
+
main.append(sec);
|
|
697
|
+
}
|
|
698
|
+
fitTerms();
|
|
699
|
+
toc();
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function natural(e) {
|
|
703
|
+
const keep = [e.style.width, e.style.whiteSpace];
|
|
704
|
+
e.style.width = "max-content";
|
|
705
|
+
e.style.whiteSpace = "nowrap";
|
|
706
|
+
const w = e.getBoundingClientRect().width;
|
|
707
|
+
[e.style.width, e.style.whiteSpace] = keep;
|
|
708
|
+
return w;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Each section's file, term, and label columns as wide as their widest entry.
|
|
712
|
+
function fitTerms() {
|
|
713
|
+
const widest = (els) => Math.ceil(Math.max(0, ...[...els].map(natural)));
|
|
714
|
+
const main = document.getElementById("main");
|
|
715
|
+
main.style.setProperty("--labelw", widest(main.querySelectorAll(":is(section, section > details) > .lines > .row > :is(.term, .label)")) + "px");
|
|
716
|
+
main.style.setProperty("--filew", widest(main.querySelectorAll(".files code")) + "px");
|
|
717
|
+
for (const sub of main.querySelectorAll(".lines .lines")) sub.style.setProperty("--subw", widest(sub.querySelectorAll(":scope > .row > .label")) + "px");
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function toc() {
|
|
721
|
+
const nav = document.getElementById("toc");
|
|
722
|
+
const links = PLAN.sections.map((s) => {
|
|
723
|
+
const a = el("a", null, s.title);
|
|
724
|
+
a.href = "#" + slug(s.title);
|
|
725
|
+
a.append(el("span", null, String(allLines(s.lines).length + (s.journey?.steps?.length ?? 0))));
|
|
726
|
+
return a;
|
|
727
|
+
});
|
|
728
|
+
nav.replaceChildren(...links);
|
|
729
|
+
const secs = [...document.querySelectorAll("section")];
|
|
730
|
+
const mark = () => {
|
|
731
|
+
const at = secs.filter((x) => x.getBoundingClientRect().top <= 120).pop() || secs[0];
|
|
732
|
+
links.forEach((a) => a.classList.toggle("on", a.hash === "#" + at?.id));
|
|
733
|
+
};
|
|
734
|
+
addEventListener("scroll", mark, { passive: true });
|
|
735
|
+
mark();
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function anchorFor(r) {
|
|
739
|
+
return r.dataset.anchor || (anchors ? anchors.domAnchor(r)[0] : null);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function pinPoint(r) {
|
|
743
|
+
const pin = r.querySelector(":scope > .pin").getBoundingClientRect();
|
|
744
|
+
if (r.classList.contains("step") || r.classList.contains("shot")) return { x: pin.left + window.scrollX, y: pin.top + window.scrollY };
|
|
745
|
+
const lead = r.firstElementChild.getBoundingClientRect();
|
|
746
|
+
return { x: pin.left + window.scrollX, y: lead.top + lead.height / 2 + window.scrollY };
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Pins for threads on lines still in the plan; a thread whose line was removed is left unplaced, in the shell's list.
|
|
750
|
+
function report() {
|
|
751
|
+
if (!anchors) return;
|
|
752
|
+
const byAnchor = new Map([...document.querySelectorAll(".row, .step, .shot")].map((r) => [anchorFor(r), r]));
|
|
753
|
+
const map = {};
|
|
754
|
+
for (const t of threads) {
|
|
755
|
+
const r = byAnchor.get(t.anchor);
|
|
756
|
+
if (r && r.getClientRects().length) map[t.id] = pinPoint(r);
|
|
757
|
+
}
|
|
758
|
+
anchors.placed(map);
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
async function comment(r) {
|
|
762
|
+
try {
|
|
763
|
+
if (anchors) await anchors.compose(anchorFor(r), r.querySelector(":scope > .pin"), { label: r.dataset.label });
|
|
764
|
+
else if (api) await api.openComposer({ element: r });
|
|
765
|
+
} catch (e) {
|
|
766
|
+
if (e?.code === "unavailable" || e?.code === "forbidden") document.getElementById("hint").textContent = "Commenting is off in this view";
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
async function offerDone() {
|
|
771
|
+
const button = document.getElementById("done");
|
|
772
|
+
if (!api?.sendToClaude) return;
|
|
773
|
+
const check = async () => { try { return (await api.canSendToClaude()) === "available"; } catch { return false; } };
|
|
774
|
+
button.hidden = !(await check());
|
|
775
|
+
button.addEventListener("click", async () => {
|
|
776
|
+
const hint = document.getElementById("hint");
|
|
777
|
+
if (!(await check())) { hint.textContent = "Claude is not listening"; return; }
|
|
778
|
+
button.disabled = true;
|
|
779
|
+
try {
|
|
780
|
+
const anchor = await api.anchorFor(document.getElementById("title"));
|
|
781
|
+
await api.sendToClaude({ anchor, text: "Done reviewing. Read the comments on the plan's lines and work them in." });
|
|
782
|
+
hint.textContent = "Sent to Claude";
|
|
783
|
+
} catch {
|
|
784
|
+
hint.textContent = "Could not reach Claude";
|
|
785
|
+
} finally {
|
|
786
|
+
button.disabled = false;
|
|
787
|
+
}
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
async function wireComments() {
|
|
792
|
+
try { api = await window.claude?.use?.("comments"); } catch { api = null; }
|
|
793
|
+
const hint = document.getElementById("hint");
|
|
794
|
+
if (!api) { hint.textContent = "Commenting is unavailable in this view"; return; }
|
|
795
|
+
try {
|
|
796
|
+
anchors = await api.customAnchors({
|
|
797
|
+
mode: (on) => { document.body.classList.toggle("commenting", on); if (!on) { threads = []; report(); } },
|
|
798
|
+
threads: (list) => { threads = list; report(); },
|
|
799
|
+
reveal: (id) => {
|
|
800
|
+
const t = threads.find((x) => x.id === id);
|
|
801
|
+
const r = t && [...document.querySelectorAll(".row, .step, .shot")].find((x) => anchorFor(x) === t.anchor);
|
|
802
|
+
const fold = r?.closest("details");
|
|
803
|
+
if (fold && !fold.open) fold.open = true;
|
|
804
|
+
if (r) r.scrollIntoView({ block: "center", behavior: matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth" });
|
|
805
|
+
},
|
|
806
|
+
});
|
|
807
|
+
report();
|
|
808
|
+
window.addEventListener("resize", report);
|
|
809
|
+
} catch {
|
|
810
|
+
anchors = null;
|
|
811
|
+
}
|
|
812
|
+
offerDone();
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// The coordinator reloads the frame when the plan is republished; window.name outlives the reload, so the reader keeps their place.
|
|
816
|
+
const PLACE = "plan-scroll:";
|
|
817
|
+
function keepPlace() {
|
|
818
|
+
const y = window.name.startsWith(PLACE) ? Number(window.name.slice(PLACE.length)) : 0;
|
|
819
|
+
if (y > 0) scrollTo(0, y);
|
|
820
|
+
addEventListener("scroll", () => { window.name = PLACE + Math.round(scrollY); }, { passive: true });
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
(async () => {
|
|
824
|
+
try {
|
|
825
|
+
PLAN = await (await fetch("plan.json", { cache: "no-cache" })).json();
|
|
826
|
+
PLAN.sections = Array.isArray(PLAN.sections) ? PLAN.sections : [];
|
|
827
|
+
} catch (error) {
|
|
828
|
+
return notice("Could not read plan.json: " + error.message);
|
|
829
|
+
}
|
|
830
|
+
if (PLAN.title) { document.getElementById("title").textContent = PLAN.title; document.title = PLAN.title; }
|
|
831
|
+
document.getElementById("eyebrow").textContent = PLAN.eyebrow || "";
|
|
832
|
+
if (PLAN.gist) {
|
|
833
|
+
const g = document.getElementById("gist");
|
|
834
|
+
for (const para of [].concat(PLAN.gist)) g.append(inline(el("p"), para));
|
|
835
|
+
g.hidden = false;
|
|
836
|
+
}
|
|
837
|
+
const status = document.getElementById("status");
|
|
838
|
+
if (PLAN.status) inline(status, PLAN.status);
|
|
839
|
+
const ask = PLAN.sections.find((x) => x.ask);
|
|
840
|
+
if (ask) {
|
|
841
|
+
const n = allLines(ask.lines).length;
|
|
842
|
+
if (PLAN.status) status.append(", ");
|
|
843
|
+
const a = el("a", null, `${n} ${ask.title.toLowerCase()}`);
|
|
844
|
+
a.href = "#" + slug(ask.title);
|
|
845
|
+
status.append(a);
|
|
846
|
+
}
|
|
847
|
+
const bad = problems();
|
|
848
|
+
if (bad) notice(bad);
|
|
849
|
+
render();
|
|
850
|
+
keepPlace();
|
|
851
|
+
document.fonts?.ready.then(() => { fitTerms(); report(); });
|
|
852
|
+
wireComments();
|
|
853
|
+
})();
|
|
854
|
+
</script>
|