@azure-id/orc 1.4.0 → 1.4.1
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 +2907 -2814
- package/README.md +631 -657
- package/bin/cli.js +36866 -36780
- package/bin/webui/api.js +1283 -1272
- package/bin/webui/css/04-motion.css +43 -0
- package/bin/webui/css/06-responsive.css +14 -0
- package/bin/webui/css/panels/hookui.css +287 -71
- package/bin/webui/fixtures/hookui.js +431 -409
- package/bin/webui/i18n/en/hookui.json +104 -43
- package/bin/webui/i18n/id/hookui.json +123 -62
- package/bin/webui/js/panels/hookui.js +784 -362
- package/package.json +1 -1
|
@@ -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,6 +51,28 @@ 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
|
+
|
|
46
76
|
PANELS.hookui = function (host) {
|
|
47
77
|
head(host, t("hookui.title"), t("hookui.sub"));
|
|
48
78
|
|
|
@@ -61,31 +91,39 @@ PANELS.hookui = function (host) {
|
|
|
61
91
|
out.append(empty(t("hookui.noData")));
|
|
62
92
|
return out;
|
|
63
93
|
}
|
|
94
|
+
// The effective board: what it WILL be once the staged ops run. Every
|
|
95
|
+
// count, every slot cap and every legality question downstream reads this
|
|
96
|
+
// and never `show.lines` directly.
|
|
97
|
+
const plan = hkPlan(show, cat);
|
|
98
|
+
|
|
64
99
|
const edits = editSet(() => bar.paint());
|
|
65
100
|
const bar = editBar(edits, {
|
|
66
101
|
onApply: async (b) => {
|
|
67
102
|
await applyActions(edits, b);
|
|
68
|
-
|
|
103
|
+
HK_OPS = [];
|
|
69
104
|
rerender();
|
|
70
105
|
},
|
|
71
106
|
onReset: () => confirmReset(),
|
|
72
107
|
onCancel: () => {
|
|
73
|
-
|
|
108
|
+
HK_OPS = [];
|
|
74
109
|
rerender();
|
|
75
110
|
},
|
|
76
111
|
resetLabel: t("hookui.resetLayout"),
|
|
77
112
|
});
|
|
113
|
+
// Filled AFTER the bar exists: `editSet`'s onChange paints it, and a
|
|
114
|
+
// paint before that binding is assigned throws.
|
|
115
|
+
for (const a of plan.actions) edits.action(a.key, a.route, a.body, a.label);
|
|
78
116
|
|
|
79
117
|
out.append(boardTabs(cat));
|
|
80
|
-
out.append(gateCard(show, cat
|
|
118
|
+
out.append(gateCard(show, cat));
|
|
81
119
|
// A caution renders ABOVE the preview, never inside a tab — a caution you
|
|
82
120
|
// have to hunt for is a caution nobody reads.
|
|
83
|
-
if (show.errors.length || show.warnings.length) out.append(cautionCard(show
|
|
84
|
-
out.append(previewCard(prev,
|
|
85
|
-
out.append(boardCard(
|
|
86
|
-
out.append(
|
|
87
|
-
out.append(presetsCard(presets));
|
|
88
|
-
out.append(advancedCard(
|
|
121
|
+
if (show.errors.length || show.warnings.length) out.append(cautionCard(show));
|
|
122
|
+
out.append(previewCard(prev, plan));
|
|
123
|
+
out.append(boardCard(cat, plan));
|
|
124
|
+
out.append(referenceCard(cat));
|
|
125
|
+
out.append(presetsCard(presets, plan));
|
|
126
|
+
out.append(advancedCard(cat, plan));
|
|
89
127
|
out.append(bar);
|
|
90
128
|
return out;
|
|
91
129
|
}
|
|
@@ -98,6 +136,129 @@ function bd(body) {
|
|
|
98
136
|
return Object.assign({ board: HK_BOARD }, body);
|
|
99
137
|
}
|
|
100
138
|
|
|
139
|
+
function rerender() {
|
|
140
|
+
const h = location.hash;
|
|
141
|
+
location.hash = "#/";
|
|
142
|
+
location.hash = h || "#/hookui";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* ══ the staged plan ══════════════════════════════════════════════════════
|
|
146
|
+
Replays every staged op onto a working copy of the saved layout and returns
|
|
147
|
+
BOTH the effective board and the ordered writes that produce it. ONE
|
|
148
|
+
function, because a second idea of where a part will land is exactly the bug
|
|
149
|
+
this replaces: the board drew one arrangement and the writes produced
|
|
150
|
+
another, silently. */
|
|
151
|
+
|
|
152
|
+
// Ops are staged by REF, never by position. A ref is either an item id the CLI
|
|
153
|
+
// already gave us, or `new:<n>` for something staged in this session.
|
|
154
|
+
function hkOp(op) {
|
|
155
|
+
// A second edit to the same field of the same part REPLACES the first — one
|
|
156
|
+
// decision typed twice is one decision, and the pending list has to read as a
|
|
157
|
+
// list of decisions rather than a list of keystrokes.
|
|
158
|
+
if (op.op === "set") HK_OPS = HK_OPS.filter((o) => !(o.op === "set" && o.ref === op.ref && o.field === op.field));
|
|
159
|
+
if (op.op === "sep") HK_OPS = HK_OPS.filter((o) => !(o.op === "sep" && o.line === op.line));
|
|
160
|
+
if (op.op === "doc") HK_OPS = HK_OPS.filter((o) => !(o.op === "doc" && o.field === op.field));
|
|
161
|
+
HK_OPS.push(op);
|
|
162
|
+
rerender();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function hkPlan(show, cat) {
|
|
166
|
+
const structural = new Set((cat.components || []).filter((c) => c.structural).map((c) => c.id));
|
|
167
|
+
const lines = (show.lines || []).map((l) => ({
|
|
168
|
+
line: l.line,
|
|
169
|
+
separator: l.separator,
|
|
170
|
+
items: (l.items || []).map((it) => ({ ref: it.id, type: it.type, src: it, over: {}, isNew: false })),
|
|
171
|
+
}));
|
|
172
|
+
const actions = [];
|
|
173
|
+
const doc = { theme: show.theme, glyphs: show.glyphs };
|
|
174
|
+
const find = (ref) => {
|
|
175
|
+
for (const l of lines) {
|
|
176
|
+
const i = l.items.findIndex((x) => x.ref === ref);
|
|
177
|
+
if (i >= 0) return { l, i, it: l.items[i] };
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
HK_OPS.forEach((op, oi) => {
|
|
183
|
+
const key = "op" + (oi + 1);
|
|
184
|
+
if (op.op === "add") {
|
|
185
|
+
const l = lines[op.line - 1];
|
|
186
|
+
if (!l) return;
|
|
187
|
+
// THE POSITION IS COMPUTED HERE, not when the button was pressed. Three
|
|
188
|
+
// adds in a row get 1, 2 and 3 and append; three adds computed at stage
|
|
189
|
+
// time all got 1, and the CLI read 2 and 3 as EDITS of the first.
|
|
190
|
+
const pos = l.items.length + 1;
|
|
191
|
+
l.items.push({ ref: "new:" + oi, type: op.type, src: null, over: {}, isNew: true });
|
|
192
|
+
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 }) });
|
|
193
|
+
} else if (op.op === "remove") {
|
|
194
|
+
const f = find(op.ref);
|
|
195
|
+
if (!f) return;
|
|
196
|
+
actions.push({ key, route: "/api/statusline/remove", body: bd({ at: f.l.line + ":" + (f.i + 1) }), label: t("hookui.opRemove", { id: f.it.type }) });
|
|
197
|
+
f.l.items.splice(f.i, 1);
|
|
198
|
+
} else if (op.op === "move") {
|
|
199
|
+
const f = find(op.ref);
|
|
200
|
+
if (!f) return;
|
|
201
|
+
const from = f.l.line + ":" + (f.i + 1);
|
|
202
|
+
f.l.items.splice(f.i, 1);
|
|
203
|
+
const dst = lines[op.toLine - 1];
|
|
204
|
+
if (!dst) return;
|
|
205
|
+
const at = Math.max(0, Math.min(op.toPos - 1, dst.items.length));
|
|
206
|
+
dst.items.splice(at, 0, f.it);
|
|
207
|
+
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 }) });
|
|
208
|
+
} else if (op.op === "clone") {
|
|
209
|
+
const f = find(op.ref);
|
|
210
|
+
if (!f) return;
|
|
211
|
+
actions.push({ key, route: "/api/statusline/clone", body: bd({ at: f.l.line + ":" + (f.i + 1) }), label: t("hookui.opClone", { id: f.it.type }) });
|
|
212
|
+
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 });
|
|
213
|
+
} else if (op.op === "set") {
|
|
214
|
+
const f = find(op.ref);
|
|
215
|
+
if (!f) return;
|
|
216
|
+
f.it.over[op.field] = op.value;
|
|
217
|
+
if (op.field === "type") f.it.type = op.value;
|
|
218
|
+
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 });
|
|
219
|
+
} else if (op.op === "sep") {
|
|
220
|
+
const l = lines[op.line - 1];
|
|
221
|
+
if (!l) return;
|
|
222
|
+
l.separator = op.value;
|
|
223
|
+
actions.push({ key, route: "/api/statusline/line", body: bd({ line: op.line, separator: op.value }), label: op.label });
|
|
224
|
+
} else if (op.op === "doc") {
|
|
225
|
+
doc[op.field] = op.value;
|
|
226
|
+
actions.push({ key, route: "/api/statusline/doc", body: bd({ [op.field]: op.value }), label: op.label });
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
for (const l of lines) {
|
|
231
|
+
// A spacer is not a thing the line SAYS, so it does not eat one of the
|
|
232
|
+
// five. Which ids those are is the CLI's answer, carried per component.
|
|
233
|
+
l.counted = l.items.filter((x) => !structural.has(x.type)).length;
|
|
234
|
+
l.full = l.counted >= cat.max_per_line;
|
|
235
|
+
}
|
|
236
|
+
return { lines, actions, doc, dirty: HK_OPS.length > 0, max: cat.max_per_line };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// The dense-prefix rule, applied to ONE line of the EFFECTIVE board — never the
|
|
240
|
+
// saved one, or a part staged onto line 1 would leave line 2 still refusing.
|
|
241
|
+
// The reason travels with the answer, because a disabled zone with no reason is
|
|
242
|
+
// a shrug.
|
|
243
|
+
function lineLegal(plan, n) {
|
|
244
|
+
if (n === 1) return { ok: true };
|
|
245
|
+
const above = plan.lines[n - 2];
|
|
246
|
+
if (above && above.items.length > 0) return { ok: true };
|
|
247
|
+
return { ok: false, why: t("hookui.fillFirst", { n: n - 1 }) };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Why a part cannot go on this line RIGHT NOW; a null answer means it can. Both
|
|
251
|
+
// reasons are the user's to read BEFORE the click, never after it.
|
|
252
|
+
function addBlocked(plan, n) {
|
|
253
|
+
const legal = lineLegal(plan, n);
|
|
254
|
+
if (!legal.ok) return legal.why;
|
|
255
|
+
const line = plan.lines[n - 1];
|
|
256
|
+
if (!line) return t("hookui.noSuchLine");
|
|
257
|
+
if (line.full) return t("hookui.lineFull", { n: n, max: plan.max });
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/* ══ the board tabs ═══════════════════════════════════════════════════════ */
|
|
101
262
|
// The two boards. Named by the CLI (`boards` in the catalogue), labelled by the
|
|
102
263
|
// panel — the id is data and the label is prose, which is the split everywhere
|
|
103
264
|
// else in this file too.
|
|
@@ -116,10 +277,11 @@ function boardTabs(cat) {
|
|
|
116
277
|
btn.type = "button";
|
|
117
278
|
btn.addEventListener("click", () => {
|
|
118
279
|
if (HK_BOARD === b) return;
|
|
280
|
+
// Switching boards with ops staged would send them to a board they were
|
|
281
|
+
// never about: every op carries a board, but the POSITIONS in them were
|
|
282
|
+
// computed against this one.
|
|
283
|
+
if (HK_OPS.length) return toast(t("hookui.applyFirst"), "bad");
|
|
119
284
|
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
285
|
HK_SEARCH = "";
|
|
124
286
|
rerender();
|
|
125
287
|
});
|
|
@@ -130,17 +292,11 @@ function boardTabs(cat) {
|
|
|
130
292
|
return c;
|
|
131
293
|
}
|
|
132
294
|
|
|
133
|
-
|
|
134
|
-
const h = location.hash;
|
|
135
|
-
location.hash = "#/";
|
|
136
|
-
location.hash = h || "#/hookui";
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// ── the gate ───────────────────────────────────────────────────────────────
|
|
295
|
+
/* ══ the gate ═════════════════════════════════════════════════════════════ */
|
|
140
296
|
// INVERTED from the Extra panel's. There, nothing exists until you connect;
|
|
141
297
|
// here the board is LIVE WHILE OFF, because you must be able to compose before
|
|
142
298
|
// you arm. What is gated is the switch, not the work.
|
|
143
|
-
function gateCard(show, cat
|
|
299
|
+
function gateCard(show, cat) {
|
|
144
300
|
const c = card(t("hookui.gate"));
|
|
145
301
|
const row = el("div", "row-actions");
|
|
146
302
|
row.append(chip(show.enabled ? t("hookui.on") : t("hookui.off"), show.enabled ? "ok" : null));
|
|
@@ -172,10 +328,10 @@ function gateCard(show, cat, edits) {
|
|
|
172
328
|
return c;
|
|
173
329
|
}
|
|
174
330
|
|
|
175
|
-
|
|
331
|
+
/* ══ cautions ═════════════════════════════════════════════════════════════ */
|
|
176
332
|
// An error is a refusal; a warning is a fact the user then owns. They are drawn
|
|
177
333
|
// differently and they are never merged into one count.
|
|
178
|
-
function cautionCard(show
|
|
334
|
+
function cautionCard(show) {
|
|
179
335
|
const c = card(t("hookui.cautions"));
|
|
180
336
|
for (const e of show.errors) {
|
|
181
337
|
const row = el("div", "hk-caution hk-caution-bad");
|
|
@@ -192,13 +348,20 @@ function cautionCard(show, edits) {
|
|
|
192
348
|
return c;
|
|
193
349
|
}
|
|
194
350
|
|
|
195
|
-
|
|
351
|
+
/* ══ the preview ══════════════════════════════════════════════════════════ */
|
|
196
352
|
// Pinned above the board and never scrolled away. `orc statusline preview
|
|
197
353
|
// --json`'s output, rendered as DOM and never as HTML.
|
|
198
|
-
|
|
354
|
+
//
|
|
355
|
+
// IT DRAWS THE SAVED LAYOUT, and while anything is staged it SAYS SO. The
|
|
356
|
+
// preview renders through the hook's own engine, which reads the file — so a
|
|
357
|
+
// preview of unsaved edits would need a second engine, which is the one thing
|
|
358
|
+
// this design refuses. "These changes are not in the picture yet" is the honest
|
|
359
|
+
// version, and it is also the answer to "I pressed Apply and nothing happened":
|
|
360
|
+
// something did, and this is where it shows up.
|
|
361
|
+
function previewCard(prev, plan) {
|
|
199
362
|
const right = el("div", "row-actions");
|
|
200
363
|
for (const w of HK_WIDTHS) {
|
|
201
|
-
const b = el("button", "btn btn-xs" + (HK_WIDTH === w ? " btn-primary" : " btn-ghost"),
|
|
364
|
+
const b = el("button", "btn btn-xs" + (HK_WIDTH === w ? " btn-primary" : " btn-ghost"), tn(w, "hookui.cols"));
|
|
202
365
|
b.type = "button";
|
|
203
366
|
b.addEventListener("click", () => {
|
|
204
367
|
HK_WIDTH = w;
|
|
@@ -207,6 +370,8 @@ function previewCard(prev, show) {
|
|
|
207
370
|
right.append(b);
|
|
208
371
|
}
|
|
209
372
|
const c = card(t("hookui.preview"), right);
|
|
373
|
+
c.append(el("div", "note", t("hookui.previewAbout")));
|
|
374
|
+
if (plan.dirty) c.append(el("div", "note note-warn", t("hookui.previewStale")));
|
|
210
375
|
if (!prev || !prev.ok) {
|
|
211
376
|
c.append(el("div", "note", t("hookui.previewUnavailable")));
|
|
212
377
|
return c;
|
|
@@ -214,6 +379,7 @@ function previewCard(prev, show) {
|
|
|
214
379
|
|
|
215
380
|
// The fixture picker. `--fixtures` must carry one of every state INCLUDING
|
|
216
381
|
// the ugly ones — you cannot design a degraded line on a healthy session.
|
|
382
|
+
c.append(el("div", "note", t("hookui.fixturesAbout")));
|
|
217
383
|
const states = el("div", "row-actions hk-fixtures");
|
|
218
384
|
for (const [k, label] of Object.entries(prev.fixtures || {})) {
|
|
219
385
|
const b = el("button", "btn btn-xs" + (HK_STATE === k ? " btn-primary" : " btn-ghost"), label);
|
|
@@ -228,7 +394,7 @@ function previewCard(prev, show) {
|
|
|
228
394
|
|
|
229
395
|
c.append(ansiBlock(prev.text, "hk-preview"));
|
|
230
396
|
|
|
231
|
-
// THE
|
|
397
|
+
// THE THREE STRIPPINGS, always visible — not an afterthought tab. You cannot
|
|
232
398
|
// design an ASCII fallback you cannot see, and a design whose only
|
|
233
399
|
// distinction is colour becomes ambiguous the moment NO_COLOR is set.
|
|
234
400
|
const deg = el("div", "hk-degrade");
|
|
@@ -262,13 +428,20 @@ function ansiBlock(text, cls) {
|
|
|
262
428
|
// compiler can emit. Anything it does not recognise is DROPPED rather than
|
|
263
429
|
// printed, because a stray escape in the middle of a preview is worse than
|
|
264
430
|
// a missing colour.
|
|
265
|
-
const re =
|
|
431
|
+
const re = /\[([0-9;]*)m/g;
|
|
266
432
|
let last = 0;
|
|
267
433
|
let m;
|
|
268
434
|
const push = (s) => {
|
|
269
435
|
if (!s) return;
|
|
270
436
|
const span = el("span", null, s);
|
|
271
|
-
|
|
437
|
+
// THROUGH CSSOM, NEVER A `style` ATTRIBUTE. This page is served under
|
|
438
|
+
// `style-src 'self'`, which blocks a parsed style attribute outright —
|
|
439
|
+
// so every colour this preview drew was thrown away by the browser and
|
|
440
|
+
// the whole thing rendered grey, with the reason only in a console
|
|
441
|
+
// nobody had open. Assigning the property is not a parse and is not
|
|
442
|
+
// blocked, and weakening the policy to `unsafe-inline` for a preview is
|
|
443
|
+
// not a trade worth making.
|
|
444
|
+
if (style) for (const k of Object.keys(style)) span.style[k] = style[k];
|
|
272
445
|
row.append(span);
|
|
273
446
|
};
|
|
274
447
|
while ((m = re.exec(line))) {
|
|
@@ -277,7 +450,7 @@ function ansiBlock(text, cls) {
|
|
|
277
450
|
last = m.index + m[0].length;
|
|
278
451
|
}
|
|
279
452
|
push(line.slice(last));
|
|
280
|
-
if (!row.childNodes.length) row.append(document.createTextNode("
|
|
453
|
+
if (!row.childNodes.length) row.append(document.createTextNode(" "));
|
|
281
454
|
pre.append(row);
|
|
282
455
|
}
|
|
283
456
|
return pre;
|
|
@@ -285,7 +458,7 @@ function ansiBlock(text, cls) {
|
|
|
285
458
|
|
|
286
459
|
// The 16 ANSI slots, as the browser's own colours. These are NOT ORC's colour
|
|
287
460
|
// tokens — they are what a terminal would paint, approximated, because the
|
|
288
|
-
// panel cannot know the user's
|
|
461
|
+
// panel cannot know the user's terminal palette and says so.
|
|
289
462
|
const HK_ANSI = [
|
|
290
463
|
"#000000", "#cc0000", "#4e9a06", "#c4a000", "#3465a4", "#75507b", "#06989a", "#d3d7cf",
|
|
291
464
|
];
|
|
@@ -293,226 +466,274 @@ const HK_ANSI_BRIGHT = [
|
|
|
293
466
|
"#555753", "#ef2929", "#8ae234", "#fce94f", "#729fcf", "#ad7fa8", "#34e2e2", "#eeeeec",
|
|
294
467
|
];
|
|
295
468
|
|
|
469
|
+
// An OBJECT of CSS properties rather than a declaration string, because the
|
|
470
|
+
// caller assigns them one at a time through CSSOM — see `push` above.
|
|
296
471
|
function sgrStyle(codes, prev) {
|
|
297
472
|
const parts = String(codes || "0").split(";").filter((x) => x !== "");
|
|
298
473
|
if (!parts.length || parts[0] === "0") return null;
|
|
299
|
-
const out =
|
|
300
|
-
if (prev) out.push(prev);
|
|
474
|
+
const out = Object.assign({}, prev || {});
|
|
301
475
|
for (let i = 0; i < parts.length; i++) {
|
|
302
476
|
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.
|
|
477
|
+
if (n === 1) out.fontWeight = "700";
|
|
478
|
+
else if (n === 2) out.opacity = ".6";
|
|
479
|
+
else if (n === 3) out.fontStyle = "italic";
|
|
480
|
+
else if (n === 4) out.textDecoration = "underline";
|
|
481
|
+
else if (n === 9) out.textDecoration = "line-through";
|
|
482
|
+
else if (n === 7) out.filter = "invert(1)";
|
|
483
|
+
else if (n >= 30 && n <= 37) out.color = HK_ANSI[n - 30];
|
|
484
|
+
else if (n >= 90 && n <= 97) out.color = HK_ANSI_BRIGHT[n - 90];
|
|
485
|
+
else if (n >= 40 && n <= 47) out.background = HK_ANSI[n - 40];
|
|
486
|
+
else if (n === 39) out.color = "inherit";
|
|
313
487
|
else if (n === 38 && parts[i + 1] === "2") {
|
|
314
|
-
out.
|
|
488
|
+
out.color = "rgb(" + parts[i + 2] + "," + parts[i + 3] + "," + parts[i + 4] + ")";
|
|
315
489
|
i += 4;
|
|
316
490
|
} else if (n === 48 && parts[i + 1] === "2") {
|
|
317
|
-
out.
|
|
491
|
+
out.background = "rgb(" + parts[i + 2] + "," + parts[i + 3] + "," + parts[i + 4] + ")";
|
|
318
492
|
i += 4;
|
|
319
493
|
}
|
|
320
494
|
}
|
|
321
|
-
return out
|
|
495
|
+
return out;
|
|
322
496
|
}
|
|
323
497
|
|
|
324
|
-
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
// be told off for it.
|
|
498
|
+
/* ══ the board ════════════════════════════════════════════════════════════ */
|
|
499
|
+
// THE ILLEGAL DROP IS MADE IMPOSSIBLE rather than allowed and then complained
|
|
500
|
+
// about: an illegal zone renders disabled with its reason on the zone itself. A
|
|
501
|
+
// user should never be able to do the wrong thing and then be told off for it.
|
|
329
502
|
//
|
|
330
503
|
// The CLI still validates. The board is a convenience; it is never the
|
|
331
504
|
// guarantee.
|
|
332
|
-
function boardCard(
|
|
505
|
+
function boardCard(cat, plan) {
|
|
333
506
|
const c = card(t("hookui.board"));
|
|
507
|
+
c.append(el("div", "note", t("hookui.boardAbout")));
|
|
334
508
|
c.append(el("div", "note", cat.dense_prefix));
|
|
335
509
|
|
|
336
|
-
for (const line of
|
|
337
|
-
const legal = lineLegal(
|
|
338
|
-
const wrap = el("div", "hk-line-wrap" + (legal ? "" : " hk-line-blocked"));
|
|
510
|
+
for (const line of plan.lines) {
|
|
511
|
+
const legal = lineLegal(plan, line.line);
|
|
512
|
+
const wrap = el("div", "hk-line-wrap" + (legal.ok ? "" : " hk-line-blocked"));
|
|
513
|
+
|
|
339
514
|
const h = el("div", "hk-line-head");
|
|
340
|
-
|
|
341
|
-
|
|
515
|
+
const title = el("div", "hk-line-title");
|
|
516
|
+
title.append(el("span", "hk-line-name", tn(line.line, "hookui.lineN")));
|
|
517
|
+
title.append(el("span", "hk-line-count" + (line.full ? " hk-full" : ""), tn(line.counted, "hookui.slotsUsed", { max: plan.max })));
|
|
518
|
+
h.append(title);
|
|
519
|
+
h.append(separatorPicker(cat, line));
|
|
342
520
|
wrap.append(h);
|
|
343
521
|
|
|
344
522
|
const zone = el("div", "hk-zone");
|
|
345
523
|
zone.setAttribute("role", "list");
|
|
346
|
-
for (const item of line.items) zone.append(chipEl(item, line,
|
|
524
|
+
for (const item of line.items) zone.append(chipEl(item, line, plan, cat));
|
|
347
525
|
|
|
348
526
|
if (!legal.ok) {
|
|
349
527
|
const blocked = el("div", "hk-blocked");
|
|
350
528
|
blocked.append(el("span", "hk-caution-mark", "✕"));
|
|
351
529
|
blocked.append(el("span", null, legal.why));
|
|
352
530
|
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
531
|
}
|
|
532
|
+
// THE ADD BUTTON ALWAYS RENDERS, and carries its own refusal. A control
|
|
533
|
+
// that vanishes when it cannot be used teaches nobody why — and the two
|
|
534
|
+
// refusals are DIFFERENT facts (nothing above it yet · this line is full),
|
|
535
|
+
// so they are two sentences and never one "invalid".
|
|
536
|
+
const b = el("button", "hk-add", t("hookui.add"));
|
|
537
|
+
b.type = "button";
|
|
538
|
+
if (!legal.ok) {
|
|
539
|
+
b.disabled = true;
|
|
540
|
+
b.title = legal.why;
|
|
541
|
+
} else if (line.full) {
|
|
542
|
+
b.disabled = true;
|
|
543
|
+
b.title = t("hookui.lineFull", { n: line.line, max: plan.max });
|
|
544
|
+
} else {
|
|
545
|
+
b.addEventListener("click", () => partPicker(cat, plan, line.line, null));
|
|
546
|
+
}
|
|
547
|
+
zone.append(b);
|
|
548
|
+
if (b.disabled && legal.ok) zone.append(el("div", "note note-warn hk-zone-why", b.title));
|
|
549
|
+
dropTarget(zone, line, plan);
|
|
363
550
|
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
551
|
c.append(wrap);
|
|
380
552
|
}
|
|
553
|
+
c.append(el("div", "note", t("hookui.dragHint")));
|
|
381
554
|
return c;
|
|
382
555
|
}
|
|
383
556
|
|
|
384
|
-
//
|
|
385
|
-
//
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
557
|
+
// THE SEPARATOR IS A DROPDOWN, and its options are the CLI's. A free-text box
|
|
558
|
+
// asked a person to invent a rhythm and then to type a character that may not
|
|
559
|
+
// be on their keyboard. A value the layout already carries that is NOT in the
|
|
560
|
+
// set keeps its slot, leads the list and is disabled — the `fixed_executor`
|
|
561
|
+
// rule: the state must be visible, never re-offerable.
|
|
562
|
+
function separatorPicker(cat, line) {
|
|
563
|
+
const wrap = el("label", "hk-line-opts");
|
|
564
|
+
wrap.append(el("span", "note", t("hookui.separator")));
|
|
565
|
+
const sel = el("select", "hk-select");
|
|
566
|
+
const known = cat.separators || [];
|
|
567
|
+
if (!known.some((s) => s.value === line.separator)) {
|
|
568
|
+
const o = el("option", null, t("hookui.sepCustom", { s: JSON.stringify(line.separator) }));
|
|
569
|
+
o.value = line.separator;
|
|
570
|
+
o.disabled = true;
|
|
571
|
+
o.selected = true;
|
|
572
|
+
sel.append(o);
|
|
573
|
+
}
|
|
574
|
+
for (const s of known) {
|
|
575
|
+
// The NAME is the CLI's word; the sample is the literal string, shown
|
|
576
|
+
// between two marks so a run of spaces is visible at all.
|
|
577
|
+
const o = el("option", null, s.name + " ‹" + s.value + "›");
|
|
578
|
+
o.value = s.value;
|
|
579
|
+
if (s.value === line.separator) o.selected = true;
|
|
580
|
+
sel.append(o);
|
|
581
|
+
}
|
|
582
|
+
sel.addEventListener("change", () => {
|
|
583
|
+
const hit = known.find((s) => s.value === sel.value);
|
|
584
|
+
hkOp({ op: "sep", line: line.line, value: sel.value, label: t("hookui.opSep", { n: line.line, s: hit ? hit.name : sel.value }) });
|
|
585
|
+
});
|
|
586
|
+
wrap.append(sel);
|
|
587
|
+
return wrap;
|
|
391
588
|
}
|
|
392
589
|
|
|
590
|
+
/* ══ a chip ═══════════════════════════════════════════════════════════════ */
|
|
393
591
|
// A chip shows ITS OWN RENDERED OUTPUT, not its component id — that is what the
|
|
394
|
-
// user is arranging
|
|
395
|
-
|
|
592
|
+
// user is arranging — with the id and a one-line description under it, because
|
|
593
|
+
// "what is this thing" is the first question anybody asks of a bar full of
|
|
594
|
+
// symbols, and the answer used to live only in a tooltip.
|
|
595
|
+
function chipEl(item, line, plan, cat) {
|
|
396
596
|
const comp = (cat.components || []).find((x) => x.id === item.type);
|
|
397
|
-
const wrap = el("div", "hk-chip" + (
|
|
597
|
+
const wrap = el("div", "hk-chip" + (item.isNew ? " hk-chip-new" : ""));
|
|
398
598
|
wrap.setAttribute("role", "listitem");
|
|
399
599
|
wrap.setAttribute("tabindex", "0");
|
|
400
|
-
wrap.
|
|
600
|
+
wrap.draggable = hkCanDrag();
|
|
601
|
+
wrap.title = comp && comp.summary ? comp.summary : item.type;
|
|
401
602
|
|
|
402
603
|
const face = el("div", "hk-chip-face");
|
|
403
|
-
|
|
404
|
-
face.append(ansiBlock(
|
|
604
|
+
face.append(el("span", "hk-grip", "∷"));
|
|
605
|
+
face.append(ansiBlock(chipSample(item, comp), "hk-preview hk-preview-chip"));
|
|
405
606
|
wrap.append(face);
|
|
406
607
|
|
|
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", "→");
|
|
608
|
+
wrap.append(el("div", "hk-chip-id", item.type));
|
|
609
|
+
if (comp && comp.summary) wrap.append(el("div", "hk-chip-desc", comp.summary));
|
|
610
|
+
|
|
611
|
+
// EDIT · MOVE · REMOVE, on the chip. Each opens a modal that says what it is
|
|
612
|
+
// about to do. The drag is the shortcut, never the mechanism: pointer drag is
|
|
613
|
+
// unusable for a real fraction of people, and it is switched off entirely
|
|
614
|
+
// below the width at which a drop gap stops being a target.
|
|
615
|
+
const acts = el("div", "hk-chip-acts");
|
|
616
|
+
const mk = (label, cls, fn) => {
|
|
617
|
+
const b = el("button", "hk-act " + cls, label);
|
|
453
618
|
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);
|
|
619
|
+
b.addEventListener("click", fn);
|
|
620
|
+
acts.append(b);
|
|
621
|
+
};
|
|
622
|
+
mk(t("hookui.edit"), "hk-act-edit", () => editModal(item, line, comp, cat));
|
|
623
|
+
mk(t("hookui.move"), "hk-act-move", () => moveModal(item, line, plan));
|
|
624
|
+
mk(t("hookui.remove"), "hk-act-remove", () => removeModal(item, comp));
|
|
625
|
+
wrap.append(acts);
|
|
477
626
|
|
|
478
627
|
// Keyboard: the settled pattern, and every drag has an equivalent here. A
|
|
479
628
|
// board only reachable by mouse is a board a lot of people cannot use.
|
|
480
629
|
wrap.addEventListener("keydown", (ev) => {
|
|
630
|
+
const pos = line.items.indexOf(item) + 1;
|
|
481
631
|
if (ev.key === "Delete" || ev.key === "Backspace") {
|
|
482
632
|
ev.preventDefault();
|
|
483
|
-
|
|
484
|
-
bd({ at: line.line + ":" + item.pos }), t("hookui.removed", { id: item.type }));
|
|
633
|
+
removeModal(item, comp);
|
|
485
634
|
} else if (ev.key === "e" || ev.key === "E") {
|
|
486
635
|
ev.preventDefault();
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
} else if (ev.key === "ArrowLeft" && item.pos > 1) {
|
|
636
|
+
editModal(item, line, comp, cat);
|
|
637
|
+
} else if (ev.key === "ArrowLeft" && pos > 1) {
|
|
490
638
|
ev.preventDefault();
|
|
491
|
-
moveTo(item, line
|
|
492
|
-
} else if (ev.key === "ArrowRight" &&
|
|
639
|
+
moveTo(item, line.line, pos - 1);
|
|
640
|
+
} else if (ev.key === "ArrowRight" && pos < line.items.length) {
|
|
493
641
|
ev.preventDefault();
|
|
494
|
-
moveTo(item, line
|
|
642
|
+
moveTo(item, line.line, pos + 1);
|
|
495
643
|
} else if (ev.key === "ArrowUp" || ev.key === "ArrowDown") {
|
|
496
644
|
const target = line.line + (ev.key === "ArrowUp" ? -1 : 1);
|
|
497
|
-
if (target < 1 || target >
|
|
645
|
+
if (target < 1 || target > plan.lines.length) return;
|
|
498
646
|
ev.preventDefault();
|
|
499
|
-
const
|
|
500
|
-
//
|
|
501
|
-
// silent no-op.
|
|
502
|
-
if (
|
|
503
|
-
moveTo(item,
|
|
647
|
+
const why = addBlocked(plan, target);
|
|
648
|
+
// A line that cannot take it is SKIPPED and the live region says why —
|
|
649
|
+
// never a silent no-op.
|
|
650
|
+
if (why) return announce(why);
|
|
651
|
+
moveTo(item, target, 1);
|
|
504
652
|
}
|
|
505
653
|
});
|
|
506
654
|
|
|
507
|
-
|
|
655
|
+
wrap.addEventListener("dragstart", (ev) => {
|
|
656
|
+
HK_DRAG = { ref: item.ref, from: line.line };
|
|
657
|
+
wrap.classList.add("hk-chip-dragging");
|
|
658
|
+
try {
|
|
659
|
+
ev.dataTransfer.effectAllowed = "move";
|
|
660
|
+
ev.dataTransfer.setData("text/plain", item.ref);
|
|
661
|
+
} catch (_) {}
|
|
662
|
+
});
|
|
663
|
+
wrap.addEventListener("dragend", () => {
|
|
664
|
+
HK_DRAG = null;
|
|
665
|
+
wrap.classList.remove("hk-chip-dragging");
|
|
666
|
+
for (const n of document.querySelectorAll(".hk-drop-before, .hk-drop-after"))
|
|
667
|
+
n.classList.remove("hk-drop-before", "hk-drop-after");
|
|
668
|
+
});
|
|
669
|
+
wrap.addEventListener("dragover", (ev) => {
|
|
670
|
+
if (!HK_DRAG || HK_DRAG.ref === item.ref) return;
|
|
671
|
+
ev.preventDefault();
|
|
672
|
+
const r = wrap.getBoundingClientRect();
|
|
673
|
+
const before = ev.clientX < r.left + r.width / 2;
|
|
674
|
+
wrap.classList.toggle("hk-drop-before", before);
|
|
675
|
+
wrap.classList.toggle("hk-drop-after", !before);
|
|
676
|
+
});
|
|
677
|
+
wrap.addEventListener("dragleave", () => wrap.classList.remove("hk-drop-before", "hk-drop-after"));
|
|
678
|
+
wrap.addEventListener("drop", (ev) => {
|
|
679
|
+
if (!HK_DRAG || HK_DRAG.ref === item.ref) return;
|
|
680
|
+
ev.preventDefault();
|
|
681
|
+
ev.stopPropagation();
|
|
682
|
+
const before = wrap.classList.contains("hk-drop-before");
|
|
683
|
+
const pos = line.items.indexOf(item) + 1;
|
|
684
|
+
dropOnto(line, plan, before ? pos : pos + 1);
|
|
685
|
+
});
|
|
508
686
|
return wrap;
|
|
509
687
|
}
|
|
510
688
|
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
689
|
+
// What this chip will look like in the bar. A part staged in this session has
|
|
690
|
+
// no rendered sample from the CLI yet, so it borrows its component's default —
|
|
691
|
+
// which is exactly what it will render as until somebody changes its shape.
|
|
692
|
+
function chipSample(item, comp) {
|
|
693
|
+
if (!comp) return item.type;
|
|
694
|
+
const render = item.over.render || (item.src ? item.src.render : (comp.defaults && comp.defaults.render) || comp.renderers[0]);
|
|
695
|
+
const s = comp.previews ? comp.previews[render] : null;
|
|
696
|
+
return s || item.type;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// A zone is a drop target across its WHOLE area, so a drop past the last chip
|
|
700
|
+
// appends instead of doing nothing.
|
|
701
|
+
function dropTarget(zone, line, plan) {
|
|
702
|
+
zone.addEventListener("dragover", (ev) => {
|
|
703
|
+
if (!HK_DRAG) return;
|
|
704
|
+
if (HK_DRAG.from !== line.line && addBlocked(plan, line.line)) return;
|
|
705
|
+
ev.preventDefault();
|
|
706
|
+
zone.classList.add("hk-zone-hot");
|
|
707
|
+
});
|
|
708
|
+
zone.addEventListener("dragleave", () => zone.classList.remove("hk-zone-hot"));
|
|
709
|
+
zone.addEventListener("drop", (ev) => {
|
|
710
|
+
zone.classList.remove("hk-zone-hot");
|
|
711
|
+
if (!HK_DRAG) return;
|
|
712
|
+
ev.preventDefault();
|
|
713
|
+
dropOnto(line, plan, line.items.length + 1);
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function dropOnto(line, plan, pos) {
|
|
718
|
+
if (!HK_DRAG) return;
|
|
719
|
+
const all = plan.lines.reduce((acc, l) => acc.concat(l.items), []);
|
|
720
|
+
const moving = all.find((x) => x.ref === HK_DRAG.ref);
|
|
721
|
+
const from = HK_DRAG.from;
|
|
722
|
+
HK_DRAG = null;
|
|
723
|
+
if (!moving) return;
|
|
724
|
+
// Reordering WITHIN a line never changes how many parts it holds, so the
|
|
725
|
+
// five-slot cap and the dense-prefix rule only apply to a move that crosses
|
|
726
|
+
// lines.
|
|
727
|
+
if (from !== line.line) {
|
|
728
|
+
const why = addBlocked(plan, line.line);
|
|
729
|
+
if (why) return toast(why, "bad");
|
|
730
|
+
}
|
|
731
|
+
moveTo(moving, line.line, pos);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function moveTo(item, toLine, toPos) {
|
|
735
|
+
hkOp({ op: "move", ref: item.ref, toLine, toPos });
|
|
736
|
+
announce(t("hookui.opMove", { id: item.type, n: toLine }));
|
|
516
737
|
}
|
|
517
738
|
|
|
518
739
|
// `aria-grabbed` is deprecated and is not used. State is announced through a
|
|
@@ -530,85 +751,278 @@ function announce(msg) {
|
|
|
530
751
|
region.textContent = msg;
|
|
531
752
|
}
|
|
532
753
|
|
|
533
|
-
|
|
534
|
-
//
|
|
535
|
-
//
|
|
754
|
+
/* ══ the part picker ══════════════════════════════════════════════════════ */
|
|
755
|
+
// ONE MODAL, one search box, one click. It is the only place a part is ever
|
|
756
|
+
// chosen — for a new slot and for swapping an existing one — so there is
|
|
757
|
+
// exactly one answer to "how do I put something on the bar".
|
|
536
758
|
//
|
|
537
|
-
//
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
//
|
|
541
|
-
|
|
542
|
-
const
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
759
|
+
// `replace` names the chip whose part is being swapped; absent, this is an add.
|
|
760
|
+
function partPicker(cat, plan, line, replace) {
|
|
761
|
+
let q = "";
|
|
762
|
+
// NOT named `body`: that name is reserved for a panel's own card container,
|
|
763
|
+
// which must carry `stack`. This is a modal's contents and spaces itself.
|
|
764
|
+
const pane = el("div", "hk-picker");
|
|
765
|
+
|
|
766
|
+
const searchRow = el("div", "hk-picker-search");
|
|
767
|
+
const search = el("input", "hk-input hk-picker-input");
|
|
768
|
+
search.type = "search";
|
|
769
|
+
search.placeholder = t("hookui.searchPlaceholder");
|
|
770
|
+
const count = el("span", "note");
|
|
771
|
+
searchRow.append(search, count);
|
|
772
|
+
pane.append(searchRow);
|
|
773
|
+
pane.append(el("div", "note", replace ? t("hookui.pickerReplaceAbout") : tn(line, "hookui.pickerAbout")));
|
|
774
|
+
|
|
775
|
+
const list = el("div", "hk-picker-list");
|
|
776
|
+
pane.append(list);
|
|
777
|
+
|
|
778
|
+
const closer = () => close();
|
|
779
|
+
const paint = () => {
|
|
780
|
+
list.replaceChildren();
|
|
781
|
+
const rows = (cat.components || []).filter((c) => (q ? matches(c, q) : true));
|
|
782
|
+
count.textContent = tn(rows.length, "hookui.matches");
|
|
783
|
+
if (!rows.length) {
|
|
784
|
+
list.append(empty(t("hookui.noMatch")));
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
for (const comp of rows) list.append(pickerRow(comp, line, replace, closer));
|
|
551
788
|
};
|
|
789
|
+
search.addEventListener("input", () => {
|
|
790
|
+
q = search.value.trim().toLowerCase();
|
|
791
|
+
paint();
|
|
792
|
+
});
|
|
793
|
+
paint();
|
|
552
794
|
|
|
553
|
-
|
|
554
|
-
|
|
795
|
+
const close = modal({
|
|
796
|
+
title: replace ? t("hookui.pickerReplaceTitle") : tn(line, "hookui.pickerTitle"),
|
|
797
|
+
body: pane,
|
|
798
|
+
actions: [{ label: t("common.cancel"), cls: "btn-ghost", onClick: (c) => c() }],
|
|
799
|
+
});
|
|
800
|
+
setTimeout(() => search.focus(), 40);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// A row says WHAT the part is, in a sentence, next to what it will look like.
|
|
804
|
+
// A REFUSED part is drawn and is NOT clickable — never a disabled button, and
|
|
805
|
+
// never absent: "we decided against this" and "we forgot" must not look the
|
|
806
|
+
// same, which is why the row exists at all.
|
|
807
|
+
function pickerRow(comp, line, replace, close) {
|
|
808
|
+
const refused = comp.cost === "refused";
|
|
809
|
+
const row = el(refused ? "div" : "button", "hk-pick-row" + (refused ? " hk-pick-row-off" : ""));
|
|
810
|
+
if (!refused) row.type = "button";
|
|
811
|
+
|
|
812
|
+
const top = el("div", "hk-pick-top");
|
|
813
|
+
top.append(el("span", "hk-prow-id", comp.id));
|
|
814
|
+
// The cost chip is the CLI's own word. A friendlier synonym would be a state
|
|
815
|
+
// that does not exist.
|
|
816
|
+
top.append(chip(comp.cost, refused ? "bad" : null));
|
|
817
|
+
row.append(top);
|
|
818
|
+
if (comp.summary) row.append(el("div", "hk-pick-desc", comp.summary));
|
|
819
|
+
const sample = comp.previews && comp.previews[(comp.defaults && comp.defaults.render) || comp.renderers[0]];
|
|
820
|
+
if (sample) row.append(ansiBlock(sample, "hk-preview hk-preview-chip"));
|
|
821
|
+
if (refused) {
|
|
822
|
+
row.append(el("div", "note note-warn", comp.refused_reason));
|
|
823
|
+
return row;
|
|
824
|
+
}
|
|
825
|
+
row.addEventListener("click", () => {
|
|
826
|
+
if (replace) hkOp({ op: "set", ref: replace.ref, field: "type", value: comp.id, label: t("hookui.opSwap", { id: comp.id }) });
|
|
827
|
+
else hkOp({ op: "add", line, type: comp.id });
|
|
828
|
+
close();
|
|
829
|
+
});
|
|
830
|
+
return row;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// Fuzzy plus INITIALISM: with a catalogue this size a substring search is not
|
|
834
|
+
// enough — two letters should find a two-word name, which plain substring
|
|
835
|
+
// matching does not.
|
|
836
|
+
function matches(comp, q) {
|
|
837
|
+
const id = comp.id.toLowerCase();
|
|
838
|
+
if (id.includes(q)) return true;
|
|
839
|
+
if ((comp.summary || "").toLowerCase().includes(q)) return true;
|
|
840
|
+
if ((comp.label || "").toLowerCase().includes(q)) return true;
|
|
841
|
+
const initials = id.split("-").map((p) => p[0]).join("");
|
|
842
|
+
if (initials.startsWith(q)) return true;
|
|
843
|
+
let i = 0;
|
|
844
|
+
for (const ch of id) if (ch === q[i]) i++;
|
|
845
|
+
return i === q.length;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/* ══ move ═════════════════════════════════════════════════════════════════ */
|
|
849
|
+
// A modal that says what it is about to do — and on a narrow screen it is the
|
|
850
|
+
// ONLY way to move a chip, because drag is off below the width at which a drop
|
|
851
|
+
// gap stops being a target.
|
|
852
|
+
function moveModal(item, line, plan) {
|
|
853
|
+
const body = el("div", "stack");
|
|
854
|
+
body.append(el("div", "note", t("hookui.moveAbout", { id: item.type })));
|
|
855
|
+
|
|
856
|
+
for (const target of plan.lines) {
|
|
857
|
+
const legal = lineLegal(plan, target.line);
|
|
858
|
+
const sameLine = target.line === line.line;
|
|
859
|
+
const box = el("div", "hk-move-line");
|
|
860
|
+
box.append(el("div", "hk-line-name", tn(target.line, "hookui.lineN")));
|
|
861
|
+
// A LINE THAT CANNOT TAKE IT SAYS SO ONCE. Six identical disabled slots is
|
|
862
|
+
// the same refusal printed six times, and the reason is about the LINE
|
|
863
|
+
// rather than about any one slot on it.
|
|
864
|
+
const blocked = sameLine ? null : addBlocked(plan, target.line);
|
|
865
|
+
if (blocked) {
|
|
866
|
+
box.append(el("div", "note note-warn", blocked));
|
|
867
|
+
body.append(box);
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
const slots = el("div", "hk-move-slots");
|
|
871
|
+
const n = Math.max(1, target.items.length + (sameLine ? 0 : 1));
|
|
872
|
+
for (let p = 1; p <= n; p++) {
|
|
873
|
+
const b = el("button", "btn btn-xs btn-ghost", tn(p, "hookui.slotN"));
|
|
874
|
+
b.type = "button";
|
|
875
|
+
if (sameLine && p === line.items.indexOf(item) + 1) {
|
|
876
|
+
b.disabled = true;
|
|
877
|
+
b.title = t("hookui.alreadyHere");
|
|
878
|
+
} else {
|
|
879
|
+
b.addEventListener("click", () => {
|
|
880
|
+
close();
|
|
881
|
+
moveTo(item, target.line, p);
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
slots.append(b);
|
|
885
|
+
}
|
|
886
|
+
box.append(slots);
|
|
887
|
+
body.append(box);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
const close = modal({
|
|
891
|
+
title: t("hookui.moveTitle"),
|
|
892
|
+
body,
|
|
893
|
+
actions: [{ label: t("common.cancel"), cls: "btn-ghost", onClick: (c) => c() }],
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/* ══ remove ═══════════════════════════════════════════════════════════════ */
|
|
898
|
+
function removeModal(item, comp) {
|
|
899
|
+
const body = el("div", "stack");
|
|
900
|
+
body.append(el("div", "note", t("hookui.removeAbout", { id: item.type })));
|
|
901
|
+
if (comp && comp.summary) body.append(el("div", "note", comp.summary));
|
|
902
|
+
modal({
|
|
903
|
+
title: t("hookui.removeTitle"),
|
|
904
|
+
body,
|
|
905
|
+
actions: [
|
|
906
|
+
{ label: t("common.cancel"), cls: "btn-ghost", onClick: (c) => c() },
|
|
907
|
+
{
|
|
908
|
+
label: t("hookui.remove"),
|
|
909
|
+
cls: "btn-primary",
|
|
910
|
+
onClick: (c) => {
|
|
911
|
+
c();
|
|
912
|
+
hkOp({ op: "remove", ref: item.ref });
|
|
913
|
+
},
|
|
914
|
+
},
|
|
915
|
+
],
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/* ══ the part editor ══════════════════════════════════════════════════════ */
|
|
920
|
+
// A MODAL, because the board is what the user is looking at, and a drawer that
|
|
921
|
+
// opened inside a chip pushed the whole board down under the thing being
|
|
922
|
+
// edited.
|
|
923
|
+
//
|
|
924
|
+
// NO FIELD NAMES, AND EVERY CONTROL CARRIES A SENTENCE. `min_width` and
|
|
925
|
+
// `hide_when` are the JSON's words; this says "Least width" and explains what
|
|
926
|
+
// it does. The JSON is for the diff, the editor is for the person — and the
|
|
927
|
+
// ids, renderers and state words that DO appear are the CLI's own and are never
|
|
928
|
+
// translated.
|
|
929
|
+
function editModal(item, line, comp, cat) {
|
|
930
|
+
// NOT named `body`: that name is reserved for a panel's own card container,
|
|
931
|
+
// which must carry `stack`. This is a modal's contents and spaces itself.
|
|
932
|
+
const pane = el("div", "hk-editor");
|
|
933
|
+
if (!comp) {
|
|
934
|
+
pane.append(el("div", "note", t("hookui.unknownComponent")));
|
|
935
|
+
modal({ title: item.type, body: pane, actions: [{ label: t("common.close"), cls: "btn-ghost", onClick: (c) => c() }] });
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const val = (field, dflt) => (field in item.over ? item.over[field] : item.src && item.src[field] !== undefined && item.src[field] !== null ? item.src[field] : dflt);
|
|
939
|
+
const stage = (field, value, label) => hkOp({ op: "set", ref: item.ref, field, value, label });
|
|
940
|
+
|
|
941
|
+
const live = el("div", "hk-editor-live");
|
|
942
|
+
live.append(ansiBlock(chipSample(item, comp), "hk-preview hk-preview-sm"));
|
|
943
|
+
pane.append(live);
|
|
944
|
+
if (comp.summary) pane.append(el("div", "note", comp.summary));
|
|
945
|
+
|
|
946
|
+
// Swapping the part itself, from inside the editor: "this slot, but a
|
|
947
|
+
// different thing" is a real edit and it should not need a remove and an add.
|
|
948
|
+
const swap = el("button", "btn btn-xs btn-ghost hk-swap", t("hookui.swap"));
|
|
949
|
+
swap.type = "button";
|
|
950
|
+
swap.addEventListener("click", () => {
|
|
951
|
+
close();
|
|
952
|
+
partPicker(cat, HK_NO_LINES, 0, item);
|
|
953
|
+
});
|
|
954
|
+
pane.append(swap);
|
|
555
955
|
|
|
556
956
|
// THE SHAPE PICKER IS A GALLERY, NOT A DROPDOWN. Every renderer the component
|
|
557
957
|
// declares, drawn with this component's real current value. A dropdown
|
|
558
|
-
// containing
|
|
958
|
+
// containing a renderer's name tells nobody anything, and this is the
|
|
559
959
|
// difference between a feature people use and a settings form they close.
|
|
560
|
-
|
|
960
|
+
pane.append(sectionHead(t("hookui.shape"), t("hookui.shapeAbout")));
|
|
561
961
|
const gallery = el("div", "hk-gallery");
|
|
962
|
+
const curRender = val("render", (comp.defaults && comp.defaults.render) || comp.renderers[0]);
|
|
562
963
|
for (const r of comp.renderers) {
|
|
563
|
-
const b = el("button", "hk-sample" + (
|
|
964
|
+
const b = el("button", "hk-sample" + (curRender === r ? " hk-sample-on" : ""));
|
|
564
965
|
b.type = "button";
|
|
565
966
|
b.title = r;
|
|
566
967
|
b.append(ansiBlock(comp.previews && comp.previews[r] ? comp.previews[r] : r, "hk-preview hk-preview-chip"));
|
|
567
968
|
b.append(el("span", "hk-sample-id", r));
|
|
568
|
-
b.addEventListener("click", () =>
|
|
969
|
+
b.addEventListener("click", () => {
|
|
970
|
+
close();
|
|
971
|
+
stage("render", r, t("hookui.shapeChange", { id: comp.id, r }));
|
|
972
|
+
});
|
|
569
973
|
gallery.append(b);
|
|
570
974
|
}
|
|
571
|
-
|
|
975
|
+
pane.append(gallery);
|
|
572
976
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
d.append(pickRow(cat.glyph_sets, null, (v) => stage("glyphs", v, t("hookui.glyphChange", { id: comp.id, v }))));
|
|
977
|
+
pane.append(sectionHead(t("hookui.glyphs"), t("hookui.glyphsAbout")));
|
|
978
|
+
pane.append(pickRow(cat.glyph_sets, val("glyphs", null), (v) => stage("glyphs", v, t("hookui.glyphChange", { id: comp.id, v }))));
|
|
576
979
|
|
|
577
|
-
|
|
980
|
+
pane.append(sectionHead(t("hookui.words"), t("hookui.wordsAbout")));
|
|
578
981
|
const label = el("input", "hk-input");
|
|
579
982
|
label.type = "text";
|
|
580
|
-
|
|
983
|
+
const cur = val("label", "");
|
|
984
|
+
label.value = cur == null ? "" : String(cur);
|
|
581
985
|
label.placeholder = t("hookui.labelPlaceholder");
|
|
582
986
|
label.addEventListener("change", () => stage("label", label.value, t("hookui.labelChange", { id: comp.id, v: label.value })));
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
987
|
+
pane.append(labelled(t("hookui.label"), label, t("hookui.labelAbout")));
|
|
988
|
+
pane.append(labelled(t("hookui.case"), pickRow(cat.cases, val("case", null), (v) => stage("case", v, t("hookui.caseChange", { id: comp.id, v }))), t("hookui.caseAbout")));
|
|
989
|
+
pane.append(labelled(t("hookui.before"), textField(val("prefix", ""), (v) => stage("prefix", v, t("hookui.beforeChange", { id: comp.id, v }))), t("hookui.beforeAbout")));
|
|
990
|
+
pane.append(labelled(t("hookui.after"), textField(val("suffix", ""), (v) => stage("suffix", v, t("hookui.afterChange", { id: comp.id, v }))), t("hookui.afterAbout")));
|
|
991
|
+
|
|
992
|
+
pane.append(sectionHead(t("hookui.colour"), t("hookui.colourAbout")));
|
|
993
|
+
pane.append(labelled(t("hookui.labelColour"), pickRow(cat.colors, val("label_color", null), (v) => stage("label_color", v, t("hookui.colourChange", { id: comp.id, v }))), t("hookui.labelColourAbout")));
|
|
994
|
+
pane.append(labelled(t("hookui.valueColour"), pickRow(cat.colors, val("value_color", null), (v) => stage("value_color", v, t("hookui.colourChange", { id: comp.id, v }))), t("hookui.valueColourAbout")));
|
|
589
995
|
if (comp.bounded)
|
|
590
|
-
|
|
996
|
+
pane.append(labelled(t("hookui.ramp"), pickRow(Object.keys(cat.ramps), val("ramp", null), (v) => stage("ramp", v, t("hookui.rampChange", { id: comp.id, v }))), t("hookui.rampAbout")));
|
|
591
997
|
// R3, AT THE CONTROL, at the moment the choice is made — never in a
|
|
592
998
|
// validation summary later.
|
|
593
|
-
if (comp.states)
|
|
999
|
+
if (comp.states) pane.append(el("div", "note note-warn", t("hookui.stateColourWarn", { s: comp.states.join(", ") })));
|
|
594
1000
|
|
|
595
|
-
// "
|
|
1001
|
+
// "Weight", never "font size". A terminal owns its font, and a picker that
|
|
596
1002
|
// did nothing would be worse than not offering one.
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
if (comp.bounded || comp.defaults.format) {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
1003
|
+
pane.append(labelled(t("hookui.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")));
|
|
1004
|
+
pane.append(el("div", "note", t("hookui.fontWhy")));
|
|
1005
|
+
|
|
1006
|
+
if (comp.bounded || (comp.defaults && comp.defaults.format)) {
|
|
1007
|
+
pane.append(sectionHead(t("hookui.numbers"), t("hookui.numbersAbout")));
|
|
1008
|
+
pane.append(labelled(t("hookui.format"), pickRow(cat.formats, val("format", null), (v) => stage("format", v, t("hookui.formatChange", { id: comp.id, v }))), t("hookui.formatAbout")));
|
|
1009
|
+
pane.append(labelled(t("hookui.compact"), pickRow(cat.compact, val("compact", null), (v) => stage("compact", v, t("hookui.compactChange", { id: comp.id, v }))), t("hookui.compactAbout")));
|
|
1010
|
+
// `min_width` is not cosmetic: a value that changes width shifts every part
|
|
1011
|
+
// to its right on the keystroke it happens, and that jitter is the main
|
|
1012
|
+
// reason people turn a status line off again.
|
|
1013
|
+
pane.append(labelled(t("hookui.minWidth"), numField(val("min_width", null), 0, 8, (v) => stage("min_width", v, t("hookui.minWidthChange", { id: comp.id, v }))), t("hookui.minWidthWarn")));
|
|
1014
|
+
pane.append(labelled(t("hookui.precision"), numField(val("precision", null), 0, 2, (v) => stage("precision", v, t("hookui.precisionChange", { id: comp.id, v }))), t("hookui.precisionAbout")));
|
|
605
1015
|
}
|
|
606
1016
|
|
|
1017
|
+
const rend = cat.renderers[curRender];
|
|
1018
|
+
if (rend && rend.width)
|
|
1019
|
+
pane.append(labelled(t("hookui.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")));
|
|
1020
|
+
|
|
607
1021
|
// A CHECKLIST, not an enum: "hide when zero" and "hide when there is no run"
|
|
608
1022
|
// are independent facts and a user wants both at once.
|
|
609
|
-
|
|
1023
|
+
pane.append(sectionHead(t("hookui.whenToShow"), t("hookui.whenToShowAbout")));
|
|
610
1024
|
const checks = el("div", "hk-checks");
|
|
611
|
-
const active = new Set(
|
|
1025
|
+
const active = new Set(val("hide_when", []) || []);
|
|
612
1026
|
for (const h of cat.hide_when) {
|
|
613
1027
|
if (h.id === "never") continue;
|
|
614
1028
|
const lab = el("label", "hk-check");
|
|
@@ -625,51 +1039,52 @@ function editorDrawer(item, line, comp, cat, edits) {
|
|
|
625
1039
|
lab.append(box, el("span", null, h.id), el("span", "note", h.says));
|
|
626
1040
|
checks.append(lab);
|
|
627
1041
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
// The rest of the number controls. `min_width` is not cosmetic: a value that
|
|
631
|
-
// changes width shifts every component to its right on the keystroke it
|
|
632
|
-
// happens, and that jitter is the main reason people turn a status line off.
|
|
633
|
-
if (comp.bounded || comp.defaults.format) {
|
|
634
|
-
d.append(labelled(t("hookui.minWidth"), numField(item.min_width, 0, 8, (v) =>
|
|
635
|
-
stage("min_width", v, t("hookui.minWidthChange", { id: comp.id, v })))));
|
|
636
|
-
d.append(labelled(t("hookui.precision"), numField(item.precision, 0, 2, (v) =>
|
|
637
|
-
stage("precision", v, t("hookui.precisionChange", { id: comp.id, v })))));
|
|
638
|
-
}
|
|
639
|
-
const rend = cat.renderers[item.render];
|
|
640
|
-
if (rend && rend.width) {
|
|
641
|
-
d.append(labelled(t("hookui.width"), numField(item.width, rend.width[0], rend.width[1], (v) =>
|
|
642
|
-
stage("width", v, t("hookui.widthChange", { id: comp.id, v })))));
|
|
643
|
-
d.append(el("div", "note", t("hookui.widthWhy")));
|
|
644
|
-
}
|
|
645
|
-
d.append(labelled(t("hookui.before"), textField(item.prefix, (v) =>
|
|
646
|
-
stage("prefix", v, t("hookui.beforeChange", { id: comp.id, v })))));
|
|
647
|
-
d.append(labelled(t("hookui.after"), textField(item.suffix, (v) =>
|
|
648
|
-
stage("suffix", v, t("hookui.afterChange", { id: comp.id, v })))));
|
|
1042
|
+
pane.append(checks);
|
|
649
1043
|
|
|
650
1044
|
// RESPONSIVE WIDTH: the range of terminal widths in which this part is worth
|
|
651
1045
|
// its cells. Strictly better than truncating the right of an overflowing
|
|
652
1046
|
// line, because the USER chooses what survives a narrow terminal.
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
1047
|
+
pane.append(sectionHead(t("hookui.narrow"), t("hookui.narrowAbout")));
|
|
1048
|
+
pane.append(labelled(t("hookui.minCols"), numField(val("min_cols", null), 0, 200, (v) => stage("min_cols", v, t("hookui.minColsChange", { id: comp.id, v }))), t("hookui.minColsAbout")));
|
|
1049
|
+
pane.append(labelled(t("hookui.priority"), numField(val("priority", null), 1, 5, (v) => stage("priority", v, t("hookui.priorityChange", { id: comp.id, v }))), t("hookui.priorityWhy")));
|
|
1050
|
+
|
|
1051
|
+
const close = modal({
|
|
1052
|
+
title: comp.id,
|
|
1053
|
+
body: pane,
|
|
1054
|
+
actions: [
|
|
1055
|
+
// WHERE EACH VALUE CAME FROM. It is how a user finds their own overrides
|
|
1056
|
+
// again after a colour set change — `orc lane config`'s problem, at part
|
|
1057
|
+
// scale. It reads the SAVED item, so it is offered only for one.
|
|
1058
|
+
...(item.src
|
|
1059
|
+
? [{
|
|
1060
|
+
label: t("hookui.explain"),
|
|
1061
|
+
cls: "btn-ghost",
|
|
1062
|
+
onClick: async () => {
|
|
1063
|
+
const r = await read("/api/statusline/explain?board=" + HK_BOARD + "&at=" + line.line + ":" + (line.items.indexOf(item) + 1));
|
|
1064
|
+
if (!r.data || !r.data.ok) return toast(t("hookui.explainFailed"), "bad");
|
|
1065
|
+
modal({
|
|
1066
|
+
title: comp.id,
|
|
1067
|
+
pane: kvList(r.data.resolved.map((x) => [x.field, x.source])),
|
|
1068
|
+
actions: [{ label: t("common.close"), cls: "btn-ghost", onClick: (c) => c() }],
|
|
1069
|
+
});
|
|
1070
|
+
},
|
|
1071
|
+
}]
|
|
1072
|
+
: []),
|
|
1073
|
+
{ label: t("common.close"), cls: "btn-ghost", onClick: (c) => c() },
|
|
1074
|
+
],
|
|
670
1075
|
});
|
|
671
|
-
|
|
672
|
-
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
// `partPicker` reads a board only to answer "can a part go on that line". A
|
|
1079
|
+
// SWAP changes nothing about where the part sits, so it is handed a board with
|
|
1080
|
+
// no lines and never asks.
|
|
1081
|
+
const HK_NO_LINES = { lines: [], max: 5 };
|
|
1082
|
+
|
|
1083
|
+
function sectionHead(name, about) {
|
|
1084
|
+
const w = el("div", "hk-section-wrap");
|
|
1085
|
+
w.append(el("div", "hk-section", name));
|
|
1086
|
+
if (about) w.append(el("div", "note", about));
|
|
1087
|
+
return w;
|
|
673
1088
|
}
|
|
674
1089
|
|
|
675
1090
|
function numField(value, lo, hi, onSet) {
|
|
@@ -690,9 +1105,15 @@ function textField(value, onSet) {
|
|
|
690
1105
|
return i;
|
|
691
1106
|
}
|
|
692
1107
|
|
|
693
|
-
|
|
1108
|
+
// A control, its name, and — because a noun on its own is not an explanation —
|
|
1109
|
+
// the sentence that says what it does. That sentence is most of this release:
|
|
1110
|
+
// a settings form whose labels are nouns is a form people close.
|
|
1111
|
+
function labelled(name, node, about) {
|
|
694
1112
|
const row = el("div", "hk-field");
|
|
695
|
-
|
|
1113
|
+
const left = el("div", "hk-field-left");
|
|
1114
|
+
left.append(el("span", "hk-field-name", name));
|
|
1115
|
+
if (about) left.append(el("span", "note", about));
|
|
1116
|
+
row.append(left);
|
|
696
1117
|
row.append(node);
|
|
697
1118
|
return row;
|
|
698
1119
|
}
|
|
@@ -710,38 +1131,39 @@ function pickRow(options, current, onPick) {
|
|
|
710
1131
|
return row;
|
|
711
1132
|
}
|
|
712
1133
|
|
|
713
|
-
|
|
714
|
-
|
|
1134
|
+
/* ══ the reference list ═══════════════════════════════════════════════════ */
|
|
1135
|
+
// WHAT EACH PART SHOWS, and nothing else. It used to carry three `+ line N`
|
|
1136
|
+
// buttons per row, which put the act of building the bar a long way from the
|
|
1137
|
+
// bar — and computed a position against a layout that was no longer the one
|
|
1138
|
+
// being staged. Adding happens on the board now; this is a reference.
|
|
1139
|
+
function referenceCard(cat) {
|
|
715
1140
|
const right = el("div", "row-actions");
|
|
716
|
-
const search = el("input", "hk-palette-search");
|
|
1141
|
+
const search = el("input", "hk-input hk-palette-search");
|
|
717
1142
|
search.type = "search";
|
|
718
1143
|
search.value = HK_SEARCH;
|
|
719
1144
|
search.placeholder = t("hookui.searchPlaceholder");
|
|
720
|
-
search.addEventListener("input", () => {
|
|
721
|
-
HK_SEARCH = search.value;
|
|
722
|
-
paint();
|
|
723
|
-
});
|
|
724
1145
|
right.append(search);
|
|
725
1146
|
|
|
726
|
-
const c = card(t("hookui.
|
|
1147
|
+
const c = card(t("hookui.reference"), right);
|
|
1148
|
+
c.append(el("div", "note", t("hookui.referenceAbout")));
|
|
727
1149
|
const body = el("div", "stack");
|
|
728
1150
|
c.append(body);
|
|
729
1151
|
|
|
730
1152
|
function paint() {
|
|
731
1153
|
body.replaceChildren();
|
|
732
1154
|
const q = HK_SEARCH.trim().toLowerCase();
|
|
733
|
-
const hits = q ? cat.components.filter((x) => matches(x, q)) : null;
|
|
1155
|
+
const hits = q ? (cat.components || []).filter((x) => matches(x, q)) : null;
|
|
734
1156
|
if (hits) {
|
|
735
1157
|
body.append(el("div", "note", tn(hits.length, "hookui.matches")));
|
|
736
|
-
for (const comp of hits) body.append(
|
|
1158
|
+
for (const comp of hits) body.append(referenceRow(comp));
|
|
737
1159
|
if (!hits.length) body.append(empty(t("hookui.noMatch")));
|
|
738
1160
|
return;
|
|
739
1161
|
}
|
|
740
1162
|
for (const [g, name] of Object.entries(cat.groups)) {
|
|
741
|
-
const rows = cat.components.filter((x) => x.group === g);
|
|
1163
|
+
const rows = (cat.components || []).filter((x) => x.group === g);
|
|
742
1164
|
if (!rows.length) continue;
|
|
743
1165
|
const inner = el("div");
|
|
744
|
-
for (const comp of rows) inner.append(
|
|
1166
|
+
for (const comp of rows) inner.append(referenceRow(comp));
|
|
745
1167
|
body.append(collapsible({
|
|
746
1168
|
title: name,
|
|
747
1169
|
count: String(rows.length),
|
|
@@ -750,68 +1172,35 @@ function paletteCard(show, cat, edits) {
|
|
|
750
1172
|
}));
|
|
751
1173
|
}
|
|
752
1174
|
}
|
|
1175
|
+
search.addEventListener("input", () => {
|
|
1176
|
+
HK_SEARCH = search.value;
|
|
1177
|
+
paint();
|
|
1178
|
+
});
|
|
753
1179
|
paint();
|
|
754
1180
|
return c;
|
|
755
1181
|
}
|
|
756
1182
|
|
|
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) {
|
|
1183
|
+
function referenceRow(comp) {
|
|
771
1184
|
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);
|
|
1185
|
+
const top = el("div", "hk-prow-head");
|
|
1186
|
+
top.append(el("span", "hk-prow-id", comp.id));
|
|
1187
|
+
top.append(chip(comp.cost, comp.cost === "refused" ? "bad" : null));
|
|
1188
|
+
row.append(top);
|
|
1189
|
+
if (comp.summary) row.append(el("div", "hk-prow-desc", comp.summary));
|
|
1190
|
+
const sample = comp.previews && comp.previews[(comp.defaults && comp.defaults.render) || comp.renderers[0]];
|
|
1191
|
+
if (sample) row.append(ansiBlock(sample, "hk-preview hk-preview-chip"));
|
|
1192
|
+
// A REFUSED part keeps its slot and carries the measurement that refused it:
|
|
1193
|
+
// "we decided against this" and "we forgot" must not look the same.
|
|
1194
|
+
if (comp.cost === "refused") row.append(el("div", "note note-warn hk-prow-why", comp.refused_reason));
|
|
807
1195
|
return row;
|
|
808
1196
|
}
|
|
809
1197
|
|
|
810
|
-
|
|
811
|
-
//
|
|
812
|
-
//
|
|
813
|
-
function presetsCard(presets) {
|
|
1198
|
+
/* ══ presets ══════════════════════════════════════════════════════════════ */
|
|
1199
|
+
// Applying one REPLACES the layout, so it is always confirmed and the loss is
|
|
1200
|
+
// NAMED — the `orc diy init --force` rule.
|
|
1201
|
+
function presetsCard(presets, plan) {
|
|
814
1202
|
const c = card(t("hookui.presets"));
|
|
1203
|
+
c.append(el("div", "note", t("hookui.presetsAbout")));
|
|
815
1204
|
if (!presets || !presets.presets) return c;
|
|
816
1205
|
for (const p of presets.presets) {
|
|
817
1206
|
const row = el("div", "hk-preset");
|
|
@@ -825,7 +1214,15 @@ function presetsCard(presets) {
|
|
|
825
1214
|
if (!p.active) {
|
|
826
1215
|
const b = el("button", "btn btn-xs btn-ghost", t("hookui.apply"));
|
|
827
1216
|
b.type = "button";
|
|
828
|
-
|
|
1217
|
+
// A preset is written STRAIGHT AWAY, so it cannot run while ops are
|
|
1218
|
+
// staged: those ops carry positions computed against a layout the preset
|
|
1219
|
+
// is about to replace.
|
|
1220
|
+
if (plan.dirty) {
|
|
1221
|
+
b.disabled = true;
|
|
1222
|
+
b.title = t("hookui.applyFirst");
|
|
1223
|
+
} else {
|
|
1224
|
+
b.addEventListener("click", () => confirmApply(p.name));
|
|
1225
|
+
}
|
|
829
1226
|
row.append(b);
|
|
830
1227
|
}
|
|
831
1228
|
c.append(row);
|
|
@@ -838,13 +1235,15 @@ function confirmApply(name) {
|
|
|
838
1235
|
title: t("hookui.applyTitle", { name }),
|
|
839
1236
|
body: el("div", "note", t("hookui.applyWarn")),
|
|
840
1237
|
actions: [
|
|
841
|
-
{ label: t("common.cancel"),
|
|
1238
|
+
{ label: t("common.cancel"), cls: "btn-ghost", onClick: (c) => c() },
|
|
842
1239
|
{
|
|
843
1240
|
label: t("hookui.apply"),
|
|
844
|
-
|
|
845
|
-
onClick: async () => {
|
|
1241
|
+
cls: "btn-primary",
|
|
1242
|
+
onClick: async (c) => {
|
|
1243
|
+
c();
|
|
846
1244
|
const r = await post("/api/statusline/apply", bd({ name }));
|
|
847
1245
|
if (!r.ok) toast(t("hookui.applyFailed"), "bad", (r.output || "").trim());
|
|
1246
|
+
HK_OPS = [];
|
|
848
1247
|
rerender();
|
|
849
1248
|
},
|
|
850
1249
|
},
|
|
@@ -857,12 +1256,14 @@ function confirmReset() {
|
|
|
857
1256
|
title: t("hookui.resetTitle"),
|
|
858
1257
|
body: el("div", "note", t("hookui.resetWarn")),
|
|
859
1258
|
actions: [
|
|
860
|
-
{ label: t("common.cancel"),
|
|
1259
|
+
{ label: t("common.cancel"), cls: "btn-ghost", onClick: (c) => c() },
|
|
861
1260
|
{
|
|
862
1261
|
label: t("hookui.resetLayout"),
|
|
863
|
-
|
|
864
|
-
onClick: async () => {
|
|
1262
|
+
cls: "btn-primary",
|
|
1263
|
+
onClick: async (c) => {
|
|
1264
|
+
c();
|
|
865
1265
|
await post("/api/statusline/reset", bd({}));
|
|
1266
|
+
HK_OPS = [];
|
|
866
1267
|
rerender();
|
|
867
1268
|
},
|
|
868
1269
|
},
|
|
@@ -870,11 +1271,32 @@ function confirmReset() {
|
|
|
870
1271
|
});
|
|
871
1272
|
}
|
|
872
1273
|
|
|
873
|
-
|
|
874
|
-
|
|
1274
|
+
/* ══ more ═════════════════════════════════════════════════════════════════ */
|
|
1275
|
+
// THE COLOUR SET IS A DOCUMENT SETTING, and it now goes to the command that
|
|
1276
|
+
// writes document settings. It used to be routed through `statusline line 1
|
|
1277
|
+
// --theme`, which set ONE line's override while the picker read the document's
|
|
1278
|
+
// value back — so the button never moved and only a third of the bar changed.
|
|
1279
|
+
// One command per scope, and the panel calls the one that matches the control.
|
|
1280
|
+
function advancedCard(cat, plan) {
|
|
875
1281
|
const c = card(t("hookui.advanced"));
|
|
876
|
-
|
|
877
|
-
|
|
1282
|
+
|
|
1283
|
+
const themes = el("div", "hk-theme-list");
|
|
1284
|
+
for (const name of Object.keys(cat.themes || {})) {
|
|
1285
|
+
const b = el("button", "hk-theme" + (plan.doc.theme === name ? " hk-theme-on" : ""));
|
|
1286
|
+
b.type = "button";
|
|
1287
|
+
b.append(el("span", "hk-theme-name", name));
|
|
1288
|
+
// WHY somebody would pick it — the CLI's sentence, not one written here.
|
|
1289
|
+
const about = (cat.themes_about || {})[name];
|
|
1290
|
+
if (about) b.append(el("span", "note", about));
|
|
1291
|
+
b.addEventListener("click", () => hkOp({ op: "doc", field: "theme", value: name, label: t("hookui.themeChange", { v: name }) }));
|
|
1292
|
+
themes.append(b);
|
|
1293
|
+
}
|
|
1294
|
+
c.append(labelled(t("hookui.theme"), themes, t("hookui.themeAbout")));
|
|
1295
|
+
c.append(el("div", "note", t("hookui.themeOverride")));
|
|
1296
|
+
|
|
1297
|
+
c.append(labelled(t("hookui.glyphs"),
|
|
1298
|
+
pickRow(cat.glyph_sets, plan.doc.glyphs, (v) => hkOp({ op: "doc", field: "glyphs", value: v, label: t("hookui.glyphsDocChange", { v }) })),
|
|
1299
|
+
t("hookui.glyphsDocAbout")));
|
|
878
1300
|
|
|
879
1301
|
const re = el("button", "btn btn-sm btn-ghost", t("hookui.recompile"));
|
|
880
1302
|
re.type = "button";
|