@miadi/chronicle-tui 0.4.1 → 0.5.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 +51 -12
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +376 -17
- package/dist/App.js.map +1 -1
- package/dist/actions.d.ts +61 -11
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +84 -24
- package/dist/actions.js.map +1 -1
- package/dist/api.d.ts +9 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +20 -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 +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# @miadi/chronicle-tui
|
|
2
2
|
|
|
3
|
-
Browse the Miadi Chronicle from a terminal
|
|
4
|
-
episode, filters them as you type,
|
|
5
|
-
files, and opens the room, the wheel or
|
|
3
|
+
Browse the Miadi Chronicle from a terminal, and take part in its talking
|
|
4
|
+
circles. `miadi-chronicle` lists every episode, filters them as you type,
|
|
5
|
+
searches their text, reads a vessel's files, and opens the room, the wheel or
|
|
6
|
+
the vessel. With your person token it enters the circles you sit in: you read
|
|
7
|
+
the turns and the diary, speak, witness, write in the diary and close a
|
|
8
|
+
ceremony, as yourself.
|
|
6
9
|
|
|
7
10
|
```bash
|
|
8
11
|
npm i -g @miadi/chronicle-tui
|
|
@@ -22,13 +25,14 @@ On Termux, install with `--ignore-scripts`. Node 22 or later.
|
|
|
22
25
|
| Tab | title filter or full-text search. In an open file: the next reference (Shift+Tab: the previous one) |
|
|
23
26
|
| ↑ ↓ `j` `k`, PgUp PgDn, `g` `G` | move |
|
|
24
27
|
| Enter, `l`, → | open: an episode's files, a file, a search hit |
|
|
25
|
-
| Enter on a reference | a chronicle reference opens that episode. A ceremony or circle
|
|
28
|
+
| Enter on a reference | a chronicle reference opens that episode. A ceremony or circle opens as your seat allows (else its pages) |
|
|
26
29
|
| Esc, `h`, ← | back |
|
|
27
30
|
| `o` | the doors of the reference, file or episode in focus: room (local, tailnet, public), wheel, vessel. Enter opens one |
|
|
28
31
|
| `r` | the phone door as a QR code (`i` inverts it for a light terminal) |
|
|
29
32
|
| `p` | the five-stage proof (git and the wheel, on demand) |
|
|
30
33
|
| `a` | the episode's attention items, Open and History (Tab), each with what it cites. Enter reads one; Enter again answers an open one |
|
|
31
34
|
| `!` | the chronicle's attention board: every open item, newest asked first, filtered by `/`. `e` on an item opens its episode |
|
|
35
|
+
| `c` | talking circles: the ceremonies held in the episode in focus, the circles gathered for it, yours, the others |
|
|
32
36
|
| `v` | hold a Miadi review in the episode (writer token) |
|
|
33
37
|
| `?` | all keys · `q` quits |
|
|
34
38
|
|
|
@@ -71,10 +75,9 @@ miadi-chronicle --url https://miadi.tail3b11eb.ts.net
|
|
|
71
75
|
visitor sees no `community` episode and nothing under `captures/` or
|
|
72
76
|
`ceremonies/`. A reader the app names sees community episodes (◇); a reader
|
|
73
77
|
the app lets write sees the room's private folders too.
|
|
74
|
-
- **A ceremony or circle reference** opens as
|
|
75
|
-
seats
|
|
76
|
-
|
|
77
|
-
Never its turns. Otherwise it shows its pages, and never the wheel's record.
|
|
78
|
+
- **A ceremony or circle reference** opens as the ceremony or circle itself
|
|
79
|
+
for a person the app seats, as its page would. Otherwise it shows its pages,
|
|
80
|
+
and never the wheel's record.
|
|
78
81
|
- **Attention** works as in the web room, through `@miadi/attention-core`: the
|
|
79
82
|
board (`!`) and each episode's panel (`a`) read the vessels exactly as the
|
|
80
83
|
room does; an answer is checked the same way and posted to
|
|
@@ -85,6 +88,39 @@ miadi-chronicle --url https://miadi.tail3b11eb.ts.net
|
|
|
85
88
|
Write actions declare `effect: "write"` and are offered only when the app said
|
|
86
89
|
it would record them.
|
|
87
90
|
|
|
91
|
+
## Talking circles
|
|
92
|
+
|
|
93
|
+
`c` opens the circles, from the episode in focus: the ceremonies held in it
|
|
94
|
+
that you may read, the circles gathered for it, the circles you sit in, then
|
|
95
|
+
the others. It is the web's `/circles` and `/ceremony/{id}` pages, through
|
|
96
|
+
their routes:
|
|
97
|
+
|
|
98
|
+
| where | key | does | route |
|
|
99
|
+
|---|---|---|---|
|
|
100
|
+
| circles | Enter | enter a ceremony, or a circle | `GET /api/circles`, `GET /api/chronicle/episodes/{ref}/wheel` |
|
|
101
|
+
| a circle | Enter, `n` | enter a ceremony; open one (kind, direction, and the episode it binds to) | `GET /api/circles/{id}`, `POST /api/circles/{id}/ceremonies` |
|
|
102
|
+
| a ceremony | Tab | its turns, or its diary | `GET /api/ceremony/{id}`, read again every 10 s |
|
|
103
|
+
| a ceremony | `s` | speak a turn | `POST /api/ceremony/{id}/turns` |
|
|
104
|
+
| a ceremony | `w` | witness the selected turn (not your own, once) | `POST …/turns/{turn}/witness` |
|
|
105
|
+
| a ceremony | `d` | write in the diary, in a phase and a kind | `POST /api/ceremony/{id}/diary` |
|
|
106
|
+
| a ceremony | `x` | close it, with what the circle learned | `POST /api/ceremony/{id}/close` |
|
|
107
|
+
| a ceremony | `e`, `o` | its episode; its pages | |
|
|
108
|
+
|
|
109
|
+
- **Each act is yours.** A turn, a witness, a diary entry, a close and a new
|
|
110
|
+
ceremony go with `MIADI_PERSON_TOKEN` as the only bearer: those routes take
|
|
111
|
+
the act from whoever the bearer is and refuse the writer token. Without a
|
|
112
|
+
person token, `c` says what to set and sends nothing.
|
|
113
|
+
- **Offered as the page offers it.** Each key appears only when the app's
|
|
114
|
+
`can` says the route would accept it from you now: a seat in the circle and
|
|
115
|
+
the role's permission, an open ceremony, a facilitator to close.
|
|
116
|
+
- **A turn is a person's words.** It shows inside its ceremony to a reader the
|
|
117
|
+
app lets read that ceremony, never in a list (jgwill/Miadi#680, D3). Its
|
|
118
|
+
text passes the same sanitizer as every file.
|
|
119
|
+
- **Typing a turn.** Enter sends, Esc cancels. A paste arrives whole, line
|
|
120
|
+
breaks kept. ↑↓ change a choice (a diary phase, a direction) and Tab moves
|
|
121
|
+
between choices. Learnings for a close go one per line, or separated by
|
|
122
|
+
` | `.
|
|
123
|
+
|
|
88
124
|
## Safe to point at any chronicle
|
|
89
125
|
|
|
90
126
|
Chronicle files are written by many agents and people. Every string the
|
|
@@ -106,10 +142,11 @@ The browser holds no chronicle rules of its own:
|
|
|
106
142
|
| references in a file: `miadi-chronicle:`, `miadi-ceremony:`, `miadi-circle:`, `{{ … }}` | `@miadi/inquiry-weave` (`findTextReferences`) |
|
|
107
143
|
| the doors, as OSC 8 links | `@miadi/inquiry-weave` (`resolveForTerminal`, `resolveWheelRefForTerminal`) |
|
|
108
144
|
| the five-stage proof | `@miadi/inquiry-weave` (`episodeDoorStatus`) |
|
|
145
|
+
| circles, ceremonies, turns and the diary | the Miadi app's circle routes, as the person; closings folded into what they close as the web folds them (`lib/ceremony-view.ts`) |
|
|
109
146
|
|
|
110
|
-
A ceremony or circle
|
|
111
|
-
their page sees the reader's seat, so
|
|
112
|
-
read
|
|
147
|
+
A ceremony or circle is read only through the app, as the person whose seat
|
|
148
|
+
it is. Ceremonies are private until their page sees the reader's seat, so
|
|
149
|
+
nothing is read from the wheel directly (jgwill/Miadi#680).
|
|
113
150
|
|
|
114
151
|
Readings are guesses from keywords in an episode's name and title, and an
|
|
115
152
|
episode with no keyword reads by its number's turn of the wheel. The detail
|
|
@@ -140,6 +177,8 @@ render(<App root={process.env.MIADI_CHRONICLE_ROOT!} actions={[mine]} />)
|
|
|
140
177
|
|
|
141
178
|
An action that changes anything declares `effect: "write"` and calls the
|
|
142
179
|
existing door for it (the episode API, inquiry-weave, the ceremony routes),
|
|
143
|
-
never the files directly.
|
|
180
|
+
never the files directly. A person's acts inside a ceremony (speak, witness,
|
|
181
|
+
the diary, close) are keys of its overlay, offered where the app's `can`
|
|
182
|
+
allows them.
|
|
144
183
|
|
|
145
184
|
Ref: jgwill/Miadi#706
|
package/dist/App.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../src/App.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAiD,MAAM,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"App.d.ts","sourceRoot":"","sources":["../src/App.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAiD,MAAM,OAAO,CAAA;AAIrE,OAAO,EAML,KAAK,eAAe,EAIrB,MAAM,cAAc,CAAA;AAmErB,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,oCAAoC;IACpC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,+CAA+C;IAC/C,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,4DAA4D;IAC5D,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,gDAAgD;IAChD,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,2CAA2C;IAC3C,OAAO,CAAC,EAAE,SAAS,eAAe,EAAE,CAAA;IACpC,kFAAkF;IAClF,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,sEAAsE;IACtE,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC7B;AAoHD,wBAAgB,GAAG,CAAC,KAAK,EAAE,QAAQ,qBAi6ClC"}
|
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" };
|
|
@@ -204,8 +206,18 @@ export function App(props) {
|
|
|
204
206
|
const mayAnswer = await api.mayAnswer();
|
|
205
207
|
if (!live)
|
|
206
208
|
return;
|
|
209
|
+
// Signing in can show more episodes (community ones), so the selection is kept by name, not by row.
|
|
210
|
+
const before = derive();
|
|
211
|
+
const keep = before.s.view === "attention" ? undefined : before.currentRow?.name;
|
|
212
|
+
const keepItem = before.boardItem;
|
|
207
213
|
details.current.clear();
|
|
208
214
|
set({ reader, mayAnswer, access: { signedIn: Boolean(reader), mayWrite: mayAnswer } });
|
|
215
|
+
const after = derive();
|
|
216
|
+
const listSel = keep ? after.filtered.findIndex((row) => row.name === keep) : -1;
|
|
217
|
+
const boardSel = keepItem ? after.board.findIndex((item) => item.episode === keepItem.episode && item.id === keepItem.id) : -1;
|
|
218
|
+
if (listSel !== -1 || boardSel !== -1) {
|
|
219
|
+
set({ ...(listSel !== -1 ? { listSel } : {}), ...(boardSel !== -1 ? { boardSel } : {}), status: ui.current.status });
|
|
220
|
+
}
|
|
209
221
|
resolvePending();
|
|
210
222
|
}
|
|
211
223
|
catch (error) {
|
|
@@ -419,7 +431,169 @@ export function App(props) {
|
|
|
419
431
|
set({ overlays: stack });
|
|
420
432
|
},
|
|
421
433
|
});
|
|
434
|
+
// ── talking circles: the overlays, and the person's acts in them ─────────
|
|
435
|
+
const host = { open: openOverlay, replace: replaceOverlay };
|
|
436
|
+
const topHandle = () => ui.current.overlays[ui.current.overlays.length - 1]?.id ?? 0;
|
|
437
|
+
/** Change overlay `handle` wherever it sits in the stack; a closed one is left alone. */
|
|
438
|
+
const updateById = (handle, change) => {
|
|
439
|
+
const stack = ui.current.overlays;
|
|
440
|
+
if (!stack.some((entry) => entry.id === handle))
|
|
441
|
+
return;
|
|
442
|
+
set({ overlays: stack.map((entry) => (entry.id === handle ? { id: handle, overlay: change(entry.overlay) } : entry)) });
|
|
443
|
+
};
|
|
444
|
+
const episodeLabel = (path) => {
|
|
445
|
+
if (!path)
|
|
446
|
+
return "";
|
|
447
|
+
const row = derive().rowByName(path);
|
|
448
|
+
return row?.number !== undefined ? `ep ${row.number}` : path;
|
|
449
|
+
};
|
|
450
|
+
/**
|
|
451
|
+
* How a ceremony fills the overlay: inside the border, a title, its
|
|
452
|
+
* intention, a line of where it is held, the tabs, the lines, the keys.
|
|
453
|
+
* Shared by the keys and the screen, so a scroll is measured as drawn.
|
|
454
|
+
*/
|
|
455
|
+
const ceremonyLayout = (view, tab) => {
|
|
456
|
+
const inner = Math.max(10, columns - 4);
|
|
457
|
+
const intention = view.intentions.join(" · ");
|
|
458
|
+
const intentLines = intention ? Math.min(compact ? 2 : 3, wrapSegments([{ text: intention, style: "text" }], inner).length) : 0;
|
|
459
|
+
const room = Math.max(1, bodyHeight - 2 - (4 + intentLines));
|
|
460
|
+
return { inner, intention, intentLines, room, screen: ceremonyLines(view, tab, inner - 1) };
|
|
461
|
+
};
|
|
462
|
+
const refreshing = useRef(false);
|
|
463
|
+
/** Read the ceremony again. A reader on the newest turn stays on the newest, so new turns come into view. */
|
|
464
|
+
const reloadCeremony = async (handle) => {
|
|
465
|
+
const entry = ui.current.overlays.find((candidate) => candidate.id === handle);
|
|
466
|
+
if (!entry || entry.overlay.type !== "ceremony" || !ui.current.api || refreshing.current)
|
|
467
|
+
return;
|
|
468
|
+
refreshing.current = true;
|
|
469
|
+
try {
|
|
470
|
+
const view = await readCeremony(ui.current.api, entry.overlay.id);
|
|
471
|
+
updateById(handle, (o) => {
|
|
472
|
+
if (o.type !== "ceremony")
|
|
473
|
+
return o;
|
|
474
|
+
const count = o.tab === "turns" ? (o.view?.turns.length ?? 0) : (o.view?.diary.length ?? 0);
|
|
475
|
+
const following = o.selected < 0 || (o.tab === "turns" && o.selected >= count - 1);
|
|
476
|
+
return { ...o, view, error: undefined, selected: following && o.tab === "turns" ? -1 : o.selected };
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
catch (error) {
|
|
480
|
+
updateById(handle, (o) => (o.type === "ceremony" ? { ...o, error: cleanLine(error.message) } : o));
|
|
481
|
+
}
|
|
482
|
+
finally {
|
|
483
|
+
refreshing.current = false;
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
const reloadCircle = async (handle) => {
|
|
487
|
+
const entry = ui.current.overlays.find((candidate) => candidate.id === handle);
|
|
488
|
+
if (!entry || entry.overlay.type !== "circle" || !ui.current.api)
|
|
489
|
+
return;
|
|
490
|
+
const view = await readCircle(ui.current.api, entry.overlay.id);
|
|
491
|
+
updateById(handle, (o) => (o.type === "circle" ? { ...o, view, error: undefined } : o));
|
|
492
|
+
};
|
|
493
|
+
// A talking circle is live: the ceremony on top is read again every ten seconds.
|
|
494
|
+
useEffect(() => {
|
|
495
|
+
const timer = setInterval(() => {
|
|
496
|
+
const top = ui.current.overlays[ui.current.overlays.length - 1];
|
|
497
|
+
if (top?.overlay.type === "ceremony" && top.overlay.view)
|
|
498
|
+
void reloadCeremony(top.id);
|
|
499
|
+
}, 10_000);
|
|
500
|
+
return () => clearInterval(timer);
|
|
501
|
+
}, []);
|
|
502
|
+
const speakInput = (handle, view) => ({
|
|
503
|
+
type: "input",
|
|
504
|
+
title: `Your turn · ${typeLabel(view.type)}${view.circle ? ` · ${view.circle.name}` : ""}`,
|
|
505
|
+
hint: "Speak. The talking piece is yours. A paste keeps its lines.",
|
|
506
|
+
value: "",
|
|
507
|
+
submit: async (value) => {
|
|
508
|
+
await speak(ui.current.api, view.id, value.trim());
|
|
509
|
+
return "Spoken in the circle.";
|
|
510
|
+
},
|
|
511
|
+
after: () => reloadCeremony(handle),
|
|
512
|
+
});
|
|
513
|
+
const diaryInput = (handle, view) => ({
|
|
514
|
+
type: "input",
|
|
515
|
+
title: `Diary · ${typeLabel(view.type)}`,
|
|
516
|
+
hint: "In your own voice. One entry is one voice, in one phase.",
|
|
517
|
+
value: "",
|
|
518
|
+
choices: [
|
|
519
|
+
{ label: "phase", values: DIARY_PHASES, shown: DIARY_PHASES.map((phase) => PHASE_LABELS[phase] ?? phase), index: 1 },
|
|
520
|
+
{ label: "kind", values: ENTRY_TYPES, index: ENTRY_TYPES.indexOf("reflection") },
|
|
521
|
+
],
|
|
522
|
+
submit: async (value, [phase, entryType]) => {
|
|
523
|
+
await writeDiary(ui.current.api, view.id, { phase: phase, entryType: entryType, content: value.trim() });
|
|
524
|
+
return `Written in the diary (${phase}, ${entryType}).`;
|
|
525
|
+
},
|
|
526
|
+
after: () => reloadCeremony(handle),
|
|
527
|
+
});
|
|
528
|
+
const closeInput = (handle, view) => ({
|
|
529
|
+
type: "input",
|
|
530
|
+
title: `Close the circle · ${typeLabel(view.type)}${view.circle ? ` · ${view.circle.name}` : ""}`,
|
|
531
|
+
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.",
|
|
532
|
+
value: "",
|
|
533
|
+
allowEmpty: true,
|
|
534
|
+
submit: async (value) => {
|
|
535
|
+
const result = await closeCeremony(ui.current.api, view.id, splitLearnings(value));
|
|
536
|
+
return result.already ? "It was already closed." : "The circle is closed.";
|
|
537
|
+
},
|
|
538
|
+
after: () => reloadCeremony(handle),
|
|
539
|
+
});
|
|
540
|
+
const openInput = (handle, circle, focus) => {
|
|
541
|
+
const episodes = [...new Set([focus, circle.episode].filter((path) => Boolean(path))), ""];
|
|
542
|
+
const directions = DIRECTIONS;
|
|
543
|
+
let opened = "";
|
|
544
|
+
return {
|
|
545
|
+
type: "input",
|
|
546
|
+
title: `Open a ceremony · ${circle.name}`,
|
|
547
|
+
hint: "Its intention, in your words. The circle's members are its participants.",
|
|
548
|
+
value: "",
|
|
549
|
+
choices: [
|
|
550
|
+
{ label: "kind", values: CEREMONY_TYPES, shown: CEREMONY_TYPES.map(typeLabel), index: 0 },
|
|
551
|
+
{ label: "direction", values: DIRECTIONS, index: Math.max(0, directions.indexOf(circle.direction ?? "east")) },
|
|
552
|
+
{ label: "episode", values: episodes, shown: episodes.map((path) => (path ? episodeLabel(path) : "none")), index: 0 },
|
|
553
|
+
],
|
|
554
|
+
submit: async (value, [type, direction, episode]) => {
|
|
555
|
+
const result = await openCeremony(ui.current.api, circle.id, { type: type, direction: direction, intention: value.trim(), ...(episode ? { episode_path: episode } : {}) });
|
|
556
|
+
opened = result.ceremony.id;
|
|
557
|
+
return `Opened a ${typeLabel(type)} in ${circle.name}${episode ? `, bound to ${episodeLabel(episode)}` : ""}.`;
|
|
558
|
+
},
|
|
559
|
+
after: async () => {
|
|
560
|
+
await reloadCircle(handle);
|
|
561
|
+
if (opened)
|
|
562
|
+
enterCeremony(host, ui.current.api, opened);
|
|
563
|
+
},
|
|
564
|
+
};
|
|
565
|
+
};
|
|
566
|
+
const witnessTurn = (handle, view, index) => {
|
|
567
|
+
const turn = view.turns[index];
|
|
568
|
+
if (!turn)
|
|
569
|
+
return;
|
|
570
|
+
if (!mayWitness(view, turn)) {
|
|
571
|
+
const why = !view.can.witness
|
|
572
|
+
? view.closed
|
|
573
|
+
? "The ceremony is closed."
|
|
574
|
+
: "Your seat does not witness here."
|
|
575
|
+
: turn.speaker === view.me?.id
|
|
576
|
+
? "You spoke this turn; another witnesses it."
|
|
577
|
+
: "You witnessed this turn.";
|
|
578
|
+
return set({ status: why });
|
|
579
|
+
}
|
|
580
|
+
set({ status: `Witnessing ${turn.speakerName}'s turn…` });
|
|
581
|
+
witness(ui.current.api, view.id, turn.id).then(async () => {
|
|
582
|
+
set({ status: `Witnessed ${turn.speakerName}'s turn.` });
|
|
583
|
+
await reloadCeremony(handle);
|
|
584
|
+
}, (error) => set({ status: cleanLine(error.message) }));
|
|
585
|
+
};
|
|
422
586
|
// ── keys ─────────────────────────────────────────────────────────────────
|
|
587
|
+
// A paste into an input arrives whole, line breaks kept (bracketed paste).
|
|
588
|
+
const pasteActive = ui.current.overlays[ui.current.overlays.length - 1]?.overlay.type === "input";
|
|
589
|
+
usePaste((text) => {
|
|
590
|
+
const top = ui.current.overlays[ui.current.overlays.length - 1]?.overlay;
|
|
591
|
+
if (top?.type !== "input" || top.busy)
|
|
592
|
+
return;
|
|
593
|
+
const added = text.replace(/\r\n?/g, "\n").replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "");
|
|
594
|
+
if (added)
|
|
595
|
+
updateTop({ ...top, value: top.value + added, error: undefined });
|
|
596
|
+
}, { isActive: pasteActive });
|
|
423
597
|
useInput((input, key) => {
|
|
424
598
|
if (key.ctrl || key.meta)
|
|
425
599
|
return;
|
|
@@ -453,12 +627,25 @@ export function App(props) {
|
|
|
453
627
|
return;
|
|
454
628
|
if (key.escape)
|
|
455
629
|
return closeTop();
|
|
630
|
+
const choices = top.choices ?? [];
|
|
631
|
+
const focus = clamp(top.focus ?? 0, choices.length);
|
|
632
|
+
if (choices.length && (key.upArrow || key.downArrow)) {
|
|
633
|
+
const choice = choices[focus];
|
|
634
|
+
const count = choice.values.length;
|
|
635
|
+
const index = (choice.index + (key.downArrow ? 1 : count - 1)) % count;
|
|
636
|
+
return updateTop({ ...top, choices: choices.map((c, i) => (i === focus ? { ...c, index } : c)) });
|
|
637
|
+
}
|
|
638
|
+
if (key.tab) {
|
|
639
|
+
if (choices.length > 1)
|
|
640
|
+
updateTop({ ...top, focus: (focus + (key.shift ? choices.length - 1 : 1)) % choices.length });
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
456
643
|
if (key.return) {
|
|
457
644
|
const value = top.value;
|
|
458
|
-
if (!value.trim())
|
|
645
|
+
if (!value.trim() && !top.allowEmpty)
|
|
459
646
|
return updateTop({ ...top, error: "Nothing to send yet." });
|
|
460
647
|
updateTop({ ...top, busy: true, error: undefined });
|
|
461
|
-
top.submit(value).then(async (message) => {
|
|
648
|
+
top.submit(value, choices.map((c) => c.values[c.index])).then(async (message) => {
|
|
462
649
|
set({ status: cleanLine(message), overlays: ui.current.overlays.slice(0, -1) });
|
|
463
650
|
try {
|
|
464
651
|
await top.after?.();
|
|
@@ -498,8 +685,113 @@ export function App(props) {
|
|
|
498
685
|
}
|
|
499
686
|
if (top.type === "qr" && input === "i")
|
|
500
687
|
return updateTop({ ...top, invert: !top.invert });
|
|
501
|
-
if (top.type === "
|
|
502
|
-
|
|
688
|
+
if (top.type === "circles" && top.index) {
|
|
689
|
+
const entries = circleEntries(top.index);
|
|
690
|
+
const step = (from, by) => {
|
|
691
|
+
let at = from;
|
|
692
|
+
for (let i = from + by; i >= 0 && i < entries.length; i += by) {
|
|
693
|
+
if (entries[i].kind !== "heading") {
|
|
694
|
+
at = i;
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return at;
|
|
699
|
+
};
|
|
700
|
+
if (key.downArrow || input === "j")
|
|
701
|
+
return updateTop({ ...top, selected: step(top.selected, 1) });
|
|
702
|
+
if (key.upArrow || input === "k")
|
|
703
|
+
return updateTop({ ...top, selected: step(top.selected, -1) });
|
|
704
|
+
const entry = entries[top.selected];
|
|
705
|
+
if (entry && (key.return || input === "l" || key.rightArrow) && s.api) {
|
|
706
|
+
if (entry.kind === "ceremony")
|
|
707
|
+
return void enterCeremony(host, s.api, entry.held.ceremony.id);
|
|
708
|
+
if (entry.kind === "circle")
|
|
709
|
+
return void enterCircle(host, s.api, entry.circle.id, top.episode);
|
|
710
|
+
}
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (top.type === "circle" && top.view) {
|
|
714
|
+
const handle = topHandle();
|
|
715
|
+
const held = top.view.held;
|
|
716
|
+
if (key.downArrow || input === "j")
|
|
717
|
+
return updateTop({ ...top, selected: clamp(top.selected + 1, held.length) });
|
|
718
|
+
if (key.upArrow || input === "k")
|
|
719
|
+
return updateTop({ ...top, selected: clamp(top.selected - 1, held.length) });
|
|
720
|
+
const chosen = held[clamp(top.selected, held.length)];
|
|
721
|
+
if (chosen && (key.return || input === "l" || key.rightArrow) && s.api)
|
|
722
|
+
return void enterCeremony(host, s.api, chosen.ceremony.id);
|
|
723
|
+
if (input === "n") {
|
|
724
|
+
if (!top.view.seat || !top.view.can.open_ceremony)
|
|
725
|
+
return set({ status: top.view.circle.active ? "Opening a ceremony here needs a seat that facilitates." : "The circle is inactive." });
|
|
726
|
+
return void openOverlay(openInput(handle, top.view.circle, top.episode));
|
|
727
|
+
}
|
|
728
|
+
if (input === "o") {
|
|
729
|
+
wheelDoors("circle", top.id).then((doors) => openOverlay({ type: "doors", doors, selected: 0 }), () => { });
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
if (top.type === "ceremony" && top.view) {
|
|
735
|
+
const handle = topHandle();
|
|
736
|
+
const view = top.view;
|
|
737
|
+
const layout = ceremonyLayout(view, top.tab);
|
|
738
|
+
const count = top.tab === "turns" ? view.turns.length : view.diary.length;
|
|
739
|
+
const selected = top.selected < 0 ? count - 1 : clamp(top.selected, count);
|
|
740
|
+
const scroll = top.selected < 0 ? Math.max(0, layout.screen.lines.length - layout.room) : top.scroll;
|
|
741
|
+
const select = (index) => {
|
|
742
|
+
const next = clamp(index, count);
|
|
743
|
+
updateTop({ ...top, selected: next, scroll: scrollTo(layout.screen, next, scroll, layout.room) });
|
|
744
|
+
};
|
|
745
|
+
if (key.tab) {
|
|
746
|
+
const tab = top.tab === "turns" ? "diary" : "turns";
|
|
747
|
+
return updateTop({ ...top, tab, selected: tab === "turns" ? -1 : 0, scroll: 0 });
|
|
748
|
+
}
|
|
749
|
+
if (key.downArrow || input === "j")
|
|
750
|
+
return select(selected + 1);
|
|
751
|
+
if (key.upArrow || input === "k")
|
|
752
|
+
return select(selected - 1);
|
|
753
|
+
if (input === "g" || key.home)
|
|
754
|
+
return select(0);
|
|
755
|
+
if (input === "G" || key.end)
|
|
756
|
+
return select(count - 1);
|
|
757
|
+
if (key.pageDown || key.pageUp) {
|
|
758
|
+
const max = Math.max(0, layout.screen.lines.length - layout.room);
|
|
759
|
+
const next = clamp(scroll + (key.pageDown ? layout.room - 1 : 1 - layout.room), max + 1);
|
|
760
|
+
const at = layout.screen.ends.findIndex((end) => end > next);
|
|
761
|
+
return updateTop({ ...top, scroll: next, selected: at === -1 ? Math.max(0, count - 1) : at });
|
|
762
|
+
}
|
|
763
|
+
if (input === "s") {
|
|
764
|
+
if (!view.can.speak)
|
|
765
|
+
return set({ status: view.closed ? "The ceremony is closed." : "Only those seated in the circle may speak." });
|
|
766
|
+
return void openOverlay(speakInput(handle, view));
|
|
767
|
+
}
|
|
768
|
+
if (input === "w") {
|
|
769
|
+
if (top.tab !== "turns")
|
|
770
|
+
return set({ status: "Witnessing is of a turn: Tab to the turns." });
|
|
771
|
+
return witnessTurn(handle, view, selected);
|
|
772
|
+
}
|
|
773
|
+
if (input === "d") {
|
|
774
|
+
if (!view.can.diary)
|
|
775
|
+
return set({ status: "Writing in the diary needs a seat in the circle." });
|
|
776
|
+
return void openOverlay(diaryInput(handle, view));
|
|
777
|
+
}
|
|
778
|
+
if (input === "x") {
|
|
779
|
+
if (!view.can.close)
|
|
780
|
+
return set({ status: view.closed ? "The ceremony is closed." : "Only the facilitator closes the circle." });
|
|
781
|
+
return void openOverlay(closeInput(handle, view));
|
|
782
|
+
}
|
|
783
|
+
if (input === "e" && view.episode) {
|
|
784
|
+
if (!derive().rowByName(view.episode.path))
|
|
785
|
+
return set({ status: `${episodeLabel(view.episode.path)} is not among the episodes you see.` });
|
|
786
|
+
set({ overlays: [] });
|
|
787
|
+
return jumpTo(view.episode.path);
|
|
788
|
+
}
|
|
789
|
+
if (input === "o") {
|
|
790
|
+
wheelDoors("ceremony", view.id).then((doors) => openOverlay({ type: "doors", doors, selected: 0 }), () => { });
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
503
795
|
if (top.type === "panel") {
|
|
504
796
|
const { shown, view } = panelView(top.items, top.view, ROOM_LABELS, top.parseError);
|
|
505
797
|
if (key.tab)
|
|
@@ -673,7 +965,21 @@ export function App(props) {
|
|
|
673
965
|
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
966
|
}
|
|
675
967
|
if (top.type === "input") {
|
|
676
|
-
|
|
968
|
+
const inner = Math.max(10, columns - 4);
|
|
969
|
+
const choices = top.choices ?? [];
|
|
970
|
+
const focus = clamp(top.focus ?? 0, choices.length);
|
|
971
|
+
const hintLines = top.hint ? Math.min(3, wrapSegments([{ text: cleanLine(top.hint), style: "text" }], inner).length) : 0;
|
|
972
|
+
const choiceText = (choice) => cleanLine(choice.shown?.[choice.index] ?? choice.values[choice.index] ?? "");
|
|
973
|
+
// Choices share a line when they fit, else take one each (a phone).
|
|
974
|
+
const stacked = stringWidth(choices.map((choice) => `${choice.label} ${choiceText(choice)}`).join(" ")) > inner;
|
|
975
|
+
const choiceLines = choices.length ? (stacked ? choices.length : 1) : 0;
|
|
976
|
+
// The end of what is typed stays in view, however long a turn grows.
|
|
977
|
+
const room = Math.max(1, bodyHeight - 2 - (1 + hintLines + choiceLines + (top.busy || top.error ? 1 : 0) + 1));
|
|
978
|
+
const typed = clean(top.value)
|
|
979
|
+
.split("\n")
|
|
980
|
+
.flatMap((paragraph) => wrapSegments([{ text: paragraph, style: "text" }], inner - 2).map((line) => line.map((part) => part.text).join("")));
|
|
981
|
+
const shown = typed.slice(-room);
|
|
982
|
+
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
983
|
}
|
|
678
984
|
if (top.type === "panel") {
|
|
679
985
|
const view = panelView(top.items, top.view, ROOM_LABELS, top.parseError);
|
|
@@ -686,14 +992,67 @@ export function App(props) {
|
|
|
686
992
|
if (top.type === "item") {
|
|
687
993
|
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
994
|
}
|
|
689
|
-
if (top.type === "
|
|
690
|
-
const
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
995
|
+
if (top.type === "circles") {
|
|
996
|
+
const entries = top.index ? circleEntries(top.index) : [];
|
|
997
|
+
const listWindow = windowOf(entries, clamp(top.selected, entries.length), Math.max(1, bodyHeight - 4));
|
|
998
|
+
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) => {
|
|
999
|
+
const at = listWindow.start + index;
|
|
1000
|
+
const isSelected = at === top.selected;
|
|
1001
|
+
if (entry.kind === "heading")
|
|
1002
|
+
return _jsx(Text, { color: "gray", bold: true, wrap: "truncate-end", children: cleanLine(entry.text) }, `h${at}`);
|
|
1003
|
+
if (entry.kind === "ceremony") {
|
|
1004
|
+
const c = entry.held.ceremony;
|
|
1005
|
+
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));
|
|
1006
|
+
}
|
|
1007
|
+
const circle = entry.circle;
|
|
1008
|
+
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));
|
|
1009
|
+
}), _jsx(Text, { color: "gray", wrap: "truncate-end", children: "\u2191\u2193 \u00B7 \u23CE enter \u00B7 Esc" })] }));
|
|
1010
|
+
}
|
|
1011
|
+
if (top.type === "circle") {
|
|
1012
|
+
const view = top.view;
|
|
1013
|
+
const inner = Math.max(10, columns - 4);
|
|
1014
|
+
const intentLines = view?.circle.intention ? Math.min(compact ? 2 : 3, wrapSegments([{ text: view.circle.intention, style: "text" }], inner).length) : 0;
|
|
1015
|
+
const rows = Math.max(1, bodyHeight - 2 - (4 + intentLines));
|
|
1016
|
+
const held = view?.held ?? [];
|
|
1017
|
+
const heldWindow = windowOf(held, clamp(top.selected, held.length), rows);
|
|
1018
|
+
const people = view ? view.members.map((m) => (m.role === "member" ? m.name : `${m.name} (${m.role})`)).join(", ") : "";
|
|
1019
|
+
const canOpen = Boolean(view?.seat && view.can.open_ceremony);
|
|
1020
|
+
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) => {
|
|
1021
|
+
const isSelected = heldWindow.start + index === clamp(top.selected, held.length);
|
|
1022
|
+
const c = h.ceremony;
|
|
1023
|
+
const where = c.episode_path ? episodeLabel(c.episode_path) : "";
|
|
1024
|
+
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));
|
|
1025
|
+
}), _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" }))] }));
|
|
1026
|
+
}
|
|
1027
|
+
if (top.type === "ceremony") {
|
|
1028
|
+
const view = top.view;
|
|
1029
|
+
if (!view) {
|
|
1030
|
+
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" })] }));
|
|
1031
|
+
}
|
|
1032
|
+
const layout = ceremonyLayout(view, top.tab);
|
|
1033
|
+
const count = top.tab === "turns" ? view.turns.length : view.diary.length;
|
|
1034
|
+
const selected = top.selected < 0 ? count - 1 : clamp(top.selected, count);
|
|
1035
|
+
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));
|
|
1036
|
+
const where = [
|
|
1037
|
+
view.episode ? `↗ ${episodeLabel(view.episode.path)}` : "",
|
|
1038
|
+
view.circle ? `○ ${view.circle.name}` : "held in no circle",
|
|
1039
|
+
view.subject ? `about ${view.subject.name}` : "",
|
|
1040
|
+
`opened ${whenOf(view.timestamp)}`,
|
|
1041
|
+
].filter(Boolean);
|
|
1042
|
+
const keys = [
|
|
1043
|
+
["Tab", top.tab === "turns" ? "diary" : "turns"],
|
|
1044
|
+
...(view.can.speak ? [["s", "speak"]] : []),
|
|
1045
|
+
...(top.tab === "turns" && view.turns[selected] && mayWitness(view, view.turns[selected]) ? [["w", "witness"]] : []),
|
|
1046
|
+
...(view.can.diary ? [["d", "diary"]] : []),
|
|
1047
|
+
...(view.can.close ? [["x", "close"]] : []),
|
|
1048
|
+
...(view.episode ? [["e", "episode"]] : []),
|
|
1049
|
+
["o", "pages"],
|
|
1050
|
+
["Esc", ""],
|
|
1051
|
+
];
|
|
1052
|
+
const toneColor = { mark: "yellow", name: undefined, meta: "gray", text: undefined, title: "cyan", witness: "green" };
|
|
1053
|
+
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
1054
|
}
|
|
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." })] }));
|
|
1055
|
+
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
1056
|
})();
|
|
698
1057
|
const where = (() => {
|
|
699
1058
|
if (s.view === "file" && s.open) {
|
|
@@ -702,7 +1061,7 @@ export function App(props) {
|
|
|
702
1061
|
// The line number before the path, so a narrow screen keeps it (UI review, F24).
|
|
703
1062
|
return `${number} · ${line} · ${middle(cleanLine(s.open.path), Math.max(12, columns - 30))}`;
|
|
704
1063
|
}
|
|
705
|
-
const count = `${d.visible.length}
|
|
1064
|
+
const count = `${d.visible.length} episode${d.visible.length === 1 ? "" : "s"}`;
|
|
706
1065
|
const waiting = d.openCount ? ` · ! ${d.openCount}` : "";
|
|
707
1066
|
return compact ? `${count}${waiting}` : `${cleanLine(s.chronicle.root)} · ${count}${waiting}`;
|
|
708
1067
|
})();
|
|
@@ -743,6 +1102,6 @@ export function App(props) {
|
|
|
743
1102
|
? "…"
|
|
744
1103
|
: "read-only"
|
|
745
1104
|
: "";
|
|
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 })] })] }));
|
|
1105
|
+
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: [(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
1106
|
}
|
|
748
1107
|
//# sourceMappingURL=App.js.map
|