@azure-id/orc 1.4.0 → 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 +229 -0
- package/README-id.md +39 -2
- package/README.md +53 -63
- package/bin/cli.js +212 -15
- package/bin/webui/api.js +16 -0
- package/bin/webui/css/04-motion.css +61 -0
- package/bin/webui/css/06-responsive.css +17 -1
- package/bin/webui/css/panels/hookui.css +493 -98
- package/bin/webui/fixtures/hookui.js +431 -409
- package/bin/webui/i18n/en/hookui.json +119 -44
- package/bin/webui/i18n/en/tour.json +1 -1
- package/bin/webui/i18n/id/hookui.json +137 -62
- package/bin/webui/i18n/id/tour.json +1 -1
- package/bin/webui/js/panels/hookui.js +1266 -466
- 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
|
@@ -14,27 +14,35 @@
|
|
|
14
14
|
(templates/hooks/orc-statusline-render.js), so the two cannot diverge.
|
|
15
15
|
|
|
16
16
|
THE PANEL DERIVES NOTHING. Not a component id, not a renderer name, not a
|
|
17
|
-
glyph set, not a ramp, not a colour token, not a
|
|
18
|
-
own rules. All of it arrives from `statusline components
|
|
19
|
-
greps this file and both string tables for those
|
|
20
|
-
rule, on a fourth surface.)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
glyph set, not a ramp, not a colour token, not a separator, not a state word,
|
|
18
|
+
not the board's own rules. All of it arrives from `statusline components
|
|
19
|
+
--json`, and a test greps this file and both string tables for those
|
|
20
|
+
literals. (The Flow-stepper rule, on a fourth surface.)
|
|
21
|
+
|
|
22
|
+
v1.4.1 — THE BOARD IS THE ONLY PLACE ANYTHING IS APPLIED, AND EVERY WRITE'S
|
|
23
|
+
POSITION IS COMPUTED WHEN THAT WRITE RUNS. The list of parts used to carry
|
|
24
|
+
three `+ line N` buttons per row, so the act of building the bar lived a long
|
|
25
|
+
way from the bar — and each of those buttons computed a position from the
|
|
26
|
+
SAVED layout rather than from the layout being staged. Three staged adds all
|
|
27
|
+
landed on position 1, and the CLI read the second and third as EDITS of the
|
|
28
|
+
first: that is why adding three parts produced one. Structural edits are now
|
|
29
|
+
semantic ops against stable refs, replayed by `hkPlan` in order, and every
|
|
30
|
+
position is derived at the moment that op will execute. The list below the
|
|
31
|
+
board is a REFERENCE now: it says what each part shows, and it applies
|
|
32
|
+
nothing. */
|
|
25
33
|
|
|
26
34
|
/* -------------------------------------------------------------- HOOK UI */
|
|
27
35
|
|
|
28
36
|
// The preview widths the panel offers. Not a fact about the layout — a fact
|
|
29
37
|
// about terminals people actually use — so it lives here rather than coming
|
|
30
|
-
// from the CLI.
|
|
38
|
+
// from the CLI. They are COLUMN COUNTS: how many character cells wide the
|
|
39
|
+
// terminal is. The card says so out loud, because three bare numbers over a
|
|
40
|
+
// picture is a control nobody can guess the meaning of.
|
|
31
41
|
const HK_WIDTHS = [80, 120, 160];
|
|
32
42
|
let HK_WIDTH = 120;
|
|
33
43
|
let HK_STATE = "healthy";
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
// The palette's search box and its open group, both surviving a re-render for
|
|
37
|
-
// the same reason.
|
|
44
|
+
// The reference list's search box and its open group, both surviving a
|
|
45
|
+
// re-render so a paint does not close a list the user just opened.
|
|
38
46
|
let HK_SEARCH = "";
|
|
39
47
|
let HK_GROUP = null;
|
|
40
48
|
// WHICH BOARD. The main status line, or the row Claude Code draws per subagent
|
|
@@ -43,54 +51,168 @@ let HK_GROUP = null;
|
|
|
43
51
|
// a re-render does not put you back on the other one.
|
|
44
52
|
let HK_BOARD = "status";
|
|
45
53
|
|
|
54
|
+
// THE STAGED OPS, in the order they were made. NOT a map of finished HTTP
|
|
55
|
+
// bodies: a body carries a POSITION, and a position computed when the op was
|
|
56
|
+
// staged is wrong the moment an earlier op shifts the line under it. These are
|
|
57
|
+
// semantic ops against stable refs; `hkPlan` turns them into writes — and
|
|
58
|
+
// recomputes every position — each time the board paints.
|
|
59
|
+
let HK_OPS = [];
|
|
60
|
+
// The chip being dragged. Transient: cleared on every drop and every paint.
|
|
61
|
+
let HK_DRAG = null;
|
|
62
|
+
|
|
63
|
+
// DRAG IS OFF ON A NARROW SCREEN, and off for real rather than merely restyled:
|
|
64
|
+
// a 40-pixel drop gap on a phone is not a target, so the three buttons on every
|
|
65
|
+
// chip are the path there. The query matches 06-responsive.css's own breakpoint
|
|
66
|
+
// — a chip that still says `draggable` while its handle is hidden is a promise
|
|
67
|
+
// the page cannot keep.
|
|
68
|
+
function hkCanDrag() {
|
|
69
|
+
try {
|
|
70
|
+
return !window.matchMedia("(max-width: 600px)").matches;
|
|
71
|
+
} catch (_) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
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
|
+
|
|
46
99
|
PANELS.hookui = function (host) {
|
|
47
100
|
head(host, t("hookui.title"), t("hookui.sub"));
|
|
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
|
+
});
|
|
114
|
+
};
|
|
48
115
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
Promise.all([
|
|
53
|
-
read("/api/statusline/show?board=" + HK_BOARD).then((r) => r.data),
|
|
54
|
-
read("/api/statusline/components?board=" + HK_BOARD).then((r) => r.data),
|
|
55
|
-
read("/api/statusline/presets?board=" + HK_BOARD).then((r) => r.data),
|
|
56
|
-
read("/api/statusline/preview?board=" + HK_BOARD + "&width=" + HK_WIDTH + "&state=" + HK_STATE).then((r) => r.data),
|
|
57
|
-
]),
|
|
58
|
-
([show, cat, presets, prev]) => {
|
|
59
|
-
const out = frag();
|
|
60
|
-
if (!show || !cat) {
|
|
61
|
-
out.append(empty(t("hookui.noData")));
|
|
62
|
-
return out;
|
|
63
|
-
}
|
|
64
|
-
const edits = editSet(() => bar.paint());
|
|
65
|
-
const bar = editBar(edits, {
|
|
66
|
-
onApply: async (b) => {
|
|
67
|
-
await applyActions(edits, b);
|
|
68
|
-
edits.clear();
|
|
69
|
-
rerender();
|
|
70
|
-
},
|
|
71
|
-
onReset: () => confirmReset(),
|
|
72
|
-
onCancel: () => {
|
|
73
|
-
edits.clear();
|
|
74
|
-
rerender();
|
|
75
|
-
},
|
|
76
|
-
resetLabel: t("hookui.resetLayout"),
|
|
77
|
-
});
|
|
116
|
+
function hkPreviewUrl() {
|
|
117
|
+
return "/api/statusline/preview?board=" + HK_BOARD + "&width=" + HK_WIDTH + "&state=" + HK_STATE;
|
|
118
|
+
}
|
|
78
119
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
);
|
|
93
|
-
|
|
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
|
+
}
|
|
94
216
|
|
|
95
217
|
// EVERY write carries the board it is about. Forgetting it on one route would
|
|
96
218
|
// edit the other board's layout, which is the one mistake here that is silent.
|
|
@@ -98,6 +220,126 @@ function bd(body) {
|
|
|
98
220
|
return Object.assign({ board: HK_BOARD }, body);
|
|
99
221
|
}
|
|
100
222
|
|
|
223
|
+
/* ══ the staged plan ══════════════════════════════════════════════════════
|
|
224
|
+
Replays every staged op onto a working copy of the saved layout and returns
|
|
225
|
+
BOTH the effective board and the ordered writes that produce it. ONE
|
|
226
|
+
function, because a second idea of where a part will land is exactly the bug
|
|
227
|
+
this replaces: the board drew one arrangement and the writes produced
|
|
228
|
+
another, silently. */
|
|
229
|
+
|
|
230
|
+
// Ops are staged by REF, never by position. A ref is either an item id the CLI
|
|
231
|
+
// already gave us, or `new:<n>` for something staged in this session.
|
|
232
|
+
function hkOp(op) {
|
|
233
|
+
// A second edit to the same field of the same part REPLACES the first — one
|
|
234
|
+
// decision typed twice is one decision, and the pending list has to read as a
|
|
235
|
+
// list of decisions rather than a list of keystrokes.
|
|
236
|
+
if (op.op === "set") HK_OPS = HK_OPS.filter((o) => !(o.op === "set" && o.ref === op.ref && o.field === op.field));
|
|
237
|
+
if (op.op === "sep") HK_OPS = HK_OPS.filter((o) => !(o.op === "sep" && o.line === op.line));
|
|
238
|
+
if (op.op === "doc") HK_OPS = HK_OPS.filter((o) => !(o.op === "doc" && o.field === op.field));
|
|
239
|
+
HK_OPS.push(op);
|
|
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();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function hkPlan(show, cat) {
|
|
247
|
+
const structural = new Set((cat.components || []).filter((c) => c.structural).map((c) => c.id));
|
|
248
|
+
const lines = (show.lines || []).map((l) => ({
|
|
249
|
+
line: l.line,
|
|
250
|
+
separator: l.separator,
|
|
251
|
+
items: (l.items || []).map((it) => ({ ref: it.id, type: it.type, src: it, over: {}, isNew: false })),
|
|
252
|
+
}));
|
|
253
|
+
const actions = [];
|
|
254
|
+
const doc = { theme: show.theme, glyphs: show.glyphs };
|
|
255
|
+
const find = (ref) => {
|
|
256
|
+
for (const l of lines) {
|
|
257
|
+
const i = l.items.findIndex((x) => x.ref === ref);
|
|
258
|
+
if (i >= 0) return { l, i, it: l.items[i] };
|
|
259
|
+
}
|
|
260
|
+
return null;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
HK_OPS.forEach((op, oi) => {
|
|
264
|
+
const key = "op" + (oi + 1);
|
|
265
|
+
if (op.op === "add") {
|
|
266
|
+
const l = lines[op.line - 1];
|
|
267
|
+
if (!l) return;
|
|
268
|
+
// THE POSITION IS COMPUTED HERE, not when the button was pressed. Three
|
|
269
|
+
// adds in a row get 1, 2 and 3 and append; three adds computed at stage
|
|
270
|
+
// time all got 1, and the CLI read 2 and 3 as EDITS of the first.
|
|
271
|
+
const pos = l.items.length + 1;
|
|
272
|
+
l.items.push({ ref: "new:" + oi, type: op.type, src: null, over: {}, isNew: true });
|
|
273
|
+
actions.push({ key, route: "/api/statusline/set", body: bd({ line: op.line, pos, type: op.type }), label: t("hookui.opAdd", { id: op.type, n: op.line }) });
|
|
274
|
+
} else if (op.op === "remove") {
|
|
275
|
+
const f = find(op.ref);
|
|
276
|
+
if (!f) return;
|
|
277
|
+
actions.push({ key, route: "/api/statusline/remove", body: bd({ at: f.l.line + ":" + (f.i + 1) }), label: t("hookui.opRemove", { id: f.it.type }) });
|
|
278
|
+
f.l.items.splice(f.i, 1);
|
|
279
|
+
} else if (op.op === "move") {
|
|
280
|
+
const f = find(op.ref);
|
|
281
|
+
if (!f) return;
|
|
282
|
+
const from = f.l.line + ":" + (f.i + 1);
|
|
283
|
+
f.l.items.splice(f.i, 1);
|
|
284
|
+
const dst = lines[op.toLine - 1];
|
|
285
|
+
if (!dst) return;
|
|
286
|
+
const at = Math.max(0, Math.min(op.toPos - 1, dst.items.length));
|
|
287
|
+
dst.items.splice(at, 0, f.it);
|
|
288
|
+
actions.push({ key, route: "/api/statusline/move", body: bd({ from, to: dst.line + ":" + (at + 1) }), label: t("hookui.opMove", { id: f.it.type, n: dst.line }) });
|
|
289
|
+
} else if (op.op === "clone") {
|
|
290
|
+
const f = find(op.ref);
|
|
291
|
+
if (!f) return;
|
|
292
|
+
actions.push({ key, route: "/api/statusline/clone", body: bd({ at: f.l.line + ":" + (f.i + 1) }), label: t("hookui.opClone", { id: f.it.type }) });
|
|
293
|
+
f.l.items.splice(f.i + 1, 0, { ref: "new:" + oi, type: f.it.type, src: f.it.src, over: Object.assign({}, f.it.over), isNew: true });
|
|
294
|
+
} else if (op.op === "set") {
|
|
295
|
+
const f = find(op.ref);
|
|
296
|
+
if (!f) return;
|
|
297
|
+
f.it.over[op.field] = op.value;
|
|
298
|
+
if (op.field === "type") f.it.type = op.value;
|
|
299
|
+
actions.push({ key, route: "/api/statusline/set", body: bd(Object.assign({ line: f.l.line, pos: f.i + 1 }, { [op.field]: op.value })), label: op.label });
|
|
300
|
+
} else if (op.op === "sep") {
|
|
301
|
+
const l = lines[op.line - 1];
|
|
302
|
+
if (!l) return;
|
|
303
|
+
l.separator = op.value;
|
|
304
|
+
actions.push({ key, route: "/api/statusline/line", body: bd({ line: op.line, separator: op.value }), label: op.label });
|
|
305
|
+
} else if (op.op === "doc") {
|
|
306
|
+
doc[op.field] = op.value;
|
|
307
|
+
actions.push({ key, route: "/api/statusline/doc", body: bd({ [op.field]: op.value }), label: op.label });
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
for (const l of lines) {
|
|
312
|
+
// A spacer is not a thing the line SAYS, so it does not eat one of the
|
|
313
|
+
// five. Which ids those are is the CLI's answer, carried per component.
|
|
314
|
+
l.counted = l.items.filter((x) => !structural.has(x.type)).length;
|
|
315
|
+
l.full = l.counted >= cat.max_per_line;
|
|
316
|
+
}
|
|
317
|
+
return { lines, actions, doc, dirty: HK_OPS.length > 0, max: cat.max_per_line };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// The dense-prefix rule, applied to ONE line of the EFFECTIVE board — never the
|
|
321
|
+
// saved one, or a part staged onto line 1 would leave line 2 still refusing.
|
|
322
|
+
// The reason travels with the answer, because a disabled zone with no reason is
|
|
323
|
+
// a shrug.
|
|
324
|
+
function lineLegal(plan, n) {
|
|
325
|
+
if (n === 1) return { ok: true };
|
|
326
|
+
const above = plan.lines[n - 2];
|
|
327
|
+
if (above && above.items.length > 0) return { ok: true };
|
|
328
|
+
return { ok: false, why: t("hookui.fillFirst", { n: n - 1 }) };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Why a part cannot go on this line RIGHT NOW; a null answer means it can. Both
|
|
332
|
+
// reasons are the user's to read BEFORE the click, never after it.
|
|
333
|
+
function addBlocked(plan, n) {
|
|
334
|
+
const legal = lineLegal(plan, n);
|
|
335
|
+
if (!legal.ok) return legal.why;
|
|
336
|
+
const line = plan.lines[n - 1];
|
|
337
|
+
if (!line) return t("hookui.noSuchLine");
|
|
338
|
+
if (line.full) return t("hookui.lineFull", { n: n, max: plan.max });
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/* ══ the board tabs ═══════════════════════════════════════════════════════ */
|
|
101
343
|
// The two boards. Named by the CLI (`boards` in the catalogue), labelled by the
|
|
102
344
|
// panel — the id is data and the label is prose, which is the split everywhere
|
|
103
345
|
// else in this file too.
|
|
@@ -116,12 +358,14 @@ function boardTabs(cat) {
|
|
|
116
358
|
btn.type = "button";
|
|
117
359
|
btn.addEventListener("click", () => {
|
|
118
360
|
if (HK_BOARD === b) return;
|
|
361
|
+
// Switching boards with ops staged would send them to a board they were
|
|
362
|
+
// never about: every op carries a board, but the POSITIONS in them were
|
|
363
|
+
// computed against this one.
|
|
364
|
+
if (HK_OPS.length) return toast(t("hookui.applyFirst"), "bad");
|
|
119
365
|
HK_BOARD = b;
|
|
120
|
-
// Opening the other board with a chip's editor still open would open a
|
|
121
|
-
// drawer for an item that is not there.
|
|
122
|
-
HK_OPEN = null;
|
|
123
366
|
HK_SEARCH = "";
|
|
124
|
-
|
|
367
|
+
HK_COMPARE_DATA = null;
|
|
368
|
+
hkReload();
|
|
125
369
|
});
|
|
126
370
|
row.append(btn);
|
|
127
371
|
}
|
|
@@ -130,17 +374,11 @@ function boardTabs(cat) {
|
|
|
130
374
|
return c;
|
|
131
375
|
}
|
|
132
376
|
|
|
133
|
-
|
|
134
|
-
const h = location.hash;
|
|
135
|
-
location.hash = "#/";
|
|
136
|
-
location.hash = h || "#/hookui";
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// ── the gate ───────────────────────────────────────────────────────────────
|
|
377
|
+
/* ══ the gate ═════════════════════════════════════════════════════════════ */
|
|
140
378
|
// INVERTED from the Extra panel's. There, nothing exists until you connect;
|
|
141
379
|
// here the board is LIVE WHILE OFF, because you must be able to compose before
|
|
142
380
|
// you arm. What is gated is the switch, not the work.
|
|
143
|
-
function gateCard(show, cat
|
|
381
|
+
function gateCard(show, cat) {
|
|
144
382
|
const c = card(t("hookui.gate"));
|
|
145
383
|
const row = el("div", "row-actions");
|
|
146
384
|
row.append(chip(show.enabled ? t("hookui.on") : t("hookui.off"), show.enabled ? "ok" : null));
|
|
@@ -164,7 +402,7 @@ function gateCard(show, cat, edits) {
|
|
|
164
402
|
// switch belongs to which board.
|
|
165
403
|
const r = await post("/api/config/set", { key: cat.config_key, value: show.enabled ? "off" : "on" });
|
|
166
404
|
if (!r.ok) toast(t("hookui.switchFailed"), "bad", (r.output || "").trim());
|
|
167
|
-
|
|
405
|
+
hkReload();
|
|
168
406
|
});
|
|
169
407
|
const acts = el("div", "row-actions");
|
|
170
408
|
acts.append(b);
|
|
@@ -172,10 +410,10 @@ function gateCard(show, cat, edits) {
|
|
|
172
410
|
return c;
|
|
173
411
|
}
|
|
174
412
|
|
|
175
|
-
|
|
413
|
+
/* ══ cautions ═════════════════════════════════════════════════════════════ */
|
|
176
414
|
// An error is a refusal; a warning is a fact the user then owns. They are drawn
|
|
177
415
|
// differently and they are never merged into one count.
|
|
178
|
-
function cautionCard(show
|
|
416
|
+
function cautionCard(show) {
|
|
179
417
|
const c = card(t("hookui.cautions"));
|
|
180
418
|
for (const e of show.errors) {
|
|
181
419
|
const row = el("div", "hk-caution hk-caution-bad");
|
|
@@ -192,63 +430,211 @@ function cautionCard(show, edits) {
|
|
|
192
430
|
return c;
|
|
193
431
|
}
|
|
194
432
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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) {
|
|
199
458
|
const right = el("div", "row-actions");
|
|
200
459
|
for (const w of HK_WIDTHS) {
|
|
201
|
-
const b = el("button", "btn btn-xs" + (HK_WIDTH === w ? " btn-primary" : " btn-ghost"),
|
|
460
|
+
const b = el("button", "btn btn-xs" + (HK_WIDTH === w ? " btn-primary" : " btn-ghost"), tn(w, "hookui.cols"));
|
|
202
461
|
b.type = "button";
|
|
203
462
|
b.addEventListener("click", () => {
|
|
204
463
|
HK_WIDTH = w;
|
|
205
|
-
|
|
464
|
+
HK_COMPARE_DATA = null;
|
|
465
|
+
hkPreviewReload();
|
|
206
466
|
});
|
|
207
467
|
right.append(b);
|
|
208
468
|
}
|
|
209
469
|
const c = card(t("hookui.preview"), right);
|
|
470
|
+
c.append(el("div", "note", t("hookui.previewAbout")));
|
|
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")));
|
|
210
474
|
if (!prev || !prev.ok) {
|
|
211
475
|
c.append(el("div", "note", t("hookui.previewUnavailable")));
|
|
212
476
|
return c;
|
|
213
477
|
}
|
|
214
478
|
|
|
215
479
|
// The fixture picker. `--fixtures` must carry one of every state INCLUDING
|
|
216
|
-
// the ugly ones
|
|
480
|
+
// the ugly ones - you cannot design a degraded line on a healthy session.
|
|
481
|
+
c.append(el("div", "note", t("hookui.fixturesAbout")));
|
|
217
482
|
const states = el("div", "row-actions hk-fixtures");
|
|
218
483
|
for (const [k, label] of Object.entries(prev.fixtures || {})) {
|
|
219
484
|
const b = el("button", "btn btn-xs" + (HK_STATE === k ? " btn-primary" : " btn-ghost"), label);
|
|
220
485
|
b.type = "button";
|
|
221
486
|
b.addEventListener("click", () => {
|
|
222
487
|
HK_STATE = k;
|
|
223
|
-
|
|
488
|
+
HK_COMPARE_DATA = null;
|
|
489
|
+
hkPreviewReload();
|
|
224
490
|
});
|
|
225
491
|
states.append(b);
|
|
226
492
|
}
|
|
227
493
|
c.append(states);
|
|
228
494
|
|
|
229
|
-
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
|
+
}
|
|
230
588
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
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
|
+
}
|
|
605
|
+
|
|
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")));
|
|
236
612
|
for (const [key, label] of [
|
|
237
613
|
["no_color", t("hookui.degNoColor")],
|
|
238
614
|
["ascii", t("hookui.degAscii")],
|
|
239
615
|
["no_motion", t("hookui.degNoMotion")],
|
|
240
616
|
]) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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));
|
|
245
625
|
}
|
|
246
|
-
|
|
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
|
+
}
|
|
247
631
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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;
|
|
252
638
|
}
|
|
253
639
|
|
|
254
640
|
// ANSI → DOM. Never innerHTML: this string comes from the CLI, and the rule for
|
|
@@ -262,13 +648,20 @@ function ansiBlock(text, cls) {
|
|
|
262
648
|
// compiler can emit. Anything it does not recognise is DROPPED rather than
|
|
263
649
|
// printed, because a stray escape in the middle of a preview is worse than
|
|
264
650
|
// a missing colour.
|
|
265
|
-
const re =
|
|
651
|
+
const re = /\[([0-9;]*)m/g;
|
|
266
652
|
let last = 0;
|
|
267
653
|
let m;
|
|
268
654
|
const push = (s) => {
|
|
269
655
|
if (!s) return;
|
|
270
656
|
const span = el("span", null, s);
|
|
271
|
-
|
|
657
|
+
// THROUGH CSSOM, NEVER A `style` ATTRIBUTE. This page is served under
|
|
658
|
+
// `style-src 'self'`, which blocks a parsed style attribute outright —
|
|
659
|
+
// so every colour this preview drew was thrown away by the browser and
|
|
660
|
+
// the whole thing rendered grey, with the reason only in a console
|
|
661
|
+
// nobody had open. Assigning the property is not a parse and is not
|
|
662
|
+
// blocked, and weakening the policy to `unsafe-inline` for a preview is
|
|
663
|
+
// not a trade worth making.
|
|
664
|
+
if (style) for (const k of Object.keys(style)) span.style[k] = style[k];
|
|
272
665
|
row.append(span);
|
|
273
666
|
};
|
|
274
667
|
while ((m = re.exec(line))) {
|
|
@@ -277,7 +670,7 @@ function ansiBlock(text, cls) {
|
|
|
277
670
|
last = m.index + m[0].length;
|
|
278
671
|
}
|
|
279
672
|
push(line.slice(last));
|
|
280
|
-
if (!row.childNodes.length) row.append(document.createTextNode("
|
|
673
|
+
if (!row.childNodes.length) row.append(document.createTextNode(" "));
|
|
281
674
|
pre.append(row);
|
|
282
675
|
}
|
|
283
676
|
return pre;
|
|
@@ -285,7 +678,7 @@ function ansiBlock(text, cls) {
|
|
|
285
678
|
|
|
286
679
|
// The 16 ANSI slots, as the browser's own colours. These are NOT ORC's colour
|
|
287
680
|
// tokens — they are what a terminal would paint, approximated, because the
|
|
288
|
-
// panel cannot know the user's
|
|
681
|
+
// panel cannot know the user's terminal palette and says so.
|
|
289
682
|
const HK_ANSI = [
|
|
290
683
|
"#000000", "#cc0000", "#4e9a06", "#c4a000", "#3465a4", "#75507b", "#06989a", "#d3d7cf",
|
|
291
684
|
];
|
|
@@ -293,226 +686,274 @@ const HK_ANSI_BRIGHT = [
|
|
|
293
686
|
"#555753", "#ef2929", "#8ae234", "#fce94f", "#729fcf", "#ad7fa8", "#34e2e2", "#eeeeec",
|
|
294
687
|
];
|
|
295
688
|
|
|
689
|
+
// An OBJECT of CSS properties rather than a declaration string, because the
|
|
690
|
+
// caller assigns them one at a time through CSSOM — see `push` above.
|
|
296
691
|
function sgrStyle(codes, prev) {
|
|
297
692
|
const parts = String(codes || "0").split(";").filter((x) => x !== "");
|
|
298
693
|
if (!parts.length || parts[0] === "0") return null;
|
|
299
|
-
const out =
|
|
300
|
-
if (prev) out.push(prev);
|
|
694
|
+
const out = Object.assign({}, prev || {});
|
|
301
695
|
for (let i = 0; i < parts.length; i++) {
|
|
302
696
|
const n = Number(parts[i]);
|
|
303
|
-
if (n === 1) out.
|
|
304
|
-
else if (n === 2) out.
|
|
305
|
-
else if (n === 3) out.
|
|
306
|
-
else if (n === 4) out.
|
|
307
|
-
else if (n === 9) out.
|
|
308
|
-
else if (n === 7) out.
|
|
309
|
-
else if (n >= 30 && n <= 37) out.
|
|
310
|
-
else if (n >= 90 && n <= 97) out.
|
|
311
|
-
else if (n >= 40 && n <= 47) out.
|
|
312
|
-
else if (n === 39) out.
|
|
697
|
+
if (n === 1) out.fontWeight = "700";
|
|
698
|
+
else if (n === 2) out.opacity = ".6";
|
|
699
|
+
else if (n === 3) out.fontStyle = "italic";
|
|
700
|
+
else if (n === 4) out.textDecoration = "underline";
|
|
701
|
+
else if (n === 9) out.textDecoration = "line-through";
|
|
702
|
+
else if (n === 7) out.filter = "invert(1)";
|
|
703
|
+
else if (n >= 30 && n <= 37) out.color = HK_ANSI[n - 30];
|
|
704
|
+
else if (n >= 90 && n <= 97) out.color = HK_ANSI_BRIGHT[n - 90];
|
|
705
|
+
else if (n >= 40 && n <= 47) out.background = HK_ANSI[n - 40];
|
|
706
|
+
else if (n === 39) out.color = "inherit";
|
|
313
707
|
else if (n === 38 && parts[i + 1] === "2") {
|
|
314
|
-
out.
|
|
708
|
+
out.color = "rgb(" + parts[i + 2] + "," + parts[i + 3] + "," + parts[i + 4] + ")";
|
|
315
709
|
i += 4;
|
|
316
710
|
} else if (n === 48 && parts[i + 1] === "2") {
|
|
317
|
-
out.
|
|
711
|
+
out.background = "rgb(" + parts[i + 2] + "," + parts[i + 3] + "," + parts[i + 4] + ")";
|
|
318
712
|
i += 4;
|
|
319
713
|
}
|
|
320
714
|
}
|
|
321
|
-
return out
|
|
715
|
+
return out;
|
|
322
716
|
}
|
|
323
717
|
|
|
324
|
-
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
// be told off for it.
|
|
718
|
+
/* ══ the board ════════════════════════════════════════════════════════════ */
|
|
719
|
+
// THE ILLEGAL DROP IS MADE IMPOSSIBLE rather than allowed and then complained
|
|
720
|
+
// about: an illegal zone renders disabled with its reason on the zone itself. A
|
|
721
|
+
// user should never be able to do the wrong thing and then be told off for it.
|
|
329
722
|
//
|
|
330
723
|
// The CLI still validates. The board is a convenience; it is never the
|
|
331
724
|
// guarantee.
|
|
332
|
-
function boardCard(
|
|
725
|
+
function boardCard(cat, plan) {
|
|
333
726
|
const c = card(t("hookui.board"));
|
|
727
|
+
c.append(el("div", "note", t("hookui.boardAbout")));
|
|
334
728
|
c.append(el("div", "note", cat.dense_prefix));
|
|
335
729
|
|
|
336
|
-
for (const line of
|
|
337
|
-
const legal = lineLegal(
|
|
338
|
-
const wrap = el("div", "hk-line-wrap" + (legal ? "" : " hk-line-blocked"));
|
|
730
|
+
for (const line of plan.lines) {
|
|
731
|
+
const legal = lineLegal(plan, line.line);
|
|
732
|
+
const wrap = el("div", "hk-line-wrap" + (legal.ok ? "" : " hk-line-blocked"));
|
|
733
|
+
|
|
339
734
|
const h = el("div", "hk-line-head");
|
|
340
|
-
|
|
341
|
-
|
|
735
|
+
const title = el("div", "hk-line-title");
|
|
736
|
+
title.append(el("span", "hk-line-name", tn(line.line, "hookui.lineN")));
|
|
737
|
+
title.append(el("span", "hk-line-count" + (line.full ? " hk-full" : ""), tn(line.counted, "hookui.slotsUsed", { max: plan.max })));
|
|
738
|
+
h.append(title);
|
|
739
|
+
h.append(separatorPicker(cat, line));
|
|
342
740
|
wrap.append(h);
|
|
343
741
|
|
|
344
742
|
const zone = el("div", "hk-zone");
|
|
345
743
|
zone.setAttribute("role", "list");
|
|
346
|
-
for (const item of line.items) zone.append(chipEl(item, line,
|
|
744
|
+
for (const item of line.items) zone.append(chipEl(item, line, plan, cat));
|
|
347
745
|
|
|
348
746
|
if (!legal.ok) {
|
|
349
747
|
const blocked = el("div", "hk-blocked");
|
|
350
748
|
blocked.append(el("span", "hk-caution-mark", "✕"));
|
|
351
749
|
blocked.append(el("span", null, legal.why));
|
|
352
750
|
zone.append(blocked);
|
|
353
|
-
} else if (!line.full) {
|
|
354
|
-
const add = el("button", "hk-add", t("hookui.add"));
|
|
355
|
-
add.type = "button";
|
|
356
|
-
add.addEventListener("click", () => {
|
|
357
|
-
HK_GROUP = HK_GROUP || Object.keys(cat.groups)[0];
|
|
358
|
-
const p = document.querySelector(".hk-palette-search");
|
|
359
|
-
if (p) p.focus();
|
|
360
|
-
});
|
|
361
|
-
zone.append(add);
|
|
362
751
|
}
|
|
752
|
+
// THE ADD BUTTON ALWAYS RENDERS, and carries its own refusal. A control
|
|
753
|
+
// that vanishes when it cannot be used teaches nobody why — and the two
|
|
754
|
+
// refusals are DIFFERENT facts (nothing above it yet · this line is full),
|
|
755
|
+
// so they are two sentences and never one "invalid".
|
|
756
|
+
const b = el("button", "hk-add", t("hookui.add"));
|
|
757
|
+
b.type = "button";
|
|
758
|
+
if (!legal.ok) {
|
|
759
|
+
b.disabled = true;
|
|
760
|
+
b.title = legal.why;
|
|
761
|
+
} else if (line.full) {
|
|
762
|
+
b.disabled = true;
|
|
763
|
+
b.title = t("hookui.lineFull", { n: line.line, max: plan.max });
|
|
764
|
+
} else {
|
|
765
|
+
b.addEventListener("click", () => partPicker(cat, plan, line.line, null));
|
|
766
|
+
}
|
|
767
|
+
zone.append(b);
|
|
768
|
+
if (b.disabled && legal.ok) zone.append(el("div", "note note-warn hk-zone-why", b.title));
|
|
769
|
+
dropTarget(zone, line, plan);
|
|
363
770
|
wrap.append(zone);
|
|
364
|
-
|
|
365
|
-
// Per-line options. The separator is what gives a line its rhythm, and it
|
|
366
|
-
// is the one line-level control worth surfacing on the board itself.
|
|
367
|
-
const opts = el("div", "hk-line-opts");
|
|
368
|
-
const sep = el("input", "hk-sep");
|
|
369
|
-
sep.type = "text";
|
|
370
|
-
sep.value = line.separator;
|
|
371
|
-
sep.setAttribute("aria-label", t("hookui.separator"));
|
|
372
|
-
sep.addEventListener("change", () => {
|
|
373
|
-
edits.action("line" + line.line, "/api/statusline/line",
|
|
374
|
-
bd({ line: line.line, separator: sep.value }),
|
|
375
|
-
t("hookui.sepChange", { n: line.line, s: sep.value }));
|
|
376
|
-
});
|
|
377
|
-
opts.append(el("span", "note", t("hookui.separator")), sep);
|
|
378
|
-
wrap.append(opts);
|
|
379
771
|
c.append(wrap);
|
|
380
772
|
}
|
|
773
|
+
c.append(el("div", "note", t("hookui.dragHint")));
|
|
381
774
|
return c;
|
|
382
775
|
}
|
|
383
776
|
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
777
|
+
// THE SEPARATOR IS A DROPDOWN, and its options are the CLI's. A free-text box
|
|
778
|
+
// asked a person to invent a rhythm and then to type a character that may not
|
|
779
|
+
// be on their keyboard. A value the layout already carries that is NOT in the
|
|
780
|
+
// set keeps its slot, leads the list and is disabled — the `fixed_executor`
|
|
781
|
+
// rule: the state must be visible, never re-offerable.
|
|
782
|
+
function separatorPicker(cat, line) {
|
|
783
|
+
const wrap = el("label", "hk-line-opts");
|
|
784
|
+
wrap.append(el("span", "note", t("hookui.separator")));
|
|
785
|
+
const sel = el("select", "hk-select");
|
|
786
|
+
const known = cat.separators || [];
|
|
787
|
+
if (!known.some((s) => s.value === line.separator)) {
|
|
788
|
+
const o = el("option", null, t("hookui.sepCustom", { s: JSON.stringify(line.separator) }));
|
|
789
|
+
o.value = line.separator;
|
|
790
|
+
o.disabled = true;
|
|
791
|
+
o.selected = true;
|
|
792
|
+
sel.append(o);
|
|
793
|
+
}
|
|
794
|
+
for (const s of known) {
|
|
795
|
+
// The NAME is the CLI's word; the sample is the literal string, shown
|
|
796
|
+
// between two marks so a run of spaces is visible at all.
|
|
797
|
+
const o = el("option", null, s.name + " ‹" + s.value + "›");
|
|
798
|
+
o.value = s.value;
|
|
799
|
+
if (s.value === line.separator) o.selected = true;
|
|
800
|
+
sel.append(o);
|
|
801
|
+
}
|
|
802
|
+
sel.addEventListener("change", () => {
|
|
803
|
+
const hit = known.find((s) => s.value === sel.value);
|
|
804
|
+
hkOp({ op: "sep", line: line.line, value: sel.value, label: t("hookui.opSep", { n: line.line, s: hit ? hit.name : sel.value }) });
|
|
805
|
+
});
|
|
806
|
+
wrap.append(sel);
|
|
807
|
+
return wrap;
|
|
391
808
|
}
|
|
392
809
|
|
|
810
|
+
/* ══ a chip ═══════════════════════════════════════════════════════════════ */
|
|
393
811
|
// A chip shows ITS OWN RENDERED OUTPUT, not its component id — that is what the
|
|
394
|
-
// user is arranging
|
|
395
|
-
|
|
812
|
+
// user is arranging — with the id and a one-line description under it, because
|
|
813
|
+
// "what is this thing" is the first question anybody asks of a bar full of
|
|
814
|
+
// symbols, and the answer used to live only in a tooltip.
|
|
815
|
+
function chipEl(item, line, plan, cat) {
|
|
396
816
|
const comp = (cat.components || []).find((x) => x.id === item.type);
|
|
397
|
-
const wrap = el("div", "hk-chip" + (
|
|
817
|
+
const wrap = el("div", "hk-chip" + (item.isNew ? " hk-chip-new" : ""));
|
|
398
818
|
wrap.setAttribute("role", "listitem");
|
|
399
819
|
wrap.setAttribute("tabindex", "0");
|
|
400
|
-
wrap.
|
|
820
|
+
wrap.draggable = hkCanDrag();
|
|
821
|
+
wrap.title = comp && comp.summary ? comp.summary : item.type;
|
|
401
822
|
|
|
402
823
|
const face = el("div", "hk-chip-face");
|
|
403
|
-
|
|
404
|
-
face.append(ansiBlock(
|
|
824
|
+
face.append(el("span", "hk-grip", "∷"));
|
|
825
|
+
face.append(ansiBlock(chipSample(item, comp), "hk-preview hk-preview-chip"));
|
|
405
826
|
wrap.append(face);
|
|
406
827
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
rm.type = "button";
|
|
418
|
-
rm.title = t("hookui.remove");
|
|
419
|
-
rm.addEventListener("click", () => {
|
|
420
|
-
edits.action("rm" + item.id, "/api/statusline/remove",
|
|
421
|
-
bd({ at: line.line + ":" + item.pos }),
|
|
422
|
-
t("hookui.removed", { id: item.type }));
|
|
423
|
-
});
|
|
424
|
-
foot.append(edit, rm);
|
|
425
|
-
wrap.append(foot);
|
|
426
|
-
|
|
427
|
-
// THE NON-DRAG PATH, and it is not an afterthought: pointer drag is unusable
|
|
428
|
-
// for a real fraction of people, so every move a drag can make is on this
|
|
429
|
-
// menu and on the keyboard.
|
|
430
|
-
const menu = el("div", "hk-move");
|
|
431
|
-
for (const target of [1, 2, 3]) {
|
|
432
|
-
if (target === line.line) continue;
|
|
433
|
-
const legal = lineLegal(show, target);
|
|
434
|
-
const b = el("button", "btn btn-xs btn-ghost", tn(target, "hookui.toLine"));
|
|
435
|
-
b.type = "button";
|
|
436
|
-
if (!legal.ok) {
|
|
437
|
-
b.disabled = true;
|
|
438
|
-
b.title = legal.why;
|
|
439
|
-
} else {
|
|
440
|
-
b.addEventListener("click", () => moveTo(item, line, target, 1, edits));
|
|
441
|
-
}
|
|
442
|
-
menu.append(b);
|
|
443
|
-
}
|
|
444
|
-
if (item.pos > 1) {
|
|
445
|
-
const b = el("button", "btn btn-xs btn-ghost", "←");
|
|
446
|
-
b.type = "button";
|
|
447
|
-
b.title = t("hookui.moveLeft");
|
|
448
|
-
b.addEventListener("click", () => moveTo(item, line, line.line, item.pos - 1, edits));
|
|
449
|
-
menu.append(b);
|
|
450
|
-
}
|
|
451
|
-
if (item.pos < line.items.length) {
|
|
452
|
-
const b = el("button", "btn btn-xs btn-ghost", "→");
|
|
828
|
+
wrap.append(el("div", "hk-chip-id", item.type));
|
|
829
|
+
if (comp && comp.summary) wrap.append(el("div", "hk-chip-desc", comp.summary));
|
|
830
|
+
|
|
831
|
+
// EDIT · MOVE · REMOVE, on the chip. Each opens a modal that says what it is
|
|
832
|
+
// about to do. The drag is the shortcut, never the mechanism: pointer drag is
|
|
833
|
+
// unusable for a real fraction of people, and it is switched off entirely
|
|
834
|
+
// below the width at which a drop gap stops being a target.
|
|
835
|
+
const acts = el("div", "hk-chip-acts");
|
|
836
|
+
const mk = (label, cls, fn) => {
|
|
837
|
+
const b = el("button", "hk-act " + cls, label);
|
|
453
838
|
b.type = "button";
|
|
454
|
-
b.
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
cl.type = "button";
|
|
462
|
-
cl.addEventListener("click", () =>
|
|
463
|
-
edits.action("cl" + item.id, "/api/statusline/clone",
|
|
464
|
-
bd({ at: line.line + ":" + item.pos }), t("hookui.cloned", { id: item.type })));
|
|
465
|
-
menu.append(cl);
|
|
466
|
-
// EXPAND turns a composite or a group back into its parts — which is how a
|
|
467
|
-
// three-in-one chip becomes three chips the moment you want to restyle one.
|
|
468
|
-
if ((comp && comp.composite) || item.type === "group") {
|
|
469
|
-
const ex = el("button", "btn btn-xs btn-ghost", t("hookui.expand"));
|
|
470
|
-
ex.type = "button";
|
|
471
|
-
ex.addEventListener("click", () =>
|
|
472
|
-
edits.action("ex" + item.id, "/api/statusline/expand",
|
|
473
|
-
bd({ at: line.line + ":" + item.pos }), t("hookui.expanded", { id: item.type })));
|
|
474
|
-
menu.append(ex);
|
|
475
|
-
}
|
|
476
|
-
wrap.append(menu);
|
|
839
|
+
b.addEventListener("click", fn);
|
|
840
|
+
acts.append(b);
|
|
841
|
+
};
|
|
842
|
+
mk(t("hookui.edit"), "hk-act-edit", () => editModal(item, line, comp, cat));
|
|
843
|
+
mk(t("hookui.move"), "hk-act-move", () => moveModal(item, line, plan));
|
|
844
|
+
mk(t("hookui.remove"), "hk-act-remove", () => removeModal(item, comp));
|
|
845
|
+
wrap.append(acts);
|
|
477
846
|
|
|
478
847
|
// Keyboard: the settled pattern, and every drag has an equivalent here. A
|
|
479
848
|
// board only reachable by mouse is a board a lot of people cannot use.
|
|
480
849
|
wrap.addEventListener("keydown", (ev) => {
|
|
850
|
+
const pos = line.items.indexOf(item) + 1;
|
|
481
851
|
if (ev.key === "Delete" || ev.key === "Backspace") {
|
|
482
852
|
ev.preventDefault();
|
|
483
|
-
|
|
484
|
-
bd({ at: line.line + ":" + item.pos }), t("hookui.removed", { id: item.type }));
|
|
853
|
+
removeModal(item, comp);
|
|
485
854
|
} else if (ev.key === "e" || ev.key === "E") {
|
|
486
855
|
ev.preventDefault();
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
} else if (ev.key === "ArrowLeft" && item.pos > 1) {
|
|
856
|
+
editModal(item, line, comp, cat);
|
|
857
|
+
} else if (ev.key === "ArrowLeft" && pos > 1) {
|
|
490
858
|
ev.preventDefault();
|
|
491
|
-
moveTo(item, line
|
|
492
|
-
} else if (ev.key === "ArrowRight" &&
|
|
859
|
+
moveTo(item, line.line, pos - 1);
|
|
860
|
+
} else if (ev.key === "ArrowRight" && pos < line.items.length) {
|
|
493
861
|
ev.preventDefault();
|
|
494
|
-
moveTo(item, line
|
|
862
|
+
moveTo(item, line.line, pos + 1);
|
|
495
863
|
} else if (ev.key === "ArrowUp" || ev.key === "ArrowDown") {
|
|
496
864
|
const target = line.line + (ev.key === "ArrowUp" ? -1 : 1);
|
|
497
|
-
if (target < 1 || target >
|
|
865
|
+
if (target < 1 || target > plan.lines.length) return;
|
|
498
866
|
ev.preventDefault();
|
|
499
|
-
const
|
|
500
|
-
//
|
|
501
|
-
// silent no-op.
|
|
502
|
-
if (
|
|
503
|
-
moveTo(item,
|
|
867
|
+
const why = addBlocked(plan, target);
|
|
868
|
+
// A line that cannot take it is SKIPPED and the live region says why —
|
|
869
|
+
// never a silent no-op.
|
|
870
|
+
if (why) return announce(why);
|
|
871
|
+
moveTo(item, target, 1);
|
|
504
872
|
}
|
|
505
873
|
});
|
|
506
874
|
|
|
507
|
-
|
|
875
|
+
wrap.addEventListener("dragstart", (ev) => {
|
|
876
|
+
HK_DRAG = { ref: item.ref, from: line.line };
|
|
877
|
+
wrap.classList.add("hk-chip-dragging");
|
|
878
|
+
try {
|
|
879
|
+
ev.dataTransfer.effectAllowed = "move";
|
|
880
|
+
ev.dataTransfer.setData("text/plain", item.ref);
|
|
881
|
+
} catch (_) {}
|
|
882
|
+
});
|
|
883
|
+
wrap.addEventListener("dragend", () => {
|
|
884
|
+
HK_DRAG = null;
|
|
885
|
+
wrap.classList.remove("hk-chip-dragging");
|
|
886
|
+
for (const n of document.querySelectorAll(".hk-drop-before, .hk-drop-after"))
|
|
887
|
+
n.classList.remove("hk-drop-before", "hk-drop-after");
|
|
888
|
+
});
|
|
889
|
+
wrap.addEventListener("dragover", (ev) => {
|
|
890
|
+
if (!HK_DRAG || HK_DRAG.ref === item.ref) return;
|
|
891
|
+
ev.preventDefault();
|
|
892
|
+
const r = wrap.getBoundingClientRect();
|
|
893
|
+
const before = ev.clientX < r.left + r.width / 2;
|
|
894
|
+
wrap.classList.toggle("hk-drop-before", before);
|
|
895
|
+
wrap.classList.toggle("hk-drop-after", !before);
|
|
896
|
+
});
|
|
897
|
+
wrap.addEventListener("dragleave", () => wrap.classList.remove("hk-drop-before", "hk-drop-after"));
|
|
898
|
+
wrap.addEventListener("drop", (ev) => {
|
|
899
|
+
if (!HK_DRAG || HK_DRAG.ref === item.ref) return;
|
|
900
|
+
ev.preventDefault();
|
|
901
|
+
ev.stopPropagation();
|
|
902
|
+
const before = wrap.classList.contains("hk-drop-before");
|
|
903
|
+
const pos = line.items.indexOf(item) + 1;
|
|
904
|
+
dropOnto(line, plan, before ? pos : pos + 1);
|
|
905
|
+
});
|
|
508
906
|
return wrap;
|
|
509
907
|
}
|
|
510
908
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
909
|
+
// What this chip will look like in the bar. A part staged in this session has
|
|
910
|
+
// no rendered sample from the CLI yet, so it borrows its component's default —
|
|
911
|
+
// which is exactly what it will render as until somebody changes its shape.
|
|
912
|
+
function chipSample(item, comp) {
|
|
913
|
+
if (!comp) return item.type;
|
|
914
|
+
const render = item.over.render || (item.src ? item.src.render : (comp.defaults && comp.defaults.render) || comp.renderers[0]);
|
|
915
|
+
const s = comp.previews ? comp.previews[render] : null;
|
|
916
|
+
return s || item.type;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// A zone is a drop target across its WHOLE area, so a drop past the last chip
|
|
920
|
+
// appends instead of doing nothing.
|
|
921
|
+
function dropTarget(zone, line, plan) {
|
|
922
|
+
zone.addEventListener("dragover", (ev) => {
|
|
923
|
+
if (!HK_DRAG) return;
|
|
924
|
+
if (HK_DRAG.from !== line.line && addBlocked(plan, line.line)) return;
|
|
925
|
+
ev.preventDefault();
|
|
926
|
+
zone.classList.add("hk-zone-hot");
|
|
927
|
+
});
|
|
928
|
+
zone.addEventListener("dragleave", () => zone.classList.remove("hk-zone-hot"));
|
|
929
|
+
zone.addEventListener("drop", (ev) => {
|
|
930
|
+
zone.classList.remove("hk-zone-hot");
|
|
931
|
+
if (!HK_DRAG) return;
|
|
932
|
+
ev.preventDefault();
|
|
933
|
+
dropOnto(line, plan, line.items.length + 1);
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function dropOnto(line, plan, pos) {
|
|
938
|
+
if (!HK_DRAG) return;
|
|
939
|
+
const all = plan.lines.reduce((acc, l) => acc.concat(l.items), []);
|
|
940
|
+
const moving = all.find((x) => x.ref === HK_DRAG.ref);
|
|
941
|
+
const from = HK_DRAG.from;
|
|
942
|
+
HK_DRAG = null;
|
|
943
|
+
if (!moving) return;
|
|
944
|
+
// Reordering WITHIN a line never changes how many parts it holds, so the
|
|
945
|
+
// five-slot cap and the dense-prefix rule only apply to a move that crosses
|
|
946
|
+
// lines.
|
|
947
|
+
if (from !== line.line) {
|
|
948
|
+
const why = addBlocked(plan, line.line);
|
|
949
|
+
if (why) return toast(why, "bad");
|
|
950
|
+
}
|
|
951
|
+
moveTo(moving, line.line, pos);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function moveTo(item, toLine, toPos) {
|
|
955
|
+
hkOp({ op: "move", ref: item.ref, toLine, toPos });
|
|
956
|
+
announce(t("hookui.opMove", { id: item.type, n: toLine }));
|
|
516
957
|
}
|
|
517
958
|
|
|
518
959
|
// `aria-grabbed` is deprecated and is not used. State is announced through a
|
|
@@ -530,146 +971,496 @@ function announce(msg) {
|
|
|
530
971
|
region.textContent = msg;
|
|
531
972
|
}
|
|
532
973
|
|
|
533
|
-
|
|
534
|
-
//
|
|
535
|
-
//
|
|
974
|
+
/* ══ the part picker ══════════════════════════════════════════════════════ */
|
|
975
|
+
// ONE MODAL, one search box, one click. It is the only place a part is ever
|
|
976
|
+
// chosen — for a new slot and for swapping an existing one — so there is
|
|
977
|
+
// exactly one answer to "how do I put something on the bar".
|
|
978
|
+
//
|
|
979
|
+
// `replace` names the chip whose part is being swapped; absent, this is an add.
|
|
980
|
+
function partPicker(cat, plan, line, replace) {
|
|
981
|
+
HK_MODAL = null;
|
|
982
|
+
let q = "";
|
|
983
|
+
// NOT named `body`: that name is reserved for a panel's own card container,
|
|
984
|
+
// which must carry `stack`. This is a modal's contents and spaces itself.
|
|
985
|
+
const pane = el("div", "hk-picker");
|
|
986
|
+
|
|
987
|
+
const searchRow = el("div", "hk-picker-search");
|
|
988
|
+
const search = el("input", "hk-input hk-picker-input");
|
|
989
|
+
search.type = "search";
|
|
990
|
+
search.placeholder = t("hookui.searchPlaceholder");
|
|
991
|
+
const count = el("span", "note");
|
|
992
|
+
searchRow.append(search, count);
|
|
993
|
+
pane.append(searchRow);
|
|
994
|
+
pane.append(el("div", "note", replace ? t("hookui.pickerReplaceAbout") : tn(line, "hookui.pickerAbout")));
|
|
995
|
+
|
|
996
|
+
const list = el("div", "hk-picker-list");
|
|
997
|
+
pane.append(list);
|
|
998
|
+
|
|
999
|
+
const closer = () => close();
|
|
1000
|
+
const paint = () => {
|
|
1001
|
+
list.replaceChildren();
|
|
1002
|
+
const rows = (cat.components || []).filter((c) => (q ? matches(c, q) : true));
|
|
1003
|
+
count.textContent = tn(rows.length, "hookui.matches");
|
|
1004
|
+
if (!rows.length) {
|
|
1005
|
+
list.append(empty(t("hookui.noMatch")));
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
for (const comp of rows) list.append(pickerRow(comp, line, replace, closer));
|
|
1009
|
+
};
|
|
1010
|
+
search.addEventListener("input", () => {
|
|
1011
|
+
q = search.value.trim().toLowerCase();
|
|
1012
|
+
paint();
|
|
1013
|
+
});
|
|
1014
|
+
paint();
|
|
1015
|
+
|
|
1016
|
+
const close = modal({
|
|
1017
|
+
title: replace ? t("hookui.pickerReplaceTitle") : tn(line, "hookui.pickerTitle"),
|
|
1018
|
+
body: pane,
|
|
1019
|
+
actions: [{ label: t("common.cancel"), cls: "btn-ghost", onClick: (c) => c() }],
|
|
1020
|
+
});
|
|
1021
|
+
setTimeout(() => search.focus(), 40);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
// A row says WHAT the part is, in a sentence, next to what it will look like.
|
|
1025
|
+
// A REFUSED part is drawn and is NOT clickable — never a disabled button, and
|
|
1026
|
+
// never absent: "we decided against this" and "we forgot" must not look the
|
|
1027
|
+
// same, which is why the row exists at all.
|
|
1028
|
+
function pickerRow(comp, line, replace, close) {
|
|
1029
|
+
const refused = comp.cost === "refused";
|
|
1030
|
+
const row = el(refused ? "div" : "button", "hk-pick-row" + (refused ? " hk-pick-row-off" : ""));
|
|
1031
|
+
if (!refused) row.type = "button";
|
|
1032
|
+
|
|
1033
|
+
const top = el("div", "hk-pick-top");
|
|
1034
|
+
top.append(el("span", "hk-prow-id", comp.id));
|
|
1035
|
+
// The cost chip is the CLI's own word. A friendlier synonym would be a state
|
|
1036
|
+
// that does not exist.
|
|
1037
|
+
top.append(chip(comp.cost, refused ? "bad" : null));
|
|
1038
|
+
row.append(top);
|
|
1039
|
+
if (comp.summary) row.append(el("div", "hk-pick-desc", comp.summary));
|
|
1040
|
+
const sample = comp.previews && comp.previews[(comp.defaults && comp.defaults.render) || comp.renderers[0]];
|
|
1041
|
+
if (sample) row.append(ansiBlock(sample, "hk-preview hk-preview-chip"));
|
|
1042
|
+
if (refused) {
|
|
1043
|
+
row.append(el("div", "note note-warn", comp.refused_reason));
|
|
1044
|
+
return row;
|
|
1045
|
+
}
|
|
1046
|
+
row.addEventListener("click", () => {
|
|
1047
|
+
if (replace) hkOp({ op: "set", ref: replace.ref, field: "type", value: comp.id, label: t("hookui.opSwap", { id: comp.id }) });
|
|
1048
|
+
else hkOp({ op: "add", line, type: comp.id });
|
|
1049
|
+
close();
|
|
1050
|
+
});
|
|
1051
|
+
return row;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// Fuzzy plus INITIALISM: with a catalogue this size a substring search is not
|
|
1055
|
+
// enough — two letters should find a two-word name, which plain substring
|
|
1056
|
+
// matching does not.
|
|
1057
|
+
function matches(comp, q) {
|
|
1058
|
+
const id = comp.id.toLowerCase();
|
|
1059
|
+
if (id.includes(q)) return true;
|
|
1060
|
+
if ((comp.summary || "").toLowerCase().includes(q)) return true;
|
|
1061
|
+
if ((comp.label || "").toLowerCase().includes(q)) return true;
|
|
1062
|
+
const initials = id.split("-").map((p) => p[0]).join("");
|
|
1063
|
+
if (initials.startsWith(q)) return true;
|
|
1064
|
+
let i = 0;
|
|
1065
|
+
for (const ch of id) if (ch === q[i]) i++;
|
|
1066
|
+
return i === q.length;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/* ══ move ═════════════════════════════════════════════════════════════════ */
|
|
1070
|
+
// A modal that says what it is about to do — and on a narrow screen it is the
|
|
1071
|
+
// ONLY way to move a chip, because drag is off below the width at which a drop
|
|
1072
|
+
// gap stops being a target.
|
|
1073
|
+
function moveModal(item, line, plan) {
|
|
1074
|
+
HK_MODAL = null;
|
|
1075
|
+
const body = el("div", "stack");
|
|
1076
|
+
body.append(el("div", "note", t("hookui.moveAbout", { id: item.type })));
|
|
1077
|
+
|
|
1078
|
+
for (const target of plan.lines) {
|
|
1079
|
+
const legal = lineLegal(plan, target.line);
|
|
1080
|
+
const sameLine = target.line === line.line;
|
|
1081
|
+
const box = el("div", "hk-move-line");
|
|
1082
|
+
box.append(el("div", "hk-line-name", tn(target.line, "hookui.lineN")));
|
|
1083
|
+
// A LINE THAT CANNOT TAKE IT SAYS SO ONCE. Six identical disabled slots is
|
|
1084
|
+
// the same refusal printed six times, and the reason is about the LINE
|
|
1085
|
+
// rather than about any one slot on it.
|
|
1086
|
+
const blocked = sameLine ? null : addBlocked(plan, target.line);
|
|
1087
|
+
if (blocked) {
|
|
1088
|
+
box.append(el("div", "note note-warn", blocked));
|
|
1089
|
+
body.append(box);
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
const slots = el("div", "hk-move-slots");
|
|
1093
|
+
const n = Math.max(1, target.items.length + (sameLine ? 0 : 1));
|
|
1094
|
+
for (let p = 1; p <= n; p++) {
|
|
1095
|
+
const b = el("button", "btn btn-xs btn-ghost", tn(p, "hookui.slotN"));
|
|
1096
|
+
b.type = "button";
|
|
1097
|
+
if (sameLine && p === line.items.indexOf(item) + 1) {
|
|
1098
|
+
b.disabled = true;
|
|
1099
|
+
b.title = t("hookui.alreadyHere");
|
|
1100
|
+
} else {
|
|
1101
|
+
b.addEventListener("click", () => {
|
|
1102
|
+
close();
|
|
1103
|
+
moveTo(item, target.line, p);
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
slots.append(b);
|
|
1107
|
+
}
|
|
1108
|
+
box.append(slots);
|
|
1109
|
+
body.append(box);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
const close = modal({
|
|
1113
|
+
title: t("hookui.moveTitle"),
|
|
1114
|
+
body,
|
|
1115
|
+
actions: [{ label: t("common.cancel"), cls: "btn-ghost", onClick: (c) => c() }],
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/* ══ remove ═══════════════════════════════════════════════════════════════ */
|
|
1120
|
+
function removeModal(item, comp) {
|
|
1121
|
+
HK_MODAL = null;
|
|
1122
|
+
const body = el("div", "stack");
|
|
1123
|
+
body.append(el("div", "note", t("hookui.removeAbout", { id: item.type })));
|
|
1124
|
+
if (comp && comp.summary) body.append(el("div", "note", comp.summary));
|
|
1125
|
+
modal({
|
|
1126
|
+
title: t("hookui.removeTitle"),
|
|
1127
|
+
body,
|
|
1128
|
+
actions: [
|
|
1129
|
+
{ label: t("common.cancel"), cls: "btn-ghost", onClick: (c) => c() },
|
|
1130
|
+
{
|
|
1131
|
+
label: t("hookui.remove"),
|
|
1132
|
+
cls: "btn-primary",
|
|
1133
|
+
onClick: (c) => {
|
|
1134
|
+
c();
|
|
1135
|
+
hkOp({ op: "remove", ref: item.ref });
|
|
1136
|
+
},
|
|
1137
|
+
},
|
|
1138
|
+
],
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
/* ══ the part editor ══════════════════════════════════════════════════════ */
|
|
1143
|
+
// A MODAL, because the board is what the user is looking at, and a drawer that
|
|
1144
|
+
// opened inside a chip pushed the whole board down under the thing being
|
|
1145
|
+
// edited.
|
|
536
1146
|
//
|
|
537
|
-
// NO FIELD NAMES
|
|
538
|
-
//
|
|
539
|
-
//
|
|
540
|
-
// CLI's own and are never
|
|
541
|
-
|
|
542
|
-
|
|
1147
|
+
// NO FIELD NAMES, AND EVERY CONTROL CARRIES A SENTENCE. `min_width` and
|
|
1148
|
+
// `hide_when` are the JSON's words; this says "Least width" and explains what
|
|
1149
|
+
// it does. The JSON is for the diff, the editor is for the person — and the
|
|
1150
|
+
// ids, renderers and state words that DO appear are the CLI's own and are never
|
|
1151
|
+
// translated.
|
|
1152
|
+
function editModal(item0, line0, comp, cat) {
|
|
1153
|
+
// NOT named `body`: that name is reserved for a panel's own card container,
|
|
1154
|
+
// which must carry `stack`. This is a modal's contents and spaces itself.
|
|
1155
|
+
const pane = el("div", "hk-editor");
|
|
543
1156
|
if (!comp) {
|
|
544
|
-
|
|
545
|
-
|
|
1157
|
+
pane.append(el("div", "note", t("hookui.unknownComponent")));
|
|
1158
|
+
modal({ title: item0.type, body: pane, actions: [{ label: t("common.close"), cls: "btn-ghost", onClick: (c) => c() }] });
|
|
1159
|
+
return;
|
|
546
1160
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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);
|
|
551
1203
|
};
|
|
552
1204
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
//
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
//
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
const
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
const next = new Set(active);
|
|
620
|
-
if (box.checked) next.add(h.id);
|
|
621
|
-
else next.delete(h.id);
|
|
622
|
-
const v = next.size ? [...next].join(",") : "never";
|
|
623
|
-
stage("hide_when", v, t("hookui.hideChange", { id: comp.id, v }));
|
|
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", () => {
|
|
1269
|
+
close();
|
|
1270
|
+
partPicker(cat, HK_NO_LINES, 0, item);
|
|
624
1271
|
});
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
const
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
+
}
|
|
1345
|
+
|
|
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")));
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
paint(null);
|
|
1382
|
+
|
|
1383
|
+
const close = modal({
|
|
1384
|
+
title: comp.id,
|
|
1385
|
+
body: pane,
|
|
1386
|
+
actions: [
|
|
1387
|
+
// WHERE EACH VALUE CAME FROM. It is how a user finds their own overrides
|
|
1388
|
+
// again after a colour set change - `orc lane config`'s problem, at part
|
|
1389
|
+
// scale. It reads the SAVED item, so it is offered only for one.
|
|
1390
|
+
...(item.src
|
|
1391
|
+
? [{
|
|
1392
|
+
label: t("hookui.explain"),
|
|
1393
|
+
cls: "btn-ghost",
|
|
1394
|
+
onClick: async () => {
|
|
1395
|
+
const r = await read("/api/statusline/explain?board=" + HK_BOARD + "&at=" + line.line + ":" + (line.items.indexOf(item) + 1));
|
|
1396
|
+
if (!r.data || !r.data.ok) return toast(t("hookui.explainFailed"), "bad");
|
|
1397
|
+
HK_MODAL = null;
|
|
1398
|
+
modal({
|
|
1399
|
+
title: comp.id,
|
|
1400
|
+
body: kvList(r.data.resolved.map((x) => [x.field, x.source])),
|
|
1401
|
+
actions: [{ label: t("common.close"), cls: "btn-ghost", onClick: (c) => c() }],
|
|
1402
|
+
});
|
|
1403
|
+
},
|
|
1404
|
+
}]
|
|
1405
|
+
: []),
|
|
1406
|
+
{
|
|
1407
|
+
label: t("common.close"),
|
|
1408
|
+
cls: "btn-ghost",
|
|
1409
|
+
onClick: (c) => {
|
|
1410
|
+
HK_MODAL = null;
|
|
1411
|
+
c();
|
|
1412
|
+
},
|
|
1413
|
+
},
|
|
1414
|
+
],
|
|
670
1415
|
});
|
|
671
|
-
|
|
672
|
-
|
|
1416
|
+
// Registered LAST, so a repaint that fires during construction cannot reach a
|
|
1417
|
+
// half-built pane.
|
|
1418
|
+
HK_MODAL = { repaint: paint };
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// `partPicker` reads a board only to answer "can a part go on that line". A
|
|
1422
|
+
// SWAP changes nothing about where the part sits, so it is handed a board with
|
|
1423
|
+
// no lines and never asks.
|
|
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
|
+
}
|
|
1458
|
+
|
|
1459
|
+
function sectionHead(name, about) {
|
|
1460
|
+
const w = el("div", "hk-section-wrap");
|
|
1461
|
+
w.append(el("div", "hk-section", name));
|
|
1462
|
+
if (about) w.append(el("div", "note", about));
|
|
1463
|
+
return w;
|
|
673
1464
|
}
|
|
674
1465
|
|
|
675
1466
|
function numField(value, lo, hi, onSet) {
|
|
@@ -690,9 +1481,15 @@ function textField(value, onSet) {
|
|
|
690
1481
|
return i;
|
|
691
1482
|
}
|
|
692
1483
|
|
|
693
|
-
|
|
1484
|
+
// A control, its name, and — because a noun on its own is not an explanation —
|
|
1485
|
+
// the sentence that says what it does. That sentence is most of this release:
|
|
1486
|
+
// a settings form whose labels are nouns is a form people close.
|
|
1487
|
+
function labelled(name, node, about) {
|
|
694
1488
|
const row = el("div", "hk-field");
|
|
695
|
-
|
|
1489
|
+
const left = el("div", "hk-field-left");
|
|
1490
|
+
left.append(el("span", "hk-field-name", name));
|
|
1491
|
+
if (about) left.append(el("span", "note", about));
|
|
1492
|
+
row.append(left);
|
|
696
1493
|
row.append(node);
|
|
697
1494
|
return row;
|
|
698
1495
|
}
|
|
@@ -710,38 +1507,39 @@ function pickRow(options, current, onPick) {
|
|
|
710
1507
|
return row;
|
|
711
1508
|
}
|
|
712
1509
|
|
|
713
|
-
|
|
714
|
-
|
|
1510
|
+
/* ══ the reference list ═══════════════════════════════════════════════════ */
|
|
1511
|
+
// WHAT EACH PART SHOWS, and nothing else. It used to carry three `+ line N`
|
|
1512
|
+
// buttons per row, which put the act of building the bar a long way from the
|
|
1513
|
+
// bar — and computed a position against a layout that was no longer the one
|
|
1514
|
+
// being staged. Adding happens on the board now; this is a reference.
|
|
1515
|
+
function referenceCard(cat) {
|
|
715
1516
|
const right = el("div", "row-actions");
|
|
716
|
-
const search = el("input", "hk-palette-search");
|
|
1517
|
+
const search = el("input", "hk-input hk-palette-search");
|
|
717
1518
|
search.type = "search";
|
|
718
1519
|
search.value = HK_SEARCH;
|
|
719
1520
|
search.placeholder = t("hookui.searchPlaceholder");
|
|
720
|
-
search.addEventListener("input", () => {
|
|
721
|
-
HK_SEARCH = search.value;
|
|
722
|
-
paint();
|
|
723
|
-
});
|
|
724
1521
|
right.append(search);
|
|
725
1522
|
|
|
726
|
-
const c = card(t("hookui.
|
|
1523
|
+
const c = card(t("hookui.reference"), right);
|
|
1524
|
+
c.append(el("div", "note", t("hookui.referenceAbout")));
|
|
727
1525
|
const body = el("div", "stack");
|
|
728
1526
|
c.append(body);
|
|
729
1527
|
|
|
730
1528
|
function paint() {
|
|
731
1529
|
body.replaceChildren();
|
|
732
1530
|
const q = HK_SEARCH.trim().toLowerCase();
|
|
733
|
-
const hits = q ? cat.components.filter((x) => matches(x, q)) : null;
|
|
1531
|
+
const hits = q ? (cat.components || []).filter((x) => matches(x, q)) : null;
|
|
734
1532
|
if (hits) {
|
|
735
1533
|
body.append(el("div", "note", tn(hits.length, "hookui.matches")));
|
|
736
|
-
for (const comp of hits) body.append(
|
|
1534
|
+
for (const comp of hits) body.append(referenceRow(comp));
|
|
737
1535
|
if (!hits.length) body.append(empty(t("hookui.noMatch")));
|
|
738
1536
|
return;
|
|
739
1537
|
}
|
|
740
1538
|
for (const [g, name] of Object.entries(cat.groups)) {
|
|
741
|
-
const rows = cat.components.filter((x) => x.group === g);
|
|
1539
|
+
const rows = (cat.components || []).filter((x) => x.group === g);
|
|
742
1540
|
if (!rows.length) continue;
|
|
743
1541
|
const inner = el("div");
|
|
744
|
-
for (const comp of rows) inner.append(
|
|
1542
|
+
for (const comp of rows) inner.append(referenceRow(comp));
|
|
745
1543
|
body.append(collapsible({
|
|
746
1544
|
title: name,
|
|
747
1545
|
count: String(rows.length),
|
|
@@ -750,68 +1548,35 @@ function paletteCard(show, cat, edits) {
|
|
|
750
1548
|
}));
|
|
751
1549
|
}
|
|
752
1550
|
}
|
|
1551
|
+
search.addEventListener("input", () => {
|
|
1552
|
+
HK_SEARCH = search.value;
|
|
1553
|
+
paint();
|
|
1554
|
+
});
|
|
753
1555
|
paint();
|
|
754
1556
|
return c;
|
|
755
1557
|
}
|
|
756
1558
|
|
|
757
|
-
|
|
758
|
-
// `cw` should find `cache-write`, which plain substring matching does not.
|
|
759
|
-
function matches(comp, q) {
|
|
760
|
-
const id = comp.id.toLowerCase();
|
|
761
|
-
if (id.includes(q)) return true;
|
|
762
|
-
if ((comp.summary || "").toLowerCase().includes(q)) return true;
|
|
763
|
-
const initials = id.split("-").map((p) => p[0]).join("");
|
|
764
|
-
if (initials.startsWith(q)) return true;
|
|
765
|
-
let i = 0;
|
|
766
|
-
for (const ch of id) if (ch === q[i]) i++;
|
|
767
|
-
return i === q.length;
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
function paletteRow(comp, show, cat, edits) {
|
|
1559
|
+
function referenceRow(comp) {
|
|
771
1560
|
const row = el("div", "hk-prow");
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
row.append(
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
// A REFUSED
|
|
780
|
-
//
|
|
781
|
-
|
|
782
|
-
if (comp.cost === "refused") {
|
|
783
|
-
row.append(el("div", "note note-warn hk-prow-why", comp.refused_reason));
|
|
784
|
-
return row;
|
|
785
|
-
}
|
|
786
|
-
const acts = el("div", "row-actions");
|
|
787
|
-
for (const n of [1, 2, 3]) {
|
|
788
|
-
const legal = lineLegal(show, n);
|
|
789
|
-
const line = show.lines[n - 1];
|
|
790
|
-
const b = el("button", "btn btn-xs btn-ghost", tn(n, "hookui.addTo"));
|
|
791
|
-
b.type = "button";
|
|
792
|
-
if (!legal.ok) {
|
|
793
|
-
b.disabled = true;
|
|
794
|
-
b.title = legal.why;
|
|
795
|
-
} else if (line.full) {
|
|
796
|
-
b.disabled = true;
|
|
797
|
-
b.title = t("hookui.lineFull", { n });
|
|
798
|
-
} else {
|
|
799
|
-
b.addEventListener("click", () =>
|
|
800
|
-
edits.action("add" + comp.id + n, "/api/statusline/set",
|
|
801
|
-
bd({ line: n, pos: line.items.length + 1, type: comp.id }),
|
|
802
|
-
t("hookui.added", { id: comp.id, n })));
|
|
803
|
-
}
|
|
804
|
-
acts.append(b);
|
|
805
|
-
}
|
|
806
|
-
row.append(acts);
|
|
1561
|
+
const top = el("div", "hk-prow-head");
|
|
1562
|
+
top.append(el("span", "hk-prow-id", comp.id));
|
|
1563
|
+
top.append(chip(comp.cost, comp.cost === "refused" ? "bad" : null));
|
|
1564
|
+
row.append(top);
|
|
1565
|
+
if (comp.summary) row.append(el("div", "hk-prow-desc", comp.summary));
|
|
1566
|
+
const sample = comp.previews && comp.previews[(comp.defaults && comp.defaults.render) || comp.renderers[0]];
|
|
1567
|
+
if (sample) row.append(ansiBlock(sample, "hk-preview hk-preview-chip"));
|
|
1568
|
+
// A REFUSED part keeps its slot and carries the measurement that refused it:
|
|
1569
|
+
// "we decided against this" and "we forgot" must not look the same.
|
|
1570
|
+
if (comp.cost === "refused") row.append(el("div", "note note-warn hk-prow-why", comp.refused_reason));
|
|
807
1571
|
return row;
|
|
808
1572
|
}
|
|
809
1573
|
|
|
810
|
-
|
|
811
|
-
//
|
|
812
|
-
//
|
|
813
|
-
function presetsCard(presets) {
|
|
1574
|
+
/* ══ presets ══════════════════════════════════════════════════════════════ */
|
|
1575
|
+
// Applying one REPLACES the layout, so it is always confirmed and the loss is
|
|
1576
|
+
// NAMED — the `orc diy init --force` rule.
|
|
1577
|
+
function presetsCard(presets, plan) {
|
|
814
1578
|
const c = card(t("hookui.presets"));
|
|
1579
|
+
c.append(el("div", "note", t("hookui.presetsAbout")));
|
|
815
1580
|
if (!presets || !presets.presets) return c;
|
|
816
1581
|
for (const p of presets.presets) {
|
|
817
1582
|
const row = el("div", "hk-preset");
|
|
@@ -825,7 +1590,15 @@ function presetsCard(presets) {
|
|
|
825
1590
|
if (!p.active) {
|
|
826
1591
|
const b = el("button", "btn btn-xs btn-ghost", t("hookui.apply"));
|
|
827
1592
|
b.type = "button";
|
|
828
|
-
|
|
1593
|
+
// A preset is written STRAIGHT AWAY, so it cannot run while ops are
|
|
1594
|
+
// staged: those ops carry positions computed against a layout the preset
|
|
1595
|
+
// is about to replace.
|
|
1596
|
+
if (plan.dirty) {
|
|
1597
|
+
b.disabled = true;
|
|
1598
|
+
b.title = t("hookui.applyFirst");
|
|
1599
|
+
} else {
|
|
1600
|
+
b.addEventListener("click", () => confirmApply(p.name));
|
|
1601
|
+
}
|
|
829
1602
|
row.append(b);
|
|
830
1603
|
}
|
|
831
1604
|
c.append(row);
|
|
@@ -838,14 +1611,17 @@ function confirmApply(name) {
|
|
|
838
1611
|
title: t("hookui.applyTitle", { name }),
|
|
839
1612
|
body: el("div", "note", t("hookui.applyWarn")),
|
|
840
1613
|
actions: [
|
|
841
|
-
{ label: t("common.cancel"),
|
|
1614
|
+
{ label: t("common.cancel"), cls: "btn-ghost", onClick: (c) => c() },
|
|
842
1615
|
{
|
|
843
1616
|
label: t("hookui.apply"),
|
|
844
|
-
|
|
845
|
-
onClick: async () => {
|
|
1617
|
+
cls: "btn-primary",
|
|
1618
|
+
onClick: async (c) => {
|
|
1619
|
+
c();
|
|
846
1620
|
const r = await post("/api/statusline/apply", bd({ name }));
|
|
847
1621
|
if (!r.ok) toast(t("hookui.applyFailed"), "bad", (r.output || "").trim());
|
|
848
|
-
|
|
1622
|
+
HK_OPS = [];
|
|
1623
|
+
HK_COMPARE_DATA = null;
|
|
1624
|
+
hkReload();
|
|
849
1625
|
},
|
|
850
1626
|
},
|
|
851
1627
|
],
|
|
@@ -857,31 +1633,55 @@ function confirmReset() {
|
|
|
857
1633
|
title: t("hookui.resetTitle"),
|
|
858
1634
|
body: el("div", "note", t("hookui.resetWarn")),
|
|
859
1635
|
actions: [
|
|
860
|
-
{ label: t("common.cancel"),
|
|
1636
|
+
{ label: t("common.cancel"), cls: "btn-ghost", onClick: (c) => c() },
|
|
861
1637
|
{
|
|
862
1638
|
label: t("hookui.resetLayout"),
|
|
863
|
-
|
|
864
|
-
onClick: async () => {
|
|
1639
|
+
cls: "btn-primary",
|
|
1640
|
+
onClick: async (c) => {
|
|
1641
|
+
c();
|
|
865
1642
|
await post("/api/statusline/reset", bd({}));
|
|
866
|
-
|
|
1643
|
+
HK_OPS = [];
|
|
1644
|
+
HK_COMPARE_DATA = null;
|
|
1645
|
+
hkReload();
|
|
867
1646
|
},
|
|
868
1647
|
},
|
|
869
1648
|
],
|
|
870
1649
|
});
|
|
871
1650
|
}
|
|
872
1651
|
|
|
873
|
-
|
|
874
|
-
|
|
1652
|
+
/* ══ more ═════════════════════════════════════════════════════════════════ */
|
|
1653
|
+
// THE COLOUR SET IS A DOCUMENT SETTING, and it now goes to the command that
|
|
1654
|
+
// writes document settings. It used to be routed through `statusline line 1
|
|
1655
|
+
// --theme`, which set ONE line's override while the picker read the document's
|
|
1656
|
+
// value back — so the button never moved and only a third of the bar changed.
|
|
1657
|
+
// One command per scope, and the panel calls the one that matches the control.
|
|
1658
|
+
function advancedCard(cat, plan) {
|
|
875
1659
|
const c = card(t("hookui.advanced"));
|
|
876
|
-
|
|
877
|
-
|
|
1660
|
+
|
|
1661
|
+
const themes = el("div", "hk-theme-list");
|
|
1662
|
+
for (const name of Object.keys(cat.themes || {})) {
|
|
1663
|
+
const b = el("button", "hk-theme" + (plan.doc.theme === name ? " hk-theme-on" : ""));
|
|
1664
|
+
b.type = "button";
|
|
1665
|
+
b.append(el("span", "hk-theme-name", name));
|
|
1666
|
+
// WHY somebody would pick it — the CLI's sentence, not one written here.
|
|
1667
|
+
const about = (cat.themes_about || {})[name];
|
|
1668
|
+
if (about) b.append(el("span", "note", about));
|
|
1669
|
+
b.addEventListener("click", () => hkOp({ op: "doc", field: "theme", value: name, label: t("hookui.themeChange", { v: name }) }));
|
|
1670
|
+
themes.append(b);
|
|
1671
|
+
}
|
|
1672
|
+
c.append(labelled(t("hookui.theme"), themes, t("hookui.themeAbout")));
|
|
1673
|
+
c.append(el("div", "note", t("hookui.themeOverride")));
|
|
1674
|
+
|
|
1675
|
+
c.append(labelled(t("hookui.glyphs"),
|
|
1676
|
+
pickRow(cat.glyph_sets, plan.doc.glyphs, (v) => hkOp({ op: "doc", field: "glyphs", value: v, label: t("hookui.glyphsDocChange", { v }) })),
|
|
1677
|
+
t("hookui.glyphsDocAbout")));
|
|
878
1678
|
|
|
879
1679
|
const re = el("button", "btn btn-sm btn-ghost", t("hookui.recompile"));
|
|
880
1680
|
re.type = "button";
|
|
881
1681
|
re.addEventListener("click", async () => {
|
|
882
1682
|
const r = await post("/api/statusline/compile", bd({}));
|
|
883
1683
|
toast(r.ok ? t("hookui.recompiled") : t("hookui.recompileFailed"), r.ok ? "ok" : "bad");
|
|
884
|
-
|
|
1684
|
+
hkReload();
|
|
885
1685
|
});
|
|
886
1686
|
const acts = el("div", "row-actions");
|
|
887
1687
|
acts.append(re);
|