@azure-id/orc 1.4.1 → 1.4.2
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/CHANGELOG.md +3043 -2907
- package/README-id.md +39 -2
- package/README.md +647 -631
- package/bin/cli.js +36977 -36866
- package/bin/webui/api.js +1288 -1283
- package/bin/webui/css/04-motion.css +18 -0
- package/bin/webui/css/06-responsive.css +3 -1
- package/bin/webui/css/panels/hookui.css +220 -41
- package/bin/webui/i18n/en/hookui.json +17 -3
- package/bin/webui/i18n/en/tour.json +1 -1
- package/bin/webui/i18n/id/hookui.json +17 -3
- package/bin/webui/i18n/id/tour.json +1 -1
- package/bin/webui/js/panels/hookui.js +586 -208
- package/package.json +1 -1
- package/templates/hooks/README.md +8 -1
- package/templates/hooks/orc-statusline.js +15 -4
- package/templates/hooks/orc-subagent-line.js +5 -2
|
@@ -73,75 +73,153 @@ function hkCanDrag() {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
// THE LAST FETCH, AND THE NODE IT IS PAINTED INTO (v1.4.2). Staging an edit
|
|
77
|
+
// used to re-enter the router — which threw the panel away, refetched four
|
|
78
|
+
// endpoints and rebuilt everything from a skeleton. Every click "refreshed the
|
|
79
|
+
// page", and worse: because the picture came back looking different, a staged
|
|
80
|
+
// change read as an applied one. Nothing is fetched now until Apply, the switch
|
|
81
|
+
// or a preset; a staged op repaints from THIS cache.
|
|
82
|
+
let HK_DATA = null;
|
|
83
|
+
// The node this panel paints into. `section` owns the skeleton and the error
|
|
84
|
+
// box; after the first load, everything goes through here.
|
|
85
|
+
let HK_SLOT = null;
|
|
86
|
+
// The open part editor, if there is one, so a staged edit updates the control
|
|
87
|
+
// the user is looking at instead of leaving it showing the old value.
|
|
88
|
+
let HK_MODAL = null;
|
|
89
|
+
// The comparison drawer's open state and its answers, both surviving a repaint:
|
|
90
|
+
// it costs one render per colour set and per symbol set, so it is fetched once
|
|
91
|
+
// and only when somebody opens it.
|
|
92
|
+
let HK_COMPARE = false;
|
|
93
|
+
let HK_COMPARE_DATA = null;
|
|
94
|
+
// Whether this panel has drawn once. The FIRST paint animates in; every one
|
|
95
|
+
// after it is instant, because nothing arrived — the same cards are still there
|
|
96
|
+
// with one value different.
|
|
97
|
+
let HK_PAINTED = false;
|
|
98
|
+
|
|
76
99
|
PANELS.hookui = function (host) {
|
|
77
100
|
head(host, t("hookui.title"), t("hookui.sub"));
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
(
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
out.append(empty(t("hookui.noData")));
|
|
92
|
-
return out;
|
|
93
|
-
}
|
|
94
|
-
// The effective board: what it WILL be once the staged ops run. Every
|
|
95
|
-
// count, every slot cap and every legality question downstream reads this
|
|
96
|
-
// and never `show.lines` directly.
|
|
97
|
-
const plan = hkPlan(show, cat);
|
|
98
|
-
|
|
99
|
-
const edits = editSet(() => bar.paint());
|
|
100
|
-
const bar = editBar(edits, {
|
|
101
|
-
onApply: async (b) => {
|
|
102
|
-
await applyActions(edits, b);
|
|
103
|
-
HK_OPS = [];
|
|
104
|
-
rerender();
|
|
105
|
-
},
|
|
106
|
-
onReset: () => confirmReset(),
|
|
107
|
-
onCancel: () => {
|
|
108
|
-
HK_OPS = [];
|
|
109
|
-
rerender();
|
|
110
|
-
},
|
|
111
|
-
resetLabel: t("hookui.resetLayout"),
|
|
112
|
-
});
|
|
113
|
-
// Filled AFTER the bar exists: `editSet`'s onChange paints it, and a
|
|
114
|
-
// paint before that binding is assigned throws.
|
|
115
|
-
for (const a of plan.actions) edits.action(a.key, a.route, a.body, a.label);
|
|
116
|
-
|
|
117
|
-
out.append(boardTabs(cat));
|
|
118
|
-
out.append(gateCard(show, cat));
|
|
119
|
-
// A caution renders ABOVE the preview, never inside a tab — a caution you
|
|
120
|
-
// have to hunt for is a caution nobody reads.
|
|
121
|
-
if (show.errors.length || show.warnings.length) out.append(cautionCard(show));
|
|
122
|
-
out.append(previewCard(prev, plan));
|
|
123
|
-
out.append(boardCard(cat, plan));
|
|
124
|
-
out.append(referenceCard(cat));
|
|
125
|
-
out.append(presetsCard(presets, plan));
|
|
126
|
-
out.append(advancedCard(cat, plan));
|
|
127
|
-
out.append(bar);
|
|
128
|
-
return out;
|
|
129
|
-
}
|
|
130
|
-
);
|
|
101
|
+
HK_DATA = null;
|
|
102
|
+
HK_SLOT = null;
|
|
103
|
+
HK_MODAL = null;
|
|
104
|
+
HK_PAINTED = false;
|
|
105
|
+
section(host, hkLoad, (data) => {
|
|
106
|
+
HK_DATA = data;
|
|
107
|
+
// `stack` is what spaces the cards; `section` gives it to its own slot, and
|
|
108
|
+
// this node lives inside that slot, so it needs its own.
|
|
109
|
+
const slot = el("div", "stack");
|
|
110
|
+
HK_SLOT = slot;
|
|
111
|
+
hkPaint();
|
|
112
|
+
return slot;
|
|
113
|
+
});
|
|
131
114
|
};
|
|
132
115
|
|
|
116
|
+
function hkPreviewUrl() {
|
|
117
|
+
return "/api/statusline/preview?board=" + HK_BOARD + "&width=" + HK_WIDTH + "&state=" + HK_STATE;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function hkLoad() {
|
|
121
|
+
return Promise.all([
|
|
122
|
+
read("/api/statusline/show?board=" + HK_BOARD).then((r) => r.data),
|
|
123
|
+
read("/api/statusline/components?board=" + HK_BOARD).then((r) => r.data),
|
|
124
|
+
read("/api/statusline/presets?board=" + HK_BOARD).then((r) => r.data),
|
|
125
|
+
read(hkPreviewUrl()).then((r) => r.data),
|
|
126
|
+
]).then(([show, cat, presets, prev]) => ({ show, cat, presets, prev }));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// THE ONLY PAINT. No fetch, no skeleton, no router. The scroll position is
|
|
130
|
+
// kept by hand: a board you are halfway down must not jump to the top because
|
|
131
|
+
// you moved one chip.
|
|
132
|
+
function hkPaint() {
|
|
133
|
+
if (!HK_SLOT || !HK_DATA) return;
|
|
134
|
+
const y = window.scrollY;
|
|
135
|
+
// NO ENTRANCE ANIMATION ON A REPAINT. `.stack > *` fades and slides each
|
|
136
|
+
// child in on a 30ms stagger, and `.hk-chip` does the same per chip — which
|
|
137
|
+
// is right the first time the panel arrives and is exactly the "the screen
|
|
138
|
+
// blinked like it reloaded" the user reported, on every keystroke, with the
|
|
139
|
+
// network tab empty. `hk-quiet` switches those entrances off; the transitions
|
|
140
|
+
// (hover, the drop marker, the staged edge) are untouched, because they are
|
|
141
|
+
// about the pointer rather than about arriving.
|
|
142
|
+
if (HK_PAINTED) HK_SLOT.classList.add("hk-quiet");
|
|
143
|
+
HK_PAINTED = true;
|
|
144
|
+
const { show, cat, presets, prev } = HK_DATA;
|
|
145
|
+
HK_SLOT.replaceChildren();
|
|
146
|
+
if (!show || !cat) {
|
|
147
|
+
HK_SLOT.append(empty(t("hookui.noData")));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
// The effective board: what it WILL be once the staged ops run. Every count,
|
|
151
|
+
// every slot cap and every legality question downstream reads this and never
|
|
152
|
+
// `show.lines` directly.
|
|
153
|
+
const plan = hkPlan(show, cat);
|
|
154
|
+
|
|
155
|
+
const edits = editSet(() => bar.paint());
|
|
156
|
+
const bar = editBar(edits, {
|
|
157
|
+
onApply: async (b) => {
|
|
158
|
+
await applyActions(edits, b);
|
|
159
|
+
HK_OPS = [];
|
|
160
|
+
HK_COMPARE_DATA = null;
|
|
161
|
+
await hkReload();
|
|
162
|
+
},
|
|
163
|
+
onReset: () => confirmReset(),
|
|
164
|
+
onCancel: () => {
|
|
165
|
+
HK_OPS = [];
|
|
166
|
+
hkPaint();
|
|
167
|
+
},
|
|
168
|
+
resetLabel: t("hookui.resetLayout"),
|
|
169
|
+
});
|
|
170
|
+
// Filled AFTER the bar exists: `editSet`'s onChange paints it, and a paint
|
|
171
|
+
// before that binding is assigned throws.
|
|
172
|
+
for (const a of plan.actions) edits.action(a.key, a.route, a.body, a.label);
|
|
173
|
+
|
|
174
|
+
HK_SLOT.append(boardTabs(cat));
|
|
175
|
+
HK_SLOT.append(gateCard(show, cat));
|
|
176
|
+
// A caution renders ABOVE the preview, never inside a tab — a caution you
|
|
177
|
+
// have to hunt for is a caution nobody reads.
|
|
178
|
+
if (show.errors.length || show.warnings.length) HK_SLOT.append(cautionCard(show));
|
|
179
|
+
HK_SLOT.append(previewCard(prev, plan, cat));
|
|
180
|
+
HK_SLOT.append(boardCard(cat, plan));
|
|
181
|
+
HK_SLOT.append(referenceCard(cat));
|
|
182
|
+
HK_SLOT.append(presetsCard(presets, plan));
|
|
183
|
+
HK_SLOT.append(advancedCard(cat, plan));
|
|
184
|
+
HK_SLOT.append(bar);
|
|
185
|
+
|
|
186
|
+
// The open editor follows the staged state. A modal the page has stopped
|
|
187
|
+
// showing is not repainted — it is forgotten.
|
|
188
|
+
if (HK_MODAL) {
|
|
189
|
+
const host = document.getElementById("modal-host");
|
|
190
|
+
if (host && host.hidden) HK_MODAL = null;
|
|
191
|
+
else HK_MODAL.repaint(plan);
|
|
192
|
+
}
|
|
193
|
+
window.scrollTo(0, y);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// A REFETCH. Used where the disk actually moved: Apply, a preset, the reset,
|
|
197
|
+
// the switch, a board change and a recompile. Never on a staged edit.
|
|
198
|
+
async function hkReload() {
|
|
199
|
+
try {
|
|
200
|
+
HK_DATA = await hkLoad();
|
|
201
|
+
} catch (_) {
|
|
202
|
+
/* the last good data stays on screen — a failed poll is not a blank page */
|
|
203
|
+
}
|
|
204
|
+
hkPaint();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// The preview is the ONLY thing a width or fixture change moves, so it is the
|
|
208
|
+
// only thing refetched. Four endpoints for a picture is three too many.
|
|
209
|
+
async function hkPreviewReload() {
|
|
210
|
+
try {
|
|
211
|
+
const r = await read(hkPreviewUrl());
|
|
212
|
+
if (HK_DATA) HK_DATA.prev = r.data;
|
|
213
|
+
} catch (_) {}
|
|
214
|
+
hkPaint();
|
|
215
|
+
}
|
|
216
|
+
|
|
133
217
|
// EVERY write carries the board it is about. Forgetting it on one route would
|
|
134
218
|
// edit the other board's layout, which is the one mistake here that is silent.
|
|
135
219
|
function bd(body) {
|
|
136
220
|
return Object.assign({ board: HK_BOARD }, body);
|
|
137
221
|
}
|
|
138
222
|
|
|
139
|
-
function rerender() {
|
|
140
|
-
const h = location.hash;
|
|
141
|
-
location.hash = "#/";
|
|
142
|
-
location.hash = h || "#/hookui";
|
|
143
|
-
}
|
|
144
|
-
|
|
145
223
|
/* ══ the staged plan ══════════════════════════════════════════════════════
|
|
146
224
|
Replays every staged op onto a working copy of the saved layout and returns
|
|
147
225
|
BOTH the effective board and the ordered writes that produce it. ONE
|
|
@@ -159,7 +237,10 @@ function hkOp(op) {
|
|
|
159
237
|
if (op.op === "sep") HK_OPS = HK_OPS.filter((o) => !(o.op === "sep" && o.line === op.line));
|
|
160
238
|
if (op.op === "doc") HK_OPS = HK_OPS.filter((o) => !(o.op === "doc" && o.field === op.field));
|
|
161
239
|
HK_OPS.push(op);
|
|
162
|
-
|
|
240
|
+
// A REPAINT, NEVER A REFETCH. Nothing has been written, so nothing on disk
|
|
241
|
+
// has moved — and re-entering the router here is what made every click look
|
|
242
|
+
// like a save.
|
|
243
|
+
hkPaint();
|
|
163
244
|
}
|
|
164
245
|
|
|
165
246
|
function hkPlan(show, cat) {
|
|
@@ -283,7 +364,8 @@ function boardTabs(cat) {
|
|
|
283
364
|
if (HK_OPS.length) return toast(t("hookui.applyFirst"), "bad");
|
|
284
365
|
HK_BOARD = b;
|
|
285
366
|
HK_SEARCH = "";
|
|
286
|
-
|
|
367
|
+
HK_COMPARE_DATA = null;
|
|
368
|
+
hkReload();
|
|
287
369
|
});
|
|
288
370
|
row.append(btn);
|
|
289
371
|
}
|
|
@@ -320,7 +402,7 @@ function gateCard(show, cat) {
|
|
|
320
402
|
// switch belongs to which board.
|
|
321
403
|
const r = await post("/api/config/set", { key: cat.config_key, value: show.enabled ? "off" : "on" });
|
|
322
404
|
if (!r.ok) toast(t("hookui.switchFailed"), "bad", (r.output || "").trim());
|
|
323
|
-
|
|
405
|
+
hkReload();
|
|
324
406
|
});
|
|
325
407
|
const acts = el("div", "row-actions");
|
|
326
408
|
acts.append(b);
|
|
@@ -348,37 +430,54 @@ function cautionCard(show) {
|
|
|
348
430
|
return c;
|
|
349
431
|
}
|
|
350
432
|
|
|
351
|
-
/*
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
433
|
+
/* == the preview ==========================================================
|
|
434
|
+
Pinned above the board and never scrolled away. `orc statusline preview
|
|
435
|
+
--json`'s output, rendered as DOM and never as HTML.
|
|
436
|
+
|
|
437
|
+
v1.4.2 - IT IS DRAWN AS A TERMINAL, because that is the claim it is making.
|
|
438
|
+
A status line in a bare grey box is a string; the same string inside a
|
|
439
|
+
window of the stated width, on a terminal ground, with a ruler that shows
|
|
440
|
+
where column 80 falls, is a picture of the thing. Nothing about the bytes
|
|
441
|
+
changed - the engine is still the hook's own - what changed is that the
|
|
442
|
+
reader can now believe it.
|
|
443
|
+
|
|
444
|
+
IT DRAWS THE SAVED LAYOUT, and while anything is staged it SAYS SO. The
|
|
445
|
+
preview renders through the hook's own engine, which reads the file - so a
|
|
446
|
+
preview of unsaved edits would need a second engine, which is the one thing
|
|
447
|
+
this design refuses. "These changes are not in the picture yet" is the honest
|
|
448
|
+
version, and it is also the answer to "I pressed Apply and nothing happened":
|
|
449
|
+
something did, and this is where it shows up.
|
|
450
|
+
|
|
451
|
+
THE HEIGHT IS THE OTHER HALF. Three strippings, four colour sets and seven
|
|
452
|
+
symbol sets is a wall of terminal, and it pushed the board - the thing the
|
|
453
|
+
user is actually working in - off the screen. All of it lives in ONE drawer
|
|
454
|
+
now, closed by default, and its rows are FETCHED ONLY WHEN IT IS OPENED:
|
|
455
|
+
eleven extra renders nobody asked to see is eleven subprocesses nobody asked
|
|
456
|
+
to pay for. */
|
|
457
|
+
function previewCard(prev, plan, cat) {
|
|
362
458
|
const right = el("div", "row-actions");
|
|
363
459
|
for (const w of HK_WIDTHS) {
|
|
364
460
|
const b = el("button", "btn btn-xs" + (HK_WIDTH === w ? " btn-primary" : " btn-ghost"), tn(w, "hookui.cols"));
|
|
365
461
|
b.type = "button";
|
|
366
462
|
b.addEventListener("click", () => {
|
|
367
463
|
HK_WIDTH = w;
|
|
368
|
-
|
|
464
|
+
HK_COMPARE_DATA = null;
|
|
465
|
+
hkPreviewReload();
|
|
369
466
|
});
|
|
370
467
|
right.append(b);
|
|
371
468
|
}
|
|
372
469
|
const c = card(t("hookui.preview"), right);
|
|
373
470
|
c.append(el("div", "note", t("hookui.previewAbout")));
|
|
374
|
-
|
|
471
|
+
// THE STAGED BANNER IS AN INSTRUCTION, not a disclaimer. It says what is
|
|
472
|
+
// missing from the picture AND what to press to put it there.
|
|
473
|
+
if (plan.dirty) c.append(el("div", "note note-warn", tn(plan.actions.length, "hookui.previewStaleN")));
|
|
375
474
|
if (!prev || !prev.ok) {
|
|
376
475
|
c.append(el("div", "note", t("hookui.previewUnavailable")));
|
|
377
476
|
return c;
|
|
378
477
|
}
|
|
379
478
|
|
|
380
479
|
// The fixture picker. `--fixtures` must carry one of every state INCLUDING
|
|
381
|
-
// the ugly ones
|
|
480
|
+
// the ugly ones - you cannot design a degraded line on a healthy session.
|
|
382
481
|
c.append(el("div", "note", t("hookui.fixturesAbout")));
|
|
383
482
|
const states = el("div", "row-actions hk-fixtures");
|
|
384
483
|
for (const [k, label] of Object.entries(prev.fixtures || {})) {
|
|
@@ -386,35 +485,156 @@ function previewCard(prev, plan) {
|
|
|
386
485
|
b.type = "button";
|
|
387
486
|
b.addEventListener("click", () => {
|
|
388
487
|
HK_STATE = k;
|
|
389
|
-
|
|
488
|
+
HK_COMPARE_DATA = null;
|
|
489
|
+
hkPreviewReload();
|
|
390
490
|
});
|
|
391
491
|
states.append(b);
|
|
392
492
|
}
|
|
393
493
|
c.append(states);
|
|
394
494
|
|
|
395
|
-
c.append(
|
|
495
|
+
c.append(termWindow(prev.text, {
|
|
496
|
+
title: tn(HK_WIDTH, "hookui.termTitle"),
|
|
497
|
+
// The colour set and the symbol set the picture was drawn with are the
|
|
498
|
+
// CLI's answer, carried on the preview itself. Naming them here would be a
|
|
499
|
+
// second idea of what is in force.
|
|
500
|
+
tags: [prev.theme, prev.glyphs].filter(Boolean),
|
|
501
|
+
ruler: true,
|
|
502
|
+
}));
|
|
503
|
+
|
|
504
|
+
// WHAT EACH LINE COSTS IN CELLS, under the window it belongs to. A number in
|
|
505
|
+
// a row of chips a long way from the picture is a number nobody reads.
|
|
506
|
+
const meta = el("div", "row-actions hk-widths");
|
|
507
|
+
(prev.static_width || []).forEach((w, i) => {
|
|
508
|
+
if (!w) return;
|
|
509
|
+
meta.append(chip(tn(i + 1, "hookui.lineN") + " " + tn(w, "hookui.cells"), null));
|
|
510
|
+
});
|
|
511
|
+
if (meta.childNodes.length) c.append(meta);
|
|
512
|
+
|
|
513
|
+
c.append(compareDrawer(prev, cat));
|
|
514
|
+
return c;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// A TERMINAL, drawn as one. The chrome is not decoration: the title bar states
|
|
518
|
+
// the width the picture was rendered at, and the ruler shows where that width
|
|
519
|
+
// falls - which is the whole question a status line asks.
|
|
520
|
+
function termWindow(text, { title, tags, ruler, small }) {
|
|
521
|
+
const box = el("div", "hk-term" + (small ? " hk-term-sm" : ""));
|
|
522
|
+
const bar = el("div", "hk-term-bar");
|
|
523
|
+
const dots = el("div", "hk-term-dots");
|
|
524
|
+
for (let i = 0; i < 3; i++) dots.append(el("span", "hk-term-dot"));
|
|
525
|
+
bar.append(dots);
|
|
526
|
+
if (title) bar.append(el("span", "hk-term-title", title));
|
|
527
|
+
const tagRow = el("div", "hk-term-tags");
|
|
528
|
+
for (const g of tags || []) tagRow.append(el("span", "hk-term-tag", g));
|
|
529
|
+
bar.append(tagRow);
|
|
530
|
+
box.append(bar);
|
|
531
|
+
|
|
532
|
+
const screen = el("div", "hk-term-screen");
|
|
533
|
+
if (ruler) screen.append(rulerRow(HK_WIDTH));
|
|
534
|
+
screen.append(ansiBlock(text, "hk-preview hk-preview-flat"));
|
|
535
|
+
box.append(screen);
|
|
536
|
+
return box;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// The column ruler. Ten-cell ticks with the count at each, in the SAME
|
|
540
|
+
// monospaced grid as the line under it - so "this is 44 cells" stops being a
|
|
541
|
+
// number and becomes a place on the picture.
|
|
542
|
+
function rulerRow(cols) {
|
|
543
|
+
const row = el("div", "hk-ruler");
|
|
544
|
+
let out = "";
|
|
545
|
+
for (let i = 0; i < cols; i++) {
|
|
546
|
+
if (i % 10 === 0) {
|
|
547
|
+
const n = String(i);
|
|
548
|
+
out += n;
|
|
549
|
+
i += n.length - 1;
|
|
550
|
+
} else out += i % 5 === 0 ? "+" : "\u00b7";
|
|
551
|
+
}
|
|
552
|
+
row.textContent = out.slice(0, cols);
|
|
553
|
+
return row;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/* == the comparison drawer ================================================
|
|
557
|
+
Three strippings, every colour set and every symbol set - the same layout,
|
|
558
|
+
the same fixture, the same width, through the same engine. A colour set
|
|
559
|
+
picked from its NAME is a guess; picked from its picture it is a decision.
|
|
560
|
+
|
|
561
|
+
It is CLOSED by default and its rows are fetched on the first open, because
|
|
562
|
+
each one is a real render. */
|
|
563
|
+
function compareDrawer(prev, cat) {
|
|
564
|
+
const wrap = el("div", "hk-compare");
|
|
565
|
+
const head = el("button", "hk-compare-head");
|
|
566
|
+
head.type = "button";
|
|
567
|
+
head.setAttribute("aria-expanded", String(HK_COMPARE));
|
|
568
|
+
head.append(el("span", "tier-caret", "\u25b8"));
|
|
569
|
+
head.append(el("span", "hk-compare-title", t("hookui.compare")));
|
|
570
|
+
head.append(el("span", "note", t("hookui.compareAbout")));
|
|
571
|
+
// NOT named `body`: that name is reserved for a panel's own card container,
|
|
572
|
+
// which must carry `stack`. This is a drawer's contents and spaces itself.
|
|
573
|
+
const drawer = el("div", "hk-compare-body");
|
|
574
|
+
wrap.classList.toggle("hk-compare-open", HK_COMPARE);
|
|
575
|
+
head.addEventListener("click", () => {
|
|
576
|
+
HK_COMPARE = !HK_COMPARE;
|
|
577
|
+
wrap.classList.toggle("hk-compare-open", HK_COMPARE);
|
|
578
|
+
head.setAttribute("aria-expanded", String(HK_COMPARE));
|
|
579
|
+
if (HK_COMPARE && !HK_COMPARE_DATA) loadCompare(drawer, cat, prev);
|
|
580
|
+
});
|
|
581
|
+
wrap.append(head, drawer);
|
|
582
|
+
if (HK_COMPARE) {
|
|
583
|
+
if (HK_COMPARE_DATA) paintCompare(drawer, prev);
|
|
584
|
+
else loadCompare(drawer, cat, prev);
|
|
585
|
+
}
|
|
586
|
+
return wrap;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async function loadCompare(body, cat, prev) {
|
|
590
|
+
body.replaceChildren(skeleton(3));
|
|
591
|
+
const themes = Object.keys(cat.themes || {});
|
|
592
|
+
const glyphs = cat.glyph_sets || [];
|
|
593
|
+
const one = (extra) => read(hkPreviewUrl() + extra).then((r) => r.data).catch(() => null);
|
|
594
|
+
try {
|
|
595
|
+
const [themeRows, glyphRows] = await Promise.all([
|
|
596
|
+
Promise.all(themes.map((n) => one("&theme=" + encodeURIComponent(n)).then((d) => [n, d]))),
|
|
597
|
+
Promise.all(glyphs.map((n) => one("&glyphs=" + encodeURIComponent(n)).then((d) => [n, d]))),
|
|
598
|
+
]);
|
|
599
|
+
HK_COMPARE_DATA = { themes: themeRows, glyphs: glyphRows };
|
|
600
|
+
} catch (_) {
|
|
601
|
+
HK_COMPARE_DATA = { themes: [], glyphs: [] };
|
|
602
|
+
}
|
|
603
|
+
paintCompare(body, prev);
|
|
604
|
+
}
|
|
396
605
|
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
//
|
|
400
|
-
|
|
401
|
-
|
|
606
|
+
function paintCompare(body, prev) {
|
|
607
|
+
body.replaceChildren();
|
|
608
|
+
// THE THREE STRIPPINGS FIRST. You cannot design an ASCII fallback you cannot
|
|
609
|
+
// see, and a design whose only distinction is colour becomes ambiguous the
|
|
610
|
+
// moment NO_COLOR is set.
|
|
611
|
+
body.append(el("div", "hk-compare-group", t("hookui.degrades")));
|
|
402
612
|
for (const [key, label] of [
|
|
403
613
|
["no_color", t("hookui.degNoColor")],
|
|
404
614
|
["ascii", t("hookui.degAscii")],
|
|
405
615
|
["no_motion", t("hookui.degNoMotion")],
|
|
406
616
|
]) {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
617
|
+
body.append(compareRow(label, (prev.strippings || {})[key]));
|
|
618
|
+
}
|
|
619
|
+
const d = HK_COMPARE_DATA || { themes: [], glyphs: [] };
|
|
620
|
+
if (d.themes.length) {
|
|
621
|
+
body.append(el("div", "hk-compare-group", t("hookui.compareThemes")));
|
|
622
|
+
// The NAME is the CLI's word, printed verbatim - a translated colour set is
|
|
623
|
+
// a colour set that does not exist.
|
|
624
|
+
for (const [name, row] of d.themes) body.append(compareRow(name, row && row.ok ? row.text : null));
|
|
411
625
|
}
|
|
412
|
-
|
|
626
|
+
if (d.glyphs.length) {
|
|
627
|
+
body.append(el("div", "hk-compare-group", t("hookui.compareGlyphs")));
|
|
628
|
+
for (const [name, row] of d.glyphs) body.append(compareRow(name, row && row.ok ? row.text : null));
|
|
629
|
+
}
|
|
630
|
+
}
|
|
413
631
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
632
|
+
function compareRow(label, text) {
|
|
633
|
+
const row = el("div", "hk-compare-row");
|
|
634
|
+
row.append(el("div", "hk-compare-label", label));
|
|
635
|
+
if (text == null) row.append(el("div", "note", t("hookui.previewUnavailable")));
|
|
636
|
+
else row.append(termWindow(text, { title: label, tags: [], ruler: false, small: true }));
|
|
637
|
+
return row;
|
|
418
638
|
}
|
|
419
639
|
|
|
420
640
|
// ANSI → DOM. Never innerHTML: this string comes from the CLI, and the rule for
|
|
@@ -758,6 +978,7 @@ function announce(msg) {
|
|
|
758
978
|
//
|
|
759
979
|
// `replace` names the chip whose part is being swapped; absent, this is an add.
|
|
760
980
|
function partPicker(cat, plan, line, replace) {
|
|
981
|
+
HK_MODAL = null;
|
|
761
982
|
let q = "";
|
|
762
983
|
// NOT named `body`: that name is reserved for a panel's own card container,
|
|
763
984
|
// which must carry `stack`. This is a modal's contents and spaces itself.
|
|
@@ -850,6 +1071,7 @@ function matches(comp, q) {
|
|
|
850
1071
|
// ONLY way to move a chip, because drag is off below the width at which a drop
|
|
851
1072
|
// gap stops being a target.
|
|
852
1073
|
function moveModal(item, line, plan) {
|
|
1074
|
+
HK_MODAL = null;
|
|
853
1075
|
const body = el("div", "stack");
|
|
854
1076
|
body.append(el("div", "note", t("hookui.moveAbout", { id: item.type })));
|
|
855
1077
|
|
|
@@ -896,6 +1118,7 @@ function moveModal(item, line, plan) {
|
|
|
896
1118
|
|
|
897
1119
|
/* ══ remove ═══════════════════════════════════════════════════════════════ */
|
|
898
1120
|
function removeModal(item, comp) {
|
|
1121
|
+
HK_MODAL = null;
|
|
899
1122
|
const body = el("div", "stack");
|
|
900
1123
|
body.append(el("div", "note", t("hookui.removeAbout", { id: item.type })));
|
|
901
1124
|
if (comp && comp.summary) body.append(el("div", "note", comp.summary));
|
|
@@ -926,134 +1149,243 @@ function removeModal(item, comp) {
|
|
|
926
1149
|
// it does. The JSON is for the diff, the editor is for the person — and the
|
|
927
1150
|
// ids, renderers and state words that DO appear are the CLI's own and are never
|
|
928
1151
|
// translated.
|
|
929
|
-
function editModal(
|
|
1152
|
+
function editModal(item0, line0, comp, cat) {
|
|
930
1153
|
// NOT named `body`: that name is reserved for a panel's own card container,
|
|
931
1154
|
// which must carry `stack`. This is a modal's contents and spaces itself.
|
|
932
1155
|
const pane = el("div", "hk-editor");
|
|
933
1156
|
if (!comp) {
|
|
934
1157
|
pane.append(el("div", "note", t("hookui.unknownComponent")));
|
|
935
|
-
modal({ title:
|
|
1158
|
+
modal({ title: item0.type, body: pane, actions: [{ label: t("common.close"), cls: "btn-ghost", onClick: (c) => c() }] });
|
|
936
1159
|
return;
|
|
937
1160
|
}
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1161
|
+
|
|
1162
|
+
// THE EDITOR FOLLOWS THE STAGED STATE (v1.4.2). It used to be built once and
|
|
1163
|
+
// then left standing while the panel behind it was thrown away and rebuilt,
|
|
1164
|
+
// so every control it showed was the value from BEFORE the change the user
|
|
1165
|
+
// had just made: you picked a colour, the swatch did not move, and the only
|
|
1166
|
+
// reasonable conclusion was that the editor did not work. The part is
|
|
1167
|
+
// re-found by its stable ref on every repaint, so what is on screen is always
|
|
1168
|
+
// the effective value.
|
|
1169
|
+
let ref = item0.ref;
|
|
1170
|
+
let item = item0;
|
|
1171
|
+
let line = line0;
|
|
1172
|
+
|
|
1173
|
+
const paint = (plan) => {
|
|
1174
|
+
if (plan) {
|
|
1175
|
+
let found = null;
|
|
1176
|
+
for (const l of plan.lines) {
|
|
1177
|
+
const hit = l.items.find((x) => x.ref === ref);
|
|
1178
|
+
if (hit) {
|
|
1179
|
+
found = { l, hit };
|
|
1180
|
+
break;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
// The part is gone — a staged remove, or an Apply that took it away.
|
|
1184
|
+
// Closing is the only honest answer; editing something that no longer
|
|
1185
|
+
// exists is worse than losing the dialog.
|
|
1186
|
+
if (!found) return close();
|
|
1187
|
+
item = found.hit;
|
|
1188
|
+
line = found.l;
|
|
1189
|
+
}
|
|
1190
|
+
// THE CARET SURVIVES THE REPAINT. The editor rebuilds on every keystroke
|
|
1191
|
+
// now, and a text box that loses focus after one letter is not a text box.
|
|
1192
|
+
// The path is positional because the controls are the same controls in the
|
|
1193
|
+
// same order across a repaint; when they are not, focus is simply not
|
|
1194
|
+
// restored, which is the right failure.
|
|
1195
|
+
const keep = focusPath(pane);
|
|
1196
|
+
pane.replaceChildren();
|
|
1197
|
+
// Same rule inside the dialog: it rebuilds on every keystroke, and a
|
|
1198
|
+
// shape gallery that fades in each time you type a letter is the blink
|
|
1199
|
+
// again in a smaller box.
|
|
1200
|
+
pane.classList.add("hk-quiet");
|
|
1201
|
+
build();
|
|
1202
|
+
restoreFocus(pane, keep);
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
// The value in force for one field: what is staged, else what the CLI
|
|
1206
|
+
// resolved, else the fallback. `show --json` now carries EVERY field, so this
|
|
1207
|
+
// can finally answer for all of them — before v1.4.2 it emitted twelve of
|
|
1208
|
+
// twenty-four and a control for one of the other twelve always read blank.
|
|
1209
|
+
const val = (field, dflt) =>
|
|
1210
|
+
field in item.over
|
|
1211
|
+
? item.over[field]
|
|
1212
|
+
: item.src && item.src[field] !== undefined && item.src[field] !== null
|
|
1213
|
+
? item.src[field]
|
|
1214
|
+
: dflt;
|
|
1215
|
+
// Whether the USER set this one, as opposed to inheriting it from the
|
|
1216
|
+
// catalogue, the colour set or the file. A control that cannot tell those
|
|
1217
|
+
// apart cannot say what it is about to change.
|
|
1218
|
+
const mine = (field) => field in item.over || !!(item.src && item.src.authored && field in item.src.authored);
|
|
1219
|
+
const stage = (field, value, label) => hkOp({ op: "set", ref, field, value, label });
|
|
1220
|
+
// A field name, plus a mark when the value is the user's own.
|
|
1221
|
+
const fieldName = (name, field) => (mine(field) ? name + " \u2022" : name);
|
|
1222
|
+
|
|
1223
|
+
// WHETHER THE CURRENT SHAPE USES THIS FIELD AT ALL. The CLI's answer, per
|
|
1224
|
+
// renderer, carried on the renderer table — the panel does not know which
|
|
1225
|
+
// shapes draw a label any more than it knows what a label is. Twenty-eight of
|
|
1226
|
+
// the thirty-five renderers ignore `label` entirely, and before this the Name
|
|
1227
|
+
// box was offered on every one of them: it took the text, the CLI wrote it to
|
|
1228
|
+
// disk correctly, and nothing ever appeared on the bar.
|
|
1229
|
+
//
|
|
1230
|
+
// The control is DISABLED and keeps its slot with the reason on it, never
|
|
1231
|
+
// hidden — "this shape cannot use it" and "we forgot to offer it" must not
|
|
1232
|
+
// look the same, and a value already set has to stay visible.
|
|
1233
|
+
const usesOf = (field) => {
|
|
1234
|
+
const rend = cat.renderers[val("render", (comp.defaults && comp.defaults.render) || comp.renderers[0])];
|
|
1235
|
+
if (!rend || !rend.uses) return true;
|
|
1236
|
+
return rend.uses.includes(field);
|
|
1237
|
+
};
|
|
1238
|
+
// Wraps a control that this shape ignores: greyed, inert, and carrying the
|
|
1239
|
+
// sentence that says which shapes DO use it.
|
|
1240
|
+
const gated = (field, node, shapes) => {
|
|
1241
|
+
if (usesOf(field)) return node;
|
|
1242
|
+
const box = el("div", "hk-inert");
|
|
1243
|
+
for (const b of node.querySelectorAll ? node.querySelectorAll("button, input, select") : []) b.disabled = true;
|
|
1244
|
+
if (node.tagName === "INPUT" || node.tagName === "SELECT") node.disabled = true;
|
|
1245
|
+
box.append(node);
|
|
1246
|
+
box.append(el("div", "note note-warn", shapes
|
|
1247
|
+
? t("hookui.shapeIgnoresLabel", { shapes: shapes.join(", ") })
|
|
1248
|
+
: t("hookui.shapeIgnores")));
|
|
1249
|
+
return box;
|
|
1250
|
+
};
|
|
1251
|
+
|
|
1252
|
+
function build() {
|
|
1253
|
+
const live = el("div", "hk-editor-live");
|
|
1254
|
+
// WHAT THIS IS, EXACTLY. `previews` is a per-renderer sample the CLI drew
|
|
1255
|
+
// from its own fixture, so it shows the SHAPE this part will take - never
|
|
1256
|
+
// the words or the numbers this component will carry at run time. Claiming
|
|
1257
|
+
// otherwise would make the one picture in this dialog a lie.
|
|
1258
|
+
live.append(el("div", "note", t("hookui.editorLive")));
|
|
1259
|
+
live.append(termWindow(chipSample(item, comp), { title: comp.id, tags: [], ruler: false, small: true }));
|
|
1260
|
+
pane.append(live);
|
|
1261
|
+
if (comp.summary) pane.append(el("div", "note", comp.summary));
|
|
1262
|
+
pane.append(el("div", "note", t("hookui.editorStaged")));
|
|
1263
|
+
|
|
1264
|
+
// Swapping the part itself, from inside the editor: "this slot, but a
|
|
1265
|
+
// different thing" is a real edit and it should not need a remove and an add.
|
|
1266
|
+
const swap = el("button", "btn btn-xs btn-ghost hk-swap", t("hookui.swap"));
|
|
1267
|
+
swap.type = "button";
|
|
1268
|
+
swap.addEventListener("click", () => {
|
|
970
1269
|
close();
|
|
971
|
-
|
|
1270
|
+
partPicker(cat, HK_NO_LINES, 0, item);
|
|
972
1271
|
});
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
//
|
|
1011
|
-
//
|
|
1012
|
-
//
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1272
|
+
pane.append(swap);
|
|
1273
|
+
|
|
1274
|
+
// THE SHAPE PICKER IS A GALLERY, NOT A DROPDOWN. Every renderer the component
|
|
1275
|
+
// declares, drawn with this component's real current value. A dropdown
|
|
1276
|
+
// containing a renderer's name tells nobody anything, and this is the
|
|
1277
|
+
// difference between a feature people use and a settings form they close.
|
|
1278
|
+
pane.append(sectionHead(t("hookui.shape"), t("hookui.shapeAbout")));
|
|
1279
|
+
const gallery = el("div", "hk-gallery");
|
|
1280
|
+
const curRender = val("render", (comp.defaults && comp.defaults.render) || comp.renderers[0]);
|
|
1281
|
+
for (const r of comp.renderers) {
|
|
1282
|
+
const b = el("button", "hk-sample" + (curRender === r ? " hk-sample-on" : ""));
|
|
1283
|
+
b.type = "button";
|
|
1284
|
+
b.title = r;
|
|
1285
|
+
b.append(ansiBlock(comp.previews && comp.previews[r] ? comp.previews[r] : r, "hk-preview hk-preview-chip"));
|
|
1286
|
+
b.append(el("span", "hk-sample-id", r));
|
|
1287
|
+
// IT NO LONGER CLOSES. Closing was how the old editor hid the fact that
|
|
1288
|
+
// it could not update itself; now the gallery marks the new shape and the
|
|
1289
|
+
// sample above redraws, which is what a person expects a picker to do.
|
|
1290
|
+
b.addEventListener("click", () => stage("render", r, t("hookui.shapeChange", { id: comp.id, r })));
|
|
1291
|
+
gallery.append(b);
|
|
1292
|
+
}
|
|
1293
|
+
pane.append(gallery);
|
|
1294
|
+
// AT THE CONTROL, at the moment the choice is made. Which shape you pick
|
|
1295
|
+
// decides whether the Name box below is live at all, so the sentence
|
|
1296
|
+
// belongs here rather than in a surprise further down the dialog.
|
|
1297
|
+
pane.append(el("div", "note", t("hookui.shapeDecides", { shapes: (cat.label_renderers || []).join(", ") })));
|
|
1298
|
+
|
|
1299
|
+
pane.append(sectionHead(t("hookui.glyphs"), t("hookui.glyphsAbout")));
|
|
1300
|
+
pane.append(pickRow(cat.glyph_sets, val("glyphs", null), (v) => stage("glyphs", v, t("hookui.glyphChange", { id: comp.id, v }))));
|
|
1301
|
+
|
|
1302
|
+
pane.append(sectionHead(t("hookui.words"), t("hookui.wordsAbout")));
|
|
1303
|
+
const label = el("input", "hk-input");
|
|
1304
|
+
label.type = "text";
|
|
1305
|
+
const cur = val("label", "");
|
|
1306
|
+
label.value = cur == null ? "" : String(cur);
|
|
1307
|
+
label.placeholder = t("hookui.labelPlaceholder");
|
|
1308
|
+
// BOTH EVENTS. `change` alone fires on blur, so a name typed and then
|
|
1309
|
+
// followed by a click on Close raced the close and was sometimes never
|
|
1310
|
+
// staged at all. `input` stages every keystroke; `hkOp` already collapses
|
|
1311
|
+
// repeated edits of one field into one decision, so the pending list still
|
|
1312
|
+
// reads as a list of decisions.
|
|
1313
|
+
const push = () => stage("label", label.value, t("hookui.labelChange", { id: comp.id, v: label.value }));
|
|
1314
|
+
label.addEventListener("change", push);
|
|
1315
|
+
label.addEventListener("input", push);
|
|
1316
|
+
pane.append(labelled(fieldName(t("hookui.label"), "label"), gated("label", label, cat.label_renderers), t("hookui.labelAbout")));
|
|
1317
|
+
pane.append(labelled(fieldName(t("hookui.case"), "case"), gated("case", pickRow(cat.cases, val("case", null), (v) => stage("case", v, t("hookui.caseChange", { id: comp.id, v })))), t("hookui.caseAbout")));
|
|
1318
|
+
pane.append(labelled(fieldName(t("hookui.before"), "prefix"), gated("prefix", textField(val("prefix", ""), (v) => stage("prefix", v, t("hookui.beforeChange", { id: comp.id, v })))), t("hookui.beforeAbout")));
|
|
1319
|
+
pane.append(labelled(fieldName(t("hookui.after"), "suffix"), gated("suffix", textField(val("suffix", ""), (v) => stage("suffix", v, t("hookui.afterChange", { id: comp.id, v })))), t("hookui.afterAbout")));
|
|
1320
|
+
|
|
1321
|
+
pane.append(sectionHead(t("hookui.colour"), t("hookui.colourAbout")));
|
|
1322
|
+
pane.append(labelled(fieldName(t("hookui.labelColour"), "label_color"), pickRow(cat.colors, val("label_color", null), (v) => stage("label_color", v, t("hookui.colourChange", { id: comp.id, v }))), t("hookui.labelColourAbout")));
|
|
1323
|
+
pane.append(labelled(fieldName(t("hookui.valueColour"), "value_color"), pickRow(cat.colors, val("value_color", null), (v) => stage("value_color", v, t("hookui.colourChange", { id: comp.id, v }))), t("hookui.valueColourAbout")));
|
|
1324
|
+
if (comp.bounded)
|
|
1325
|
+
pane.append(labelled(fieldName(t("hookui.ramp"), "ramp"), pickRow(Object.keys(cat.ramps), val("ramp", null), (v) => stage("ramp", v, t("hookui.rampChange", { id: comp.id, v }))), t("hookui.rampAbout")));
|
|
1326
|
+
// R3, AT THE CONTROL, at the moment the choice is made - never in a
|
|
1327
|
+
// validation summary later.
|
|
1328
|
+
if (comp.states) pane.append(el("div", "note note-warn", t("hookui.stateColourWarn", { s: comp.states.join(", ") })));
|
|
1329
|
+
|
|
1330
|
+
// "Weight", never "font size". A terminal owns its font, and a picker that
|
|
1331
|
+
// did nothing would be worse than not offering one.
|
|
1332
|
+
pane.append(labelled(fieldName(t("hookui.emphasis"), "emphasis"), pickRow((cat.emphasis || []).filter((e) => !(cat.refused_emphasis || []).includes(e)), (val("emphasis", []) || [])[0], (v) => stage("emphasis", v, t("hookui.emphasisChange", { id: comp.id, v }))), t("hookui.emphasisAbout")));
|
|
1333
|
+
pane.append(el("div", "note", t("hookui.fontWhy")));
|
|
1334
|
+
|
|
1335
|
+
if (comp.bounded || (comp.defaults && comp.defaults.format)) {
|
|
1336
|
+
pane.append(sectionHead(t("hookui.numbers"), t("hookui.numbersAbout")));
|
|
1337
|
+
pane.append(labelled(fieldName(t("hookui.format"), "format"), gated("format", pickRow(cat.formats, val("format", null), (v) => stage("format", v, t("hookui.formatChange", { id: comp.id, v })))), t("hookui.formatAbout")));
|
|
1338
|
+
pane.append(labelled(fieldName(t("hookui.compact"), "compact"), gated("compact", pickRow(cat.compact, val("compact", null), (v) => stage("compact", v, t("hookui.compactChange", { id: comp.id, v })))), t("hookui.compactAbout")));
|
|
1339
|
+
// `min_width` is not cosmetic: a value that changes width shifts every part
|
|
1340
|
+
// to its right on the keystroke it happens, and that jitter is the main
|
|
1341
|
+
// reason people turn a status line off again.
|
|
1342
|
+
pane.append(labelled(fieldName(t("hookui.minWidth"), "min_width"), gated("min_width", numField(val("min_width", null), 0, 8, (v) => stage("min_width", v, t("hookui.minWidthChange", { id: comp.id, v })))), t("hookui.minWidthWarn")));
|
|
1343
|
+
pane.append(labelled(fieldName(t("hookui.precision"), "precision"), gated("precision", numField(val("precision", null), 0, 2, (v) => stage("precision", v, t("hookui.precisionChange", { id: comp.id, v })))), t("hookui.precisionAbout")));
|
|
1344
|
+
}
|
|
1016
1345
|
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1346
|
+
const rend = cat.renderers[curRender];
|
|
1347
|
+
if (rend && rend.width)
|
|
1348
|
+
pane.append(labelled(fieldName(t("hookui.width"), "width"), numField(val("width", null), rend.width[0], rend.width[1], (v) => stage("width", v, t("hookui.widthChange", { id: comp.id, v }))), t("hookui.widthWhy")));
|
|
1349
|
+
|
|
1350
|
+
// A CHECKLIST, not an enum: "hide when zero" and "hide when there is no run"
|
|
1351
|
+
// are independent facts and a user wants both at once.
|
|
1352
|
+
pane.append(sectionHead(t("hookui.whenToShow"), t("hookui.whenToShowAbout")));
|
|
1353
|
+
const checks = el("div", "hk-checks");
|
|
1354
|
+
const active = new Set(val("hide_when", []) || []);
|
|
1355
|
+
for (const h of cat.hide_when) {
|
|
1356
|
+
if (h.id === "never") continue;
|
|
1357
|
+
const lab = el("label", "hk-check");
|
|
1358
|
+
const box = el("input");
|
|
1359
|
+
box.type = "checkbox";
|
|
1360
|
+
box.checked = active.has(h.id);
|
|
1361
|
+
box.addEventListener("change", () => {
|
|
1362
|
+
const next = new Set(active);
|
|
1363
|
+
if (box.checked) next.add(h.id);
|
|
1364
|
+
else next.delete(h.id);
|
|
1365
|
+
const v = next.size ? [...next].join(",") : "never";
|
|
1366
|
+
stage("hide_when", v, t("hookui.hideChange", { id: comp.id, v }));
|
|
1367
|
+
});
|
|
1368
|
+
lab.append(box, el("span", null, h.id), el("span", "note", h.says));
|
|
1369
|
+
checks.append(lab);
|
|
1370
|
+
}
|
|
1371
|
+
pane.append(checks);
|
|
1372
|
+
|
|
1373
|
+
// RESPONSIVE WIDTH: the range of terminal widths in which this part is worth
|
|
1374
|
+
// its cells. Strictly better than truncating the right of an overflowing
|
|
1375
|
+
// line, because the USER chooses what survives a narrow terminal.
|
|
1376
|
+
pane.append(sectionHead(t("hookui.narrow"), t("hookui.narrowAbout")));
|
|
1377
|
+
pane.append(labelled(fieldName(t("hookui.minCols"), "min_cols"), numField(val("min_cols", null), 0, 200, (v) => stage("min_cols", v, t("hookui.minColsChange", { id: comp.id, v }))), t("hookui.minColsAbout")));
|
|
1378
|
+
pane.append(labelled(fieldName(t("hookui.priority"), "priority"), numField(val("priority", null), 1, 5, (v) => stage("priority", v, t("hookui.priorityChange", { id: comp.id, v }))), t("hookui.priorityWhy")));
|
|
1041
1379
|
}
|
|
1042
|
-
pane.append(checks);
|
|
1043
1380
|
|
|
1044
|
-
|
|
1045
|
-
// its cells. Strictly better than truncating the right of an overflowing
|
|
1046
|
-
// line, because the USER chooses what survives a narrow terminal.
|
|
1047
|
-
pane.append(sectionHead(t("hookui.narrow"), t("hookui.narrowAbout")));
|
|
1048
|
-
pane.append(labelled(t("hookui.minCols"), numField(val("min_cols", null), 0, 200, (v) => stage("min_cols", v, t("hookui.minColsChange", { id: comp.id, v }))), t("hookui.minColsAbout")));
|
|
1049
|
-
pane.append(labelled(t("hookui.priority"), numField(val("priority", null), 1, 5, (v) => stage("priority", v, t("hookui.priorityChange", { id: comp.id, v }))), t("hookui.priorityWhy")));
|
|
1381
|
+
paint(null);
|
|
1050
1382
|
|
|
1051
1383
|
const close = modal({
|
|
1052
1384
|
title: comp.id,
|
|
1053
1385
|
body: pane,
|
|
1054
1386
|
actions: [
|
|
1055
1387
|
// WHERE EACH VALUE CAME FROM. It is how a user finds their own overrides
|
|
1056
|
-
// again after a colour set change
|
|
1388
|
+
// again after a colour set change - `orc lane config`'s problem, at part
|
|
1057
1389
|
// scale. It reads the SAVED item, so it is offered only for one.
|
|
1058
1390
|
...(item.src
|
|
1059
1391
|
? [{
|
|
@@ -1062,23 +1394,67 @@ function editModal(item, line, comp, cat) {
|
|
|
1062
1394
|
onClick: async () => {
|
|
1063
1395
|
const r = await read("/api/statusline/explain?board=" + HK_BOARD + "&at=" + line.line + ":" + (line.items.indexOf(item) + 1));
|
|
1064
1396
|
if (!r.data || !r.data.ok) return toast(t("hookui.explainFailed"), "bad");
|
|
1397
|
+
HK_MODAL = null;
|
|
1065
1398
|
modal({
|
|
1066
1399
|
title: comp.id,
|
|
1067
|
-
|
|
1400
|
+
body: kvList(r.data.resolved.map((x) => [x.field, x.source])),
|
|
1068
1401
|
actions: [{ label: t("common.close"), cls: "btn-ghost", onClick: (c) => c() }],
|
|
1069
1402
|
});
|
|
1070
1403
|
},
|
|
1071
1404
|
}]
|
|
1072
1405
|
: []),
|
|
1073
|
-
{
|
|
1406
|
+
{
|
|
1407
|
+
label: t("common.close"),
|
|
1408
|
+
cls: "btn-ghost",
|
|
1409
|
+
onClick: (c) => {
|
|
1410
|
+
HK_MODAL = null;
|
|
1411
|
+
c();
|
|
1412
|
+
},
|
|
1413
|
+
},
|
|
1074
1414
|
],
|
|
1075
1415
|
});
|
|
1416
|
+
// Registered LAST, so a repaint that fires during construction cannot reach a
|
|
1417
|
+
// half-built pane.
|
|
1418
|
+
HK_MODAL = { repaint: paint };
|
|
1076
1419
|
}
|
|
1077
1420
|
|
|
1078
1421
|
// `partPicker` reads a board only to answer "can a part go on that line". A
|
|
1079
1422
|
// SWAP changes nothing about where the part sits, so it is handed a board with
|
|
1080
1423
|
// no lines and never asks.
|
|
1081
|
-
const HK_NO_LINES = { lines: [], max:
|
|
1424
|
+
const HK_NO_LINES = { lines: [], max: 0 };
|
|
1425
|
+
|
|
1426
|
+
// Where the caret is, as a path of child indexes from the editor's own root,
|
|
1427
|
+
// plus the selection inside the field. Not an id: a control here has no
|
|
1428
|
+
// identity of its own, and inventing one for every field would be a second
|
|
1429
|
+
// idea of what the editor holds.
|
|
1430
|
+
function focusPath(root) {
|
|
1431
|
+
const a = document.activeElement;
|
|
1432
|
+
if (!a || !root.contains(a)) return null;
|
|
1433
|
+
const path = [];
|
|
1434
|
+
let n = a;
|
|
1435
|
+
while (n && n !== root) {
|
|
1436
|
+
path.unshift([...n.parentNode.childNodes].indexOf(n));
|
|
1437
|
+
n = n.parentNode;
|
|
1438
|
+
}
|
|
1439
|
+
const sel = "selectionStart" in a && a.selectionStart !== null ? [a.selectionStart, a.selectionEnd] : null;
|
|
1440
|
+
return { path, sel };
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
function restoreFocus(root, keep) {
|
|
1444
|
+
if (!keep) return;
|
|
1445
|
+
let n = root;
|
|
1446
|
+
for (const i of keep.path) {
|
|
1447
|
+
n = n && n.childNodes[i];
|
|
1448
|
+
if (!n) return;
|
|
1449
|
+
}
|
|
1450
|
+
if (!n.focus) return;
|
|
1451
|
+
n.focus();
|
|
1452
|
+
if (keep.sel && "setSelectionRange" in n) {
|
|
1453
|
+
try {
|
|
1454
|
+
n.setSelectionRange(keep.sel[0], keep.sel[1]);
|
|
1455
|
+
} catch (_) {}
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1082
1458
|
|
|
1083
1459
|
function sectionHead(name, about) {
|
|
1084
1460
|
const w = el("div", "hk-section-wrap");
|
|
@@ -1244,7 +1620,8 @@ function confirmApply(name) {
|
|
|
1244
1620
|
const r = await post("/api/statusline/apply", bd({ name }));
|
|
1245
1621
|
if (!r.ok) toast(t("hookui.applyFailed"), "bad", (r.output || "").trim());
|
|
1246
1622
|
HK_OPS = [];
|
|
1247
|
-
|
|
1623
|
+
HK_COMPARE_DATA = null;
|
|
1624
|
+
hkReload();
|
|
1248
1625
|
},
|
|
1249
1626
|
},
|
|
1250
1627
|
],
|
|
@@ -1264,7 +1641,8 @@ function confirmReset() {
|
|
|
1264
1641
|
c();
|
|
1265
1642
|
await post("/api/statusline/reset", bd({}));
|
|
1266
1643
|
HK_OPS = [];
|
|
1267
|
-
|
|
1644
|
+
HK_COMPARE_DATA = null;
|
|
1645
|
+
hkReload();
|
|
1268
1646
|
},
|
|
1269
1647
|
},
|
|
1270
1648
|
],
|
|
@@ -1303,7 +1681,7 @@ function advancedCard(cat, plan) {
|
|
|
1303
1681
|
re.addEventListener("click", async () => {
|
|
1304
1682
|
const r = await post("/api/statusline/compile", bd({}));
|
|
1305
1683
|
toast(r.ok ? t("hookui.recompiled") : t("hookui.recompileFailed"), r.ok ? "ok" : "bad");
|
|
1306
|
-
|
|
1684
|
+
hkReload();
|
|
1307
1685
|
});
|
|
1308
1686
|
const acts = el("div", "row-actions");
|
|
1309
1687
|
acts.append(re);
|