@miadi/chronicle-tui 0.4.2 → 0.6.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/README.md +58 -12
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +411 -21
- package/dist/App.js.map +1 -1
- package/dist/actions.d.ts +74 -11
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +141 -24
- package/dist/actions.js.map +1 -1
- package/dist/api.d.ts +46 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +52 -5
- package/dist/api.js.map +1 -1
- package/dist/circles.d.ts +250 -0
- package/dist/circles.d.ts.map +1 -0
- package/dist/circles.js +308 -0
- package/dist/circles.js.map +1 -0
- package/dist/data.d.ts +2 -0
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +8 -3
- package/dist/data.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/model.d.ts +10 -0
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +15 -0
- package/dist/model.js.map +1 -1
- package/package.json +5 -3
package/dist/App.js
CHANGED
|
@@ -12,12 +12,14 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
12
12
|
* taken, so tmux (C-b) and herdr (prefix+…) keep all of theirs.
|
|
13
13
|
*/
|
|
14
14
|
import { useEffect, useMemo, useReducer, useRef } from "react";
|
|
15
|
-
import
|
|
15
|
+
import stringWidth from "string-width";
|
|
16
|
+
import { Box, Text, useApp, useInput, usePaste, useWindowSize } from "ink";
|
|
16
17
|
import { MAX_FILES } from "@miadi/episode-vessel";
|
|
17
|
-
import { DEFAULT_ACTIONS, actionFor, } from "./actions.js";
|
|
18
|
+
import { DEFAULT_ACTIONS, actionFor, enterCeremony, enterCircle, } from "./actions.js";
|
|
18
19
|
import { TOKEN_ENV, answerAttention, connectApi } from "./api.js";
|
|
19
20
|
import { BOARD_LABELS, ROOM_LABELS, answeredMessage, checkAnswer, filterBoard, loadBoard, loadEpisodePanel, panelView, readOnlyLabel, } from "./attention.js";
|
|
20
|
-
import {
|
|
21
|
+
import { CEREMONY_TYPES, DIARY_PHASES, DIRECTIONS, ENTRY_TYPES, PHASE_LABELS, ceremonyHeadline, ceremonyLines, circleEntries, closeCeremony, mayWitness, openCeremony, readCeremony, readCircle, scrollTo, speak, splitLearnings, typeLabel, whenOf, witness, writeDiary, } from "./circles.js";
|
|
22
|
+
import { VISITOR, wheelDoors, loadChronicle, loadDetail, loadText, openUrl, qrLines, search, visibleRows, } from "./data.js";
|
|
21
23
|
import { ageOf, clamp, fileView, filterRows, formatDate, screenLineOf, windowOf, wrapSegments, } from "./model.js";
|
|
22
24
|
import { clean, cleanLine, link } from "./text.js";
|
|
23
25
|
const DIRECTION_MARK = { east: "E", south: "S", west: "W", north: "N" };
|
|
@@ -106,6 +108,7 @@ export function App(props) {
|
|
|
106
108
|
reader: null,
|
|
107
109
|
access: VISITOR,
|
|
108
110
|
mayAnswer: false,
|
|
111
|
+
mayMint: false,
|
|
109
112
|
board: null,
|
|
110
113
|
boardSel: 0,
|
|
111
114
|
history: [],
|
|
@@ -201,11 +204,21 @@ export function App(props) {
|
|
|
201
204
|
return;
|
|
202
205
|
try {
|
|
203
206
|
const reader = await api.me();
|
|
204
|
-
const mayAnswer = await api.mayAnswer();
|
|
207
|
+
const [mayAnswer, mayMint] = await Promise.all([api.mayAnswer(), api.mayMint()]);
|
|
205
208
|
if (!live)
|
|
206
209
|
return;
|
|
210
|
+
// Signing in can show more episodes (community ones), so the selection is kept by name, not by row.
|
|
211
|
+
const before = derive();
|
|
212
|
+
const keep = before.s.view === "attention" ? undefined : before.currentRow?.name;
|
|
213
|
+
const keepItem = before.boardItem;
|
|
207
214
|
details.current.clear();
|
|
208
|
-
set({ reader, mayAnswer, access: { signedIn: Boolean(reader), mayWrite: mayAnswer } });
|
|
215
|
+
set({ reader, mayAnswer, mayMint, access: { signedIn: Boolean(reader), mayWrite: mayAnswer } });
|
|
216
|
+
const after = derive();
|
|
217
|
+
const listSel = keep ? after.filtered.findIndex((row) => row.name === keep) : -1;
|
|
218
|
+
const boardSel = keepItem ? after.board.findIndex((item) => item.episode === keepItem.episode && item.id === keepItem.id) : -1;
|
|
219
|
+
if (listSel !== -1 || boardSel !== -1) {
|
|
220
|
+
set({ ...(listSel !== -1 ? { listSel } : {}), ...(boardSel !== -1 ? { boardSel } : {}), status: ui.current.status });
|
|
221
|
+
}
|
|
209
222
|
resolvePending();
|
|
210
223
|
}
|
|
211
224
|
catch (error) {
|
|
@@ -317,8 +330,22 @@ export function App(props) {
|
|
|
317
330
|
api: s.api,
|
|
318
331
|
reader: s.reader,
|
|
319
332
|
mayAnswer: s.mayAnswer,
|
|
333
|
+
mayMint: s.mayMint,
|
|
334
|
+
relateFrom: s.relateFrom,
|
|
335
|
+
setRelateFrom: (name) => set({ relateFrom: name }),
|
|
336
|
+
reload: reloadChronicle,
|
|
320
337
|
};
|
|
321
338
|
};
|
|
339
|
+
/** Read the chronicle again after a write landed in it, keeping the episode in focus. */
|
|
340
|
+
async function reloadChronicle() {
|
|
341
|
+
const keep = derive().currentRow?.name;
|
|
342
|
+
const chronicle = await loadChronicle(root);
|
|
343
|
+
details.current.clear();
|
|
344
|
+
set({ chronicle, status: ui.current.status });
|
|
345
|
+
const index = keep ? derive().filtered.findIndex((row) => row.name === keep) : -1;
|
|
346
|
+
if (index !== -1)
|
|
347
|
+
set({ listSel: index, status: ui.current.status });
|
|
348
|
+
}
|
|
322
349
|
const openFile = (name, file, from, line) => {
|
|
323
350
|
const s = ui.current;
|
|
324
351
|
const text = loadText(s.chronicle.root, name, file, s.access);
|
|
@@ -419,7 +446,169 @@ export function App(props) {
|
|
|
419
446
|
set({ overlays: stack });
|
|
420
447
|
},
|
|
421
448
|
});
|
|
449
|
+
// ── talking circles: the overlays, and the person's acts in them ─────────
|
|
450
|
+
const host = { open: openOverlay, replace: replaceOverlay };
|
|
451
|
+
const topHandle = () => ui.current.overlays[ui.current.overlays.length - 1]?.id ?? 0;
|
|
452
|
+
/** Change overlay `handle` wherever it sits in the stack; a closed one is left alone. */
|
|
453
|
+
const updateById = (handle, change) => {
|
|
454
|
+
const stack = ui.current.overlays;
|
|
455
|
+
if (!stack.some((entry) => entry.id === handle))
|
|
456
|
+
return;
|
|
457
|
+
set({ overlays: stack.map((entry) => (entry.id === handle ? { id: handle, overlay: change(entry.overlay) } : entry)) });
|
|
458
|
+
};
|
|
459
|
+
const episodeLabel = (path) => {
|
|
460
|
+
if (!path)
|
|
461
|
+
return "";
|
|
462
|
+
const row = derive().rowByName(path);
|
|
463
|
+
return row?.number !== undefined ? `ep ${row.number}` : path;
|
|
464
|
+
};
|
|
465
|
+
/**
|
|
466
|
+
* How a ceremony fills the overlay: inside the border, a title, its
|
|
467
|
+
* intention, a line of where it is held, the tabs, the lines, the keys.
|
|
468
|
+
* Shared by the keys and the screen, so a scroll is measured as drawn.
|
|
469
|
+
*/
|
|
470
|
+
const ceremonyLayout = (view, tab) => {
|
|
471
|
+
const inner = Math.max(10, columns - 4);
|
|
472
|
+
const intention = view.intentions.join(" · ");
|
|
473
|
+
const intentLines = intention ? Math.min(compact ? 2 : 3, wrapSegments([{ text: intention, style: "text" }], inner).length) : 0;
|
|
474
|
+
const room = Math.max(1, bodyHeight - 2 - (4 + intentLines));
|
|
475
|
+
return { inner, intention, intentLines, room, screen: ceremonyLines(view, tab, inner - 1) };
|
|
476
|
+
};
|
|
477
|
+
const refreshing = useRef(false);
|
|
478
|
+
/** Read the ceremony again. A reader on the newest turn stays on the newest, so new turns come into view. */
|
|
479
|
+
const reloadCeremony = async (handle) => {
|
|
480
|
+
const entry = ui.current.overlays.find((candidate) => candidate.id === handle);
|
|
481
|
+
if (!entry || entry.overlay.type !== "ceremony" || !ui.current.api || refreshing.current)
|
|
482
|
+
return;
|
|
483
|
+
refreshing.current = true;
|
|
484
|
+
try {
|
|
485
|
+
const view = await readCeremony(ui.current.api, entry.overlay.id);
|
|
486
|
+
updateById(handle, (o) => {
|
|
487
|
+
if (o.type !== "ceremony")
|
|
488
|
+
return o;
|
|
489
|
+
const count = o.tab === "turns" ? (o.view?.turns.length ?? 0) : (o.view?.diary.length ?? 0);
|
|
490
|
+
const following = o.selected < 0 || (o.tab === "turns" && o.selected >= count - 1);
|
|
491
|
+
return { ...o, view, error: undefined, selected: following && o.tab === "turns" ? -1 : o.selected };
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
updateById(handle, (o) => (o.type === "ceremony" ? { ...o, error: cleanLine(error.message) } : o));
|
|
496
|
+
}
|
|
497
|
+
finally {
|
|
498
|
+
refreshing.current = false;
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
const reloadCircle = async (handle) => {
|
|
502
|
+
const entry = ui.current.overlays.find((candidate) => candidate.id === handle);
|
|
503
|
+
if (!entry || entry.overlay.type !== "circle" || !ui.current.api)
|
|
504
|
+
return;
|
|
505
|
+
const view = await readCircle(ui.current.api, entry.overlay.id);
|
|
506
|
+
updateById(handle, (o) => (o.type === "circle" ? { ...o, view, error: undefined } : o));
|
|
507
|
+
};
|
|
508
|
+
// A talking circle is live: the ceremony on top is read again every ten seconds.
|
|
509
|
+
useEffect(() => {
|
|
510
|
+
const timer = setInterval(() => {
|
|
511
|
+
const top = ui.current.overlays[ui.current.overlays.length - 1];
|
|
512
|
+
if (top?.overlay.type === "ceremony" && top.overlay.view)
|
|
513
|
+
void reloadCeremony(top.id);
|
|
514
|
+
}, 10_000);
|
|
515
|
+
return () => clearInterval(timer);
|
|
516
|
+
}, []);
|
|
517
|
+
const speakInput = (handle, view) => ({
|
|
518
|
+
type: "input",
|
|
519
|
+
title: `Your turn · ${typeLabel(view.type)}${view.circle ? ` · ${view.circle.name}` : ""}`,
|
|
520
|
+
hint: "Speak. The talking piece is yours. A paste keeps its lines.",
|
|
521
|
+
value: "",
|
|
522
|
+
submit: async (value) => {
|
|
523
|
+
await speak(ui.current.api, view.id, value.trim());
|
|
524
|
+
return "Spoken in the circle.";
|
|
525
|
+
},
|
|
526
|
+
after: () => reloadCeremony(handle),
|
|
527
|
+
});
|
|
528
|
+
const diaryInput = (handle, view) => ({
|
|
529
|
+
type: "input",
|
|
530
|
+
title: `Diary · ${typeLabel(view.type)}`,
|
|
531
|
+
hint: "In your own voice. One entry is one voice, in one phase.",
|
|
532
|
+
value: "",
|
|
533
|
+
choices: [
|
|
534
|
+
{ label: "phase", values: DIARY_PHASES, shown: DIARY_PHASES.map((phase) => PHASE_LABELS[phase] ?? phase), index: 1 },
|
|
535
|
+
{ label: "kind", values: ENTRY_TYPES, index: ENTRY_TYPES.indexOf("reflection") },
|
|
536
|
+
],
|
|
537
|
+
submit: async (value, [phase, entryType]) => {
|
|
538
|
+
await writeDiary(ui.current.api, view.id, { phase: phase, entryType: entryType, content: value.trim() });
|
|
539
|
+
return `Written in the diary (${phase}, ${entryType}).`;
|
|
540
|
+
},
|
|
541
|
+
after: () => reloadCeremony(handle),
|
|
542
|
+
});
|
|
543
|
+
const closeInput = (handle, view) => ({
|
|
544
|
+
type: "input",
|
|
545
|
+
title: `Close the circle · ${typeLabel(view.type)}${view.circle ? ` · ${view.circle.name}` : ""}`,
|
|
546
|
+
hint: "What the circle learned: one per line (a paste keeps its lines) or separated by ' | '. Nothing is fine. Enter closes it; Esc keeps it open.",
|
|
547
|
+
value: "",
|
|
548
|
+
allowEmpty: true,
|
|
549
|
+
submit: async (value) => {
|
|
550
|
+
const result = await closeCeremony(ui.current.api, view.id, splitLearnings(value));
|
|
551
|
+
return result.already ? "It was already closed." : "The circle is closed.";
|
|
552
|
+
},
|
|
553
|
+
after: () => reloadCeremony(handle),
|
|
554
|
+
});
|
|
555
|
+
const openInput = (handle, circle, focus) => {
|
|
556
|
+
const episodes = [...new Set([focus, circle.episode].filter((path) => Boolean(path))), ""];
|
|
557
|
+
const directions = DIRECTIONS;
|
|
558
|
+
let opened = "";
|
|
559
|
+
return {
|
|
560
|
+
type: "input",
|
|
561
|
+
title: `Open a ceremony · ${circle.name}`,
|
|
562
|
+
hint: "Its intention, in your words. The circle's members are its participants.",
|
|
563
|
+
value: "",
|
|
564
|
+
choices: [
|
|
565
|
+
{ label: "kind", values: CEREMONY_TYPES, shown: CEREMONY_TYPES.map(typeLabel), index: 0 },
|
|
566
|
+
{ label: "direction", values: DIRECTIONS, index: Math.max(0, directions.indexOf(circle.direction ?? "east")) },
|
|
567
|
+
{ label: "episode", values: episodes, shown: episodes.map((path) => (path ? episodeLabel(path) : "none")), index: 0 },
|
|
568
|
+
],
|
|
569
|
+
submit: async (value, [type, direction, episode]) => {
|
|
570
|
+
const result = await openCeremony(ui.current.api, circle.id, { type: type, direction: direction, intention: value.trim(), ...(episode ? { episode_path: episode } : {}) });
|
|
571
|
+
opened = result.ceremony.id;
|
|
572
|
+
return `Opened a ${typeLabel(type)} in ${circle.name}${episode ? `, bound to ${episodeLabel(episode)}` : ""}.`;
|
|
573
|
+
},
|
|
574
|
+
after: async () => {
|
|
575
|
+
await reloadCircle(handle);
|
|
576
|
+
if (opened)
|
|
577
|
+
enterCeremony(host, ui.current.api, opened);
|
|
578
|
+
},
|
|
579
|
+
};
|
|
580
|
+
};
|
|
581
|
+
const witnessTurn = (handle, view, index) => {
|
|
582
|
+
const turn = view.turns[index];
|
|
583
|
+
if (!turn)
|
|
584
|
+
return;
|
|
585
|
+
if (!mayWitness(view, turn)) {
|
|
586
|
+
const why = !view.can.witness
|
|
587
|
+
? view.closed
|
|
588
|
+
? "The ceremony is closed."
|
|
589
|
+
: "Your seat does not witness here."
|
|
590
|
+
: turn.speaker === view.me?.id
|
|
591
|
+
? "You spoke this turn; another witnesses it."
|
|
592
|
+
: "You witnessed this turn.";
|
|
593
|
+
return set({ status: why });
|
|
594
|
+
}
|
|
595
|
+
set({ status: `Witnessing ${turn.speakerName}'s turn…` });
|
|
596
|
+
witness(ui.current.api, view.id, turn.id).then(async () => {
|
|
597
|
+
set({ status: `Witnessed ${turn.speakerName}'s turn.` });
|
|
598
|
+
await reloadCeremony(handle);
|
|
599
|
+
}, (error) => set({ status: cleanLine(error.message) }));
|
|
600
|
+
};
|
|
422
601
|
// ── keys ─────────────────────────────────────────────────────────────────
|
|
602
|
+
// A paste into an input arrives whole, line breaks kept (bracketed paste).
|
|
603
|
+
const pasteActive = ui.current.overlays[ui.current.overlays.length - 1]?.overlay.type === "input";
|
|
604
|
+
usePaste((text) => {
|
|
605
|
+
const top = ui.current.overlays[ui.current.overlays.length - 1]?.overlay;
|
|
606
|
+
if (top?.type !== "input" || top.busy)
|
|
607
|
+
return;
|
|
608
|
+
const added = text.replace(/\r\n?/g, "\n").replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "");
|
|
609
|
+
if (added)
|
|
610
|
+
updateTop({ ...top, value: top.value + added, error: undefined });
|
|
611
|
+
}, { isActive: pasteActive });
|
|
423
612
|
useInput((input, key) => {
|
|
424
613
|
if (key.ctrl || key.meta)
|
|
425
614
|
return;
|
|
@@ -453,12 +642,29 @@ export function App(props) {
|
|
|
453
642
|
return;
|
|
454
643
|
if (key.escape)
|
|
455
644
|
return closeTop();
|
|
645
|
+
const choices = top.choices ?? [];
|
|
646
|
+
const focus = clamp(top.focus ?? 0, choices.length);
|
|
647
|
+
if (choices.length && (key.upArrow || key.downArrow)) {
|
|
648
|
+
const choice = choices[focus];
|
|
649
|
+
const count = choice.values.length;
|
|
650
|
+
const index = (choice.index + (key.downArrow ? 1 : count - 1)) % count;
|
|
651
|
+
return updateTop({ ...top, choices: choices.map((c, i) => (i === focus ? { ...c, index } : c)) });
|
|
652
|
+
}
|
|
653
|
+
if (key.tab) {
|
|
654
|
+
if (choices.length > 1)
|
|
655
|
+
updateTop({ ...top, focus: (focus + (key.shift ? choices.length - 1 : 1)) % choices.length });
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
456
658
|
if (key.return) {
|
|
457
659
|
const value = top.value;
|
|
458
|
-
if (!value.trim())
|
|
660
|
+
if (!value.trim() && !top.allowEmpty)
|
|
459
661
|
return updateTop({ ...top, error: "Nothing to send yet." });
|
|
460
662
|
updateTop({ ...top, busy: true, error: undefined });
|
|
461
|
-
top.submit(value).then(async (
|
|
663
|
+
top.submit(value, choices.map((c) => c.values[c.index])).then(async (result) => {
|
|
664
|
+
// A two-part form: the next input takes this one's place.
|
|
665
|
+
if (typeof result !== "string")
|
|
666
|
+
return updateTop(result.next);
|
|
667
|
+
const message = result;
|
|
462
668
|
set({ status: cleanLine(message), overlays: ui.current.overlays.slice(0, -1) });
|
|
463
669
|
try {
|
|
464
670
|
await top.after?.();
|
|
@@ -498,8 +704,113 @@ export function App(props) {
|
|
|
498
704
|
}
|
|
499
705
|
if (top.type === "qr" && input === "i")
|
|
500
706
|
return updateTop({ ...top, invert: !top.invert });
|
|
501
|
-
if (top.type === "
|
|
502
|
-
|
|
707
|
+
if (top.type === "circles" && top.index) {
|
|
708
|
+
const entries = circleEntries(top.index);
|
|
709
|
+
const step = (from, by) => {
|
|
710
|
+
let at = from;
|
|
711
|
+
for (let i = from + by; i >= 0 && i < entries.length; i += by) {
|
|
712
|
+
if (entries[i].kind !== "heading") {
|
|
713
|
+
at = i;
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return at;
|
|
718
|
+
};
|
|
719
|
+
if (key.downArrow || input === "j")
|
|
720
|
+
return updateTop({ ...top, selected: step(top.selected, 1) });
|
|
721
|
+
if (key.upArrow || input === "k")
|
|
722
|
+
return updateTop({ ...top, selected: step(top.selected, -1) });
|
|
723
|
+
const entry = entries[top.selected];
|
|
724
|
+
if (entry && (key.return || input === "l" || key.rightArrow) && s.api) {
|
|
725
|
+
if (entry.kind === "ceremony")
|
|
726
|
+
return void enterCeremony(host, s.api, entry.held.ceremony.id);
|
|
727
|
+
if (entry.kind === "circle")
|
|
728
|
+
return void enterCircle(host, s.api, entry.circle.id, top.episode);
|
|
729
|
+
}
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
if (top.type === "circle" && top.view) {
|
|
733
|
+
const handle = topHandle();
|
|
734
|
+
const held = top.view.held;
|
|
735
|
+
if (key.downArrow || input === "j")
|
|
736
|
+
return updateTop({ ...top, selected: clamp(top.selected + 1, held.length) });
|
|
737
|
+
if (key.upArrow || input === "k")
|
|
738
|
+
return updateTop({ ...top, selected: clamp(top.selected - 1, held.length) });
|
|
739
|
+
const chosen = held[clamp(top.selected, held.length)];
|
|
740
|
+
if (chosen && (key.return || input === "l" || key.rightArrow) && s.api)
|
|
741
|
+
return void enterCeremony(host, s.api, chosen.ceremony.id);
|
|
742
|
+
if (input === "n") {
|
|
743
|
+
if (!top.view.seat || !top.view.can.open_ceremony)
|
|
744
|
+
return set({ status: top.view.circle.active ? "Opening a ceremony here needs a seat that facilitates." : "The circle is inactive." });
|
|
745
|
+
return void openOverlay(openInput(handle, top.view.circle, top.episode));
|
|
746
|
+
}
|
|
747
|
+
if (input === "o") {
|
|
748
|
+
wheelDoors("circle", top.id).then((doors) => openOverlay({ type: "doors", doors, selected: 0 }), () => { });
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (top.type === "ceremony" && top.view) {
|
|
754
|
+
const handle = topHandle();
|
|
755
|
+
const view = top.view;
|
|
756
|
+
const layout = ceremonyLayout(view, top.tab);
|
|
757
|
+
const count = top.tab === "turns" ? view.turns.length : view.diary.length;
|
|
758
|
+
const selected = top.selected < 0 ? count - 1 : clamp(top.selected, count);
|
|
759
|
+
const scroll = top.selected < 0 ? Math.max(0, layout.screen.lines.length - layout.room) : top.scroll;
|
|
760
|
+
const select = (index) => {
|
|
761
|
+
const next = clamp(index, count);
|
|
762
|
+
updateTop({ ...top, selected: next, scroll: scrollTo(layout.screen, next, scroll, layout.room) });
|
|
763
|
+
};
|
|
764
|
+
if (key.tab) {
|
|
765
|
+
const tab = top.tab === "turns" ? "diary" : "turns";
|
|
766
|
+
return updateTop({ ...top, tab, selected: tab === "turns" ? -1 : 0, scroll: 0 });
|
|
767
|
+
}
|
|
768
|
+
if (key.downArrow || input === "j")
|
|
769
|
+
return select(selected + 1);
|
|
770
|
+
if (key.upArrow || input === "k")
|
|
771
|
+
return select(selected - 1);
|
|
772
|
+
if (input === "g" || key.home)
|
|
773
|
+
return select(0);
|
|
774
|
+
if (input === "G" || key.end)
|
|
775
|
+
return select(count - 1);
|
|
776
|
+
if (key.pageDown || key.pageUp) {
|
|
777
|
+
const max = Math.max(0, layout.screen.lines.length - layout.room);
|
|
778
|
+
const next = clamp(scroll + (key.pageDown ? layout.room - 1 : 1 - layout.room), max + 1);
|
|
779
|
+
const at = layout.screen.ends.findIndex((end) => end > next);
|
|
780
|
+
return updateTop({ ...top, scroll: next, selected: at === -1 ? Math.max(0, count - 1) : at });
|
|
781
|
+
}
|
|
782
|
+
if (input === "s") {
|
|
783
|
+
if (!view.can.speak)
|
|
784
|
+
return set({ status: view.closed ? "The ceremony is closed." : "Only those seated in the circle may speak." });
|
|
785
|
+
return void openOverlay(speakInput(handle, view));
|
|
786
|
+
}
|
|
787
|
+
if (input === "w") {
|
|
788
|
+
if (top.tab !== "turns")
|
|
789
|
+
return set({ status: "Witnessing is of a turn: Tab to the turns." });
|
|
790
|
+
return witnessTurn(handle, view, selected);
|
|
791
|
+
}
|
|
792
|
+
if (input === "d") {
|
|
793
|
+
if (!view.can.diary)
|
|
794
|
+
return set({ status: "Writing in the diary needs a seat in the circle." });
|
|
795
|
+
return void openOverlay(diaryInput(handle, view));
|
|
796
|
+
}
|
|
797
|
+
if (input === "x") {
|
|
798
|
+
if (!view.can.close)
|
|
799
|
+
return set({ status: view.closed ? "The ceremony is closed." : "Only the facilitator closes the circle." });
|
|
800
|
+
return void openOverlay(closeInput(handle, view));
|
|
801
|
+
}
|
|
802
|
+
if (input === "e" && view.episode) {
|
|
803
|
+
if (!derive().rowByName(view.episode.path))
|
|
804
|
+
return set({ status: `${episodeLabel(view.episode.path)} is not among the episodes you see.` });
|
|
805
|
+
set({ overlays: [] });
|
|
806
|
+
return jumpTo(view.episode.path);
|
|
807
|
+
}
|
|
808
|
+
if (input === "o") {
|
|
809
|
+
wheelDoors("ceremony", view.id).then((doors) => openOverlay({ type: "doors", doors, selected: 0 }), () => { });
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
503
814
|
if (top.type === "panel") {
|
|
504
815
|
const { shown, view } = panelView(top.items, top.view, ROOM_LABELS, top.parseError);
|
|
505
816
|
if (key.tab)
|
|
@@ -585,6 +896,8 @@ export function App(props) {
|
|
|
585
896
|
return move(1_000_000, PLAIN_KEY);
|
|
586
897
|
if (key.return || key.rightArrow || input === "l")
|
|
587
898
|
return forward();
|
|
899
|
+
if (key.escape && s.relateFrom)
|
|
900
|
+
return set({ relateFrom: undefined, status: "No longer relating." });
|
|
588
901
|
if (key.escape || key.leftArrow || input === "h" || key.backspace)
|
|
589
902
|
return back();
|
|
590
903
|
if (input === "?")
|
|
@@ -641,9 +954,19 @@ export function App(props) {
|
|
|
641
954
|
const linesOf = (text, cap) => Math.min(cap, Math.max(1, wrapSegments([{ text, style: "text" }], textWidth).length));
|
|
642
955
|
const titleLines = d.currentRow ? linesOf(`${d.currentRow.number ?? "·"} ${d.currentRow.title}`, 3) : 0;
|
|
643
956
|
const goalLines = goal ? linesOf(goal, compact ? 6 : 8) : 0;
|
|
644
|
-
|
|
957
|
+
// The episodes this one continues from or relates to, by number where it has one.
|
|
958
|
+
const lineage = d.currentRow?.lineage.length
|
|
959
|
+
? ["continues from", "relates to", "branch of"]
|
|
960
|
+
.map((kind) => {
|
|
961
|
+
const targets = d.currentRow.lineage.filter((link) => link.kind === kind).map((link) => (link.number !== undefined ? String(link.number) : link.target));
|
|
962
|
+
return targets.length ? `${kind} ${targets.join(", ")}` : "";
|
|
963
|
+
})
|
|
964
|
+
.filter(Boolean)
|
|
965
|
+
.join(" · ")
|
|
966
|
+
: "";
|
|
967
|
+
const fixedLines = titleLines + 2 + (openHere ? 1 : 0) + (lineage ? 1 : 0) + goalLines + (d.detail?.error ? 1 : 0) + 1;
|
|
645
968
|
const fileWindow = windowOf(d.files, clamp(s.fileSel, d.files.length), Math.max(1, bodyHeight - fixedLines));
|
|
646
|
-
const detailPane = (_jsx(Box, { flexDirection: "column", width: rightWidth, height: bodyHeight, overflow: "hidden", paddingLeft: compact ? 0 : 1, children: _jsx(Box, { flexDirection: "column", flexShrink: 0, children: s.view === "attention" ? (d.boardItem ? itemBody(d.boardItem, rightWidth - 1) : _jsx(Text, { color: "gray", children: "Nothing selected." })) : d.currentRow ? (_jsxs(_Fragment, { children: [_jsx(Box, { height: titleLines, overflow: "hidden", flexShrink: 0, children: _jsxs(Text, { bold: true, wrap: "wrap", children: [_jsx(Text, { color: "yellow", children: d.currentRow.number ?? "·" }), " ", d.currentRow.title] }) }), _jsxs(Text, { color: "gray", wrap: "truncate-end", children: [[formatDate(d.currentRow.date), d.currentRow.status, d.currentRow.type].filter(Boolean).join(" · "), d.currentRow.structured ? "" : " · no episode.yaml", d.currentRow.audience === "community" ? " · community" : ""] }), _jsxs(Text, { color: "green", wrap: "truncate-end", children: ["reading: ", d.currentRow.readings.direction, " \u00B7 ", d.currentRow.readings.theme, " \u00B7 ", d.currentRow.readings.arc] }), openHere ? _jsxs(Text, { color: "yellow", children: ["? ", openHere, " open question", openHere === 1 ? "" : "s", " \u00B7 a"] }) : null, goal ? (_jsx(Box, { height: goalLines, overflow: "hidden", flexShrink: 0, children: _jsx(Text, { wrap: "wrap", children: goal }) })) : null, d.detail?.error ? _jsx(Text, { color: "red", children: cleanLine(d.detail.error) }) : null, _jsxs(Text, { color: "gray", wrap: "truncate-end", children: ["── ", d.files.length, d.files.length >= MAX_FILES ? "+" : "", " file", d.files.length === 1 ? "" : "s", s.view === "files" ? "" : " · Enter", " ──"] }), fileWindow.items.map((file, index) => {
|
|
969
|
+
const detailPane = (_jsx(Box, { flexDirection: "column", width: rightWidth, height: bodyHeight, overflow: "hidden", paddingLeft: compact ? 0 : 1, children: _jsx(Box, { flexDirection: "column", flexShrink: 0, children: s.view === "attention" ? (d.boardItem ? itemBody(d.boardItem, rightWidth - 1) : _jsx(Text, { color: "gray", children: "Nothing selected." })) : d.currentRow ? (_jsxs(_Fragment, { children: [_jsx(Box, { height: titleLines, overflow: "hidden", flexShrink: 0, children: _jsxs(Text, { bold: true, wrap: "wrap", children: [_jsx(Text, { color: "yellow", children: d.currentRow.number ?? "·" }), " ", d.currentRow.title] }) }), _jsxs(Text, { color: "gray", wrap: "truncate-end", children: [[formatDate(d.currentRow.date), d.currentRow.status, d.currentRow.type].filter(Boolean).join(" · "), d.currentRow.structured ? "" : " · no episode.yaml", d.currentRow.audience === "community" ? " · community" : ""] }), _jsxs(Text, { color: "green", wrap: "truncate-end", children: ["reading: ", d.currentRow.readings.direction, " \u00B7 ", d.currentRow.readings.theme, " \u00B7 ", d.currentRow.readings.arc] }), openHere ? _jsxs(Text, { color: "yellow", children: ["? ", openHere, " open question", openHere === 1 ? "" : "s", " \u00B7 a"] }) : null, lineage ? _jsxs(Text, { color: "cyan", wrap: "truncate-end", children: ["\u2197 ", lineage] }) : null, goal ? (_jsx(Box, { height: goalLines, overflow: "hidden", flexShrink: 0, children: _jsx(Text, { wrap: "wrap", children: goal }) })) : null, d.detail?.error ? _jsx(Text, { color: "red", children: cleanLine(d.detail.error) }) : null, _jsxs(Text, { color: "gray", wrap: "truncate-end", children: ["── ", d.files.length, d.files.length >= MAX_FILES ? "+" : "", " file", d.files.length === 1 ? "" : "s", s.view === "files" ? "" : " · Enter", " ──"] }), fileWindow.items.map((file, index) => {
|
|
647
970
|
const isSelected = s.view === "files" && fileWindow.start + index === clamp(s.fileSel, d.files.length);
|
|
648
971
|
return (_jsxs(Text, { wrap: "truncate-start", inverse: isSelected, color: file.kind === "text" ? "blue" : "gray", children: [cleanLine(file.relativePath), file.kind === "text" ? "" : ` · ${file.kind}`] }, file.relativePath));
|
|
649
972
|
})] })) : (_jsx(Text, { color: "gray", children: "Nothing selected." })) }) }));
|
|
@@ -673,7 +996,21 @@ export function App(props) {
|
|
|
673
996
|
return box("yellow", _jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: ["Proof \u00B7 ", d.rowByName(top.episode)?.number ?? cleanLine(top.episode)] }), !top.status && !top.error ? _jsx(Text, { color: "gray", children: "Asking git and the wheel\u2026" }) : null, top.error ? _jsx(Text, { color: "red", wrap: "wrap", children: cleanLine(top.error) }) : null, top.status?.closing.map((stage) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsxs(Text, { color: stage.state === "proven" ? "green" : stage.state === "owed" ? "yellow" : "red", children: [stage.state === "proven" ? "✓" : stage.state === "owed" ? "…" : "✗", " ", stage.stage] }), compact ? null : _jsxs(Text, { color: "gray", children: [" ", cleanLine(stage.observed)] })] }), compact ? _jsxs(Text, { color: "gray", wrap: "truncate-end", children: [" ", cleanLine(stage.observed)] }) : null] }, stage.stage))), top.status?.drift.map((line) => (_jsxs(Text, { color: "yellow", wrap: "wrap", children: ["drift: ", cleanLine(line)] }, line))), _jsx(Text, { color: "gray", children: "Esc" })] }));
|
|
674
997
|
}
|
|
675
998
|
if (top.type === "input") {
|
|
676
|
-
|
|
999
|
+
const inner = Math.max(10, columns - 4);
|
|
1000
|
+
const choices = top.choices ?? [];
|
|
1001
|
+
const focus = clamp(top.focus ?? 0, choices.length);
|
|
1002
|
+
const hintLines = top.hint ? Math.min(3, wrapSegments([{ text: cleanLine(top.hint), style: "text" }], inner).length) : 0;
|
|
1003
|
+
const choiceText = (choice) => cleanLine(choice.shown?.[choice.index] ?? choice.values[choice.index] ?? "");
|
|
1004
|
+
// Choices share a line when they fit, else take one each (a phone).
|
|
1005
|
+
const stacked = stringWidth(choices.map((choice) => `${choice.label} ${choiceText(choice)}`).join(" ")) > inner;
|
|
1006
|
+
const choiceLines = choices.length ? (stacked ? choices.length : 1) : 0;
|
|
1007
|
+
// The end of what is typed stays in view, however long a turn grows.
|
|
1008
|
+
const room = Math.max(1, bodyHeight - 2 - (1 + hintLines + choiceLines + (top.busy || top.error ? 1 : 0) + 1));
|
|
1009
|
+
const typed = clean(top.value)
|
|
1010
|
+
.split("\n")
|
|
1011
|
+
.flatMap((paragraph) => wrapSegments([{ text: paragraph, style: "text" }], inner - 2).map((line) => line.map((part) => part.text).join("")));
|
|
1012
|
+
const shown = typed.slice(-room);
|
|
1013
|
+
return box("cyan", _jsxs(_Fragment, { children: [_jsx(Text, { bold: true, wrap: "truncate-end", children: cleanLine(top.title) }), top.hint ? (_jsx(Box, { height: hintLines, overflow: "hidden", flexShrink: 0, children: _jsx(Text, { color: "gray", wrap: "wrap", children: cleanLine(top.hint) }) })) : null, shown.map((line, index) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", children: index === 0 && typed.length <= room ? "› " : " " }), line, index === shown.length - 1 && !top.busy ? _jsx(Text, { inverse: true, children: " " }) : null] }, index))), choices.length ? (_jsx(Box, { flexDirection: stacked ? "column" : "row", flexShrink: 0, children: choices.map((choice, index) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsxs(Text, { color: index === focus ? "yellow" : "gray", children: [choice.label, " "] }), _jsx(Text, { inverse: index === focus, children: choiceText(choice) }), stacked ? "" : " "] }, choice.label))) })) : null, top.busy ? _jsx(Text, { color: "gray", children: "Sending\u2026" }) : top.error ? _jsx(Text, { color: "red", wrap: "truncate-end", children: cleanLine(top.error) }) : null, _jsxs(Text, { color: "gray", wrap: "truncate-end", children: ["Enter sends", choices.length ? ` · ↑↓ ${choices.length > 1 ? "change · Tab next" : "change"}` : "", " \u00B7 Esc cancels"] })] }));
|
|
677
1014
|
}
|
|
678
1015
|
if (top.type === "panel") {
|
|
679
1016
|
const view = panelView(top.items, top.view, ROOM_LABELS, top.parseError);
|
|
@@ -686,14 +1023,67 @@ export function App(props) {
|
|
|
686
1023
|
if (top.type === "item") {
|
|
687
1024
|
return box("yellow", _jsxs(_Fragment, { children: [itemBody(top.item, columns - 4), _jsxs(Text, { color: "gray", wrap: "wrap", children: [top.item.state === "open" ? (s.mayAnswer ? "Enter answers · " : `${readOnlyLabel(TOKEN_ENV)} · `) : "", "e opens its episode \u00B7 Esc"] })] }));
|
|
688
1025
|
}
|
|
689
|
-
if (top.type === "
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
1026
|
+
if (top.type === "circles") {
|
|
1027
|
+
const entries = top.index ? circleEntries(top.index) : [];
|
|
1028
|
+
const listWindow = windowOf(entries, clamp(top.selected, entries.length), Math.max(1, bodyHeight - 4));
|
|
1029
|
+
return box("magenta", _jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: ["Circles", top.index?.episode ? _jsxs(Text, { color: "gray", children: [" \u00B7 from ", episodeLabel(top.index.episode.path)] }) : null] }), !top.index && !top.error ? _jsx(Text, { color: "gray", children: "Asking the wheel\u2026" }) : null, top.error ? _jsx(Text, { color: "red", wrap: "wrap", children: cleanLine(top.error) }) : null, listWindow.items.map((entry, index) => {
|
|
1030
|
+
const at = listWindow.start + index;
|
|
1031
|
+
const isSelected = at === top.selected;
|
|
1032
|
+
if (entry.kind === "heading")
|
|
1033
|
+
return _jsx(Text, { color: "gray", bold: true, wrap: "truncate-end", children: cleanLine(entry.text) }, `h${at}`);
|
|
1034
|
+
if (entry.kind === "ceremony") {
|
|
1035
|
+
const c = entry.held.ceremony;
|
|
1036
|
+
return (_jsxs(Text, { wrap: "truncate-end", inverse: isSelected, children: [_jsxs(Text, { color: entry.held.open ? "green" : "gray", children: ["\u25C6 ", entry.held.open ? "open " : "closed"] }), " ", ceremonyHeadline(c), _jsxs(Text, { color: "gray", children: [" \u00B7 ", typeLabel(c.type), " \u00B7 ", cleanLine(c.direction ?? ""), " \u00B7 ", whenOf(c.timestamp)] })] }, c.id));
|
|
1037
|
+
}
|
|
1038
|
+
const circle = entry.circle;
|
|
1039
|
+
return (_jsxs(Text, { wrap: "truncate-end", inverse: isSelected, children: [_jsx(Text, { color: "magenta", children: "\u25CB" }), " ", circle.name, _jsxs(Text, { color: "gray", children: [" · ", circle.member ? "you sit here" : circle.facilitator ? `facilitated by ${circle.facilitator}` : "a circle", circle.active ? "" : " · inactive", !compact && circle.intention ? ` · ${circle.intention}` : ""] })] }, circle.id));
|
|
1040
|
+
}), _jsx(Text, { color: "gray", wrap: "truncate-end", children: "\u2191\u2193 \u00B7 \u23CE enter \u00B7 Esc" })] }));
|
|
1041
|
+
}
|
|
1042
|
+
if (top.type === "circle") {
|
|
1043
|
+
const view = top.view;
|
|
1044
|
+
const inner = Math.max(10, columns - 4);
|
|
1045
|
+
const intentLines = view?.circle.intention ? Math.min(compact ? 2 : 3, wrapSegments([{ text: view.circle.intention, style: "text" }], inner).length) : 0;
|
|
1046
|
+
const rows = Math.max(1, bodyHeight - 2 - (4 + intentLines));
|
|
1047
|
+
const held = view?.held ?? [];
|
|
1048
|
+
const heldWindow = windowOf(held, clamp(top.selected, held.length), rows);
|
|
1049
|
+
const people = view ? view.members.map((m) => (m.role === "member" ? m.name : `${m.name} (${m.role})`)).join(", ") : "";
|
|
1050
|
+
const canOpen = Boolean(view?.seat && view.can.open_ceremony);
|
|
1051
|
+
return box("magenta", _jsxs(_Fragment, { children: [!view && !top.error ? _jsx(Text, { color: "gray", children: "Entering the circle\u2026" }) : null, top.error ? _jsx(Text, { color: "red", wrap: "wrap", children: cleanLine(top.error) }) : null, view ? (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: ["\u25CB ", view.circle.name, " ", _jsxs(Text, { color: "gray", children: ["\u00B7 ", cleanLine(view.circle.direction ?? "no direction"), " \u00B7 ", view.circle.active ? "active" : "inactive"] })] }), intentLines ? (_jsx(Box, { height: intentLines, overflow: "hidden", flexShrink: 0, children: _jsx(Text, { wrap: "wrap", children: view.circle.intention }) })) : null, _jsx(Text, { color: "gray", wrap: "truncate-end", children: [view.seat ? `you: ${view.seat}` : "you are not in this circle", people ? `with ${people}` : "", view.circle.episode ? `for ${episodeLabel(view.circle.episode)}` : ""].filter(Boolean).join(" · ") }), _jsx(Text, { color: "gray", wrap: "truncate-end", children: "\u2500\u2500 ceremonies \u2500\u2500" }), held.length === 0 ? _jsx(Text, { color: "gray", children: "None yet." }) : null, heldWindow.items.map((h, index) => {
|
|
1052
|
+
const isSelected = heldWindow.start + index === clamp(top.selected, held.length);
|
|
1053
|
+
const c = h.ceremony;
|
|
1054
|
+
const where = c.episode_path ? episodeLabel(c.episode_path) : "";
|
|
1055
|
+
return (_jsxs(Text, { wrap: "truncate-end", inverse: isSelected, children: [_jsxs(Text, { color: h.open ? "green" : "gray", children: ["\u25C6 ", h.open ? "open " : "closed"] }), " ", ceremonyHeadline(c), _jsxs(Text, { color: "gray", children: [" \u00B7 ", [typeLabel(c.type), c.direction, where, whenOf(c.timestamp)].filter(Boolean).map((part) => cleanLine(String(part))).join(" · ")] })] }, c.id));
|
|
1056
|
+
}), _jsxs(Text, { color: "gray", wrap: "truncate-end", children: ["\u2191\u2193 \u00B7 \u23CE enter", canOpen ? " · n open a ceremony" : "", " \u00B7 o pages \u00B7 Esc"] })] })) : (_jsx(Text, { color: "gray", children: "Esc" }))] }));
|
|
1057
|
+
}
|
|
1058
|
+
if (top.type === "ceremony") {
|
|
1059
|
+
const view = top.view;
|
|
1060
|
+
if (!view) {
|
|
1061
|
+
return box("magenta", _jsxs(_Fragment, { children: [top.error ? _jsx(Text, { color: "red", wrap: "wrap", children: cleanLine(top.error) }) : _jsx(Text, { color: "gray", children: "Entering the ceremony\u2026" }), _jsx(Text, { color: "gray", children: "Esc" })] }));
|
|
1062
|
+
}
|
|
1063
|
+
const layout = ceremonyLayout(view, top.tab);
|
|
1064
|
+
const count = top.tab === "turns" ? view.turns.length : view.diary.length;
|
|
1065
|
+
const selected = top.selected < 0 ? count - 1 : clamp(top.selected, count);
|
|
1066
|
+
const scroll = top.selected < 0 ? Math.max(0, layout.screen.lines.length - layout.room) : Math.min(top.scroll, Math.max(0, layout.screen.lines.length - layout.room));
|
|
1067
|
+
const where = [
|
|
1068
|
+
view.episode ? `↗ ${episodeLabel(view.episode.path)}` : "",
|
|
1069
|
+
view.circle ? `○ ${view.circle.name}` : "held in no circle",
|
|
1070
|
+
view.subject ? `about ${view.subject.name}` : "",
|
|
1071
|
+
`opened ${whenOf(view.timestamp)}`,
|
|
1072
|
+
].filter(Boolean);
|
|
1073
|
+
const keys = [
|
|
1074
|
+
["Tab", top.tab === "turns" ? "diary" : "turns"],
|
|
1075
|
+
...(view.can.speak ? [["s", "speak"]] : []),
|
|
1076
|
+
...(top.tab === "turns" && view.turns[selected] && mayWitness(view, view.turns[selected]) ? [["w", "witness"]] : []),
|
|
1077
|
+
...(view.can.diary ? [["d", "diary"]] : []),
|
|
1078
|
+
...(view.can.close ? [["x", "close"]] : []),
|
|
1079
|
+
...(view.episode ? [["e", "episode"]] : []),
|
|
1080
|
+
["o", "pages"],
|
|
1081
|
+
["Esc", ""],
|
|
1082
|
+
];
|
|
1083
|
+
const toneColor = { mark: "yellow", name: undefined, meta: "gray", text: undefined, title: "cyan", witness: "green" };
|
|
1084
|
+
return box("magenta", _jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, wrap: "truncate-end", children: ["\u25C6 ", typeLabel(view.type), " ", _jsxs(Text, { color: "gray", children: ["\u00B7 ", cleanLine(view.direction ?? "no direction"), " \u00B7 "] }), _jsx(Text, { color: view.closed ? "gray" : "green", children: view.closed ? "closed" : "open" })] }), layout.intentLines ? (_jsx(Box, { height: layout.intentLines, overflow: "hidden", flexShrink: 0, children: _jsx(Text, { wrap: "wrap", children: layout.intention }) })) : null, _jsx(Text, { color: "gray", wrap: "truncate-end", children: where.map(cleanLine).join(" · ") }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: top.tab === "turns" ? "yellow" : "gray", children: "Turns" }), _jsx(Text, { color: "gray", children: " | " }), _jsx(Text, { color: top.tab === "diary" ? "yellow" : "gray", children: "Diary" }), _jsxs(Text, { color: "gray", children: [view.me ? ` · you: ${view.me.facilitator ? "facilitator" : view.me.member ? "member" : "reader"}` : "", top.error ? ` · ${cleanLine(top.error)}` : ""] })] }), _jsx(Box, { flexDirection: "column", height: layout.room, overflow: "hidden", flexShrink: 0, children: layout.screen.lines.slice(scroll, scroll + layout.room).map((line, index) => (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", children: line.item !== undefined && line.item === selected ? "▌" : " " }), line.parts.map((part, at) => (_jsx(Text, { color: toneColor[part.tone], bold: part.tone === "name", children: part.text }, at)))] }, scroll + index))) }), _jsx(Text, { wrap: "truncate-end", children: keys.map(([k, label]) => (_jsxs(Text, { children: [_jsx(Text, { color: ["s", "w", "d", "x"].includes(k) ? "red" : "yellow", children: k }), label && (!compact || ["Tab", "s", "w", "d", "x"].includes(k)) ? ` ${label}` : "", compact ? " " : " "] }, k))) })] }));
|
|
695
1085
|
}
|
|
696
|
-
return box("yellow", _jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Keys" }), _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: "yellow", children: "/" }), " filter \u00B7 ", _jsx(Text, { color: "yellow", children: "Tab" }), " title or full text (in a file: next reference)"] }), _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: "yellow", children: "!" }), " the chronicle's attention board"] }), _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193 j k" }), " move \u00B7 ", _jsx(Text, { color: "yellow", children: "PgUp PgDn g G" }), " jump"] }), _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: "yellow", children: "Enter l \u2192" }), " open \u00B7 ", _jsx(Text, { color: "yellow", children: "Esc h \u2190" }), " back \u00B7 ", _jsx(Text, { color: "yellow", children: "q" }), " quit"] }), actions.map((action) => (_jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: "yellow", children: action.key }), " ", action.description ?? action.label, action.effect === "write" ? _jsx(Text, { color: "red", children: " (writes)" }) : null] }, action.id))), _jsx(Text, { color: "gray", children: "Any key closes." })] }));
|
|
1086
|
+
return box("yellow", _jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Keys" }), _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: "yellow", children: "/" }), " filter \u00B7 ", _jsx(Text, { color: "yellow", children: "Tab" }), " title or full text (in a file: next reference)"] }), _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: "yellow", children: "!" }), " the chronicle's attention board"] }), _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193 j k" }), " move \u00B7 ", _jsx(Text, { color: "yellow", children: "PgUp PgDn g G" }), " jump"] }), _jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: "yellow", children: "Enter l \u2192" }), " open \u00B7 ", _jsx(Text, { color: "yellow", children: "Esc h \u2190" }), " back \u00B7 ", _jsx(Text, { color: "yellow", children: "q" }), " quit"] }), actions.map((action) => (_jsxs(Text, { wrap: "wrap", children: [_jsx(Text, { color: "yellow", children: action.key }), " ", action.description ?? action.label, action.effect === "write" ? _jsx(Text, { color: "red", children: " (writes)" }) : null] }, action.id))), _jsxs(Text, { wrap: "wrap", children: ["In a circle: ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " enters a ceremony \u00B7 ", _jsx(Text, { color: "yellow", children: "n" }), " opens one. In a ceremony:", " ", _jsx(Text, { color: "yellow", children: "Tab" }), " turns or diary \u00B7 ", _jsx(Text, { color: "red", children: "s" }), " speak \u00B7 ", _jsx(Text, { color: "red", children: "w" }), " witness \u00B7", " ", _jsx(Text, { color: "red", children: "d" }), " diary \u00B7 ", _jsx(Text, { color: "red", children: "x" }), " close \u00B7 ", _jsx(Text, { color: "yellow", children: "e" }), " its episode \u00B7", " ", _jsx(Text, { color: "yellow", children: "o" }), " its pages. Turns are read again every 10 s."] }), _jsx(Text, { color: "gray", children: "Any key closes." })] }));
|
|
697
1087
|
})();
|
|
698
1088
|
const where = (() => {
|
|
699
1089
|
if (s.view === "file" && s.open) {
|
|
@@ -702,7 +1092,7 @@ export function App(props) {
|
|
|
702
1092
|
// The line number before the path, so a narrow screen keeps it (UI review, F24).
|
|
703
1093
|
return `${number} · ${line} · ${middle(cleanLine(s.open.path), Math.max(12, columns - 30))}`;
|
|
704
1094
|
}
|
|
705
|
-
const count = `${d.visible.length}
|
|
1095
|
+
const count = `${d.visible.length} episode${d.visible.length === 1 ? "" : "s"}`;
|
|
706
1096
|
const waiting = d.openCount ? ` · ! ${d.openCount}` : "";
|
|
707
1097
|
return compact ? `${count}${waiting}` : `${cleanLine(s.chronicle.root)} · ${count}${waiting}`;
|
|
708
1098
|
})();
|
|
@@ -743,6 +1133,6 @@ export function App(props) {
|
|
|
743
1133
|
? "…"
|
|
744
1134
|
: "read-only"
|
|
745
1135
|
: "";
|
|
746
|
-
return (_jsxs(Box, { flexDirection: "column", height: rows, width: columns, children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: "yellow", children: "miadi-chronicle" }), " ", _jsx(Text, { color: "gray", children: where }), readerLabel ? _jsx(Text, { color: s.reader ? "green" : "gray", children: ` · ${readerLabel}` }) : null] }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: s.inputMode ? "yellow" : "gray", children: "/ " }), cleanLine(s.query), s.inputMode ? _jsx(Text, { inverse: true, children: " " }) : null, " ", s.view === "attention" ? (_jsx(Text, { color: "gray", children: "attention" })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: s.searchMode === "title" ? "yellow" : "gray", children: "title" }), _jsx(Text, { color: "gray", children: compact ? "|" : " | " }), _jsx(Text, { color: s.searchMode === "full" ? "yellow" : "gray", children: compact ? "full" : "full text" })] })), s.view === "list" ? _jsxs(Text, { color: "gray", children: [" \u00B7 ", d.filtered.length] }) : null, s.view === "attention" ? _jsxs(Text, { color: "gray", children: [" \u00B7 ", d.board.length] }) : null] }), _jsx(Box, { flexDirection: "row", height: bodyHeight, overflow: "hidden", children: body }), _jsx(Text, { color: "gray", wrap: "truncate-end", children: s.status }), _jsxs(Text, { wrap: "truncate-end", children: [available.map((action) => (_jsxs(Text, { children: [_jsx(Text, { color: action.effect === "write" ? "red" : "yellow", children: action.key === "Enter" ? "⏎" : action.key }), " ", action.label, " "] }, action.id))), _jsx(Text, { color: "gray", children: hints })] })] }));
|
|
1136
|
+
return (_jsxs(Box, { flexDirection: "column", height: rows, width: columns, children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: "yellow", children: "miadi-chronicle" }), " ", _jsx(Text, { color: "gray", children: where }), readerLabel ? _jsx(Text, { color: s.reader ? "green" : "gray", children: ` · ${readerLabel}` }) : null] }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { color: s.inputMode ? "yellow" : "gray", children: "/ " }), cleanLine(s.query), s.inputMode ? _jsx(Text, { inverse: true, children: " " }) : null, " ", s.view === "attention" ? (_jsx(Text, { color: "gray", children: "attention" })) : (_jsxs(_Fragment, { children: [_jsx(Text, { color: s.searchMode === "title" ? "yellow" : "gray", children: "title" }), _jsx(Text, { color: "gray", children: compact ? "|" : " | " }), _jsx(Text, { color: s.searchMode === "full" ? "yellow" : "gray", children: compact ? "full" : "full text" })] })), s.view === "list" ? _jsxs(Text, { color: "gray", children: [" \u00B7 ", d.filtered.length] }) : null, s.relateFrom ? _jsxs(Text, { color: "red", children: [" \u00B7 relating ", episodeLabel(s.relateFrom), " \u2192 L"] }) : null, s.view === "attention" ? _jsxs(Text, { color: "gray", children: [" \u00B7 ", d.board.length] }) : null] }), _jsx(Box, { flexDirection: "row", height: bodyHeight, overflow: "hidden", children: body }), _jsx(Text, { color: "gray", wrap: "truncate-end", children: s.status }), _jsxs(Text, { wrap: "truncate-end", children: [(s.overlays.length ? [] : available).map((action) => (_jsxs(Text, { children: [_jsx(Text, { color: action.effect === "write" ? "red" : "yellow", children: action.key === "Enter" ? "⏎" : action.key }), " ", action.label, " "] }, action.id))), _jsx(Text, { color: "gray", children: hints })] })] }));
|
|
747
1137
|
}
|
|
748
1138
|
//# sourceMappingURL=App.js.map
|