samagotchi 0.136.1 → 0.138.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +58 -1
- data/docs/architecture.md +2 -2
- data/docs/cli.md +15 -7
- data/docs/context.md +7 -9
- data/docs/guardrails.md +12 -5
- data/docs/internals/engine.md +4 -4
- data/docs/internals/participants.md +2 -1
- data/docs/internals/web-frontend.md +19 -9
- data/docs/plugins.md +2 -2
- data/docs/sessions.md +8 -8
- data/docs/testing.md +2 -1
- data/lib/samagotchi/bridge.rb +4 -0
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/desktop/macos.rb +3 -2
- data/lib/samagotchi/engine.rb +6 -1
- data/lib/samagotchi/guardrail_wiring.rb +3 -1
- data/lib/samagotchi/guardrails/chi_dirs.rb +2 -2
- data/lib/samagotchi/guardrails/mise_paths.rb +36 -0
- data/lib/samagotchi/guardrails/outside.rb +5 -5
- data/lib/samagotchi/guardrails/pair_boundary.rb +11 -4
- data/lib/samagotchi/guardrails/pair_command.rb +3 -19
- data/lib/samagotchi/guardrails/protected_paths.rb +5 -2
- data/lib/samagotchi/guardrails/review_paths.rb +3 -2
- data/lib/samagotchi/guardrails/shell_paths.rb +0 -4
- data/lib/samagotchi/guardrails/verdict.rb +1 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/message_display.rb +6 -3
- data/lib/samagotchi/web/participant_review.rb +2 -1
- data/lib/samagotchi/web/public/answer_fold.js +24 -6
- data/lib/samagotchi/web/public/app.js +24 -19
- data/lib/samagotchi/web/public/chains.js +6 -5
- data/lib/samagotchi/web/public/chip_popover.js +20 -42
- data/lib/samagotchi/web/public/composer.css +15 -11
- data/lib/samagotchi/web/public/composer_grip.js +11 -13
- data/lib/samagotchi/web/public/composer_mount.js +2 -2
- data/lib/samagotchi/web/public/context_chips.js +29 -98
- data/lib/samagotchi/web/public/copy.js +8 -5
- data/lib/samagotchi/web/public/data.js +2 -0
- data/lib/samagotchi/web/public/families_view.js +30 -38
- data/lib/samagotchi/web/public/index.html +176 -132
- data/lib/samagotchi/web/public/info_bar.js +19 -8
- data/lib/samagotchi/web/public/info_bar_model.js +3 -1
- data/lib/samagotchi/web/public/lists_view.js +69 -42
- data/lib/samagotchi/web/public/participants_panel.js +4 -4
- data/lib/samagotchi/web/public/popover_modal.js +57 -0
- data/lib/samagotchi/web/public/revise_stack.js +76 -6
- data/lib/samagotchi/web/public/revise_switch.js +55 -0
- data/lib/samagotchi/web/public/scope.js +3 -3
- data/lib/samagotchi/web/public/send_turn.js +4 -2
- data/lib/samagotchi/web/public/settings_chip.js +2 -2
- data/lib/samagotchi/web/public/stage_view.js +51 -2
- data/lib/samagotchi/web/public/stale_params.js +26 -0
- data/lib/samagotchi/web/public/start_settings.js +1 -1
- data/lib/samagotchi/web/public/start_toolkit.js +1 -1
- data/lib/samagotchi/web/public/stream_handlers.js +10 -5
- data/lib/samagotchi/web/public/working_set_rail.js +2 -0
- data/lib/samagotchi/web/public/working_set_view.js +2 -2
- data/lib/samagotchi/web/session_summary.rb +1 -1
- metadata +5 -1
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
// red for a failing refresh), and a popover with its summary, hint link,
|
|
4
4
|
// text and detach. The chips refresh when a context note arrives in the
|
|
5
5
|
// stream and when the page gets focus (no event of their own), and their
|
|
6
|
-
// ages tick every minute.
|
|
7
|
-
//
|
|
8
|
-
// attaches a URL (a GitHub PR); the web never adds a command.
|
|
6
|
+
// ages tick every minute. The web attaches nothing itself (`chi context
|
|
7
|
+
// add` does; GET …/context's can_add_url is left unread).
|
|
9
8
|
|
|
10
9
|
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
11
10
|
import { keepFocus } from "./keep_focus.js";
|
|
12
11
|
import { popoverPlace } from "./chip_popover.js";
|
|
12
|
+
import { focusBack, modalDialog } from "./popover_modal.js";
|
|
13
13
|
import { cantText } from "./format.js";
|
|
14
|
-
import { createLatest
|
|
14
|
+
import { createLatest } from "./latest.js";
|
|
15
15
|
|
|
16
16
|
// "now", "3m", "2h", "4d" since an ISO time; "" for none.
|
|
17
17
|
export function ageText(iso, now = Date.now()) {
|
|
@@ -44,30 +44,13 @@ export function chipModel(rows, now = Date.now()) {
|
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
const ADD_TITLE = "Attach a URL (a GitHub PR) the agent can read and hears about when it changes";
|
|
48
|
-
|
|
49
47
|
// The chips (a lit-html template; attributes after a bound one are bound
|
|
50
|
-
// too, so they keep their order).
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
export function chipsTemplate(chips, { addUrl = false, open = null, addOpen = false } = {}) {
|
|
48
|
+
// too, so they keep their order). open: the chip whose popover is open (a
|
|
49
|
+
// source's name).
|
|
50
|
+
export function chipsTemplate(chips, { open = null } = {}) {
|
|
54
51
|
return html`${chips.map((chip) => html`<button type="button" class=${`ctx-chip ${chip.state}${chip.name === open ? " open" : ""}`}
|
|
55
|
-
data-name=${chip.name} title=${chip.title} aria-haspopup=${"dialog"}>${
|
|
56
|
-
chip.state === "unread" ? html`<span class="ctx-dot" aria-hidden="true"></span>` : nothing}${chip.label}</button>`)}
|
|
57
|
-
addUrl ? html`<button type="button" class=${`ctx-chip ctx-add${addOpen ? " open" : ""}`} data-act=${"add"} title=${ADD_TITLE}
|
|
58
|
-
aria-haspopup=${"dialog"}>+ URL</button>` : nothing}`;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// The "+ URL" popover: the URL, an optional why, Attach; +error+ after a
|
|
62
|
-
// try that failed, +busy+ while one runs.
|
|
63
|
-
export function addFormTemplate({ error = null, busy = false } = {}) {
|
|
64
|
-
return html`<form class="ctx-add-form"><div class="ctx-pop-head"><strong>Attach a URL</strong
|
|
65
|
-
><button type="button" class="ctx-pop-close" data-act="close" aria-label="Close">×</button></div
|
|
66
|
-
><div class="ctx-pop-meta">A link an installed bundle knows (github-pr: a pull request). The agent gets a note when it changes.</div
|
|
67
|
-
><input class="ctx-add-input" name="url" type="url" inputmode="url" placeholder="https://github.com/owner/repo/pull/123" required autocomplete="off"
|
|
68
|
-
><input class="ctx-add-input" name="why" type="text" placeholder="why (optional)" maxlength="300" autocomplete="off"
|
|
69
|
-
>${error ? html`<div class="ctx-pop-error" role="alert">${error}</div>` : nothing
|
|
70
|
-
}<div class="ctx-pop-actions"><button type="submit" class="ctx-add-submit" ?disabled=${busy}>Attach</button></div></form>`;
|
|
52
|
+
data-name=${chip.name} title=${chip.title} aria-haspopup=${"dialog"} aria-expanded=${chip.name === open ? "true" : "false"}>${
|
|
53
|
+
chip.state === "unread" ? html`<span class="ctx-dot" aria-hidden="true"></span>` : nothing}${chip.label}</button>`)}`;
|
|
71
54
|
}
|
|
72
55
|
|
|
73
56
|
// The popover's inside for one row (GET …/context's); +text+ the row's
|
|
@@ -99,43 +82,36 @@ export function popoverTemplate(row, { text = null, now = Date.now() } = {}) {
|
|
|
99
82
|
}
|
|
100
83
|
|
|
101
84
|
// The chips for the session bar. el: #contextChips; api: {list(id),
|
|
102
|
-
// show(id, name), detach(id, name)
|
|
103
|
-
//
|
|
104
|
-
//
|
|
85
|
+
// show(id, name), detach(id, name)}; toast(text). Chips and popover are
|
|
86
|
+
// lit templates: a redraw (a refresh, the minute's tick) keeps their nodes,
|
|
87
|
+
// so focus stays. The
|
|
88
|
+
// popover behaves as every popover does (popover_modal.js): a modal dialog, its chip's
|
|
89
|
+
// aria-expanded, Esc closes it and focus goes back to the chip.
|
|
105
90
|
export function createContextChips({ el, api, toast = () => {}, doc = document, win = window }) {
|
|
106
91
|
let sessionId = null;
|
|
107
92
|
let rows = [];
|
|
108
|
-
let canAdd = false;
|
|
109
93
|
let openName = null;
|
|
110
94
|
// Only the newest list read for the open session is drawn (latest.js).
|
|
111
95
|
const latest = createLatest(() => sessionId);
|
|
112
|
-
// The open row's text once read (view text)
|
|
113
|
-
// state ({error, busy}).
|
|
96
|
+
// The open row's text once read (view text).
|
|
114
97
|
let text = null;
|
|
115
|
-
let addState = {};
|
|
116
98
|
const pop = doc.createElement("div");
|
|
117
99
|
pop.id = "contextPopover";
|
|
118
100
|
pop.className = "ctx-popover";
|
|
119
|
-
pop.setAttribute("role", "dialog");
|
|
120
101
|
pop.hidden = true;
|
|
121
102
|
doc.body.appendChild(pop);
|
|
122
|
-
|
|
123
|
-
// openName ADD: the "+ URL" form is open.
|
|
124
|
-
const ADD = "\u0000add";
|
|
103
|
+
modalDialog(pop, { isOpen: () => openName !== null, doc });
|
|
125
104
|
|
|
126
105
|
function drawChips() {
|
|
127
106
|
const chips = chipModel(rows);
|
|
128
107
|
// A source inserted before the focused chip: the focus stays on its source.
|
|
129
|
-
keepFocus(el, "data-name", () => render(chipsTemplate(chips, {
|
|
130
|
-
|
|
131
|
-
el.hidden = chips.length === 0 && !canAdd;
|
|
132
|
-
// The "+ URL" chip alone sits inline, not on a row of its own.
|
|
133
|
-
el.classList.toggle("only-add", chips.length === 0 && canAdd);
|
|
108
|
+
keepFocus(el, "data-name", () => render(chipsTemplate(chips, { open: openName }), el));
|
|
109
|
+
el.hidden = chips.length === 0;
|
|
134
110
|
}
|
|
135
111
|
|
|
136
112
|
// The chips, and the open popover closed when what it is for has gone.
|
|
137
113
|
function draw() {
|
|
138
|
-
if (openName
|
|
114
|
+
if (openName && !chipModel(rows).some((chip) => chip.name === openName)) close();
|
|
139
115
|
else drawChips();
|
|
140
116
|
}
|
|
141
117
|
|
|
@@ -147,16 +123,14 @@ export function createContextChips({ el, api, toast = () => {}, doc = document,
|
|
|
147
123
|
const data = await api.list(id);
|
|
148
124
|
if (!fresh()) return;
|
|
149
125
|
rows = data.sources || [];
|
|
150
|
-
canAdd = data.can_add_url === true;
|
|
151
126
|
} catch {
|
|
152
127
|
// A session the server doesn't know (gone, or an older chi web): no chips.
|
|
153
128
|
if (!fresh()) return;
|
|
154
129
|
rows = [];
|
|
155
|
-
canAdd = false;
|
|
156
130
|
}
|
|
157
131
|
draw();
|
|
158
132
|
// An open popover follows its row, but not while it shows the text.
|
|
159
|
-
if (openName &&
|
|
133
|
+
if (openName && text === null) {
|
|
160
134
|
const row = rows.find((r) => r.name === openName);
|
|
161
135
|
if (row) fill(row);
|
|
162
136
|
}
|
|
@@ -165,7 +139,6 @@ export function createContextChips({ el, api, toast = () => {}, doc = document,
|
|
|
165
139
|
function load(id) {
|
|
166
140
|
sessionId = id;
|
|
167
141
|
rows = [];
|
|
168
|
-
canAdd = false;
|
|
169
142
|
close();
|
|
170
143
|
return refresh();
|
|
171
144
|
}
|
|
@@ -173,7 +146,6 @@ export function createContextChips({ el, api, toast = () => {}, doc = document,
|
|
|
173
146
|
function clear() {
|
|
174
147
|
sessionId = null;
|
|
175
148
|
rows = [];
|
|
176
|
-
canAdd = false;
|
|
177
149
|
close();
|
|
178
150
|
}
|
|
179
151
|
|
|
@@ -208,62 +180,28 @@ export function createContextChips({ el, api, toast = () => {}, doc = document,
|
|
|
208
180
|
render(nothing, pop);
|
|
209
181
|
openName = name;
|
|
210
182
|
text = null;
|
|
183
|
+
pop.setAttribute("aria-label", name);
|
|
211
184
|
fill(row);
|
|
212
185
|
pop.hidden = false;
|
|
213
186
|
place(chip);
|
|
214
187
|
drawChips();
|
|
215
188
|
}
|
|
216
189
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
function openAdd(chip) {
|
|
223
|
-
render(nothing, pop);
|
|
224
|
-
openName = ADD;
|
|
225
|
-
text = null;
|
|
226
|
-
addState = {};
|
|
227
|
-
drawAdd();
|
|
228
|
-
pop.hidden = false;
|
|
229
|
-
place(chip);
|
|
230
|
-
drawChips();
|
|
231
|
-
pop.querySelector('input[name="url"]')?.focus();
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
async function submitAdd(form) {
|
|
235
|
-
const id = sessionId;
|
|
236
|
-
const url = form.elements.url.value.trim();
|
|
237
|
-
const why = form.elements.why.value.trim();
|
|
238
|
-
if (!id || !url) return;
|
|
239
|
-
const still = stillOn(() => sessionId);
|
|
240
|
-
drawAdd({ busy: true });
|
|
241
|
-
try {
|
|
242
|
-
const data = await api.add(id, { url, why: why || undefined });
|
|
243
|
-
if (!still()) return;
|
|
244
|
-
close();
|
|
245
|
-
await refresh();
|
|
246
|
-
toast(`Attached ${data.name}: the agent gets a note before your next message`);
|
|
247
|
-
} catch (e) {
|
|
248
|
-
if (!still() || openName !== ADD) return;
|
|
249
|
-
// The same form again: what the user typed stays; the URL gets focus.
|
|
250
|
-
drawAdd({ busy: false, error: e.message });
|
|
251
|
-
pop.querySelector('input[name="url"]')?.focus();
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function close() {
|
|
190
|
+
// Focus that was in it (or +refocus+: Esc) goes back to its chip.
|
|
191
|
+
function close({ refocus = false } = {}) {
|
|
192
|
+
const back = focusBack(pop, { refocus, doc });
|
|
193
|
+
const name = openName;
|
|
256
194
|
openName = null;
|
|
257
195
|
text = null;
|
|
258
|
-
addState = {};
|
|
259
196
|
pop.hidden = true;
|
|
260
197
|
render(nothing, pop);
|
|
261
198
|
drawChips();
|
|
199
|
+
if (name !== null) back([...el.querySelectorAll(".ctx-chip[data-name]")].find((chip) => chip.dataset.name === name));
|
|
262
200
|
}
|
|
263
201
|
|
|
264
202
|
async function viewText() {
|
|
265
203
|
const name = openName;
|
|
266
|
-
if (!name
|
|
204
|
+
if (!name) return;
|
|
267
205
|
try {
|
|
268
206
|
const data = await api.show(sessionId, name);
|
|
269
207
|
if (name !== openName) return;
|
|
@@ -298,16 +236,9 @@ export function createContextChips({ el, api, toast = () => {}, doc = document,
|
|
|
298
236
|
if (!chip) return;
|
|
299
237
|
// On to the document: a session-bar chip's popover closes (the close
|
|
300
238
|
// below leaves clicks in +el+ alone).
|
|
301
|
-
if (chip.dataset.
|
|
302
|
-
if (openName === ADD) close();
|
|
303
|
-
else openAdd(chip);
|
|
304
|
-
} else if (openName === chip.dataset.name) close();
|
|
239
|
+
if (openName === chip.dataset.name) close();
|
|
305
240
|
else open(chip.dataset.name, chip);
|
|
306
241
|
});
|
|
307
|
-
pop.addEventListener("submit", (e) => {
|
|
308
|
-
e.preventDefault();
|
|
309
|
-
submitAdd(e.target);
|
|
310
|
-
});
|
|
311
242
|
pop.addEventListener("click", (e) => {
|
|
312
243
|
const act = e.target.closest("[data-act]")?.dataset.act;
|
|
313
244
|
if (act === "close") close();
|
|
@@ -318,7 +249,7 @@ export function createContextChips({ el, api, toast = () => {}, doc = document,
|
|
|
318
249
|
if (openName && !pop.contains(e.target) && !el.contains(e.target)) close();
|
|
319
250
|
});
|
|
320
251
|
doc.addEventListener("keydown", (e) => {
|
|
321
|
-
if (e.key === "Escape" && openName) close();
|
|
252
|
+
if (e.key === "Escape" && openName) close({ refocus: true });
|
|
322
253
|
});
|
|
323
254
|
win.addEventListener("resize", placeAtOpenChip);
|
|
324
255
|
win.addEventListener("focus", () => refresh());
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// Copy buttons: one on every prompt and answer bubble, one on every code
|
|
2
|
-
// block of a rendered answer. An answer copies its
|
|
3
|
-
// saved message's `display` when a hook presented one,
|
|
4
|
-
// kept on the bubble as data-copy-source when the page
|
|
5
|
-
// html), a prompt the text as typed, a code block just its
|
|
2
|
+
// block of a rendered answer or delegate report. An answer copies its
|
|
3
|
+
// markdown source (the saved message's `display` when a hook presented one,
|
|
4
|
+
// else its `content`, kept on the bubble as data-copy-source when the page
|
|
5
|
+
// shows rendered html), a prompt the text as typed, a code block just its
|
|
6
|
+
// code.
|
|
6
7
|
//
|
|
7
8
|
// The buttons carry no text (an icon; "copied ✓" is CSS content), so a
|
|
8
9
|
// bubble's textContent, the turn output dedupe and an annotation's selected
|
|
@@ -78,7 +79,9 @@ export function showsAnswer(bubble, message, normalize) {
|
|
|
78
79
|
const ICON = html`<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"><rect x="5.5" y="5.5" width="8" height="8" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.4"/><path d="M10.5 3.5v-.5A1.5 1.5 0 0 0 9 1.5H3A1.5 1.5 0 0 0 1.5 3v6A1.5 1.5 0 0 0 3 10.5h.5" fill="none" stroke="currentColor" stroke-width="1.4"/></svg>`;
|
|
79
80
|
|
|
80
81
|
const BUBBLES = ".bubble.output:not(.streaming), .bubble.user";
|
|
81
|
-
|
|
82
|
+
// An answer's code blocks and a delegate report's (a user bubble chi
|
|
83
|
+
// brought in, rendered as markdown; answer_fold.js REPORTS).
|
|
84
|
+
const CODE = '.bubble.output pre, .bubble.user[data-source="delegate_report"] pre';
|
|
82
85
|
|
|
83
86
|
// The button as a lit template, for a templated part (a tool row's
|
|
84
87
|
// command block, command_view.js); the same click handler answers it.
|
|
@@ -397,6 +397,8 @@ export function getContext(id, name, opts = {}) {
|
|
|
397
397
|
}
|
|
398
398
|
|
|
399
399
|
// A URL an installed bundle's provider attaches: {url, why} → {name}.
|
|
400
|
+
// Unused since the web's "+ URL" went; kept while an older app.js that
|
|
401
|
+
// imports it may still be cached (internals/web-frontend.md, new exports).
|
|
400
402
|
export function addContext(id, body, opts = {}) {
|
|
401
403
|
return api(`/api/sessions/${encodeURIComponent(id)}/context`, {
|
|
402
404
|
method: "POST", body: JSON.stringify(body), ...opts,
|
|
@@ -8,40 +8,39 @@ import { ownerBadge, previewForCard, relativeTime } from "./format.js";
|
|
|
8
8
|
import { familySummary, stoppedBadge, waitingBadge } from "./sessions_list.js";
|
|
9
9
|
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
10
10
|
import { keepFocus } from "./keep_focus.js";
|
|
11
|
+
import { focusBack, modalDialog } from "./popover_modal.js";
|
|
11
12
|
|
|
12
13
|
// The chip in a parent's meta row: "▸ 3 delegates · ● 1 live · 1 waiting"
|
|
13
14
|
// (a waiting one in the attention colour), its tooltip the delegates by
|
|
14
|
-
// state.
|
|
15
|
+
// state. Open, it is just "▾ 3 delegates": the rows carry the marks then.
|
|
16
|
+
// nothing for a family with no members.
|
|
15
17
|
// @param open whether the list is open (aria-expanded; the arrow turns)
|
|
16
18
|
export function familyChipTemplate(f, { open = false } = {}) {
|
|
17
19
|
const sum = familySummary(f);
|
|
18
20
|
if (!sum) return nothing;
|
|
19
|
-
const waiting = sum.counts.waiting;
|
|
20
|
-
const live = sum.live ? html`<span class="fc-live"> · <span class="live-dot" aria-hidden="true"></span>${sum.live} live</span>` : nothing;
|
|
21
|
+
const waiting = open ? 0 : sum.counts.waiting;
|
|
22
|
+
const live = sum.live && !open ? html`<span class="fc-live"> · <span class="live-dot" aria-hidden="true"></span>${sum.live} live</span>` : nothing;
|
|
21
23
|
const waitingT = waiting ? html` · <span class="fc-waiting">${waiting} waiting</span>` : nothing;
|
|
22
24
|
return html`<button type="button" class=${`family-chip${waiting ? " waiting" : ""}`} aria-expanded=${open ? "true" : "false"}
|
|
23
25
|
title=${sum.title}><span class="fc-arrow" aria-hidden="true">▸</span>${sum.total} ${sum.total === 1 ? "delegate" : "delegates"}${
|
|
24
26
|
live}${waitingT}</button>`;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
// A row's
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return html`<span class=${`status dot ${s.status || ""}${waiting ? " waiting" : ""}`} title=${word} aria-label=${word}></span>`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// A row's waiting or stopped badge, and the archived one.
|
|
35
|
-
export function rowBadgesTemplate(waiting, stopped) {
|
|
29
|
+
// A row's waiting or stopped badge (a family's or a chain's; no status
|
|
30
|
+
// dot: one mark per state, as a card's), its `error` badge when +status+
|
|
31
|
+
// is "error", and the archived one.
|
|
32
|
+
export function rowBadgesTemplate(waiting, stopped, status = null) {
|
|
36
33
|
return html`${waiting ? html`<span class=${`attn attn-${waiting.kind}`} title=${waiting.title}>${waiting.text}</span>` : nothing}${
|
|
34
|
+
status === "error" ? html`<span class="error-badge" title=${status}>error</span>` : nothing}${
|
|
37
35
|
stopped ? html`<span class="stopped-badge" title=${stopped.title}>${stopped.text}</span>` : nothing}`;
|
|
38
36
|
}
|
|
39
37
|
|
|
40
38
|
export const archivedBadge = html`<span class="archived-badge" title="Archived: hidden from the lists, kept for good">archived</span>`;
|
|
41
39
|
|
|
42
40
|
// The family's list: one row per member, depth first, indented one step
|
|
43
|
-
// per depth (--depth):
|
|
44
|
-
//
|
|
41
|
+
// per depth (--depth): preview, the waiting / error / stopped badge, time,
|
|
42
|
+
// the owner badge (not on a waiting row: waiting means a worker), archived.
|
|
43
|
+
// The status is in the row's tooltip. A row's click opens that session.
|
|
45
44
|
// @param activeId the open session's id (its row is .active)
|
|
46
45
|
// @param matchIds a search's matching members (.match; the rest .dim), or
|
|
47
46
|
// null without a search
|
|
@@ -55,15 +54,15 @@ export function familyRowsTemplate(f, { activeId = null, matchIds = null, archiv
|
|
|
55
54
|
const ts = s.updated_at || s.created_at || "";
|
|
56
55
|
const waiting = waitingBadge(s, { inFamily: true });
|
|
57
56
|
const stopped = waiting ? null : stoppedBadge(s);
|
|
58
|
-
const owner = ownerBadge(s.owner, s.id);
|
|
57
|
+
const owner = waiting ? null : ownerBadge(s.owner, s.id);
|
|
59
58
|
const ago = relativeTime(ts, now);
|
|
60
59
|
const short = s.short_id || String(s.id).slice(0, 8);
|
|
61
60
|
const arch = !archivable ? nothing : s.archived
|
|
62
61
|
? html`<button type="button" class="card-arch" data-arch=${s.id} title=${"Unarchive: back to the lists"} aria-label=${`Unarchive session ${short}`}></button>`
|
|
63
62
|
: html`<button type="button" class="card-arch" data-arch=${s.id} title=${"Archive: hide from the lists, keep for good"} aria-label=${`Archive session ${short}`}></button>`;
|
|
64
63
|
return html`<div class=${cls.join(" ")} data-id=${s.id} style=${`--depth:${depth}`} role=${"listitem"}><button type="button" class="fr-open"
|
|
65
|
-
title=${`${s.id} · ${s.status || ""}`}
|
|
66
|
-
rowBadgesTemplate(waiting, stopped)}${ago ? html`<span class="time">${ago}</span>` : nothing}${
|
|
64
|
+
title=${`${s.id} · ${s.status || ""}`}><span class="fr-preview">${previewForCard(s)}</span>${
|
|
65
|
+
rowBadgesTemplate(waiting, stopped, s.status)}${ago ? html`<span class="time">${ago}</span>` : nothing}${
|
|
67
66
|
owner ? html`<span class=${`owner ${owner.kind}`} title=${owner.title}>${owner.text}</span>` : nothing}${
|
|
68
67
|
s.archived ? archivedBadge : nothing}</button>${arch}</div>`;
|
|
69
68
|
});
|
|
@@ -73,20 +72,6 @@ export function familyRowsTemplate(f, { activeId = null, matchIds = null, archiv
|
|
|
73
72
|
// The widest window index.html lays out as a phone (@media (max-width:600px)).
|
|
74
73
|
const PHONE_MAX_PX = 600;
|
|
75
74
|
|
|
76
|
-
// The status dots' pulse in +root+ on the document timeline's clock (each
|
|
77
|
-
// animation's start time 0), the phase the cards' --pulse-delay gives
|
|
78
|
-
// theirs (lists_view.js cardAnimStyle): a popover keeps its row nodes
|
|
79
|
-
// across draws, reused by position, so a dot that moves to another node
|
|
80
|
-
// or starts pulsing there (a member inserted above, a status flip) would
|
|
81
|
-
// otherwise start its pulse over, out of step with the rest.
|
|
82
|
-
export function syncPulses(root) {
|
|
83
|
-
root.querySelectorAll(".dot").forEach((dot) => {
|
|
84
|
-
for (const a of dot.getAnimations?.() || []) {
|
|
85
|
-
if (a.startTime !== 0) a.startTime = 0;
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
75
|
// The strip's family popover: one #familyPop in body (position: fixed, so
|
|
91
76
|
// the strip's sideways scroll on a phone doesn't clip it), under the chip
|
|
92
77
|
// it was opened from and as wide as that card (300 px at least, inside the
|
|
@@ -101,7 +86,11 @@ export function syncPulses(root) {
|
|
|
101
86
|
// @param onPick (id) a row's click
|
|
102
87
|
// @param onClose () after it closed (the strip's held order catches up)
|
|
103
88
|
// @param id the element's id (a chain's popover, chains.js, is another one)
|
|
104
|
-
|
|
89
|
+
// @param label its accessible name
|
|
90
|
+
// It behaves as every popover does (popover_modal.js): a modal dialog (Tab wraps in
|
|
91
|
+
// its rows; a Tab from the chip goes in), the chip's aria-expanded, Esc
|
|
92
|
+
// closes it and focus goes back to the chip.
|
|
93
|
+
export function createFamilyPop({ id = "familyPop", label = "Delegates", doc, win, rowsFor, anchorFor, onPick, onClose = () => {} }) {
|
|
105
94
|
let el = null;
|
|
106
95
|
let headId = null;
|
|
107
96
|
|
|
@@ -111,6 +100,7 @@ export function createFamilyPop({ id = "familyPop", doc, win, rowsFor, anchorFor
|
|
|
111
100
|
el.id = id;
|
|
112
101
|
el.className = "family-pop";
|
|
113
102
|
el.hidden = true;
|
|
103
|
+
modalDialog(el, { label, isOpen: () => headId != null, doc });
|
|
114
104
|
el.addEventListener("click", (e) => {
|
|
115
105
|
const row = e.target?.closest?.(".family-row[data-id]");
|
|
116
106
|
if (row) onPick(row.dataset.id);
|
|
@@ -142,7 +132,6 @@ export function createFamilyPop({ id = "familyPop", doc, win, rowsFor, anchorFor
|
|
|
142
132
|
}
|
|
143
133
|
// A row inserted before the focused one: the focus stays on its session.
|
|
144
134
|
keepFocus(el, "data-id", () => render(rows, el));
|
|
145
|
-
syncPulses(el);
|
|
146
135
|
anchor.setAttribute("aria-expanded", "true");
|
|
147
136
|
place(anchor);
|
|
148
137
|
return true;
|
|
@@ -155,13 +144,18 @@ export function createFamilyPop({ id = "familyPop", doc, win, rowsFor, anchorFor
|
|
|
155
144
|
draw();
|
|
156
145
|
}
|
|
157
146
|
|
|
158
|
-
|
|
147
|
+
// Focus that was in it (or +refocus+: Esc) goes back to the chip as
|
|
148
|
+
// drawn after onClose (a held strip catches up).
|
|
149
|
+
function close({ refocus = false } = {}) {
|
|
159
150
|
if (!headId) return;
|
|
160
|
-
|
|
151
|
+
const back = focusBack(el, { refocus, doc });
|
|
152
|
+
const id = headId;
|
|
153
|
+
anchorFor(id)?.setAttribute("aria-expanded", "false");
|
|
161
154
|
headId = null;
|
|
162
155
|
el.hidden = true;
|
|
163
156
|
render(nothing, el);
|
|
164
157
|
onClose();
|
|
158
|
+
back(anchorFor(id));
|
|
165
159
|
}
|
|
166
160
|
|
|
167
161
|
return {
|
|
@@ -181,12 +175,10 @@ export function createFamilyPop({ id = "familyPop", doc, win, rowsFor, anchorFor
|
|
|
181
175
|
// capture phase): true when it did.
|
|
182
176
|
onKeydown(e) {
|
|
183
177
|
if (!headId || e.key !== "Escape") return false;
|
|
184
|
-
const id = headId;
|
|
185
178
|
e.preventDefault();
|
|
186
179
|
e.stopImmediatePropagation();
|
|
187
180
|
e.stopPropagation();
|
|
188
|
-
close();
|
|
189
|
-
anchorFor(id)?.focus(); // the chip as drawn after close() (a held strip catches up)
|
|
181
|
+
close({ refocus: true });
|
|
190
182
|
return true;
|
|
191
183
|
},
|
|
192
184
|
// A pointer down outside it and its chip closes it.
|