@arjunkhera/atlas 0.3.22 → 0.3.25

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/view/page.html ADDED
@@ -0,0 +1,563 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>@@TITLE@@</title>
7
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&amp;family=IBM+Plex+Mono:wght@400;500&amp;family=Source+Serif+4:opsz,wght@8..60,600&amp;display=swap">
8
+ <style>
9
+ /* The project view: the four views locked on the canvas "Project view"
10
+ (9 to 14 October 2026). Every colour is a token; the dark set mirrors the
11
+ light set. Built by `atlas view build`; do not edit a built copy. */
12
+ :root{color-scheme:light;
13
+ --paper:#fbfbf9;--card:#ffffff;--ink:#1b1f1d;--body:#3d4743;--muted:#5b6661;--quiet:#6b7570;
14
+ --line:#e3e5e1;--line-soft:#f0f1ee;--line-strong:#c9cec9;--rail:#f0f2ef;--hover:#f6f8fc;
15
+ --link-ink:#1b3566;--link-bg:#f4f6fa;--link-line:#c6d4ec;--sel-bg:#eef3fb;--sel-bar:#2a4d8f;
16
+ --go:#2a4d8f;--go-bg:#dbe5f5;--done:#2f6b57;--done-bg:#cfe3d8;--done-ink:#1f5a46;--ready-bg:#e7f1ec;
17
+ --you-ink:#7a4300;--you-bg:#fdf3e3;--you-bar:#e9a64a;--you-mark:#b8650f;--before:#b8650f;--after:#7d9bd0;
18
+ --grey-bg:#f0f2ef;--est-a:#eceeeb;--est-b:#ffffff;--est-line:#b8bfbb;--danger:#8f2a24;--danger-bg:#fbeceb;
19
+ --shadow:0 1px 2px rgba(27,31,29,.12);--menu-shadow:0 8px 24px rgba(27,31,29,.16)}
20
+ @media (prefers-color-scheme:dark){:root:not([data-theme="light"]){color-scheme:dark;
21
+ --paper:#151816;--card:#1d211f;--ink:#e8ebe8;--body:#c9cfcb;--muted:#a3ada8;--quiet:#8f9a95;
22
+ --line:#2f3532;--line-soft:#262b28;--line-strong:#3d4440;--rail:#232825;--hover:#232a33;
23
+ --link-ink:#b9cdf0;--link-bg:#1f2735;--link-line:#3a4b68;--sel-bg:#1f2a3b;--sel-bar:#7fa2e0;
24
+ --go:#7fa2e0;--go-bg:#24324a;--done:#7cc3a6;--done-bg:#1f3a30;--done-ink:#a6dcc4;--ready-bg:#1f3329;
25
+ --you-ink:#f0c58a;--you-bg:#3a2c18;--you-bar:#c98a35;--you-mark:#e0a050;--before:#e0a050;--after:#7f9fd6;
26
+ --grey-bg:#262b28;--est-a:#2a302d;--est-b:#1d211f;--est-line:#4a524e;--danger:#f0a39d;--danger-bg:#3a2220;
27
+ --shadow:0 1px 2px rgba(0,0,0,.5);--menu-shadow:0 8px 24px rgba(0,0,0,.5)}}
28
+ :root[data-theme="dark"]{color-scheme:dark;
29
+ --paper:#151816;--card:#1d211f;--ink:#e8ebe8;--body:#c9cfcb;--muted:#a3ada8;--quiet:#8f9a95;
30
+ --line:#2f3532;--line-soft:#262b28;--line-strong:#3d4440;--rail:#232825;--hover:#232a33;
31
+ --link-ink:#b9cdf0;--link-bg:#1f2735;--link-line:#3a4b68;--sel-bg:#1f2a3b;--sel-bar:#7fa2e0;
32
+ --go:#7fa2e0;--go-bg:#24324a;--done:#7cc3a6;--done-bg:#1f3a30;--done-ink:#a6dcc4;--ready-bg:#1f3329;
33
+ --you-ink:#f0c58a;--you-bg:#3a2c18;--you-bar:#c98a35;--you-mark:#e0a050;--before:#e0a050;--after:#7f9fd6;
34
+ --grey-bg:#262b28;--est-a:#2a302d;--est-b:#1d211f;--est-line:#4a524e;--danger:#f0a39d;--danger-bg:#3a2220;
35
+ --shadow:0 1px 2px rgba(0,0,0,.5);--menu-shadow:0 8px 24px rgba(0,0,0,.5)}
36
+ *{box-sizing:border-box}
37
+ body{margin:0;background:var(--paper);color:var(--ink);font-family:"IBM Plex Sans",system-ui,sans-serif;font-size:14px;line-height:1.45}
38
+ button{font-family:inherit;color:inherit;font-size:inherit}
39
+ .wrap{max-width:1440px;margin:0 auto;padding:16px 16px 48px;display:flex;flex-direction:column;gap:12px}
40
+ .bar{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px;padding:8px 12px;background:var(--card);border:1px solid var(--line);border-radius:10px}
41
+ .brand{display:flex;flex-wrap:wrap;align-items:center;gap:10px}
42
+ .brand b{font-family:"Source Serif 4",Georgia,serif;font-size:17px;font-weight:600}
43
+ .sep{color:var(--quiet)}
44
+ .needs{border:0;font-size:13px;padding:6px 12px;min-height:32px;border-radius:99px;background:var(--you-bg);color:var(--you-ink);cursor:pointer}
45
+ .needs.zero{background:var(--grey-bg);color:var(--muted)}
46
+ .tools{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:8px}
47
+ .switch{display:flex;flex-wrap:wrap;gap:4px;background:var(--rail);border-radius:8px;padding:3px}
48
+ .switch button{border:0;padding:6px 12px;min-height:34px;border-radius:6px;background:transparent;color:var(--body);cursor:pointer}
49
+ .switch button[aria-pressed="true"]{background:var(--card);box-shadow:var(--shadow);color:var(--ink);font-weight:600}
50
+ .meta{font-size:12px;color:var(--quiet);display:flex;gap:8px;align-items:center;flex-wrap:wrap}
51
+ .ghost{border:1px solid var(--line-strong);background:var(--card);border-radius:6px;padding:4px 10px;min-height:30px;cursor:pointer;font-size:12px}
52
+ .card{background:var(--card);border:1px solid var(--line-strong);border-radius:10px}
53
+ .banner{display:flex;flex-wrap:wrap;gap:6px 14px;align-items:center;padding:10px 14px;background:var(--link-bg);border:1px solid var(--link-line);border-radius:10px;font-size:13px}
54
+ .banner b{color:var(--link-ink)}
55
+ .k-before{color:var(--before);font-weight:600}.k-after{color:var(--link-ink);font-weight:600}
56
+ .notice{padding:16px;border-radius:10px;border:1px solid var(--line-strong);background:var(--card);display:flex;flex-direction:column;gap:6px}
57
+ .notice.bad{border-color:var(--danger);background:var(--danger-bg)}
58
+ .group{display:flex;justify-content:space-between;align-items:center;width:100%;border:0;font-size:12px;font-weight:600;letter-spacing:.06em;padding:8px 16px;background:var(--grey-bg);color:var(--body);text-align:left;cursor:pointer;min-height:34px}
59
+ .group.you{background:var(--you-bg);color:var(--you-ink)}
60
+ .row{display:flex;align-items:center;gap:8px;padding:4px 10px 4px 6px;min-height:42px;border-bottom:1px solid var(--line-soft);background:var(--card);box-shadow:inset 3px 0 0 transparent;transition:opacity .15s}
61
+ .row:hover{background:var(--hover)}
62
+ .row.sel{background:var(--sel-bg);box-shadow:inset 3px 0 0 var(--sel-bar)}
63
+ .row.before{box-shadow:inset 3px 0 0 var(--before)}.row.after{box-shadow:inset 3px 0 0 var(--after)}
64
+ .row.dim{opacity:.4}
65
+ .indent{flex:none;align-self:stretch}
66
+ .caret{width:28px;height:28px;flex:none;border:0;background:none;cursor:pointer;color:var(--muted);padding:0;font-size:12px}
67
+ .caret.none{visibility:hidden}
68
+ .dot{width:14px;height:14px;flex:none;border-radius:99px;border:1.5px solid var(--quiet)}
69
+ .dot.proposed{border-style:dashed}
70
+ .dot.active{border-color:var(--go);background:conic-gradient(var(--go) 0 40%,transparent 0)}
71
+ .dot.done{border-color:var(--done);background:var(--done)}
72
+ .dot.dropped{opacity:.5}
73
+ .dot.blocked{background:linear-gradient(135deg,transparent 44%,var(--quiet) 44% 56%,transparent 56%)}
74
+ .dot.paused{background:linear-gradient(90deg,transparent 30%,var(--quiet) 30% 42%,transparent 42% 58%,var(--quiet) 58% 70%,transparent 70%)}
75
+ .body{display:flex;flex-wrap:wrap;align-items:center;gap:6px;flex:1 1 auto;min-width:0}
76
+ .title{border:0;background:none;padding:4px 0;cursor:pointer;text-align:left;min-width:0;max-width:100%;color:var(--ink)}
77
+ .title.l0{font-weight:600}.title.l1{font-weight:500}
78
+ .title.grey{color:var(--muted)}
79
+ .tag{font-size:12px;padding:1px 8px;border-radius:99px;white-space:nowrap;background:var(--grey-bg);color:var(--body)}
80
+ .tag.ready{background:var(--ready-bg);color:var(--done-ink)}
81
+ .tag.you{background:var(--you-bg);color:var(--you-ink)}
82
+ .tag.go{background:var(--go-bg);color:var(--link-ink)}
83
+ .tag.with{background:transparent;color:var(--quiet);border:1px solid var(--line)}
84
+ .tag.done{background:var(--done-bg);color:var(--done-ink)}
85
+ .chip{border:1px solid var(--link-line);background:var(--link-bg);color:var(--link-ink);font-size:12px;padding:2px 8px;min-height:26px;border-radius:99px;cursor:pointer;white-space:nowrap;max-width:100%;overflow:hidden;text-overflow:ellipsis}
86
+ .chip:hover{filter:brightness(.96)}
87
+ .count{font-size:12px;color:var(--quiet);flex:none}
88
+ .dots{flex:none;width:32px;height:32px;border:1px solid transparent;border-radius:6px;background:none;cursor:pointer;color:var(--muted);font-size:18px;line-height:1;position:relative}
89
+ .dots:hover,.dots[aria-expanded="true"]{border-color:var(--line-strong);background:var(--card)}
90
+ .menu{position:absolute;z-index:20;min-width:210px;background:var(--card);border:1px solid var(--line-strong);border-radius:8px;box-shadow:var(--menu-shadow);padding:6px;display:flex;flex-direction:column}
91
+ .menu .h{font-size:11px;font-weight:600;letter-spacing:.06em;color:var(--quiet);padding:6px 8px 2px}
92
+ .menu button{border:0;background:none;text-align:left;padding:7px 8px;min-height:34px;border-radius:5px;cursor:pointer;display:flex;justify-content:space-between;gap:12px}
93
+ .menu button:hover{background:var(--hover)}
94
+ .menu .k{color:var(--quiet);font-family:"IBM Plex Mono",monospace;font-size:11px}
95
+ .menu hr{border:0;border-top:1px solid var(--line);margin:4px 0}
96
+ .split{display:grid;grid-template-columns:minmax(300px,5fr) minmax(0,7fr);gap:12px;align-items:start}
97
+ @media (max-width:900px){.split{grid-template-columns:1fr}}
98
+ .stack{display:flex;flex-direction:column;gap:10px}
99
+ .panel{background:var(--card);border:1px solid var(--line-strong);border-radius:10px}
100
+ .phead{display:flex;justify-content:space-between;gap:8px;padding:12px 14px;border-bottom:1px solid var(--line)}
101
+ .pl{display:flex;gap:10px;min-width:0}
102
+ .badge{flex:none;width:24px;height:24px;border-radius:6px;background:var(--ink);color:var(--card);font-family:"IBM Plex Mono",monospace;font-size:12px;display:flex;align-items:center;justify-content:center}
103
+ .path{font-size:12px;color:var(--quiet)}
104
+ .phead h2{margin:0;font-family:"Source Serif 4",Georgia,serif;font-size:19px;font-weight:600}
105
+ .pr{display:flex;align-items:flex-start;gap:4px;position:relative;flex:none}
106
+ .facts{display:grid;grid-template-columns:120px minmax(0,1fr);gap:6px 12px;padding:12px 14px;font-size:13px}
107
+ .facts>span:nth-child(odd){color:var(--quiet)}
108
+ .tabs{display:flex;flex-wrap:wrap;gap:4px;padding:0 14px 8px;border-bottom:1px solid var(--line)}
109
+ .tabs button{border:0;background:none;padding:6px 10px;min-height:32px;border-radius:6px;cursor:pointer;color:var(--body)}
110
+ .tabs button[aria-pressed="true"]{background:var(--rail);color:var(--ink);font-weight:600}
111
+ .entries{display:flex;flex-direction:column}
112
+ .entry{padding:10px 14px;border-bottom:1px solid var(--line-soft);display:flex;flex-direction:column;gap:4px;font-size:13px}
113
+ .entry .eh{display:flex;justify-content:space-between;gap:8px;color:var(--quiet);font-size:12px}
114
+ .entry .ev{white-space:pre-wrap;overflow-wrap:anywhere}
115
+ .opts{display:flex;flex-wrap:wrap;gap:6px}
116
+ .fold1{padding:10px 14px;font-size:13px;color:var(--body)}
117
+ .empty{padding:14px;color:var(--quiet);font-size:13px}
118
+ .cols{display:flex;overflow-x:auto;align-items:stretch}
119
+ .col{flex:0 0 280px;border-right:1px solid var(--line);display:flex;flex-direction:column;min-height:300px}
120
+ .colh{display:flex;justify-content:space-between;padding:10px 12px;font-size:12px;font-weight:600;letter-spacing:.06em;color:var(--quiet);border-bottom:1px solid var(--line)}
121
+ .citem{border-bottom:1px solid var(--line-soft);padding:6px 10px;display:flex;flex-direction:column;gap:4px}
122
+ .citem.sel{background:var(--sel-bg);box-shadow:inset 3px 0 0 var(--sel-bar)}
123
+ .citem.dim{opacity:.4}
124
+ .citem>button{border:0;background:none;display:flex;justify-content:space-between;gap:8px;text-align:left;cursor:pointer;padding:2px 0;min-height:30px;align-items:center}
125
+ .citem .sub{display:block;font-size:12px;color:var(--quiet)}
126
+ .selpane{flex:1 0 340px;padding:12px 14px;display:flex;flex-direction:column;gap:10px;min-width:300px}
127
+ .watch{display:flex;flex-direction:column;gap:8px}
128
+ .watch .h{font-size:12px;font-weight:600;letter-spacing:.06em;color:var(--quiet)}
129
+ .pins{display:flex;flex-wrap:wrap;gap:8px}
130
+ .pin{display:flex;gap:8px;align-items:flex-start;padding:10px 12px;border:1px solid var(--line-strong);background:var(--card);border-radius:8px;min-width:240px;max-width:360px}
131
+ .pin .t{display:flex;flex-direction:column;gap:2px;font-size:13px;min-width:0}
132
+ .tl{overflow-x:auto}
133
+ .tlin{min-width:760px}
134
+ .tlh,.tlr{display:flex}
135
+ .tll{flex:0 0 300px;display:flex;align-items:center;gap:4px;padding:0 8px 0 4px;border-right:1px solid var(--line);min-width:0}
136
+ @media (max-width:700px){.tll{flex-basis:200px}}
137
+ .tlg{flex:1 1 auto;position:relative;min-height:40px}
138
+ .tlh{border-bottom:1px solid var(--line);background:var(--rail)}
139
+ .tlh .tll{font-size:12px;font-weight:600;letter-spacing:.06em;color:var(--quiet);padding:10px}
140
+ .tick{position:absolute;top:0;height:40px;display:flex;align-items:center;padding-left:4px;font-size:11px;color:var(--muted);border-left:1px solid var(--line)}
141
+ .tlr{border-bottom:1px solid var(--line-soft);min-height:40px}
142
+ .tlr.sel{background:var(--sel-bg)}.tlr.dim{opacity:.42}
143
+ .tlr .title{font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1 1 auto}
144
+ .b{position:absolute;top:9px;height:22px;border-radius:4px;font-size:11px;line-height:20px;padding:0 6px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;margin-left:1px}
145
+ .b.parent{top:13px;height:14px;line-height:12px}
146
+ .b.done{background:var(--done-bg);border:1px solid var(--done);color:var(--done-ink)}
147
+ .b.going{background:var(--go-bg);border:1px solid var(--go);color:var(--link-ink)}
148
+ .b.planned{background:var(--card);border:1.5px solid var(--go);color:var(--link-ink)}
149
+ .b.estimate{background:repeating-linear-gradient(135deg,var(--est-a) 0 4px,var(--est-b) 4px 8px);border:1px solid var(--est-line);color:var(--body)}
150
+ .m-ask{position:absolute;top:14px;width:11px;height:11px;transform:rotate(45deg);background:var(--you-mark);margin-left:-5px}
151
+ .m-wait{position:absolute;top:32px;height:4px;border-radius:2px;background:var(--you-bar)}
152
+ .m-dead{position:absolute;top:4px;bottom:4px;width:2px;background:var(--danger)}
153
+ .today{position:absolute;top:0;bottom:0;width:2px;background:var(--you-mark);pointer-events:none}
154
+ .legend{display:flex;flex-wrap:wrap;gap:14px;font-size:12px;color:var(--body);align-items:center}
155
+ .legend span{display:flex;align-items:center;gap:6px}
156
+ .sw{width:26px;height:12px;border-radius:3px;display:inline-block}
157
+ .toast{position:fixed;left:50%;bottom:20px;transform:translateX(-50%);z-index:50;background:var(--ink);color:var(--card);padding:10px 14px;border-radius:8px;box-shadow:var(--menu-shadow);max-width:min(92vw,560px);display:flex;flex-direction:column;gap:6px;font-size:13px}
158
+ .toast textarea{width:100%;min-height:64px;font:12px "IBM Plex Mono",monospace;color:var(--ink);background:var(--card);border-radius:6px;border:0;padding:6px}
159
+ .foot{font-size:12px;color:var(--quiet)}
160
+ .needlist .entry{cursor:pointer}
161
+ .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}
162
+ </style>
163
+ </head>
164
+ <body>
165
+ <div class="wrap" id="app"><div class="notice">Loading the tracker…</div></div>
166
+ <script type="module">
167
+ // ---- The view rules, from packages/atlas/work/lib/view-rules.mjs ----
168
+ /*@@VIEW_RULES@@*/
169
+ // ---- The page ----
170
+ const PRODUCT = "@@PRODUCT@@";
171
+ const PRODUCT_NAME = "@@PRODUCT_NAME@@";
172
+ const ATLAS_VERSION = "@@ATLAS_VERSION@@";
173
+ const SERVER = 'Engram';
174
+ const PAGE = 200, MAX_RECORDS = 2000;
175
+ // DAY comes from the view rules above.
176
+ const VIEWS = [['outline', 'E1 · Outline'], ['detail', 'E3 · Outline and detail'], ['columns', 'E2 · Columns'], ['timeline', 'Timeline']];
177
+
178
+ const store = { get(key) { try { return localStorage.getItem(`atlas-view:${PRODUCT}:${key}`); } catch { return null; } }, set(key, value) { try { localStorage.setItem(`atlas-view:${PRODUCT}:${key}`, value); } catch {} } };
179
+ const S = { view: VIEWS.some(([key]) => key === store.get('view')) ? store.get('view') : 'outline', sel: null, closed: {}, opened: {}, showClosed: false, stack: [], folded: {}, tabs: {}, path: [], pins: [], menu: null, needs: false, model: null, error: null, readAt: null, loading: true, cut: false };
180
+
181
+ // ---- Small DOM helpers. Every text from the tracker goes in as text. ----
182
+ function h(tag, attrs, ...kids) {
183
+ const el = document.createElement(tag);
184
+ for (const [key, value] of Object.entries(attrs ?? {})) {
185
+ if (value === undefined || value === null || value === false) continue;
186
+ if (key === 'class') el.className = value;
187
+ else if (key === 'style') el.setAttribute('style', value);
188
+ else if (key.startsWith('on')) el.addEventListener(key.slice(2), value);
189
+ else el.setAttribute(key, value === true ? '' : String(value));
190
+ }
191
+ for (const kid of kids.flat(Infinity)) if (kid !== null && kid !== undefined && kid !== false) el.append(kid instanceof Node ? kid : document.createTextNode(String(kid)));
192
+ return el;
193
+ }
194
+ const set = (patch) => { Object.assign(S, patch); render(); };
195
+ const M = () => S.model;
196
+ const titleOf = (id) => M().items.get(id)?.title ?? id;
197
+ const short = (text, max = 42) => oneLine(text, max);
198
+ const dateOf = (value) => (value ? String(value).slice(0, 10) : '');
199
+
200
+ // ---- Reading the tracker through the Engram connector ----
201
+ function payloadOf(result) {
202
+ let value = result?.payload;
203
+ if (typeof value === 'string') {
204
+ // Engram adds a line after its JSON that marks the data as untrusted.
205
+ const cut = value.indexOf('<<<');
206
+ try { value = JSON.parse(cut >= 0 ? value.slice(0, cut) : value); } catch { value = null; }
207
+ }
208
+ if (value && Array.isArray(value.entities)) return value;
209
+ for (const block of result?.content ?? []) {
210
+ if (block?.type !== 'text') continue;
211
+ const text = String(block.text ?? ''); const cut = text.indexOf('<<<');
212
+ try { const parsed = JSON.parse(cut >= 0 ? text.slice(0, cut) : text); if (parsed && Array.isArray(parsed.entities)) return parsed; } catch {}
213
+ }
214
+ throw Object.assign(new Error('The connector answered in a shape this page does not read.'), { code: 'shape' });
215
+ }
216
+ async function load() {
217
+ set({ loading: true, error: null });
218
+ let mcp = null;
219
+ try { mcp = await window.claude?.use?.('mcp'); } catch { mcp = null; }
220
+ if (!mcp) { set({ loading: false, error: { code: 'no_mcp' } }); return; }
221
+ const records = []; let offset = 0, total = Infinity;
222
+ try {
223
+ while (offset < total && offset < MAX_RECORDS) {
224
+ const result = await mcp.callTool(SERVER, 'entities_list', { tag: `product:${PRODUCT}`, state: 'active', limit: PAGE, offset, order: 'created_at_asc' });
225
+ const page = payloadOf(result);
226
+ total = Number(page.total ?? page.entities.length);
227
+ records.push(...page.entities);
228
+ if (!page.entities.length) break;
229
+ offset += page.entities.length;
230
+ }
231
+ } catch (error) { set({ loading: false, error: { code: error?.code ?? 'upstream_error', message: error?.message ?? String(error) } }); return; }
232
+ const readAt = new Date().toISOString();
233
+ const model = buildModel(records, { product: PRODUCT, now: readAt });
234
+ const opened = {};
235
+ for (const id of model.groups.find((g) => g.key === 'active')?.ids ?? []) opened[id] = true;
236
+ set({ loading: false, model, readAt, cut: total > MAX_RECORDS, opened: Object.keys(S.opened).length ? S.opened : opened, stack: S.stack.filter((id) => model.items.has(id)) });
237
+ }
238
+
239
+ // ---- Copy a line for a thread. The page never writes to the tracker. ----
240
+ let toastTimer = null;
241
+ function toast(text, line) {
242
+ document.querySelector('.toast')?.remove();
243
+ const box = h('div', { class: 'toast', role: 'status' }, h('span', {}, text), line ? h('textarea', { readonly: true, 'aria-label': 'The line to paste in a thread' }, line) : null);
244
+ document.body.append(box);
245
+ clearTimeout(toastTimer); toastTimer = setTimeout(() => box.remove(), line ? 9000 : 3000);
246
+ }
247
+ async function copyLine(line) {
248
+ S.menu = null; render();
249
+ try { await navigator.clipboard.writeText(line); toast('Copied. Paste it in a thread, and the lead does it.'); }
250
+ catch { toast('Copy it from here, then paste it in a thread:', line); }
251
+ }
252
+ const ask = {
253
+ question: (id) => `${PRODUCT_NAME}: ask me a question on item ${id} (${titleOf(id)}): `,
254
+ link: (id) => `${PRODUCT_NAME}: add a link to item ${id} (${titleOf(id)}): `,
255
+ split: (id) => `${PRODUCT_NAME}: split item ${id} (${titleOf(id)}) into parts: `,
256
+ park: (id) => `${PRODUCT_NAME}: park item ${id} (${titleOf(id)}), because `,
257
+ id: (id) => `${titleOf(id)} · item ${id}`,
258
+ answer: (question, item, option) => `${PRODUCT_NAME}: answer question ${question} on item ${item} (${titleOf(item)}): ${option}`,
259
+ };
260
+
261
+ // ---- Shared pieces: the state of a row, its chips, the chain ----
262
+ function lightOf(id) {
263
+ if (!S.sel || !M().items.has(S.sel)) return '';
264
+ if (id === S.sel) return 'sel';
265
+ const chain = M().chainOf(S.sel);
266
+ if (chain.before.includes(id)) return 'before';
267
+ if (chain.after.includes(id)) return 'after';
268
+ return 'dim';
269
+ }
270
+ function focus(id) {
271
+ const patch = { sel: id, menu: null };
272
+ const opened = { ...S.opened };
273
+ for (const parent of M().ancestorsOf(id)) opened[parent] = true;
274
+ patch.opened = opened;
275
+ const group = M().groups.find((g) => g.ids.includes(M().ancestorsOf(id).at(-1) ?? id));
276
+ if (group?.folded) patch.showClosed = true;
277
+ if (S.view === 'detail' && !S.stack.includes(id)) patch.stack = [id, ...S.stack].slice(0, 3);
278
+ if (S.view === 'columns') patch.path = [...M().ancestorsOf(id).reverse(), id];
279
+ set(patch);
280
+ requestAnimationFrame(() => document.querySelector(`[data-id="${CSS.escape(id)}"]`)?.scrollIntoView({ block: 'nearest' }));
281
+ }
282
+ function statusBits(id, { compact = false } = {}) {
283
+ const st = M().stateOf(id);
284
+ const out = [];
285
+ if (st.kind === 'ready') out.push(h('span', { class: 'tag ready' }, 'Ready to start'));
286
+ if (st.kind === 'doing') out.push(h('span', { class: 'tag go' }, `Going on${st.stage ? ` · ${st.stage}` : ''}`));
287
+ if (st.questions.length) out.push(h('span', { class: 'tag you' }, `Waits on you${compact ? '' : `: ${short(st.questions[0].fields?.text ?? st.questions[0].title, 48)}`}`));
288
+ for (const row of st.blockers) out.push(h('button', { class: 'chip', onclick: (event) => { event.stopPropagation(); focus(row.item); }, title: `Blocked by ${row.title ?? row.item}. Click to light its chain.` }, `Blocked by ${short(row.title ?? row.item, compact ? 22 : 40)}`));
289
+ if (st.waitsWith) out.push(h('span', { class: 'tag with' }, `waits with ${short(titleOf(st.waitsWith), 32)}`));
290
+ if (st.kind === 'paused') out.push(h('span', { class: 'tag' }, 'Paused'));
291
+ if (st.kind === 'done') out.push(h('span', { class: 'tag done' }, 'Done'));
292
+ if (st.kind === 'dropped') out.push(h('span', { class: 'tag' }, 'Dropped'));
293
+ if (!compact && st.blocks.length && !['done', 'dropped'].includes(st.kind)) out.push(h('span', { class: 'tag' }, `Blocks ${st.blocks.length === 1 ? short(st.blocks[0].title, 30) : `${st.blocks.length} items`}`));
294
+ return out;
295
+ }
296
+ const dotOf = (id) => { const st = M().stateOf(id); return h('span', { class: `dot ${st.lifecycle} ${st.kind === 'blocked' || st.kind === 'waits-with' ? 'blocked' : ''}`, 'aria-hidden': 'true' }); };
297
+ const indentOf = (depth) => h('span', { class: 'indent', style: `width:${depth * 20}px;background:repeating-linear-gradient(90deg,transparent 0 13px,var(--line) 13px 14px,transparent 14px 20px)` });
298
+
299
+ function menuFor(id, where, extra = []) {
300
+ const key = `${where}:${id}`;
301
+ const open = S.menu === key;
302
+ const button = h('button', { class: 'dots', 'aria-label': `Actions for ${titleOf(id)}`, 'aria-haspopup': 'menu', 'aria-expanded': open ? 'true' : 'false', title: 'Actions', onclick: (event) => { event.stopPropagation(); set({ menu: open ? null : key }); } }, h('span', { 'aria-hidden': 'true' }, '⋯'));
303
+ if (!open) return button;
304
+ const item = (label, fn, hint) => h('button', { role: 'menuitem', onclick: (event) => { event.stopPropagation(); fn(); } }, h('span', {}, label), hint ? h('span', { class: 'k' }, hint) : null);
305
+ const viewItems = extra.length ? extra : [item('Light its chain', () => focus(id)), item('Open in E3', () => { set({ view: 'detail' }); focus(id); })];
306
+ const menu = h('div', { class: 'menu', role: 'menu', style: 'right:0;top:36px', onclick: (event) => event.stopPropagation() },
307
+ h('span', { class: 'h' }, 'View'), viewItems, h('hr'),
308
+ h('span', { class: 'h' }, 'Add · copies a line'), item('Ask a question', () => copyLine(ask.question(id))), item('Add a link', () => copyLine(ask.link(id))), item('Split into parts', () => copyLine(ask.split(id))), h('hr'),
309
+ h('span', { class: 'h' }, 'Item · copies a line'), item('Park', () => copyLine(ask.park(id))), item('Copy item id', () => copyLine(ask.id(id))));
310
+ return h('span', { class: 'pr' }, button, menu);
311
+ }
312
+
313
+ // ---- E1 and the outline half of E3: one tree ----
314
+ function visibleIds() {
315
+ const out = [];
316
+ const walk = (id, depth) => { out.push([id, depth]); if (S.opened[id]) for (const kid of M().childrenOf(id)) walk(kid, depth + 1); };
317
+ for (const group of M().groups) if (!group.folded || S.showClosed) for (const id of group.ids) walk(id, 0);
318
+ return out;
319
+ }
320
+ function outline({ withAdd = false } = {}) {
321
+ const box = h('div', { class: 'card', style: 'overflow:hidden' });
322
+ for (const group of M().groups) {
323
+ if (!group.ids.length) continue;
324
+ const shut = group.folded && !S.showClosed;
325
+ box.append(h('button', { class: 'group', 'aria-expanded': shut ? 'false' : 'true', onclick: () => group.folded && set({ showClosed: !S.showClosed }) }, h('span', {}, `${group.folded ? (shut ? '▸ ' : '▾ ') : ''}${group.name.toUpperCase()}`), h('span', {}, String(group.ids.length))));
326
+ if (shut) continue;
327
+ const walk = (id, depth) => {
328
+ const kids = M().childrenOf(id);
329
+ const light = lightOf(id);
330
+ box.append(h('div', { class: `row ${light}`, 'data-id': id },
331
+ indentOf(depth),
332
+ h('button', { class: `caret ${kids.length ? '' : 'none'}`, 'aria-label': S.opened[id] ? 'Close the parts' : 'Open the parts', 'aria-expanded': kids.length ? String(!!S.opened[id]) : null, onclick: () => set({ opened: { ...S.opened, [id]: !S.opened[id] } }) }, S.opened[id] ? '▾' : '▸'),
333
+ dotOf(id),
334
+ h('span', { class: 'body' }, h('button', { class: `title l${Math.min(depth, 2)} ${['blocked', 'waits-with'].includes(M().stateOf(id).kind) ? 'grey' : ''}`, onclick: () => (S.view === 'detail' ? set({ sel: id, stack: [id, ...S.stack.filter((x) => x !== id)].slice(0, 3) }) : set({ sel: S.sel === id ? null : id })) }, M().items.get(id).title), statusBits(id)),
335
+ kids.length ? h('span', { class: 'count' }, `${kids.length} part${kids.length === 1 ? '' : 's'}`) : null,
336
+ withAdd ? h('button', { class: 'dots', 'aria-label': 'Open this item as well', title: 'Open as well', onclick: () => set({ stack: [...S.stack.filter((x) => x !== id), id].slice(-3) }) }, '+') : menuFor(id, 'row')));
337
+ if (S.opened[id]) for (const kid of kids) walk(kid, depth + 1);
338
+ };
339
+ for (const id of group.ids) walk(id, 0);
340
+ }
341
+ return box;
342
+ }
343
+ function chainBanner() {
344
+ if (!S.sel || !M().items.has(S.sel)) return null;
345
+ const chain = M().chainOf(S.sel);
346
+ const names = (ids) => (ids.length ? ids.map((id) => short(titleOf(id), 36)).join(', ') : 'Nothing');
347
+ return h('div', { class: 'banner' }, h('b', {}, `Chain of ${short(titleOf(S.sel), 60)}`), h('span', {}, h('span', { class: 'k-before' }, 'Before it: '), names(chain.before)), h('span', {}, h('span', { class: 'k-after' }, 'Waits on it: '), names(chain.after)), h('button', { class: 'ghost', onclick: () => set({ sel: null }) }, 'Clear'));
348
+ }
349
+
350
+ // ---- E3: the detail stack ----
351
+ const TABS = [['questions', 'Questions'], ['decisions', 'Decisions'], ['parts', 'Parts'], ['links', 'Links'], ['delivery', 'Delivery'], ['history', 'History']];
352
+ function facts(d) {
353
+ const st = M().stateOf(d.id);
354
+ const rows = [['Lifecycle', d.lifecycle], ['Step', d.stage || '—'], ['Size', d.tier || '—'], ['Driver', d.driver || '—'], ['Next step', d.next_step || '—']];
355
+ if (d.waits_on && d.waits_on.on && d.waits_on.on !== 'nothing') rows.push(['Waits on', `${d.waits_on.on}${d.waits_on.note ? `: ${d.waits_on.note}` : ''}`]);
356
+ rows.push(['Starts', st.blockers.length ? statusBits(d.id).filter((el) => el.classList?.contains('chip')) : (st.waitsWith ? `with ${titleOf(st.waitsWith)}` : (st.kind === 'ready' ? 'Ready to start' : '—'))]);
357
+ if (d.plan && (d.plan.start || d.plan.end || d.plan.deadline)) rows.push(['Planned', [d.plan.start && `start ${d.plan.start}`, d.plan.end && `end ${d.plan.end}`, d.plan.deadline && `deadline ${d.plan.deadline}`].filter(Boolean).join(' · ')]);
358
+ if (d.origin?.text) rows.push(['Your words', `"${short(d.origin.text, 240)}" (${d.origin.date ?? ''})`]);
359
+ rows.push(['Made', dateOf(d.created_at)]);
360
+ if (d.locked_at) rows.push(['Locked', dateOf(d.locked_at)]);
361
+ if (d.delivered_at) rows.push(['Delivered', dateOf(d.delivered_at)]);
362
+ rows.push(['Item id', d.id]);
363
+ return h('div', { class: 'facts' }, rows.map(([k, v]) => [h('span', {}, k), h('span', { style: 'overflow-wrap:anywhere' }, v)]));
364
+ }
365
+ function entries(d, tab) {
366
+ const list = h('div', { class: 'entries' });
367
+ const entry = (k, meta, v, more) => list.append(h('div', { class: 'entry' }, h('div', { class: 'eh' }, h('b', { style: 'color:var(--ink);font-weight:500' }, k), h('span', {}, meta)), v ? h('div', { class: 'ev' }, v) : null, more));
368
+ if (tab === 'questions') for (const q of d.questions) {
369
+ const opts = Array.isArray(q.fields?.context?.options) ? q.fields.context.options : [];
370
+ entry(q.fields?.status === 'open' ? 'Open question' : 'Answered', `${q.fields?.cost ?? ''} · asked ${dateOf(q.fields?.asked_at)}`, q.fields?.text ?? q.title,
371
+ q.fields?.status === 'open' && opts.length ? h('div', { class: 'opts' }, opts.map((opt) => h('button', { class: 'chip', onclick: () => copyLine(ask.answer(q.id, d.id, opt)), title: 'Copies your answer for a thread' }, short(opt, 60)))) : null);
372
+ }
373
+ if (tab === 'decisions') for (const row of d.decisions) entry(row.fields?.kind ?? 'decision', row.fields?.date ?? '', row.fields?.text ?? row.title, row.fields?.source ? h('div', { class: 'path' }, row.fields.source) : null);
374
+ if (tab === 'parts') for (const id of d.parts) list.append(h('div', { class: 'entry', style: 'flex-direction:row;align-items:center;gap:8px' }, dotOf(id), h('button', { class: 'title', onclick: () => focus(id) }, titleOf(id)), statusBits(id, { compact: true })));
375
+ if (tab === 'links') for (const link of d.links) entry(`${link.kind ?? 'link'}${link.role ? ` · ${link.role}` : ''}`, dateOf(link.added_at), link.note || '', h('a', { href: link.url, target: '_blank', rel: 'noopener noreferrer', style: 'overflow-wrap:anywhere' }, link.url));
376
+ if (tab === 'delivery') for (const mark of d.marks) entry(mark.fields?.verdict ?? mark.fields?.kind ?? 'mark', dateOf(mark.fields?.marked_at), (mark.fields?.prs ?? []).map((pr) => pr.url).join('\n') || mark.fields?.reason || '');
377
+ if (tab === 'history') for (const edit of d.edits) entry(edit.by ?? 'edit', dateOf(edit.at), edit.reason ?? '');
378
+ if (!list.children.length) list.append(h('div', { class: 'empty' }, 'Nothing here yet.'));
379
+ return list;
380
+ }
381
+ function panel(id, index) {
382
+ const d = M().detailOf(id);
383
+ const folded = !!S.folded[id];
384
+ const counts = { questions: d.questions.length, decisions: d.decisions.length, parts: d.parts.length, links: d.links.length, delivery: d.marks.length, history: d.edits.length };
385
+ const tab = S.tabs[id] ?? (counts.questions ? 'questions' : 'parts');
386
+ const close = () => set({ stack: S.stack.filter((x) => x !== id), menu: null });
387
+ const parent = M().parentOf(id);
388
+ const extra = [
389
+ h('button', { role: 'menuitem', onclick: () => set({ stack: [id], menu: null }) }, h('span', {}, 'Open alone'), h('span', { class: 'k' }, 'O')),
390
+ parent ? h('button', { role: 'menuitem', onclick: () => set({ stack: [...S.stack.filter((x) => x !== parent), parent].slice(-3), menu: null }) }, h('span', {}, 'Open the parent too'), h('span', { class: 'k' }, 'P')) : null,
391
+ h('button', { role: 'menuitem', onclick: () => { focus(id); } }, h('span', {}, 'Light its chain')),
392
+ h('button', { role: 'menuitem', onclick: close }, h('span', {}, 'Close'), h('span', { class: 'k' }, 'Esc')),
393
+ ].filter(Boolean);
394
+ return h('section', { class: 'panel', 'aria-label': d.title },
395
+ h('div', { class: 'phead' },
396
+ h('div', { class: 'pl' }, h('span', { class: 'badge' }, 'ABC'[index]), h('div', {}, d.path.length ? h('div', { class: 'path' }, d.path.join(' / ')) : null, h('h2', {}, d.title), h('div', { class: 'body', style: 'margin-top:4px' }, statusBits(id)))),
397
+ h('div', { class: 'pr' }, h('button', { class: 'dots', 'aria-label': folded ? 'Unfold this panel' : 'Fold this panel', title: 'Fold', onclick: () => set({ folded: { ...S.folded, [id]: !folded } }) }, folded ? '▸' : '▾'), menuFor(id, 'panel', extra))),
398
+ folded ? h('div', { class: 'fold1' }, d.next_step ? `Next: ${short(d.next_step, 140)}` : `${d.lifecycle}${d.stage ? ` · ${d.stage}` : ''}`) : [
399
+ facts(d),
400
+ h('div', { class: 'tabs', role: 'group', 'aria-label': 'Records of this item' }, TABS.map(([key, label]) => h('button', { 'aria-pressed': String(tab === key), onclick: () => set({ tabs: { ...S.tabs, [id]: key } }) }, `${label} ${counts[key]}`))),
401
+ entries(d, tab),
402
+ ]);
403
+ }
404
+ function detailView() {
405
+ const stack = S.stack.filter((id) => M().items.has(id));
406
+ return h('div', { class: 'split' }, outline({ withAdd: true }),
407
+ h('div', { class: 'stack' }, stack.length ? stack.map(panel) : h('div', { class: 'notice' }, h('b', {}, 'Click a row to read it.'), h('span', {}, 'Press + on a row to open it as well, up to three. Fold a panel to one line to keep several in view.'))));
408
+ }
409
+
410
+ // ---- E2: columns ----
411
+ function columnsView() {
412
+ const path = S.path.filter((id) => M().items.has(id));
413
+ const cols = [];
414
+ const live = M().groups.filter((g) => !g.folded).flatMap((g) => g.ids);
415
+ const closed = M().groups.filter((g) => g.folded).flatMap((g) => g.ids);
416
+ cols.push({ title: PRODUCT_NAME.toUpperCase(), ids: S.showClosed ? [...live, ...closed] : live, foot: closed.length });
417
+ path.forEach((id) => { const kids = M().childrenOf(id); if (kids.length) cols.push({ title: short(titleOf(id), 30).toUpperCase(), ids: kids }); });
418
+ const sel = path.at(-1) ?? null;
419
+ const wrap = h('div', { class: 'card cols' });
420
+ cols.forEach((col, level) => {
421
+ const box = h('div', { class: 'col' }, h('div', { class: 'colh' }, h('span', {}, col.title), h('span', {}, String(col.ids.length))));
422
+ for (const id of col.ids) {
423
+ const kids = M().childrenOf(id).length;
424
+ const onPath = path[level] === id;
425
+ const light = lightOf(id);
426
+ box.append(h('div', { class: `citem ${onPath ? 'sel' : ''} ${light === 'dim' ? 'dim' : ''}`, 'data-id': id },
427
+ h('button', { onclick: () => set({ path: [...path.slice(0, level), id], sel: id }) }, h('span', {}, h('span', {}, M().items.get(id).title), h('span', { class: 'sub' }, `${M().stateOf(id).lifecycle}${kids ? ` · ${kids} part${kids === 1 ? '' : 's'}` : ''}`)), h('span', { 'aria-hidden': 'true' }, kids ? '›' : '')),
428
+ h('span', { class: 'body' }, statusBits(id, { compact: true }))));
429
+ }
430
+ if (col.foot && level === 0) box.append(h('button', { class: 'group', onclick: () => set({ showClosed: !S.showClosed }) }, h('span', {}, S.showClosed ? 'Hide done and dropped' : `Show done and dropped (${col.foot})`)));
431
+ wrap.append(box);
432
+ });
433
+ if (sel) {
434
+ const d = M().detailOf(sel);
435
+ const pinned = S.pins.includes(sel);
436
+ const extra = [
437
+ h('button', { role: 'menuitem', onclick: () => set({ pins: pinned ? S.pins.filter((x) => x !== sel) : [...S.pins, sel], menu: null }) }, h('span', {}, pinned ? 'Stop watching' : 'Watch below')),
438
+ h('button', { role: 'menuitem', onclick: () => { set({ view: 'detail', stack: [sel, ...S.stack.filter((x) => x !== sel)].slice(0, 3), menu: null }); } }, h('span', {}, 'Open in E3')),
439
+ h('button', { role: 'menuitem', onclick: () => focus(sel) }, h('span', {}, 'Light its chain')),
440
+ ];
441
+ wrap.append(h('div', { class: 'selpane' },
442
+ h('div', { style: 'display:flex;justify-content:space-between;gap:8px' }, h('div', {}, h('div', { class: 'path' }, 'Selected'), h('h2', { style: 'margin:0;font-family:"Source Serif 4",Georgia,serif;font-size:19px' }, d.title)), menuFor(sel, 'pane', extra)),
443
+ h('div', { class: 'body' }, statusBits(sel)), facts(d),
444
+ d.questions.filter((q) => q.fields?.status === 'open').length ? h('div', {}, h('div', { class: 'path' }, 'OPEN QUESTIONS'), d.questions.filter((q) => q.fields?.status === 'open').map((q) => h('div', { class: 'entry' }, q.fields?.text ?? q.title))) : null));
445
+ } else wrap.append(h('div', { class: 'selpane' }, h('div', { class: 'empty' }, 'Pick an item. Each level opens a new column to the right.')));
446
+ const out = [wrap];
447
+ if (S.pins.length) out.push(h('div', { class: 'watch' }, h('span', { class: 'h' }, 'WATCHING · SEVERAL ITEMS AT ONCE'), h('div', { class: 'pins' }, S.pins.filter((id) => M().items.has(id)).map((id) => {
448
+ const d = M().detailOf(id); const q = d.questions.find((row) => row.fields?.status === 'open');
449
+ return h('div', { class: 'pin' }, h('span', { class: 't' }, h('b', {}, d.title), h('span', { class: 'path' }, `${d.lifecycle}${d.stage ? ` · ${d.stage}` : ''}`), h('span', {}, `Next: ${short(d.next_step || '—', 120)}`), q ? h('span', { class: 'tag you' }, `? ${short(q.fields?.text, 80)}`) : null), h('button', { class: 'dots', 'aria-label': 'Stop watching', onclick: () => set({ pins: S.pins.filter((x) => x !== id) }) }, '×'));
450
+ }))));
451
+ return out;
452
+ }
453
+
454
+ // ---- Timeline ----
455
+ function timelineView() {
456
+ const rows = visibleIds().filter(([id]) => M().spanOf(id).bars.length || M().childrenOf(id).length || M().stateOf(id).lifecycle === 'paused');
457
+ const now = Date.parse(M().now);
458
+ const spans = rows.map(([id]) => M().spanOf(id));
459
+ let from = Math.min(now - 7 * DAY, ...spans.map((s) => s.from));
460
+ from = Math.max(from, now - 30 * DAY);
461
+ let to = Math.max(now + 7 * DAY, ...spans.map((s) => s.to)) + DAY;
462
+ to = Math.min(to, now + 120 * DAY);
463
+ from = Math.floor(from / DAY) * DAY; to = Math.ceil(to / DAY) * DAY;
464
+ const pct = (at) => (Math.min(Math.max(at, from), to) - from) / (to - from) * 100;
465
+ const days = (to - from) / DAY;
466
+ const step = days <= 21 ? 1 : days <= 45 ? 3 : days <= 90 ? 7 : 14;
467
+ const ticks = [];
468
+ for (let at = from; at < to; at += step * DAY) { if (Math.abs(pct(at) - pct(now)) < 3) continue; const d = new Date(at); ticks.push(h('span', { class: 'tick', style: `left:${pct(at)}%` }, d.getUTCDate() === 1 || at === from || step >= 7 ? d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', timeZone: 'UTC' }) : String(d.getUTCDate()))); }
469
+ const label = (bar) => ({ done: 'Done', going: 'going on', planned: 'planned', estimate: 'estimate' })[bar.kind];
470
+ const body = h('div', { class: 'tlin' }, h('div', { class: 'tlh' }, h('div', { class: 'tll' }, 'ITEMS AND PARTS'), h('div', { class: 'tlg', style: 'height:40px' }, ticks)));
471
+ for (const [id, depth] of rows) {
472
+ const span = M().spanOf(id);
473
+ const kids = M().childrenOf(id);
474
+ const light = lightOf(id);
475
+ const bars = span.bars.map((bar) => { const width = Math.max(pct(bar.to) - pct(bar.from), 0.8); return h('span', { class: `b ${bar.kind}`, style: `left:${pct(bar.from)}%;width:calc(${width}% - 2px)`, title: `${label(bar)}: ${dateOf(new Date(bar.from).toISOString())} to ${dateOf(new Date(bar.to).toISOString())}` }, width >= 6 ? label(bar) : ''); });
476
+ if (!span.bars.length && kids.length && span.to > span.from) bars.push(h('span', { class: 'b parent estimate', style: `left:${pct(span.from)}%;width:calc(${Math.max(pct(span.to) - pct(span.from), 0.8)}% - 2px)`, title: 'Its parts' }));
477
+ if (!span.bars.length && M().stateOf(id).lifecycle === 'paused') bars.push(h('span', { class: 'tag', style: `position:absolute;top:10px;left:${pct(now)}%;margin-left:6px` }, 'Paused, no date'));
478
+ for (const mark of span.marks) {
479
+ if (mark.kind === 'asked') { bars.push(h('span', { class: 'm-ask', style: `left:${pct(mark.at)}%`, title: `Question asked ${dateOf(new Date(mark.at).toISOString())}` })); bars.push(h('span', { class: 'm-wait', style: `left:${pct(mark.at)}%;width:${pct(mark.until) - pct(mark.at)}%`, title: 'Waits on you' })); }
480
+ if (mark.kind === 'deadline') bars.push(h('span', { class: 'm-dead', style: `left:${pct(mark.at + DAY)}%`, title: `Deadline ${dateOf(new Date(mark.at).toISOString())}` }));
481
+ }
482
+ const st = M().stateOf(id);
483
+ body.append(h('div', { class: `tlr ${light}`, 'data-id': id },
484
+ h('div', { class: 'tll' }, indentOf(depth),
485
+ h('button', { class: `caret ${kids.length ? '' : 'none'}`, 'aria-label': S.opened[id] ? 'Close the parts' : 'Open the parts', onclick: () => set({ opened: { ...S.opened, [id]: !S.opened[id] } }) }, S.opened[id] ? '▾' : '▸'),
486
+ h('button', { class: `title l${Math.min(depth, 2)} ${st.blockers.length ? 'grey' : ''}`, onclick: () => set({ sel: S.sel === id ? null : id }), title: M().items.get(id).title }, M().items.get(id).title),
487
+ st.blockers.slice(0, 1).map((row) => h('button', { class: 'chip', style: 'font-size:11px;min-height:22px', onclick: () => focus(row.item), title: `Blocked by ${row.title}` }, `after ${short(row.title ?? row.item, 14)}`))),
488
+ h('div', { class: 'tlg' }, bars)));
489
+ }
490
+ const grid = h('div', { class: 'card tl' }, h('div', { style: 'position:relative' }, body));
491
+ const todayWrap = body.querySelector('.tlh');
492
+ body.style.position = 'relative';
493
+ const line = h('span', { class: 'today', style: `left:calc(300px + (100% - 300px) * ${pct(now) / 100})` });
494
+ body.append(line);
495
+ if (todayWrap) todayWrap.querySelector('.tlg').append(h('span', { class: 'tick', style: `left:${pct(now)}%;color:var(--you-mark);font-weight:600;border-left:2px solid var(--you-mark)` }, 'Today'));
496
+ const legend = h('div', { class: 'legend' },
497
+ h('span', {}, h('i', { class: 'sw', style: 'background:var(--done-bg);border:1px solid var(--done)' }), 'Done'),
498
+ h('span', {}, h('i', { class: 'sw', style: 'background:var(--go-bg);border:1px solid var(--go)' }), 'Going on'),
499
+ h('span', {}, h('i', { class: 'sw', style: 'background:var(--card);border:1.5px solid var(--go)' }), 'Your planned dates'),
500
+ h('span', {}, h('i', { class: 'sw', style: 'background:repeating-linear-gradient(135deg,var(--est-a) 0 4px,var(--est-b) 4px 8px);border:1px solid var(--est-line)' }), `Estimate from the size: hotfix ${ESTIMATE_DAYS.hotfix} day, standard ${ESTIMATE_DAYS.standard}, initiative ${ESTIMATE_DAYS.initiative}`),
501
+ h('span', {}, h('i', { class: 'sw', style: 'height:4px;background:var(--you-bar)' }), 'Waits on you'),
502
+ h('span', {}, h('i', { style: 'width:10px;height:10px;transform:rotate(45deg);background:var(--you-mark);display:inline-block' }), 'Question asked'));
503
+ return [grid, legend];
504
+ }
505
+
506
+ // ---- Needs you ----
507
+ function needsPanel() {
508
+ const rows = M().needsYou;
509
+ return h('div', { class: 'card needlist' }, h('div', { class: 'colh' }, h('span', {}, `NEEDS YOU · ${rows.length}`), h('button', { class: 'ghost', onclick: () => set({ needs: false }) }, 'Close')),
510
+ rows.length ? rows.map((row, index) => h('div', { class: 'entry', role: 'button', tabindex: '0', onclick: () => { set({ needs: false, view: 'detail', stack: [row.item.id, ...S.stack.filter((x) => x !== row.item.id)].slice(0, 3), tabs: { ...S.tabs, [row.item.id]: 'questions' } }); focus(row.item.id); }, onkeydown: (event) => { if (event.key === 'Enter') event.currentTarget.click(); } },
511
+ h('div', { class: 'ev' }, h('b', {}, `${index + 1}. `), short(row.question.fields?.text ?? row.question.title, 200)),
512
+ h('div', { class: 'eh' }, h('span', {}, `${short(row.item.title, 80)} · ${row.lifecycle}`), h('span', {}, `${row.cost}: ${COST_MEANING[row.cost] ?? ''} · asked ${dateOf(row.asked_at)}`)))) : h('div', { class: 'empty' }, 'Nothing needs you: no open question on a proposed, active or paused item.'));
513
+ }
514
+
515
+ // ---- Errors: say which, and how to fix it ----
516
+ function errorView(error) {
517
+ const fix = {
518
+ no_mcp: ['This page cannot reach your connectors here.', 'Open it on claude.ai, as a page of your own. A saved copy or a preview cannot read the tracker.'],
519
+ server_not_connected: ['The Engram connector is not connected.', 'Add Engram in claude.ai, Settings, Connectors. Then reload this page.'],
520
+ needs_reauth: ['Your Engram sign-in has lapsed.', 'Reconnect Engram in claude.ai, Settings, Connectors. Then reload this page.'],
521
+ not_in_manifest: ['This page is not allowed to read Engram.', 'Allow Engram for this page when claude.ai asks, then reload. The page asks only for two read tools.'],
522
+ consent_required: ['This page is not allowed to read Engram yet.', 'Reload, and allow Engram when claude.ai asks.'],
523
+ not_granted: ['This page is not allowed to read Engram.', 'Reload, and allow Engram when claude.ai asks.'],
524
+ selection_required: ['You have more than one Engram connector.', 'Pick one when claude.ai asks, then reload.'],
525
+ blocked_by_policy: ['A policy blocks this page from Engram.', 'Ask the person who runs your claude.ai settings.'],
526
+ server_unavailable: ['Engram did not answer.', 'Wait a moment and reload. Engram may be restarting.'],
527
+ shape: ['Engram answered in a shape this page does not read.', 'Rebuild the page with the newest Atlas: atlas view build.'],
528
+ }[error.code] ?? ['The tracker could not be read.', error.message ?? 'Reload the page to try again.'];
529
+ return h('div', { class: 'notice bad', role: 'alert' }, h('b', {}, fix[0]), h('span', {}, fix[1]), h('span', { class: 'foot' }, `Code: ${error.code}. The page shows nothing rather than an empty view that looks true.`), h('div', {}, h('button', { class: 'ghost', onclick: load }, 'Reload')));
530
+ }
531
+
532
+ function render() {
533
+ const app = document.getElementById('app');
534
+ const scroll = window.scrollY;
535
+ const head = h('header', { class: 'bar' },
536
+ h('span', { class: 'brand' }, h('b', {}, 'Atlas'), h('span', { class: 'sep' }, '/'), h('span', {}, `Product: ${PRODUCT_NAME}`)),
537
+ S.model ? h('button', { class: `needs ${S.model.needsYou.length ? '' : 'zero'}`, 'aria-expanded': String(S.needs), onclick: () => set({ needs: !S.needs }) }, `Needs you · ${S.model.needsYou.length}`) : null);
538
+ const kids = [head];
539
+ if (S.error) kids.push(errorView(S.error));
540
+ else if (S.loading && !S.model) kids.push(h('div', { class: 'notice' }, 'Reading the tracker…'));
541
+ if (S.model) {
542
+ kids.push(h('div', { class: 'tools' },
543
+ h('nav', { class: 'switch', 'aria-label': 'View' }, VIEWS.map(([key, label]) => h('button', { 'aria-pressed': String(S.view === key), onclick: () => { store.set('view', key); set({ view: key, menu: null }); } }, label))),
544
+ h('span', { class: 'meta' }, S.loading ? 'Reading…' : `Read ${new Date(S.readAt).toLocaleString('en-GB', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' })}`, h('button', { class: 'ghost', onclick: load }, 'Refresh'))));
545
+ if (S.cut) kids.push(h('div', { class: 'notice' }, `This product has more than ${MAX_RECORDS} records. The page read the first ${MAX_RECORDS}.`));
546
+ if (S.needs) kids.push(needsPanel());
547
+ kids.push(chainBanner());
548
+ if (S.view === 'outline') kids.push(outline());
549
+ if (S.view === 'detail') kids.push(detailView());
550
+ if (S.view === 'columns') kids.push(columnsView());
551
+ if (S.view === 'timeline') kids.push(timelineView());
552
+ kids.push(h('div', { class: 'foot' }, `Read only. The ⋯ menu copies a line for a thread, and the lead writes. Built by Atlas ${ATLAS_VERSION}. ${S.model.all.length} items.`));
553
+ }
554
+ app.replaceChildren(...kids.flat().filter(Boolean));
555
+ window.scrollTo(0, scroll);
556
+ }
557
+ document.addEventListener('click', () => { if (S.menu) set({ menu: null }); });
558
+ document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && (S.menu || S.sel)) set(S.menu ? { menu: null } : { sel: null }); });
559
+ render();
560
+ load();
561
+ </script>
562
+ </body>
563
+ </html>