@lamemind/loom-deck 0.44.0 → 0.46.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 +20 -10
- package/dist/cli.js +44 -88
- package/dist/fields.js +108 -0
- package/dist/layout.js +0 -9
- package/dist/model.js +14 -0
- package/dist/overlays/sheet.js +84 -72
- package/dist/prompt-catalog.js +55 -0
- package/dist/spawn.js +26 -4
- package/dist/ui/detail-screen.js +63 -78
- package/dist/ui/fields.js +23 -0
- package/dist/ui/modals.js +9 -16
- package/dist/viewport.js +14 -15
- package/package.json +1 -1
- package/scripts/deck-run +103 -26
- package/scripts/prompt-catalog +21 -0
package/dist/overlays/sheet.js
CHANGED
|
@@ -18,7 +18,20 @@ import { scanText, topForOffset } from '../text-search.js';
|
|
|
18
18
|
import { parseMarkdown } from '../markdown.js';
|
|
19
19
|
import { cpLen, insertAt, removeAt } from '../layout.js';
|
|
20
20
|
import { sanitizeTyped } from '../glyphs.js';
|
|
21
|
-
import { DETAIL_ACTIONS, MODELS, MODEL_DEFAULT } from '../spawn.js';
|
|
21
|
+
import { ACTION_HOTKEYS, DETAIL_ACTIONS, MODELS, MODEL_DEFAULT, } from '../spawn.js';
|
|
22
|
+
import { fieldsKey } from '../fields.js';
|
|
23
|
+
import { loadPromptCatalog, promptFor } from '../prompt-catalog.js';
|
|
24
|
+
// T117 — le quattro righe dell'area di compilazione del detail, nell'ordine in
|
|
25
|
+
// cui si leggono dall'alto. Gli indici sono nominati perché li usano insieme
|
|
26
|
+
// l'handler dei tasti e la resa, e un `2` nudo in due file diversi è la
|
|
27
|
+
// coordinata che scade appena una riga si sposta.
|
|
28
|
+
export const DROW = { action: 0, prompt: 1, model: 2, title: 3 };
|
|
29
|
+
export const DETAIL_FIELDS = [
|
|
30
|
+
{ kind: 'choice', count: DETAIL_ACTIONS.length, hotkeys: ACTION_HOTKEYS },
|
|
31
|
+
{ kind: 'text' },
|
|
32
|
+
{ kind: 'choice', count: MODELS.length },
|
|
33
|
+
{ kind: 'text' },
|
|
34
|
+
];
|
|
22
35
|
export function useSheetOverlay(deps) {
|
|
23
36
|
const { rows, columns, setMode, setNote, onAction } = deps;
|
|
24
37
|
const [sheet, setSheet] = useState(null);
|
|
@@ -28,13 +41,19 @@ export function useSheetOverlay(deps) {
|
|
|
28
41
|
// deck: si azzera a ogni apertura come scroll e azione, quindi non esiste una
|
|
29
42
|
// selezione invisibile che cambi il comportamento dei tasti della lista.
|
|
30
43
|
const [model, setModel] = useState(MODEL_DEFAULT);
|
|
31
|
-
// T111 —
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
// stato invisibile che cambia il titolo dello spawn successivo.
|
|
44
|
+
// T111 — il titolo con cui nascerà la conversazione (nel sidecar e in
|
|
45
|
+
// `deck-run` il dato si chiama ancora `note`: il rename è di sola etichetta).
|
|
46
|
+
// Si azzera a ogni apertura come scroll, azione e modello: un titolo armato
|
|
47
|
+
// che sopravvive alla chiusura sarebbe uno stato invisibile che cambia il
|
|
48
|
+
// titolo dello spawn successivo.
|
|
37
49
|
const [spawnNote, setSpawnNote] = useState('');
|
|
50
|
+
// T117 — il prompt iniziale, EDITABILE. Quello che si legge nel campo è quello
|
|
51
|
+
// che parte: non un'anteprima di qualcos'altro.
|
|
52
|
+
const [prompt, setPrompt] = useState('');
|
|
53
|
+
const [cursor, setCursor] = useState({ row: DROW.action, caret: 0 });
|
|
54
|
+
// Il catalogo si legge una volta per vita del deck: è un file di quattro righe
|
|
55
|
+
// accanto al codice, non un dato che cambia sotto i piedi.
|
|
56
|
+
const catalog = useMemo(() => loadPromptCatalog(), []);
|
|
38
57
|
// T91 — ricerca dentro il detail. `open` distingue i due modi in cui la si
|
|
39
58
|
// lascia: `esc` butta via ciò che il modale ha prodotto (`find` a null,
|
|
40
59
|
// evidenziazione via), `⏎` lo congela e restituisce il controllo allo strato
|
|
@@ -84,18 +103,43 @@ export function useSheetOverlay(deps) {
|
|
|
84
103
|
return;
|
|
85
104
|
setTop(topForOffset(lines, o.start, capacity));
|
|
86
105
|
}, [findRes, occCur, lines, capacity]);
|
|
87
|
-
/** Apre il detail su una task, azzerando scroll,
|
|
106
|
+
/** Apre il detail su una task, azzerando scroll, area di compilazione e ricerca. */
|
|
88
107
|
function open(next) {
|
|
89
108
|
setSheet(next);
|
|
90
109
|
setTop(0);
|
|
91
110
|
setAction(0);
|
|
92
111
|
setModel(MODEL_DEFAULT);
|
|
93
112
|
setSpawnNote('');
|
|
113
|
+
setPrompt(promptFor(catalog, DETAIL_ACTIONS[0].kind, next.id));
|
|
114
|
+
setCursor({ row: DROW.action, caret: 0 });
|
|
94
115
|
setFind(null);
|
|
95
116
|
setOccIdx(0);
|
|
96
117
|
setNote('');
|
|
97
118
|
setMode('detail');
|
|
98
119
|
}
|
|
120
|
+
/** Cambia l'azione e RISCRIVE il prompt col default del nuovo kind (D2).
|
|
121
|
+
*
|
|
122
|
+
* Nessuna preservazione del testo modificato a mano, e non è una svista: la
|
|
123
|
+
* regola `initialDetail` del modale edit protegge da un cambio di sorgente
|
|
124
|
+
* ACCIDENTALE, e col fuoco per riga quel caso non esiste — `←→` cambiano
|
|
125
|
+
* azione solo dalla riga azione, mentre sul campo prompt muovono il caret.
|
|
126
|
+
* Senza il cambio accidentale la preservazione difenderebbe da nulla, e
|
|
127
|
+
* costerebbe un campo che non torna più al default. */
|
|
128
|
+
function selectAction(index) {
|
|
129
|
+
setAction(index);
|
|
130
|
+
const id = sheet?.id;
|
|
131
|
+
if (id)
|
|
132
|
+
setPrompt(promptFor(catalog, DETAIL_ACTIONS[index].kind, id));
|
|
133
|
+
}
|
|
134
|
+
// Il ponte fra le quattro righe e i quattro stati. Le righe restano
|
|
135
|
+
// TIPIZZATE dove vivono (`ModelKind`, indice dell'azione) invece di finire in
|
|
136
|
+
// un record generico: `fields.ts` governa la grammatica, non il modello dati.
|
|
137
|
+
const fieldsIO = {
|
|
138
|
+
text: (row) => (row === DROW.prompt ? prompt : spawnNote),
|
|
139
|
+
setText: (row, next) => (row === DROW.prompt ? setPrompt(next) : setSpawnNote(next)),
|
|
140
|
+
choice: (row) => (row === DROW.action ? action : Math.max(0, MODELS.indexOf(model))),
|
|
141
|
+
setChoice: (row, index) => row === DROW.action ? selectAction(index) : setModel(MODELS[index]),
|
|
142
|
+
};
|
|
99
143
|
function close() {
|
|
100
144
|
setMode('normal');
|
|
101
145
|
setSheet(null);
|
|
@@ -154,36 +198,26 @@ export function useSheetOverlay(deps) {
|
|
|
154
198
|
editFind((f) => ({ ...f, q: insertAt(f.q, f.caret, ins), caret: f.caret + cpLen(ins) }));
|
|
155
199
|
}
|
|
156
200
|
}
|
|
157
|
-
// T66 — detail della task: due zone (testo scrollabile +
|
|
158
|
-
//
|
|
159
|
-
// compresi: chi è già nel detail ha i bottoni. L'unica deroga è
|
|
160
|
-
// apre la ricerca nel testo invece di quella sulle conversazioni.
|
|
201
|
+
// T66 — detail della task: due zone (testo scrollabile + area di compilazione)
|
|
202
|
+
// e una posizione per ognuna. Il modo cattura TUTTO, acceleratori
|
|
203
|
+
// `^K`/`^P`/`^R` compresi: chi è già nel detail ha i bottoni. L'unica deroga è
|
|
204
|
+
// `^F`, che qui apre la ricerca nel testo invece di quella sulle conversazioni.
|
|
161
205
|
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
206
|
+
// T117 — le quattro righe si scorrono con `↑↓`, che quindi NON scrollano più il
|
|
207
|
+
// testo: la lettura del task file resta su `PgUp`/`PgDn`, cioè una granularità
|
|
208
|
+
// sola invece di due. È un costo reale su un task file lungo, ed è il prezzo di
|
|
209
|
+
// avere `↑↓` nel loro significato di sempre (muovere il fuoco) su una schermata
|
|
210
|
+
// che ospita insieme un documento e dei campi.
|
|
165
211
|
function onKey(input, key) {
|
|
166
212
|
if (find?.open) {
|
|
167
213
|
onFindKey(input, key);
|
|
168
214
|
return;
|
|
169
215
|
}
|
|
170
|
-
if (key.ctrl) {
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
|
|
175
|
-
//
|
|
176
|
-
// `^F` è la deroga dichiarata (`CTRL_DEROGATIONS.detail`): fuori dal
|
|
177
|
-
// detail è la ricerca conversazioni, qui è la ricerca nel testo. La query
|
|
178
|
-
// sopravvive a una chiusura con `⏎`, quindi riaprire riprende da dov'era.
|
|
179
|
-
// `^U` svuota la nota — non è una deroga ma una combo che il modo gestisce
|
|
180
|
-
// da sé, come il filtro del modale assegnazione. Ogni altro CTRL è inerte.
|
|
181
|
-
if (input === 'f') {
|
|
182
|
-
setFind((f) => (f ? { ...f, open: true } : { q: '', caret: 0, open: true }));
|
|
183
|
-
}
|
|
184
|
-
else if (input === 'u') {
|
|
185
|
-
setSpawnNote('');
|
|
186
|
-
}
|
|
216
|
+
if (key.ctrl && input === 'f') {
|
|
217
|
+
// Deroga dichiarata (`CTRL_DEROGATIONS.detail`): fuori dal detail `^F` è
|
|
218
|
+
// la ricerca conversazioni, qui la ricerca nel testo. La query sopravvive
|
|
219
|
+
// a una chiusura con `⏎`, quindi riaprire riprende da dov'era.
|
|
220
|
+
setFind((f) => (f ? { ...f, open: true } : { q: '', caret: 0, open: true }));
|
|
187
221
|
return;
|
|
188
222
|
}
|
|
189
223
|
if (key.escape) {
|
|
@@ -195,57 +229,33 @@ export function useSheetOverlay(deps) {
|
|
|
195
229
|
setFind(null);
|
|
196
230
|
else
|
|
197
231
|
close();
|
|
232
|
+
return;
|
|
198
233
|
}
|
|
199
|
-
|
|
200
|
-
// `⏎` esegue SEMPRE l'azione selezionata,
|
|
201
|
-
//
|
|
202
|
-
// competizione sul tasto.
|
|
234
|
+
if (key.return) {
|
|
235
|
+
// `⏎` esegue SEMPRE l'azione selezionata, da qualunque riga: i campi sono
|
|
236
|
+
// di una riga sola, quindi nessuno di loro ha da farci un a-capo.
|
|
203
237
|
const act = DETAIL_ACTIONS[action];
|
|
204
238
|
const id = sheet?.id;
|
|
205
239
|
const note = spawnNote.trim();
|
|
240
|
+
const text = prompt.trim();
|
|
206
241
|
close();
|
|
207
242
|
if (id)
|
|
208
|
-
onAction(id, act.kind, model, note);
|
|
209
|
-
|
|
210
|
-
else if (key.tab) {
|
|
211
|
-
// T108 — `tab` scorre il catalogo dei modelli, e da T111 è il SOLO canale:
|
|
212
|
-
// le cifre `1`-`4` sono passate al campo nota. Libero solo QUI: in vista
|
|
213
|
-
// normale cicla la vista del pane a fuoco. Il detail cattura l'input per
|
|
214
|
-
// intero, quindi è dove un alfabeto già speso torna disponibile.
|
|
215
|
-
setModel((m) => MODELS[(MODELS.indexOf(m) + 1) % MODELS.length]);
|
|
216
|
-
}
|
|
217
|
-
else if (key.leftArrow || key.rightArrow) {
|
|
218
|
-
// Scorrimento CICLICO come le righe di scelta del modale edit: cinque
|
|
219
|
-
// voci, arrivare in fondo e ripartire costa meno che invertire direzione.
|
|
220
|
-
const d = key.leftArrow ? -1 : 1;
|
|
221
|
-
setAction((i) => (i + d + DETAIL_ACTIONS.length) % DETAIL_ACTIONS.length);
|
|
222
|
-
}
|
|
223
|
-
else if (key.upArrow) {
|
|
224
|
-
scroll(-1);
|
|
225
|
-
}
|
|
226
|
-
else if (key.downArrow) {
|
|
227
|
-
scroll(1);
|
|
243
|
+
onAction(id, act.kind, model, note, text);
|
|
244
|
+
return;
|
|
228
245
|
}
|
|
229
|
-
|
|
246
|
+
if (key.pageUp) {
|
|
230
247
|
scroll(-capacity);
|
|
248
|
+
return;
|
|
231
249
|
}
|
|
232
|
-
|
|
250
|
+
if (key.pageDown) {
|
|
233
251
|
scroll(capacity);
|
|
252
|
+
return;
|
|
234
253
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
else if (input && !key.meta) {
|
|
242
|
-
// Ultimo ramo: tutto ciò che nessun tasto vivo ha reclamato è testo. Ci
|
|
243
|
-
// cadono le cifre `1`-`4` (prima il selettore modello) e `g`/`G` (prima
|
|
244
|
-
// gli estremi del testo, che nel detail si raggiungono con
|
|
245
|
-
// `PgUp`/`PgDn`); nel reader fullscreen restano, perché lì nessun campo
|
|
246
|
-
// contende le lettere.
|
|
247
|
-
setSpawnNote((s) => s + sanitizeTyped(input));
|
|
248
|
-
}
|
|
254
|
+
// Tutto il resto è dell'area di compilazione. Ciò che non consuma resta
|
|
255
|
+
// inerte — dentro un modo capturing è la scelta giusta: gli acceleratori
|
|
256
|
+
// globali non devono riattivarsi, e su una riga a scelta una lettera che
|
|
257
|
+
// non è la sua non deve finire in nessun campo.
|
|
258
|
+
fieldsKey(input, key, DETAIL_FIELDS, cursor, setCursor, fieldsIO);
|
|
249
259
|
}
|
|
250
260
|
return {
|
|
251
261
|
sheet,
|
|
@@ -254,6 +264,8 @@ export function useSheetOverlay(deps) {
|
|
|
254
264
|
action,
|
|
255
265
|
model,
|
|
256
266
|
spawnNote,
|
|
267
|
+
prompt,
|
|
268
|
+
cursor,
|
|
257
269
|
find,
|
|
258
270
|
lines,
|
|
259
271
|
capacity,
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// T117 — il testo dei prompt iniziali, letto dal file dati che il deck CONDIVIDE
|
|
2
|
+
// con `deck-run`.
|
|
3
|
+
//
|
|
4
|
+
// Fino a T116 il catalogo era un `case` dentro `deck-run` e il deck non ne aveva
|
|
5
|
+
// mai visto una stringa: passava un simbolo (`--prompt-kind`) e il testo restava
|
|
6
|
+
// dall'altra parte. Un campo che MOSTRA il prompt prima dello spawn rompe quella
|
|
7
|
+
// divisione — per mostrarlo bisogna averlo — e le due strade erano ricopiare il
|
|
8
|
+
// catalogo qui (seconda scrittura delle stesse regole, divergente alla prima
|
|
9
|
+
// voce aggiunta da una parte sola) oppure spostarlo in un dato che entrambi
|
|
10
|
+
// leggono. È la seconda.
|
|
11
|
+
//
|
|
12
|
+
// Il file sta in `scripts/`, sibling di `deck-run`, e non in `src/`: la cartella
|
|
13
|
+
// è già dentro `files` di package.json, quindi viene pubblicata as-is su npm,
|
|
14
|
+
// mentre `tsc` non copia asset in `dist/` e un file dati sotto `src/` avrebbe
|
|
15
|
+
// richiesto un passo di build apposta.
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { dirname, join } from 'node:path';
|
|
19
|
+
/** Stessa risalita di `DECK_RUN`: src/ (dev) e dist/ (build) sono entrambi
|
|
20
|
+
* figli della package root, quindi il sibling è un livello sopra. */
|
|
21
|
+
export const PROMPT_CATALOG = join(dirname(fileURLToPath(import.meta.url)), '..', 'scripts', 'prompt-catalog');
|
|
22
|
+
/**
|
|
23
|
+
* Il catalogo come mappa kind → template, col placeholder `{TASK}` ancora
|
|
24
|
+
* dentro.
|
|
25
|
+
*
|
|
26
|
+
* File illeggibile → mappa VUOTA, non un fallback cablato: un default scritto
|
|
27
|
+
* qui sarebbe esattamente la copia del catalogo che questo modulo esiste per non
|
|
28
|
+
* avere, e divergerebbe in silenzio. L'assenza si vede — il campo prompt resta
|
|
29
|
+
* vuoto — e uno spawn senza prompt è un esito benigno.
|
|
30
|
+
*/
|
|
31
|
+
export function loadPromptCatalog(path = PROMPT_CATALOG) {
|
|
32
|
+
const out = new Map();
|
|
33
|
+
let raw;
|
|
34
|
+
try {
|
|
35
|
+
raw = readFileSync(path, 'utf8');
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
for (const line of raw.split('\n')) {
|
|
41
|
+
if (!line.trim() || line.startsWith('#'))
|
|
42
|
+
continue;
|
|
43
|
+
const tab = line.indexOf('\t');
|
|
44
|
+
if (tab < 0)
|
|
45
|
+
continue;
|
|
46
|
+
out.set(line.slice(0, tab).trim(), line.slice(tab + 1).trim());
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
/** Il prompt di un kind su una task. Kind fuori catalogo (`none`, o un file
|
|
51
|
+
* mutilo) → stringa vuota, che è la stessa cosa che `deck-run` fa con `none`. */
|
|
52
|
+
export function promptFor(catalog, kind, taskId) {
|
|
53
|
+
const template = catalog.get(kind);
|
|
54
|
+
return template ? template.replaceAll('{TASK}', taskId) : '';
|
|
55
|
+
}
|
package/dist/spawn.js
CHANGED
|
@@ -142,6 +142,14 @@ export const DETAIL_ACTIONS = [
|
|
|
142
142
|
{ kind: 'recap', label: 'status' },
|
|
143
143
|
{ kind: 'checkpoint', label: 'checkpoint' },
|
|
144
144
|
];
|
|
145
|
+
// T117 — selezione DIRETTA di un'azione con la sua iniziale (`o p r s c`),
|
|
146
|
+
// accanto allo scorrimento `←→`. Derivate dalle label e non cablate: una voce
|
|
147
|
+
// aggiunta al catalogo porta con sé la propria lettera, invece di lasciare
|
|
148
|
+
// indietro una seconda lista.
|
|
149
|
+
// Il vincolo che la derivazione impone al catalogo: le iniziali devono restare
|
|
150
|
+
// DISTINTE fra loro. Due label con la stessa lettera renderebbero la seconda
|
|
151
|
+
// irraggiungibile, e in silenzio — chi aggiunge una voce lo controlla qui.
|
|
152
|
+
export const ACTION_HOTKEYS = Object.fromEntries(DETAIL_ACTIONS.map((a, i) => [a.label[0], i]));
|
|
145
153
|
// Spawn detached: il deck spawna ma NON contiene la sessione (la possiede
|
|
146
154
|
// ptyxis-agent). unref + stdio ignore → ritorna subito, la TUI resta viva.
|
|
147
155
|
// sessionId pinnato (T27) → il binding sidecar è deterministico allo spawn.
|
|
@@ -157,14 +165,28 @@ export const DETAIL_ACTIONS = [
|
|
|
157
165
|
// separino, quindi il flag non appartiene alla ripresa — vale su ogni spawn.
|
|
158
166
|
// Assente quando la nota è vuota, e non passato vuoto: `--title-note ''`
|
|
159
167
|
// produrrebbe un `«»` a vuoto nel titolo.
|
|
160
|
-
|
|
161
|
-
|
|
168
|
+
// T117 — `prompt` è il TESTO letterale, e quando c'è sostituisce il kind:
|
|
169
|
+
// `--prompt` e `--prompt-kind` sono mutuamente esclusivi in deck-run. Serve al
|
|
170
|
+
// detail, dove il prompt è un campo editabile e quello che l'utente legge è
|
|
171
|
+
// quello che parte — dopo una modifica nessun kind lo descrive più.
|
|
172
|
+
// `undefined` = questo percorso non ha un campo prompt (gli acceleratori della
|
|
173
|
+
// lista) e viaggia col simbolo, come prima. Stringa VUOTA ≠ undefined: è la
|
|
174
|
+
// richiesta esplicita di nessun prompt (azione `open`, o un campo svuotato a
|
|
175
|
+
// mano) e si esprime col kind `none`, perché senza flag deck-run cadrebbe sul
|
|
176
|
+
// proprio default `recap`.
|
|
177
|
+
export function deckArgs(id, sessionId, kind, model = MODEL_DEFAULT, spawnNote, prompt) {
|
|
178
|
+
const promptArgs = prompt === undefined
|
|
179
|
+
? ['--prompt-kind', kind]
|
|
180
|
+
: prompt
|
|
181
|
+
? ['--prompt', prompt]
|
|
182
|
+
: ['--prompt-kind', 'none'];
|
|
183
|
+
const args = [id, '--session-id', sessionId, ...promptArgs, '--model', model];
|
|
162
184
|
if (spawnNote)
|
|
163
185
|
args.push('--title-note', spawnNote);
|
|
164
186
|
return args;
|
|
165
187
|
}
|
|
166
|
-
export function spawnDeck(id, cwd, sessionId, kind, model = MODEL_DEFAULT, spawnNote) {
|
|
167
|
-
return launchDeckRun(deckArgs(id, sessionId, kind, model, spawnNote), cwd);
|
|
188
|
+
export function spawnDeck(id, cwd, sessionId, kind, model = MODEL_DEFAULT, spawnNote, prompt) {
|
|
189
|
+
return launchDeckRun(deckArgs(id, sessionId, kind, model, spawnNote, prompt), cwd);
|
|
168
190
|
}
|
|
169
191
|
// T49 — resume di una sessione esistente come nuova tab Ptyxis. Scoped (taskId
|
|
170
192
|
// presente) → `deck-run <task> --resume <sid>`: la ripresa eredita LOOM_TASK +
|
package/dist/ui/detail-screen.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// Detail della task (T66): quarta schermata sostitutiva, col markdown reso in
|
|
3
3
|
// span tipizzati (T75) e la ricerca interna (T91).
|
|
4
4
|
import { Box, Text } from 'ink';
|
|
5
|
-
import {
|
|
5
|
+
import { cut, cutParts } from '../width.js';
|
|
6
6
|
import { sliceLine } from '../text-search.js';
|
|
7
7
|
import { sliceSpans } from '../markdown.js';
|
|
8
|
-
import {
|
|
8
|
+
import { FieldText } from './fields.js';
|
|
9
9
|
import { DETAIL_ACTIONS, MODELS } from '../spawn.js';
|
|
10
|
-
import {
|
|
10
|
+
import { DROW } from '../overlays/sheet.js';
|
|
11
|
+
import { CARET, CARET_OFF, WARN } from '../glyphs.js';
|
|
11
12
|
/** Resa di ogni costrutto markdown (T75 · D4): un solo livello di enfasi per
|
|
12
13
|
* costrutto, senza un secondo alfabeto da imparare. Heading uguali a ogni
|
|
13
14
|
* livello — la gerarchia la porta già il testo. `code` e `fence` condividono
|
|
@@ -52,90 +53,74 @@ export function DetailLine({ line, spans, occ, current, }) {
|
|
|
52
53
|
return (_jsx(Text, { bold: st?.bold, color: st?.color, children: sliceLine(seg.text, at, occ, current).map((p, j) => p.hit ? (_jsx(Text, { backgroundColor: p.current ? 'cyan' : 'yellow', color: "black", children: p.text }, j)) : (_jsx(Text, { children: p.text }, j))) }, i));
|
|
53
54
|
}) }));
|
|
54
55
|
}
|
|
55
|
-
/**
|
|
56
|
-
*
|
|
57
|
-
* perché il campo vive in FLUSSO su una riga condivisa con altro (il contatore
|
|
58
|
-
* di occorrenze per la ricerca, il segnaposto per la nota) — non su una riga
|
|
59
|
-
* propria. Lo usano i due campi del detail: la ricerca `^F`, dove il caret si
|
|
60
|
-
* muove, e la nota (T111), dove il caret sta sempre in coda. */
|
|
61
|
-
export function DetailTextField({ value, caret, cols }) {
|
|
62
|
-
const win = caretWindow(value, caret, cols);
|
|
63
|
-
return (_jsxs(_Fragment, { children: [_jsx(Text, { children: sanitize(win.head) }), _jsx(Text, { inverse: true, children: sanitize(win.at) }), _jsx(Text, { children: sanitize(win.tail) })] }));
|
|
64
|
-
}
|
|
65
|
-
/** Segnaposto della riga nota quando il campo è vuoto. Costante e non letterale
|
|
66
|
-
* inline perché la sua larghezza entra nel budget del campo accanto. */
|
|
56
|
+
/** Segnaposti delle due righe di testo a campo vuoto. Costanti e non letterali
|
|
57
|
+
* inline perché la loro larghezza entra nel budget del campo accanto. */
|
|
67
58
|
const NOTE_HINT = ' · nome della conversazione';
|
|
59
|
+
const PROMPT_HINT = ' · nessun prompt iniziale';
|
|
60
|
+
/** Le lettere di selezione diretta dell'azione, per la riga hint. Derivate dallo
|
|
61
|
+
* stesso catalogo di `ACTION_HOTKEYS`, così una voce aggiunta compare qui senza
|
|
62
|
+
* che nessuno ci ripassi — un'etichetta che nomina dei tasti a mano è accoppiata
|
|
63
|
+
* al binding, e resta indietro appena il binding cambia. */
|
|
64
|
+
const ACTION_KEYS = DETAIL_ACTIONS.map((a) => a.label[0]).join(' ');
|
|
65
|
+
/** Prefisso incolonnato delle quattro righe dell'area di compilazione: le
|
|
66
|
+
* etichette si leggono una sotto l'altra, quindi la larghezza è quella della
|
|
67
|
+
* più lunga. Entra nel budget di ogni campo, da cui la costante. */
|
|
68
|
+
const LABEL_W = 8;
|
|
69
|
+
/** Una riga a SCELTA dell'area di compilazione: bottoni affiancati, voce attiva
|
|
70
|
+
* in video inverso.
|
|
71
|
+
*
|
|
72
|
+
* Due passate di `cutParts`: la seconda serve SOLO quando qualcosa cade, e
|
|
73
|
+
* riserva le colonne del contatore. Riservarle sempre costerebbe 6 colonne su
|
|
74
|
+
* ogni terminale largo per un avviso che lì non comparirà mai.
|
|
75
|
+
*
|
|
76
|
+
* `priority` sulla voce SELEZIONATA e non sulla prima: qui il troncamento
|
|
77
|
+
* cancellerebbe l'unica informazione che la riga esiste per dare — quale valore
|
|
78
|
+
* sta per essere usato.
|
|
79
|
+
*/
|
|
80
|
+
function ChoiceRow({ label, values, index, focused, width, }) {
|
|
81
|
+
const segs = values.map((v) => `[ ${v} ]`);
|
|
82
|
+
const parts = [];
|
|
83
|
+
segs.forEach((s, i) => {
|
|
84
|
+
if (i > 0)
|
|
85
|
+
parts.push(' ');
|
|
86
|
+
parts.push(s);
|
|
87
|
+
});
|
|
88
|
+
const avail = Math.max(0, width - LABEL_W - CARET_OFF.length);
|
|
89
|
+
let shown = cutParts(parts, avail, index * 2);
|
|
90
|
+
const dropped = (v) => segs.filter((s, i) => v[i * 2] !== s).length;
|
|
91
|
+
if (dropped(shown) > 0)
|
|
92
|
+
shown = cutParts(parts, Math.max(0, avail - 6), index * 2);
|
|
93
|
+
const cut = dropped(shown);
|
|
94
|
+
return (_jsxs(Text, { wrap: "truncate-end", children: [focused ? CARET : CARET_OFF, _jsx(Text, { dimColor: true, children: label.padEnd(LABEL_W) }), shown.map((part, i) => i % 2 === 1 ? (_jsx(Text, { children: part }, i)) : (_jsx(Text, { inverse: i / 2 === index, color: i / 2 === index ? 'green' : 'gray', children: part }, i))), cut > 0 ? _jsxs(Text, { color: "yellow", children: [" \u00B7 +", cut] }) : null] }));
|
|
95
|
+
}
|
|
68
96
|
/**
|
|
69
|
-
* Detail della task (T66): il task file scrollabile più
|
|
97
|
+
* Detail della task (T66): il task file scrollabile più l'area di compilazione.
|
|
70
98
|
*
|
|
71
99
|
* Unisce due gesti che erano due schermate — leggere la task e agire su di essa
|
|
72
100
|
* — perché convergono sullo stesso oggetto: si legge la Description proprio per
|
|
73
101
|
* decidere QUALE azione lanciare, e con due overlay separati quella decisione
|
|
74
102
|
* costava uscire dal viewer e ricordarsi la combo.
|
|
75
103
|
*
|
|
76
|
-
*
|
|
104
|
+
* T117 — i quattro parametri dello spawn sono quattro RIGHE con un fuoco solo,
|
|
105
|
+
* la stessa forma del modale edit: prima erano tre alfabeti diversi sulla stessa
|
|
106
|
+
* schermata (`←→` per l'azione, `tab` per il modello, i tasti nudi per la nota),
|
|
107
|
+
* e ogni parametro aggiunto ne chiedeva un quarto.
|
|
108
|
+
*
|
|
109
|
+
* Le righe a scelta restano BOTTONI AFFIANCATI e non menu verticali: un
|
|
77
110
|
* rettangolo ha già coordinate e area cliccabile, quindi il layout sopravvive
|
|
78
|
-
* all'arrivo del mouse (T21 · SGR enable + hit-test) senza migrazione.
|
|
79
|
-
* navigazione da tastiera ci si sovrappone senza conflitti.
|
|
111
|
+
* all'arrivo del mouse (T21 · SGR enable + hit-test) senza migrazione.
|
|
80
112
|
*/
|
|
81
|
-
export function DetailScreen({ id, title, missing, lines, spans, top, total, capacity, action, model, spawnNote, columns, find, occ, occCur, }) {
|
|
113
|
+
export function DetailScreen({ id, title, missing, lines, spans, top, total, capacity, action, model, spawnNote, prompt, cursor, columns, find, occ, occCur, }) {
|
|
82
114
|
const last = Math.min(total, top + capacity);
|
|
83
|
-
// Il taglio lo fa il chiamante (invariante ③ di width.ts):
|
|
84
|
-
// ASCII, quindi `truncate-end` oggi darebbe il risultato giusto per caso
|
|
85
|
-
// la correttezza non deve dipendere dall'alfabeto che capita nella riga.
|
|
115
|
+
// Il taglio lo fa il chiamante (invariante ③ di width.ts): le righe bottoni
|
|
116
|
+
// sono ASCII, quindi `truncate-end` oggi darebbe il risultato giusto per caso
|
|
117
|
+
// — ma la correttezza non deve dipendere dall'alfabeto che capita nella riga.
|
|
86
118
|
const width = Math.max(20, (columns || 80) - 4);
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
});
|
|
94
|
-
const dropped = (v, of) => of.filter((s, i) => v[i * 2] !== s).length;
|
|
95
|
-
// Due passate: la seconda serve SOLO quando qualcosa cade, e riserva le
|
|
96
|
-
// colonne del contatore. Riservarle sempre costerebbe 6 colonne su ogni
|
|
97
|
-
// terminale largo per un avviso che lì non comparirà mai.
|
|
98
|
-
let shown = cutParts(parts, width);
|
|
99
|
-
if (dropped(shown, segs) > 0)
|
|
100
|
-
shown = cutParts(parts, Math.max(0, width - 6));
|
|
101
|
-
const cutCount = dropped(shown, segs);
|
|
102
|
-
// T108 — riga del selettore modello: bottoni affiancati IDENTICI a quelli
|
|
103
|
-
// della barra azioni, quadre comprese, e voce attiva in video inverso. Due
|
|
104
|
-
// enfasi diverse su due righe adiacenti (là `inverse`, qui un grassetto
|
|
105
|
-
// colorato) obbligano a guardare da vicino per capire quale voce è scelta —
|
|
106
|
-
// la stessa resa si legge di colpo su entrambe.
|
|
107
|
-
// T111 — la cifra è USCITA dalla quadra: `1`-`4` sono passate al campo nota e
|
|
108
|
-
// il modello si scorre col solo `tab`. Un'etichetta che nomina un tasto è
|
|
109
|
-
// accoppiata al binding, e tenerla dopo che il binding è morto non è
|
|
110
|
-
// un'informazione parziale — è una resa che dichiara un tasto che non fa più
|
|
111
|
-
// quella cosa, cioè peggio di nessuna indicazione.
|
|
112
|
-
// `priority` sulla voce SELEZIONATA e non sulla prima: qui il troncamento
|
|
113
|
-
// cancellerebbe l'unica informazione che la riga esiste per dare — quale
|
|
114
|
-
// modello sta per essere usato — mentre nella barra azioni la voce attiva è
|
|
115
|
-
// comunque nota dal tasto appena premuto.
|
|
116
|
-
const mSegs = MODELS.map((m) => `[ ${m} ]`);
|
|
117
|
-
const mParts = [];
|
|
118
|
-
mSegs.forEach((s, i) => {
|
|
119
|
-
if (i > 0)
|
|
120
|
-
mParts.push(' ');
|
|
121
|
-
mParts.push(s);
|
|
122
|
-
});
|
|
123
|
-
const mIdx = Math.max(0, MODELS.indexOf(model));
|
|
124
|
-
// `- 8` = le colonne del prefisso "modello ", che sta fuori dai parts perché
|
|
125
|
-
// non è un bottone e non deve mai cadere.
|
|
126
|
-
const mWidth = Math.max(0, width - 8);
|
|
127
|
-
let mShown = cutParts(mParts, mWidth, mIdx * 2);
|
|
128
|
-
if (dropped(mShown, mSegs) > 0)
|
|
129
|
-
mShown = cutParts(mParts, Math.max(0, mWidth - 6), mIdx * 2);
|
|
130
|
-
const mCut = dropped(mShown, mSegs);
|
|
131
|
-
// T111 — riga della nota. FISSA (D1): il valore armato si vede sempre, e il
|
|
132
|
-
// suo costo sta in `DETAIL_CHROME` invece che in un secondo `extra`
|
|
133
|
-
// condizionale di `detailCapacity`. Prefisso largo 8 come `modello `, così i
|
|
134
|
-
// due parametri dello spawn si leggono incolonnati.
|
|
135
|
-
// Il campo non ha nessun tasto che lo apra: il cursore è l'unica cosa che lo
|
|
136
|
-
// dichiara attivo, e il segnaposto dice cosa ci si scrive. Le colonne del
|
|
137
|
-
// segnaposto si riservano SOLO quando c'è (cioè a nota vuota): riservarle
|
|
138
|
-
// sempre toglierebbe testo visibile a una nota lunga per un avviso assente.
|
|
139
|
-
const nWidth = Math.max(10, width - 8 - (spawnNote ? 0 : NOTE_HINT.length));
|
|
140
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", children: id }), " \u00B7 ", cut(title, Math.max(10, width - 34)), missing ? '' : ` · righe ${total === 0 ? 0 : top + 1}-${last} di ${total}`] }), find?.open ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " occorrenza \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " caret \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^U" }), " svuota \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " tieni \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193/PgUp/PgDn" }), " testo \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " azione \u00B7", ' ', _jsx(Text, { color: "yellow", children: "tab" }), " modello \u00B7", ' ', _jsx(Text, { color: "yellow", children: "scrivi" }), " nota \u00B7 ", _jsx(Text, { color: "yellow", children: "^U" }), " svuota \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^F" }), " cerca \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " esegui \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " chiudi"] })), _jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: missing ? (_jsxs(Text, { color: "yellow", wrap: "truncate-end", children: [WARN, " task file non trovato \u00B7 le azioni restano attive (deck-run risolve la task per id)"] })) : (lines.map((l, i) => (_jsx(DetailLine, { line: l, spans: spans, occ: occ, current: occCur }, top + i)))) }), find?.open ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { dimColor: true, children: "cerca " }), _jsx(DetailTextField, { value: find.q, caret: find.caret, cols: Math.max(10, width - 28) }), find.q.length === 0 ? (_jsx(Text, { dimColor: true, children: " \u00B7 digita per cercare" })) : occ.length === 0 ? (_jsx(Text, { color: "yellow", children: " \u00B7 nessuna occorrenza" })) : (_jsxs(Text, { color: "cyan", children: [' ', "\u00B7 ", occCur + 1, "/", occ.length] }))] }) })) : null, _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { dimColor: true, children: "modello " }), mShown.map((part, i) => i % 2 === 1 ? (_jsx(Text, { children: part }, i)) : (_jsx(Text, { inverse: i / 2 === mIdx, color: i / 2 === mIdx ? 'green' : 'gray', children: part }, i))), mCut > 0 ? _jsxs(Text, { color: "yellow", children: [" \u00B7 +", mCut] }) : null] }), _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { dimColor: true, children: "nota " }), _jsx(DetailTextField, { value: spawnNote, caret: cpLen(spawnNote), cols: nWidth }), spawnNote ? null : _jsx(Text, { dimColor: true, children: NOTE_HINT })] }), _jsxs(Text, { wrap: "truncate-end", children: [shown.map((part, i) => i % 2 === 1 ? (_jsx(Text, { children: part }, i)) : (_jsx(Text, { inverse: i / 2 === action, color: i / 2 === action ? 'green' : 'gray', children: part }, i))), cutCount > 0 ? _jsxs(Text, { color: "yellow", children: [" \u00B7 +", cutCount] }) : null] })] })] }));
|
|
119
|
+
const mark = (row) => (cursor.row === row ? CARET : CARET_OFF);
|
|
120
|
+
// Budget dei due campi di testo: la cornice, il marker di riga e l'etichetta
|
|
121
|
+
// incolonnata. Le colonne del segnaposto si riservano SOLO quando c'è (cioè a
|
|
122
|
+
// campo vuoto): riservarle sempre toglierebbe testo visibile a un titolo lungo
|
|
123
|
+
// per un avviso assente.
|
|
124
|
+
const textW = (extra) => Math.max(10, width - LABEL_W - CARET_OFF.length - extra);
|
|
125
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "cyan", children: id }), " \u00B7 ", cut(title, Math.max(10, width - 34)), missing ? '' : ` · righe ${total === 0 ? 0 : top + 1}-${last} di ${total}`] }), find?.open ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " occorrenza \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " caret \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^U" }), " svuota \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " tieni \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [_jsx(Text, { color: "yellow", children: "PgUp/PgDn" }), " testo \u00B7", ' ', _jsx(Text, { color: "yellow", children: ACTION_KEYS }), " azione \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^U" }), " svuota campo \u00B7 ", _jsx(Text, { color: "yellow", children: "^F" }), " cerca"] })), _jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, marginTop: 1, children: missing ? (_jsxs(Text, { color: "yellow", wrap: "truncate-end", children: [WARN, " task file non trovato \u00B7 le azioni restano attive (deck-run risolve la task per id)"] })) : (lines.map((l, i) => (_jsx(DetailLine, { line: l, spans: spans, occ: occ, current: occCur }, top + i)))) }), find?.open ? (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { dimColor: true, children: "cerca " }), _jsx(FieldText, { value: find.q, caret: find.caret, focused: true, cols: Math.max(10, width - 28) }), find.q.length === 0 ? (_jsx(Text, { dimColor: true, children: " \u00B7 digita per cercare" })) : occ.length === 0 ? (_jsx(Text, { color: "yellow", children: " \u00B7 nessuna occorrenza" })) : (_jsxs(Text, { color: "cyan", children: [' ', "\u00B7 ", occCur + 1, "/", occ.length] }))] }) })) : null, _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(ChoiceRow, { label: "azione", values: DETAIL_ACTIONS.map((a) => a.label), index: action, focused: cursor.row === DROW.action, width: width }), _jsxs(Text, { wrap: "truncate-end", children: [mark(DROW.prompt), _jsx(Text, { dimColor: true, children: 'prompt'.padEnd(LABEL_W) }), _jsx(FieldText, { value: prompt, caret: cursor.caret, focused: cursor.row === DROW.prompt, cols: textW(prompt ? 0 : PROMPT_HINT.length) }), prompt ? null : _jsx(Text, { dimColor: true, children: PROMPT_HINT })] }), _jsx(ChoiceRow, { label: "modello", values: MODELS, index: Math.max(0, MODELS.indexOf(model)), focused: cursor.row === DROW.model, width: width }), _jsxs(Text, { wrap: "truncate-end", children: [mark(DROW.title), _jsx(Text, { dimColor: true, children: 'titolo'.padEnd(LABEL_W) }), _jsx(FieldText, { value: spawnNote, caret: cursor.caret, focused: cursor.row === DROW.title, cols: textW(spawnNote ? 0 : NOTE_HINT.length) }), spawnNote ? null : _jsx(Text, { dimColor: true, children: NOTE_HINT })] })] })] }));
|
|
141
126
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
// T117 — resa di un campo di testo dentro un'area di compilazione: finestra
|
|
3
|
+
// ancorata al caret, cursore inverso sulla cella REALE.
|
|
4
|
+
//
|
|
5
|
+
// Uno solo per le due schermate. Esisteva in due copie — `EditTextField` nel
|
|
6
|
+
// modale edit e `DetailTextField` nel detail — e la seconda si dichiarava
|
|
7
|
+
// «gemello del primo senza la label»: la label infatti non è del campo, è del
|
|
8
|
+
// LAYOUT che lo ospita, e i due layout la mettono in posti diversi (l'edit dopo
|
|
9
|
+
// il marker di riga, il detail dentro un prefisso incolonnato). Tolta di mezzo,
|
|
10
|
+
// dei due gemelli resta una cosa sola.
|
|
11
|
+
import { Text } from 'ink';
|
|
12
|
+
import { caretWindow, sanitize } from '../width.js';
|
|
13
|
+
import { cpLen } from '../layout.js';
|
|
14
|
+
/**
|
|
15
|
+
* Fuori fuoco (`focused` falso) il caret non si disegna e la finestra si ancora
|
|
16
|
+
* in fondo, che è la vista utile per un campo che non si sta scrivendo: `at` lì
|
|
17
|
+
* è solo la cella virtuale di fine campo, e disegnarla aggiungerebbe al testo
|
|
18
|
+
* uno spazio che non gli appartiene.
|
|
19
|
+
*/
|
|
20
|
+
export function FieldText({ value, caret, focused, cols, }) {
|
|
21
|
+
const win = caretWindow(value, focused ? caret : cpLen(value), cols);
|
|
22
|
+
return (_jsxs(_Fragment, { children: [_jsx(Text, { children: sanitize(win.head) }), focused ? _jsx(Text, { inverse: true, children: sanitize(win.at) }) : null, _jsx(Text, { children: sanitize(win.tail) })] }));
|
|
23
|
+
}
|
package/dist/ui/modals.js
CHANGED
|
@@ -3,8 +3,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
3
3
|
// schermate sostitutive costano righe al budget d'altezza, quindi ognuno ha un
|
|
4
4
|
// costo dichiarato in `viewport.ts`.
|
|
5
5
|
import { Box, Text } from 'ink';
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { cut, cutParts, sanitize } from '../width.js';
|
|
7
|
+
import { FieldText } from './fields.js';
|
|
8
8
|
import { CARET, CARET_OFF, WARN } from '../glyphs.js';
|
|
9
9
|
import { EDIT_PRI, EDIT_PROG } from '../model.js';
|
|
10
10
|
import { PRI_ENTRIES, PROG_ENTRIES } from '../view.js';
|
|
@@ -80,24 +80,17 @@ function IgnoredChoice({ survivors, mode, width, }) {
|
|
|
80
80
|
const shown = cutParts(parts, width, mode === 'keep' ? 1 : 3);
|
|
81
81
|
return (_jsx(Text, { wrap: "truncate-end", children: shown.map((s, i) => s ? (_jsx(Text, { color: "yellow", inverse: (i === 1 && mode === 'keep') || (i === 3 && mode === 'purge'), children: s }, i)) : null) }));
|
|
82
82
|
}
|
|
83
|
-
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
* della finestra — cioè il carattere su cui il caret sta davvero. Fuori fuoco
|
|
89
|
-
* (`focused` falso) il caret non si disegna e la finestra si ancora in fondo,
|
|
90
|
-
* che è la vista utile per un campo che non si sta scrivendo.
|
|
91
|
-
*/
|
|
92
|
-
export function EditTextField({ label, value, caret, focused, cols, }) {
|
|
93
|
-
const win = caretWindow(value, focused ? caret : cpLen(value), cols);
|
|
94
|
-
return (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: label }), _jsxs(Text, { children: [' ', sanitize(win.head)] }), focused ? _jsx(Text, { inverse: true, children: sanitize(win.at) }) : null, _jsx(Text, { children: sanitize(win.tail) })] }));
|
|
83
|
+
/** Campo di testo con la sua etichetta, nel layout dell'edit: label, due spazi,
|
|
84
|
+
* campo. La label sta qui e non dentro `FieldText` perché è del LAYOUT, non del
|
|
85
|
+
* campo — il detail la mette in un prefisso incolonnato di larghezza diversa. */
|
|
86
|
+
function LabelledField({ label, value, caret, focused, cols, }) {
|
|
87
|
+
return (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: label }), _jsx(Text, { children: ' ' }), _jsx(FieldText, { value: value, caret: caret, focused: focused, cols: cols })] }));
|
|
95
88
|
}
|
|
96
89
|
// T41 — modale edit, in flusso come gli altri (spinge giù i pane invece di
|
|
97
90
|
// coprirli: la riga che stai modificando resta visibile sopra la lista).
|
|
98
91
|
// La riga di anteprima mostra il testo ESATTO che finirà nel campo `Progress`
|
|
99
92
|
// del task file — così il default (`✔️ Done at <oggi>`) non è una sorpresa.
|
|
100
|
-
export function EditModal({ id, draft, row, columns, }) {
|
|
93
|
+
export function EditModal({ id, draft, row, caret, columns, }) {
|
|
101
94
|
const mark = (r) => (row === r ? CARET : CARET_OFF);
|
|
102
95
|
// Budget dei campi di testo, DERIVATO da `columns` (mai una costante): il box
|
|
103
96
|
// del modale è ANNIDATO nella cornice del deck, quindi le cornici da scalare
|
|
@@ -106,5 +99,5 @@ export function EditModal({ id, draft, row, columns, }) {
|
|
|
106
99
|
// Un titolo di tasks.md arriva a ~64 caratteri: senza taglio la riga va a capo
|
|
107
100
|
// dentro il box, che si alza di una riga e sfonda il budget verticale (invariante ③).
|
|
108
101
|
const fieldBudget = Math.max(8, columns - 19);
|
|
109
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsxs(Text, { color: "yellow", children: ["E \u203A ", id, " \u00B7 titolo, priorit\u00E0 e stato"] }), _jsxs(Text, { children: [mark(0), _jsx(Text, { dimColor: true, children: "pri " }), EDIT_PRI.map((p) => (_jsxs(Text, { inverse: draft.pri === p, color: draft.pri === p ? 'green' : 'gray', children: [' ', sanitize(PRI_GLYPH[p]), " ", PRI_LABEL[p]] }, p)))] }), _jsxs(Text, { children: [mark(1), _jsx(Text, { dimColor: true, children: "stato " }), EDIT_PROG.map((p) => (_jsxs(Text, { inverse: draft.prog === p, color: draft.prog === p ? 'green' : 'gray', children: [' ', sanitize(PROG_GLYPH[p]), " ", p] }, p)))] }), _jsxs(Text, { children: [mark(2), _jsx(
|
|
102
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsxs(Text, { color: "yellow", children: ["E \u203A ", id, " \u00B7 titolo, priorit\u00E0 e stato"] }), _jsxs(Text, { children: [mark(0), _jsx(Text, { dimColor: true, children: "pri " }), EDIT_PRI.map((p) => (_jsxs(Text, { inverse: draft.pri === p, color: draft.pri === p ? 'green' : 'gray', children: [' ', sanitize(PRI_GLYPH[p]), " ", PRI_LABEL[p]] }, p)))] }), _jsxs(Text, { children: [mark(1), _jsx(Text, { dimColor: true, children: "stato " }), EDIT_PROG.map((p) => (_jsxs(Text, { inverse: draft.prog === p, color: draft.prog === p ? 'green' : 'gray', children: [' ', sanitize(PROG_GLYPH[p]), " ", p] }, p)))] }), _jsxs(Text, { children: [mark(2), _jsx(LabelledField, { label: "prog ", value: draft.detail, caret: caret, focused: row === 2, cols: fieldBudget }), !draft.detail && row !== 2 ? _jsx(Text, { dimColor: true, children: "(default)" }) : null] }), _jsxs(Text, { children: [mark(3), _jsx(LabelledField, { label: "titolo", value: draft.title, caret: caret, focused: row === 3, cols: fieldBudget })] }), _jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["\u21B3 ", sanitize(progressText(draft.prog, draft.detail))] })] }));
|
|
110
103
|
}
|