@lamemind/loom-deck 0.49.1 → 0.51.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 +13 -0
- package/dist/cli.js +127 -16
- package/dist/config.js +11 -2
- package/dist/input-modes.js +20 -0
- package/dist/mouse.js +201 -0
- package/dist/overlays/search.js +1 -0
- package/dist/overlays/sheet.js +1 -0
- package/dist/overlays/status.js +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -123,6 +123,19 @@ Le emoji sono quelle del menu compass. Per il terminale compass usa 🖥️, che
|
|
|
123
123
|
|
|
124
124
|
`t` e `c` sono gemelle: entrambe aprono una surface del cappello nella stessa finestra Ptyxis, senza passare da un modale. `c` (minuscola, azione) e `C` (maiuscola, modale create-task) restano distinte per la regola sopra — così come `f` (fork) e `F` (filtri).
|
|
125
125
|
|
|
126
|
+
### Il mouse
|
|
127
|
+
|
|
128
|
+
Le voci della riga surface sono **cliccabili**: un click su `t 💻`, su `c 🤖` o su una voce `launch` fa la stessa cosa del tasto che le sta scritto sopra. Non c'è una semantica nuova da imparare, e non c'è doppio-click — il protocollo non lo prevede, andrebbe ricostruito con un timer. Le liste (task, conversazioni, viste dell'header) **non** rispondono ancora al mouse, né per selezionare né per attivare.
|
|
129
|
+
|
|
130
|
+
Il click è l'unico gesto tracciato: modi `1000` + `1006`, niente movimento, niente drag, niente hover. I modi che li porterebbero (`1002`, `1003`) generano una valanga di eventi che sotto render pesante si frammentano su `stdin` e leakano dentro i campi di testo — è una classe di difetto osservata in più TUI. Con `1000`+`1006` gli eventi sono due per click, pressione e rilascio, e il deck agisce solo sulla pressione.
|
|
131
|
+
|
|
132
|
+
Il tracking si spegne su ogni via d'uscita: uscita normale, `^C`, segnale, crash. Un tracking lasciato acceso non è un difetto cosmetico — il terminale continua a mandare sequenze a qualunque programma prenda il posto del deck, che se le ritrova stampate come testo.
|
|
133
|
+
|
|
134
|
+
Due conseguenze visibili di come è fatto:
|
|
135
|
+
|
|
136
|
+
- **Il deck pulisce lo schermo all'avvio.** Non è cosmesi: Ink non posiziona mai il cursore in modo assoluto, quindi il frame nasce dove capitava il cursore all'avvio del processo e una coordinata del mouse non sarebbe traducibile in una riga del frame. Pulire e tornare in alto a sinistra pinna il frame a riga 1, e da lì non si sposta più — il frame è alto al massimo `rows - 1`, quindi non fa mai scorrere lo schermo.
|
|
137
|
+
- **Incollare un testo che contenga `[<0;5;3M` viene letto come un click.** Ink consegna le sequenze mouse a `useInput` come testo normale, con l'`ESC` iniziale già tolto: non resta niente che distingua la sequenza vera da quella scritta a mano.
|
|
138
|
+
|
|
126
139
|
### `CANC` — eliminare task
|
|
127
140
|
|
|
128
141
|
Il deck **ordina** la potatura, non la esegue: la fa `loom-works:clean-tasks`, invocato da un processo Claude headless. Nessun `git rm` e nessuna riscrittura di `tasks.md` vivono qui — quella sequenza (task file + folder dot-prefixed + riga in `tasks.md`, un commit atomico per task, symlink `current-task.md` rimosso, righe orfane riconciliate) è implementata una volta sola, e averne una seconda darebbe due rimozioni capaci di divergere.
|
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,8 @@ import { resolveTasksPath, resolveTasksDir, loadTasks, loadTaskFileText, } from
|
|
|
7
7
|
import { rowIndexOfKey, selectedRow, } from './search.js';
|
|
8
8
|
import { appendNote, appendPin, appendSessionRecord, appendTaskBinding, } from './task-index.js';
|
|
9
9
|
import { assembleSessionList, firstSelectableId, moveSelection, neighborId, rowIndexOf, selectedSession, unpinLandingId, } from './session-list.js';
|
|
10
|
-
import { cellWidth, launchLegend, loadArchivableDays, loadIdentity, loadLaunch, } from './config.js';
|
|
10
|
+
import { cellWidth, launchLegend, LAUNCH_SEP, loadArchivableDays, loadIdentity, loadLaunch, } from './config.js';
|
|
11
|
+
import { anchorFrame, enableMouse, FRAME_TEXT_COL, hitRegion, isWheel, LAUNCH_ROW, rowRegions, takeMouse, WHEEL_LINES, wheelDir, } from './mouse.js';
|
|
11
12
|
import { cycleSessionView, cycleTaskView, selectSessionRows, selectTasks, sessionView, taskView, TASK_VIEWS, } from './pane-views.js';
|
|
12
13
|
import { isCompact, layoutBudget, searchPreviewCapacity, windowRange, } from './viewport.js';
|
|
13
14
|
import { cut, cutMiddle, sanitize, termWidth } from './width.js';
|
|
@@ -32,7 +33,7 @@ import { useSearchOverlay } from './overlays/search.js';
|
|
|
32
33
|
import { useSheetOverlay } from './overlays/sheet.js';
|
|
33
34
|
import { useAssignOverlay } from './overlays/assign.js';
|
|
34
35
|
import { useProjectStatus } from './overlays/status.js';
|
|
35
|
-
import { captures } from './input-modes.js';
|
|
36
|
+
import { captures, scrolls } from './input-modes.js';
|
|
36
37
|
import { VERSION } from './version.js';
|
|
37
38
|
// T116 — l'avviso della prima pressione di `^C`. La durata è INTERPOLATA dalla
|
|
38
39
|
// finestra, non ricopiata: un numero scritto a mano in un testo che promette un
|
|
@@ -40,6 +41,43 @@ import { VERSION } from './version.js';
|
|
|
40
41
|
// lo segnala. Vive a livello di modulo perché ha due lettori — il ramo che la
|
|
41
42
|
// scrive e il timer che la ritira, e quest'ultimo deve poterla riconoscere.
|
|
42
43
|
const QUIT_NOTE = `⚠ ^C di nuovo entro ${QUIT_WINDOW_MS / 1000}s per chiudere il deck`;
|
|
44
|
+
// Le due surface built-in del cappello, in testa alla riga launch. Non stanno
|
|
45
|
+
// fra i tasti perché hanno la stessa natura delle voci `launch` — fire-once,
|
|
46
|
+
// cwd = project root, nessuno stato — e la differenza è solo che sono
|
|
47
|
+
// universali (nessun progetto le dichiara) invece che custom. Emoji del menu
|
|
48
|
+
// compass: 🤖 = nuova sessione claude. Per il terminale compass usa 🖥️, che nel
|
|
49
|
+
// frame Ink NON passa — `sanitize` lo sostituisce (VTE lo disegna largo 1,
|
|
50
|
+
// string-width dice 2: discordante, invariante ① di width.ts) e resterebbe un
|
|
51
|
+
// `·` muto. 💻 è il gemello concorde; il `sanitize` qui rende il vincolo
|
|
52
|
+
// automatico invece che da ricordare.
|
|
53
|
+
//
|
|
54
|
+
// T21 — `key` è il tasto che la superficie rappresenta: un click su di essa
|
|
55
|
+
// entra nell'handler di tastiera con quel tasto, invece di chiamare l'azione
|
|
56
|
+
// per conto proprio. È ciò che tiene click e tasto per costruzione allineati.
|
|
57
|
+
const SURFACE_SEGMENTS = [
|
|
58
|
+
{ key: 't', text: sanitize('t 💻') },
|
|
59
|
+
{ key: 'c', text: sanitize('c 🤖') },
|
|
60
|
+
];
|
|
61
|
+
// T21 — il `Key` che accompagna un tasto sintetizzato da un click: nessun
|
|
62
|
+
// modificatore, nessun tasto speciale. Deve elencare ogni campo, o i rami che
|
|
63
|
+
// leggono `key.ctrl` o `key.tab` riceverebbero `undefined` invece di `false` —
|
|
64
|
+
// equivalente nel test di verità, ma non nel tipo.
|
|
65
|
+
const NO_MODIFIERS = {
|
|
66
|
+
upArrow: false,
|
|
67
|
+
downArrow: false,
|
|
68
|
+
leftArrow: false,
|
|
69
|
+
rightArrow: false,
|
|
70
|
+
pageDown: false,
|
|
71
|
+
pageUp: false,
|
|
72
|
+
return: false,
|
|
73
|
+
escape: false,
|
|
74
|
+
ctrl: false,
|
|
75
|
+
shift: false,
|
|
76
|
+
tab: false,
|
|
77
|
+
backspace: false,
|
|
78
|
+
delete: false,
|
|
79
|
+
meta: false,
|
|
80
|
+
};
|
|
43
81
|
function Deck({ cwd, tasksPath, tasksDir }) {
|
|
44
82
|
const { tasks, loadError } = useTasks(tasksPath);
|
|
45
83
|
// `notes` esce dall'indice come `sessionNotes`: in questo componente `note` è
|
|
@@ -956,7 +994,73 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
956
994
|
edit: onEditKey,
|
|
957
995
|
purge: onPurgeKey,
|
|
958
996
|
};
|
|
959
|
-
|
|
997
|
+
// T21 (mandata 2) — la ROTELLA, per i soli modi che scorrono un contenuto
|
|
998
|
+
// lungo (`SCROLLING_MODES`). Stessa forma di `MODE_KEYS`: un `Record` sul
|
|
999
|
+
// catalogo, quindi un modo dichiarato scorrevole senza uno scroll da chiamare
|
|
1000
|
+
// non compila. Il delta è in righe, col segno del verso.
|
|
1001
|
+
const MODE_WHEEL = {
|
|
1002
|
+
detail: sheet.scroll,
|
|
1003
|
+
status: status.scroll,
|
|
1004
|
+
reader: search.scrollReader,
|
|
1005
|
+
};
|
|
1006
|
+
/**
|
|
1007
|
+
* T21 — il MOUSE precede tutto, in ogni modo del deck.
|
|
1008
|
+
*
|
|
1009
|
+
* Il filtro è incondizionato — anche dentro un campo di testo, anche in un
|
|
1010
|
+
* modo che non ha nessuna superficie cliccabile — e non è una precauzione:
|
|
1011
|
+
* `useInput` riceve le sequenze SGR come testo (`mouse.ts`, fatto ①), quindi
|
|
1012
|
+
* senza filtro un click battuto mentre è aperto il campo del titolo ci
|
|
1013
|
+
* scriverebbe dentro `[<0;5;3M`. Un listener raw su stdin non risolverebbe:
|
|
1014
|
+
* riceve gli stessi chunk in broadcast e non li sottrae a `useInput`.
|
|
1015
|
+
*
|
|
1016
|
+
* Il chunk MISTO — un tasto e un click nella stessa scrittura, che arriva
|
|
1017
|
+
* come una sola chiamata — si separa a mano: gli eventi vanno al mouse, il
|
|
1018
|
+
* testo residuo prosegue verso la tastiera. Solo un chunk di solo mouse
|
|
1019
|
+
* chiude qui.
|
|
1020
|
+
*/
|
|
1021
|
+
useInput((raw, key) => {
|
|
1022
|
+
const { text, events } = takeMouse(raw);
|
|
1023
|
+
for (const ev of events)
|
|
1024
|
+
onMouse(ev);
|
|
1025
|
+
if (events.length > 0 && !text)
|
|
1026
|
+
return;
|
|
1027
|
+
onKey(text, key);
|
|
1028
|
+
});
|
|
1029
|
+
function onMouse(ev) {
|
|
1030
|
+
// Un click produce due eventi (pressione e rilascio): agire su entrambi
|
|
1031
|
+
// spawnerebbe due volte. La rotella arriva come sola pressione, quindi il
|
|
1032
|
+
// filtro su `press` la lascia passare e non la dedoppia.
|
|
1033
|
+
if (!ev.press)
|
|
1034
|
+
return;
|
|
1035
|
+
if (isWheel(ev.button)) {
|
|
1036
|
+
// La rotella NON rientra dalla porta della tastiera come il click: nel
|
|
1037
|
+
// detail `↑↓` muovono il fuoco fra i campi (T117), non il testo, e il
|
|
1038
|
+
// tasto che scorre — `PgUp`/`PgDn` — ha la granularità sbagliata per una
|
|
1039
|
+
// tacca. Va quindi allo scroll del modo, in righe. Fuori dai modi
|
|
1040
|
+
// scorrevoli è inerte: nelle liste la rotella non muove mai la
|
|
1041
|
+
// selezione (D5), e una tacca mentre il deck è sulla lista non deve
|
|
1042
|
+
// fare niente.
|
|
1043
|
+
if (scrolls(mode))
|
|
1044
|
+
MODE_WHEEL[mode](wheelDir(ev.button) * WHEEL_LINES);
|
|
1045
|
+
return;
|
|
1046
|
+
}
|
|
1047
|
+
// Il bottone si legge sui due bit bassi, perché gli alti portano i
|
|
1048
|
+
// modificatori (shift/meta/ctrl).
|
|
1049
|
+
if ((ev.button & 3) !== 0)
|
|
1050
|
+
return;
|
|
1051
|
+
// Le superfici esistono solo nella lista: un modo capturing prende il
|
|
1052
|
+
// frame intero e a quella riga c'è dell'altro.
|
|
1053
|
+
if (mode !== 'normal' || ev.row !== LAUNCH_ROW)
|
|
1054
|
+
return;
|
|
1055
|
+
const hit = hitRegion(launchRegions, ev.col);
|
|
1056
|
+
// Il click NON chiama l'azione: rientra dalla porta della tastiera col
|
|
1057
|
+
// tasto che la superficie annuncia. Un ramo nuovo su `t`/`c`/cifre nasce
|
|
1058
|
+
// così già cliccabile, e non esiste un secondo posto in cui il click possa
|
|
1059
|
+
// dire una cosa diversa dal tasto che gli sta scritto sopra.
|
|
1060
|
+
if (hit)
|
|
1061
|
+
onKey(hit, NO_MODIFIERS);
|
|
1062
|
+
}
|
|
1063
|
+
function onKey(input, key) {
|
|
960
1064
|
// T116 — `^C` sta SOPRA il dispatch dei modi, unico tasto a scavalcarlo.
|
|
961
1065
|
// Ogni altra combo `ctrl` è un acceleratore, e un acceleratore dentro un
|
|
962
1066
|
// modo capturing dev'essere inerte (`input-modes.ts`); questo non è un
|
|
@@ -1251,27 +1355,27 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1251
1355
|
// chiuderlo, perché quel ramo esce prima di qui. L'unica uscita da tastiera
|
|
1252
1356
|
// è `^C` battuto due volte (T116, in testa a questo handler); resta poi la
|
|
1253
1357
|
// chiusura della tab Ptyxis che ospita il processo.
|
|
1254
|
-
}
|
|
1358
|
+
}
|
|
1255
1359
|
const canSpawn = focus === 'tasks' && selTask !== null;
|
|
1256
1360
|
const canResume = focus === 'sessions' && selSessionObj !== null;
|
|
1257
1361
|
// T50 — il pin agisce su qualunque riga selezionata (anche stale, per
|
|
1258
1362
|
// spinnarla); basta il focus sul pane e una selezione.
|
|
1259
1363
|
const canPin = focus === 'sessions' && selSessionId !== null;
|
|
1260
|
-
|
|
1261
|
-
// fra i tasti: hanno la stessa natura delle voci `launch` — fire-once, cwd =
|
|
1262
|
-
// project root, nessuno stato — e la differenza è solo che sono universali
|
|
1263
|
-
// (nessun progetto le dichiara) invece che custom. Emoji del menu compass:
|
|
1264
|
-
// 🤖 = nuova sessione claude. Per il terminale compass usa 🖥️, che nel frame
|
|
1265
|
-
// Ink NON passa — `sanitize` lo sostituisce (VTE lo disegna largo 1,
|
|
1266
|
-
// string-width dice 2: discordante, invariante ① di width.ts) e resterebbe un
|
|
1267
|
-
// `·` muto. 💻 è il gemello concorde; il `sanitize` qui rende il vincolo
|
|
1268
|
-
// automatico invece che da ricordare.
|
|
1269
|
-
const surfaceLegend = sanitize('t 💻 · c 🤖');
|
|
1364
|
+
const surfaceLegend = SURFACE_SEGMENTS.map((s) => s.text).join(LAUNCH_SEP);
|
|
1270
1365
|
// Larghezza dal medesimo hook che dà l'altezza: dopo un resize la legenda si
|
|
1271
1366
|
// ricalcola con lo stesso re-render che ridimensiona i pane. Le celle delle
|
|
1272
1367
|
// surface (più il ` · ` che le separa dalle voci) sono già spese sulla riga →
|
|
1273
1368
|
// vanno riservate, o le voci launch la sfonderebbero di quel tanto.
|
|
1274
|
-
const legend = launchLegend(launch, columns, cellWidth(surfaceLegend) +
|
|
1369
|
+
const legend = launchLegend(launch, columns, cellWidth(surfaceLegend) + cellWidth(LAUNCH_SEP));
|
|
1370
|
+
// T21 — la riga launch come DATO, non come stringa: gli stessi segmenti
|
|
1371
|
+
// compongono il testo renderizzato e le colonne dell'hit-test. Derivare le
|
|
1372
|
+
// seconde ri-splittando il primo sarebbe un conto parallelo, che diverge alla
|
|
1373
|
+
// prima label che contenga il separatore.
|
|
1374
|
+
const launchSegments = [
|
|
1375
|
+
...SURFACE_SEGMENTS,
|
|
1376
|
+
...legend.taken.map((text, i) => ({ key: String(i + 1), text })),
|
|
1377
|
+
];
|
|
1378
|
+
const launchRegions = rowRegions(launchSegments, LAUNCH_SEP, FRAME_TEXT_COL);
|
|
1275
1379
|
// Legenda della modalità normale. Elenca SOLO i tasti che fanno qualcosa qui e
|
|
1276
1380
|
// ora: le voci contestuali compaiono quando il pane a fuoco le rende possibili
|
|
1277
1381
|
// e altrimenti spariscono, invece di annunciarsi inerti con un `—`.
|
|
@@ -1446,7 +1550,7 @@ function Deck({ cwd, tasksPath, tasksDir }) {
|
|
|
1446
1550
|
if (budget.compact) {
|
|
1447
1551
|
return (_jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { bold: true, color: "cyan", children: "loom-deck" }), _jsxs(Text, { dimColor: true, children: [' ', "v", VERSION, " \u00B7 ", viewTasks.length, " task \u00B7 sel ", selectedTaskId ?? parentLabel, " \u00B7 terminale ", rows, "\u00D7", columns, ": troppo basso, allarga"] })] }));
|
|
1448
1552
|
}
|
|
1449
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(StatusHeadline, { name: projectCore ?? projectName, label: status.label, building: status.building, failed: status.failed, cols: Math.max(4, columns - 4 - `v${VERSION}`.length - 1) }), _jsxs(Text, { dimColor: true, children: ["v", VERSION] })] }), mode === 'create' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["nuova task \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " crea \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'sort' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["sort \u00B7 ", _jsx(Text, { color: "yellow", children: "p" }), " pri ", _jsx(Text, { color: "yellow", children: "s" }), " stato", ' ', _jsx(Text, { color: "yellow", children: "i" }), " id (asc\u2192desc\u2192off) \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'filter' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["filtri \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193\u2190\u2192" }), " naviga \u00B7 ", _jsx(Text, { color: "yellow", children: "spazio" }), ' ', "mostra/nascondi \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'note' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["titolo conversazione \u00B7 ", _jsx(Text, { color: "yellow", children: "^U" }), " svuota \u00B7", ' ', _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva (vuoto = rimuove) \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'purge' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["elimina task \u00B7", ' ', purge?.ignored ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " keep/purge dei file non tracciati \u00B7", ' '] })) : null, _jsx(Text, { color: "yellow", children: "\u23CE" }), " conferma \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'edit' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["edit \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " campo \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " valore, o cursore sul testo \u00B7 ", _jsx(Text, { color: "yellow", children: "^A/^E" }), " inizio/fine \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^D" }), " canc \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva+commit \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : (_jsx(Text, { dimColor: true, wrap: "truncate-end", children: keyLegend })), mode === 'normal' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [
|
|
1553
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(StatusHeadline, { name: projectCore ?? projectName, label: status.label, building: status.building, failed: status.failed, cols: Math.max(4, columns - 4 - `v${VERSION}`.length - 1) }), _jsxs(Text, { dimColor: true, children: ["v", VERSION] })] }), mode === 'create' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["nuova task \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " crea \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'sort' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["sort \u00B7 ", _jsx(Text, { color: "yellow", children: "p" }), " pri ", _jsx(Text, { color: "yellow", children: "s" }), " stato", ' ', _jsx(Text, { color: "yellow", children: "i" }), " id (asc\u2192desc\u2192off) \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'filter' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["filtri \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193\u2190\u2192" }), " naviga \u00B7 ", _jsx(Text, { color: "yellow", children: "spazio" }), ' ', "mostra/nascondi \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " ok \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'note' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["titolo conversazione \u00B7 ", _jsx(Text, { color: "yellow", children: "^U" }), " svuota \u00B7", ' ', _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva (vuoto = rimuove) \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'purge' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["elimina task \u00B7", ' ', purge?.ignored ? (_jsxs(_Fragment, { children: [_jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " keep/purge dei file non tracciati \u00B7", ' '] })) : null, _jsx(Text, { color: "yellow", children: "\u23CE" }), " conferma \u00B7 ", _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : mode === 'edit' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: ["edit \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2191\u2193" }), " campo \u00B7 ", _jsx(Text, { color: "yellow", children: "\u2190\u2192" }), " valore, o cursore sul testo \u00B7 ", _jsx(Text, { color: "yellow", children: "^A/^E" }), " inizio/fine \u00B7", ' ', _jsx(Text, { color: "yellow", children: "^D" }), " canc \u00B7 ", _jsx(Text, { color: "yellow", children: "\u23CE" }), " salva+commit \u00B7", ' ', _jsx(Text, { color: "yellow", children: "esc" }), " annulla"] })) : (_jsx(Text, { dimColor: true, wrap: "truncate-end", children: keyLegend })), mode === 'normal' ? (_jsxs(Text, { dimColor: true, wrap: "truncate-end", children: [launchSegments.map((s) => s.text).join(LAUNCH_SEP), legend.overflow > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 +", legend.overflow, " fuori riga"] })) : null, legend.unreachable > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 ", legend.unreachable, " oltre la 9\u00AA (non raggiungibili)"] })) : null] })) : null, mode === 'create' ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "C \u203A " }), _jsx(Text, { children: draft }), _jsx(Text, { inverse: true, children: " " })] })) : null, mode === 'note' ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, marginTop: 1, children: [_jsx(Text, { color: "yellow", children: "\u270E \u203A " }), _jsx(Text, { children: noteDraft }), _jsx(Text, { inverse: true, children: " " })] })) : null, mode === 'sort' ? _jsx(SortModal, { sort: view.sort }) : null, mode === 'filter' ? _jsx(FilterModal, { view: view, cursor: filterCursor }) : null, mode === 'edit' && edit && selTask ? (_jsx(EditModal, { id: selTask.id, draft: edit, row: editCursor.row, caret: editCursor.caret, columns: columns })) : null, mode === 'purge' && purge ? _jsx(PurgeModal, { draft: purge, columns: columns }) : null, _jsxs(Box, { flexDirection: "row", marginTop: 1, children: [_jsx(TasksPane, { tasks: windowTasks, counts: taskCounts, activeView: taskViewId, paneCount: paneTasks.length, view: view, selected: selIndex, spotCount: spotCount, allCount: sessions.length, idW: taskCols.id, tailW: taskCols.tail, focused: focus === 'tasks', loadError: loadError, windowStart: taskWin.start, above: taskWin.start, below: paneTasks.length - taskWin.end, columns: columns, data: taskRowData }), _jsx(SessionsPane, { parentLabel: parentLabel, isSpot: isSpot, isAll: isAll, bindings: bindings, taskW: sessionCols.task, ageW: sessionCols.age, rows: windowRows, counts: sessionCounts, activeView: sessionViewId, paneCount: sessionRows.length, selectedId: selSessionId ?? undefined, focused: focus === 'sessions', above: sessionWin.start, below: sessionRows.length - sessionWin.end, columns: columns, forkOf: forkOf, sessionNotes: sessionNotes, projectCore: projectCore, live: live })] }), budget.preview && previewKind === 'task' && detail ? (_jsx(PreviewPane, { kind: "task", detail: detail, maxLines: budget.detailLines, columns: columns })) : budget.preview && previewKind === 'session' && selSessionObj ? (_jsx(PreviewPane, { kind: "session", s: selSessionObj, firstLines: budget.sessionFirstLines, lastLines: budget.sessionLastLines, columns: columns, origin: forkOf.get(selSessionObj.sessionId) ?? null, note: sessionNotes.get(selSessionObj.sessionId) ?? '', live: live.get(selSessionObj.sessionId) ?? null })) : null, note ? _jsx(Text, { color: "green", wrap: "truncate-end", children: sanitize(note) }) : null] }));
|
|
1450
1554
|
}
|
|
1451
1555
|
const cwd = process.cwd();
|
|
1452
1556
|
// T116 — `exitOnCtrlC: false` toglie a Ink l'uscita immediata su `^C`: senza,
|
|
@@ -1454,6 +1558,13 @@ const cwd = process.cwd();
|
|
|
1454
1558
|
// osservabile. In raw mode `^C` non è un SIGINT — è il byte `\x03` nello
|
|
1455
1559
|
// stream di input, e chi lo consuma per primo decide. Da qui in poi lo consuma
|
|
1456
1560
|
// il deck, quindi l'uscita è tutta a carico del suo handler.
|
|
1561
|
+
// T21 — l'ordine dei tre passi è vincolante. `anchorFrame` va PRIMA del render:
|
|
1562
|
+
// pinna il frame a riga 1 e con lui l'origine dell'hit-test, che senza sarebbe
|
|
1563
|
+
// la posizione casuale del cursore all'avvio del processo. `enableMouse`
|
|
1564
|
+
// registra il ripristino del tracking sulle vie d'uscita, quindi prima che
|
|
1565
|
+
// esista qualcosa da cui uscire.
|
|
1566
|
+
anchorFrame();
|
|
1567
|
+
enableMouse();
|
|
1457
1568
|
render(_jsx(Deck, { cwd: cwd, tasksPath: resolveTasksPath(cwd), tasksDir: resolveTasksDir(cwd) }), {
|
|
1458
1569
|
exitOnCtrlC: false,
|
|
1459
1570
|
});
|
package/dist/config.js
CHANGED
|
@@ -48,6 +48,10 @@ export function loadLaunch(projectRoot) {
|
|
|
48
48
|
// dallo schema config (che ne ammette quante se ne vogliono) ma dai tasti
|
|
49
49
|
// disponibili. Le voci oltre la nona restano configurate e non raggiungibili.
|
|
50
50
|
export const LAUNCH_MAX = 9;
|
|
51
|
+
/** Separatore fra le voci della riga launch. Costante e non letterale sparso:
|
|
52
|
+
* lo usano il calcolo di quante voci entrano in riga, la resa, e l'hit-test
|
|
53
|
+
* del mouse — tre conti che devono misurare lo stesso spazio. */
|
|
54
|
+
export const LAUNCH_SEP = ' · ';
|
|
51
55
|
// Larghezza in celle terminale, approssimata: emoji astrali (U+1F000+) e simboli
|
|
52
56
|
// BMP portati a presentazione emoji occupano 2 colonne, il VS16 è un modificatore
|
|
53
57
|
// a larghezza 0, tutto il resto 1. Serve solo a decidere quante voci stanno in
|
|
@@ -85,7 +89,7 @@ export function launchLegend(entries, columns, reserved = 0) {
|
|
|
85
89
|
const taken = [];
|
|
86
90
|
let used = 0;
|
|
87
91
|
for (const p of parts) {
|
|
88
|
-
const cost = cellWidth(p) + (taken.length > 0 ?
|
|
92
|
+
const cost = cellWidth(p) + (taken.length > 0 ? cellWidth(LAUNCH_SEP) : 0);
|
|
89
93
|
if (used + cost > budget - reserve)
|
|
90
94
|
break;
|
|
91
95
|
taken.push(p);
|
|
@@ -98,7 +102,12 @@ export function launchLegend(entries, columns, reserved = 0) {
|
|
|
98
102
|
let taken = fit(0);
|
|
99
103
|
if (taken.length < parts.length)
|
|
100
104
|
taken = fit(10);
|
|
101
|
-
return {
|
|
105
|
+
return {
|
|
106
|
+
shown: taken.join(LAUNCH_SEP),
|
|
107
|
+
taken,
|
|
108
|
+
overflow: parts.length - taken.length,
|
|
109
|
+
unreachable,
|
|
110
|
+
};
|
|
102
111
|
}
|
|
103
112
|
// T61 — soglia d'età del contatore archiviabili, campo `archivableDays`.
|
|
104
113
|
//
|
package/dist/input-modes.js
CHANGED
|
@@ -55,3 +55,23 @@ export function captures(mode) {
|
|
|
55
55
|
export const CTRL_DEROGATIONS = {
|
|
56
56
|
detail: ['f'],
|
|
57
57
|
};
|
|
58
|
+
/**
|
|
59
|
+
* T21 (mandata 2) — i modi che SCORRONO un contenuto lungo, cioè gli unici in
|
|
60
|
+
* cui la rotella del mouse fa qualcosa.
|
|
61
|
+
*
|
|
62
|
+
* La rotella scorre il TESTO, mai la selezione di una lista (D5): nelle liste
|
|
63
|
+
* la selezione è un'intenzione — la riga su cui si preme `⏎` — e una rotella
|
|
64
|
+
* che la muovesse trasformerebbe ogni sfioramento in una scelta. Nei tre modi
|
|
65
|
+
* qui sotto lo scroll non sceglie niente, è solo posizione di lettura. Non
|
|
66
|
+
* coincide con «ha un documento a schermo»: `search` mostra un'anteprima, ma
|
|
67
|
+
* il fuoco lì è sulla lista dei risultati, che è una selezione.
|
|
68
|
+
*
|
|
69
|
+
* Il custode è `MODE_WHEEL` in `cli.tsx`, un `Record<ScrollingMode, …>` che non
|
|
70
|
+
* compila se un modo entra qui senza uno scroll da chiamare.
|
|
71
|
+
*/
|
|
72
|
+
export const SCROLLING_MODES = ['detail', 'status', 'reader'];
|
|
73
|
+
const SCROLLING = new Set(SCROLLING_MODES);
|
|
74
|
+
/** `true` se la rotella scorre il contenuto del modo. */
|
|
75
|
+
export function scrolls(mode) {
|
|
76
|
+
return SCROLLING.has(mode);
|
|
77
|
+
}
|
package/dist/mouse.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// T21 — MOUSE del deck: abilitazione del tracking SGR, parse delle sequenze,
|
|
2
|
+
// hit-test sulle superfici cliccabili.
|
|
3
|
+
//
|
|
4
|
+
// Tre fatti misurati sotto pty reggono tutto il modulo, e ognuno spiega una
|
|
5
|
+
// scelta che altrimenti sembrerebbe arbitraria.
|
|
6
|
+
//
|
|
7
|
+
// ① `useInput` di Ink riceve le sequenze mouse come TESTO, con l'ESC iniziale
|
|
8
|
+
// strippato e nessun flag di `key` attivo: un click arriva come `[<0;5;3M`.
|
|
9
|
+
// Un listener `data` raw agganciato a stdin accanto a Ink riceve gli stessi
|
|
10
|
+
// identici chunk (i listener `data` sono broadcast) e NON impedisce che le
|
|
11
|
+
// sequenze raggiungano comunque `useInput`. Il filtro dev'essere quindi
|
|
12
|
+
// dentro `useInput` in ogni caso — da cui: un punto solo, non due.
|
|
13
|
+
// ② L'ESC è strippato solo se la sequenza APRE il chunk. In un chunk misto
|
|
14
|
+
// tasto+mouse (`a\x1b[<0;7;2M`, una sola chiamata) l'ESC interno resta.
|
|
15
|
+
// Il pattern lo rende quindi opzionale e lo consuma quando c'è, o
|
|
16
|
+
// resterebbe un ESC orfano nel testo restituito al deck.
|
|
17
|
+
// ③ Modi di tracking `1000` (click) + `1006` (SGR) e basta. `1002` (drag) e
|
|
18
|
+
// `1003` (ogni movimento) sono la sorgente documentata delle valanghe di
|
|
19
|
+
// eventi che sotto render pesante si frammentano su stdin e leakano dentro
|
|
20
|
+
// i campi di testo. Con soli 1000+1006 gli eventi sono due per click.
|
|
21
|
+
import { termWidth } from './width.js';
|
|
22
|
+
/**
|
|
23
|
+
* Sequenza SGR: `ESC [ < b ; x ; y M|m`, con l'ESC opzionale (fatto ①).
|
|
24
|
+
*
|
|
25
|
+
* Il prezzo dell'ESC opzionale è che un INCOLLAGGIO della stringa letterale
|
|
26
|
+
* `[<0;5;3M` dentro un campo di testo verrebbe letto come un click. È accettato
|
|
27
|
+
* e non compensato: distinguerlo richiederebbe di fidarsi dell'ESC, che nel
|
|
28
|
+
* caso più comune — la sequenza che apre il chunk — non c'è.
|
|
29
|
+
*/
|
|
30
|
+
const SGR_MOUSE = /\x1b?\[<(\d+);(\d+);(\d+)([Mm])/g;
|
|
31
|
+
/** `true` se il codice è una pressione di rotella (bit 6 del bottone). */
|
|
32
|
+
export function isWheel(button) {
|
|
33
|
+
return (button & 64) !== 0;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Verso della rotella: `-1` su (codice 64), `+1` giù (65), `0` se non è una
|
|
37
|
+
* rotella. Il verso sta nel bit 0, i modificatori nei bit 2-4 (shift/meta/
|
|
38
|
+
* ctrl): `66`/`67` sono le rotelle orizzontali, che qui valgono `0` perché il
|
|
39
|
+
* deck non ha nulla da scorrere in orizzontale.
|
|
40
|
+
*
|
|
41
|
+
* Un terminale manda la rotella come SOLA pressione (`M`), mai seguita da un
|
|
42
|
+
* rilascio: un tacca = un evento, e il chiamante non deve dedoppiare come fa
|
|
43
|
+
* per il click.
|
|
44
|
+
*/
|
|
45
|
+
export function wheelDir(button) {
|
|
46
|
+
if (!isWheel(button) || (button & 2) !== 0)
|
|
47
|
+
return 0;
|
|
48
|
+
return (button & 1) === 0 ? -1 : 1;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Righe scorse per tacca di rotella nei contenuti lunghi (detail, project
|
|
52
|
+
* status, reader). Tre è la convenzione dei terminali e dei browser: una tacca
|
|
53
|
+
* a riga singola costringe a girare la rotella quanto basta a stancare la mano,
|
|
54
|
+
* una a pagina salta il testo che si stava leggendo.
|
|
55
|
+
*/
|
|
56
|
+
export const WHEEL_LINES = 3;
|
|
57
|
+
/**
|
|
58
|
+
* Separa le sequenze mouse dal testo di un chunk di `useInput`.
|
|
59
|
+
*
|
|
60
|
+
* Ritorna il testo RIPULITO (quello che il deck deve continuare a trattare come
|
|
61
|
+
* input di tastiera) e gli eventi trovati, nell'ordine di arrivo.
|
|
62
|
+
*
|
|
63
|
+
* Una sequenza spezzata su due chunk — succede sotto render pesante — arriva
|
|
64
|
+
* come due frammenti monchi: nessuno dei due matcha, quindi entrambi finiscono
|
|
65
|
+
* nel testo ripulito. Il click va perso. È voluto: bufferizzare significherebbe
|
|
66
|
+
* tenere uno stato fra due chiamate di `useInput` e decidere quando scade, e un
|
|
67
|
+
* click perso costa una ripetizione mentre un buffer che non scade mai
|
|
68
|
+
* inghiotte i tasti che seguono.
|
|
69
|
+
*/
|
|
70
|
+
export function takeMouse(input) {
|
|
71
|
+
if (!input.includes('[<'))
|
|
72
|
+
return { text: input, events: [] };
|
|
73
|
+
const events = [];
|
|
74
|
+
const text = input.replace(SGR_MOUSE, (_all, b, x, y, kind) => {
|
|
75
|
+
events.push({
|
|
76
|
+
button: Number(b),
|
|
77
|
+
col: Number(x),
|
|
78
|
+
row: Number(y),
|
|
79
|
+
press: kind === 'M',
|
|
80
|
+
});
|
|
81
|
+
return '';
|
|
82
|
+
});
|
|
83
|
+
return { text, events };
|
|
84
|
+
}
|
|
85
|
+
// ── Tracking: accensione, spegnimento, ripristino ───────────────────────────
|
|
86
|
+
const ENABLE = '\x1b[?1000h\x1b[?1006h';
|
|
87
|
+
const DISABLE = '\x1b[?1006l\x1b[?1000l';
|
|
88
|
+
/**
|
|
89
|
+
* Ancoraggio del frame: pulisce lo schermo e riporta il cursore in alto a
|
|
90
|
+
* sinistra PRIMA del primo render di Ink.
|
|
91
|
+
*
|
|
92
|
+
* Senza, l'hit-test non ha origine. Ink non posiziona mai il cursore in modo
|
|
93
|
+
* assoluto (verificato sul flusso di byte: solo `\x1b[2K` e `\x1b[1A`, cioè
|
|
94
|
+
* `log-update` puro): il frame nasce dove si trovava il cursore quando il
|
|
95
|
+
* processo è partito — riga 1 in una tab appena aperta, più in basso dopo un
|
|
96
|
+
* prompt di shell, più in alto ancora se la prima scrittura ha fatto scorrere
|
|
97
|
+
* lo schermo. Una coordinata assoluta del mouse non sarebbe traducibile.
|
|
98
|
+
*
|
|
99
|
+
* Pinnato a riga 1 il frame ci RESTA, senza bisogno di ri-ancorarlo a ogni
|
|
100
|
+
* resize: `log-update` cancella all'insù tante righe quante ne aveva scritte e
|
|
101
|
+
* poi riscrive da lì, e il frame è alto al massimo `rows - 1` (lo garantisce lo
|
|
102
|
+
* SLACK di `viewport.ts`) — quindi la scrittura non fa mai scorrere lo schermo
|
|
103
|
+
* e la cancellazione riporta sempre il cursore a riga 1. È un punto fisso.
|
|
104
|
+
*/
|
|
105
|
+
export function anchorFrame(out = process.stdout) {
|
|
106
|
+
out.write('\x1b[2J\x1b[H');
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Accende il tracking e registra lo spegnimento su ogni via d'uscita.
|
|
110
|
+
*
|
|
111
|
+
* Un tracking lasciato acceso non è un difetto cosmetico: il terminale continua
|
|
112
|
+
* a mandare sequenze a qualunque programma prenda il posto del deck, che se le
|
|
113
|
+
* ritrova stampate come testo a ogni movimento.
|
|
114
|
+
*
|
|
115
|
+
* Due agganci coprono tutte le uscite, e la ragione per cui non ne basta uno è
|
|
116
|
+
* che le due classi non si attraversano. L'evento `exit` copre l'uscita normale
|
|
117
|
+
* (`exit()` di Ink, che è anche il secondo `^C` del deck) e il CRASH, perché
|
|
118
|
+
* un'eccezione non catturata termina il processo passando comunque di lì. I
|
|
119
|
+
* segnali no: `SIGINT`/`SIGTERM` terminano il processo senza emettere `exit`,
|
|
120
|
+
* quindi vogliono un handler proprio — che poi deve ri-emettere il segnale, o
|
|
121
|
+
* il processo resterebbe vivo (agganciare un handler ne disattiva la
|
|
122
|
+
* terminazione di default). `^C` battuto dentro il deck non è nessuno dei due:
|
|
123
|
+
* in raw mode è il byte `\x03` nello stream di input, che il deck consuma da sé
|
|
124
|
+
* e che sfocia nell'uscita normale.
|
|
125
|
+
*
|
|
126
|
+
* Ritorna la funzione di spegnimento per chi voglia chiamarla da sé.
|
|
127
|
+
*/
|
|
128
|
+
export function enableMouse(out = process.stdout) {
|
|
129
|
+
let off = false;
|
|
130
|
+
const disable = () => {
|
|
131
|
+
if (off)
|
|
132
|
+
return;
|
|
133
|
+
off = true;
|
|
134
|
+
try {
|
|
135
|
+
out.write(DISABLE);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// stdout già chiuso: non c'è più niente da ripristinare.
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
out.write(ENABLE);
|
|
142
|
+
process.on('exit', disable);
|
|
143
|
+
for (const sig of ['SIGINT', 'SIGTERM']) {
|
|
144
|
+
process.on(sig, () => {
|
|
145
|
+
disable();
|
|
146
|
+
process.kill(process.pid, sig);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return disable;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Colonne occupate da ogni segmento di una riga, dato il separatore che li
|
|
153
|
+
* unisce e la colonna in cui la riga comincia.
|
|
154
|
+
*
|
|
155
|
+
* Deriva dall'ARITMETICA di layout, non da un registro popolato al render:
|
|
156
|
+
* `measureElement` di Ink espone width e height ma mai la posizione assoluta di
|
|
157
|
+
* un elemento, quindi la mappa coordinate→superficie va costruita comunque. Il
|
|
158
|
+
* vincolo che la tiene onesta è che i `Segment` che entrano qui sono gli stessi
|
|
159
|
+
* che compongono la stringa renderizzata — una sola fonte, non due conti
|
|
160
|
+
* paralleli che possono divergere.
|
|
161
|
+
*
|
|
162
|
+
* La larghezza è quella del TERMINALE (`termWidth`), non quella di `cellWidth`
|
|
163
|
+
* in `config.ts`: la seconda è una stima prudente che serve a decidere quante
|
|
164
|
+
* voci stanno in riga e ha licenza di sovrastimare, mentre qui una colonna di
|
|
165
|
+
* troppo sposta il bersaglio.
|
|
166
|
+
*/
|
|
167
|
+
export function rowRegions(segments, sep, startCol) {
|
|
168
|
+
const sepW = termWidth(sep);
|
|
169
|
+
const out = [];
|
|
170
|
+
let col = startCol;
|
|
171
|
+
for (const seg of segments) {
|
|
172
|
+
const w = termWidth(seg.text);
|
|
173
|
+
if (w > 0)
|
|
174
|
+
out.push({ key: seg.key, start: col, end: col + w - 1 });
|
|
175
|
+
col += w + sepW;
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
/** Il tasto della superficie sotto la colonna, o `null` fuori da tutte. */
|
|
180
|
+
export function hitRegion(regions, col) {
|
|
181
|
+
for (const r of regions) {
|
|
182
|
+
if (col >= r.start && col <= r.end)
|
|
183
|
+
return r.key;
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Riga del terminale (1-based) su cui vive la riga launch in modalità normale.
|
|
189
|
+
*
|
|
190
|
+
* Il frame è ancorato a riga 1 (`anchorFrame`) e le righe sopra la launch sono
|
|
191
|
+
* fisse e sempre presenti: bordo superiore, testata (status + versione),
|
|
192
|
+
* legenda tasti. Nessuna delle tre è condizionale in modalità normale — la
|
|
193
|
+
* legenda tasti è una catena di ternari che rende SEMPRE esattamente un `Text`,
|
|
194
|
+
* e la testata è una riga `space-between`. Tutto ciò che varia in altezza (i
|
|
195
|
+
* due pane, il blocco preview, la riga di nota) sta SOTTO, quindi il conto non
|
|
196
|
+
* dipende da quanto è popolato il progetto.
|
|
197
|
+
*/
|
|
198
|
+
export const LAUNCH_ROW = 4;
|
|
199
|
+
/** Colonna (1-based) del primo carattere di testo dentro il box esterno:
|
|
200
|
+
* bordo + `paddingX={1}`. */
|
|
201
|
+
export const FRAME_TEXT_COL = 3;
|
package/dist/overlays/search.js
CHANGED
package/dist/overlays/sheet.js
CHANGED
package/dist/overlays/status.js
CHANGED
package/package.json
CHANGED