@wildwinter/app-shell 0.50.0 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1104 @@
1
+ 'use strict';
2
+
3
+ var extensions = require('@wildwinter/toolkit/extensions');
4
+
5
+ // src/value-rows.ts
6
+
7
+ // src/tooltip.ts
8
+ function renderTip(host, text) {
9
+ host.textContent = "";
10
+ const re = /\u0002([^\u0003]*)\u0003/g;
11
+ let last = 0;
12
+ let m;
13
+ while ((m = re.exec(text)) !== null) {
14
+ if (m.index > last) host.append(text.slice(last, m.index));
15
+ const strong = document.createElement("strong");
16
+ strong.textContent = m[1] ?? "";
17
+ host.append(strong);
18
+ last = m.index + m[0].length;
19
+ }
20
+ if (last < text.length) host.append(text.slice(last));
21
+ }
22
+ var inited = false;
23
+ var checked = false;
24
+ var suppressed;
25
+ var SHOW_DELAY = 350;
26
+ var EDGE = 6;
27
+ var GAP = 7;
28
+ var tip = null;
29
+ var timer = 0;
30
+ var active = null;
31
+ function ensureEl() {
32
+ if (tip) return tip;
33
+ const el2 = document.createElement("div");
34
+ el2.className = "tooltip";
35
+ el2.setAttribute("role", "tooltip");
36
+ el2.hidden = true;
37
+ document.body.appendChild(el2);
38
+ tip = el2;
39
+ return el2;
40
+ }
41
+ var rectOf = (anchor) => anchor instanceof HTMLElement ? anchor.getBoundingClientRect() : anchor;
42
+ function place(anchor) {
43
+ const t = ensureEl();
44
+ const r = rectOf(anchor);
45
+ const tr = t.getBoundingClientRect();
46
+ let top = r.top - tr.height - GAP;
47
+ if (top < EDGE) top = r.top + r.height + GAP;
48
+ let left = r.left + r.width / 2 - tr.width / 2;
49
+ left = Math.max(EDGE, Math.min(left, window.innerWidth - tr.width - EDGE));
50
+ t.style.left = `${Math.round(left)}px`;
51
+ t.style.top = `${Math.round(top)}px`;
52
+ }
53
+ function show(anchor) {
54
+ const text = anchor.dataset["tip"];
55
+ if (!text) return;
56
+ paint(anchor, text);
57
+ }
58
+ function paint(anchor, text) {
59
+ if (suppressed?.()) return;
60
+ const t = ensureEl();
61
+ const host = (anchor instanceof HTMLElement ? anchor.closest("dialog[open]") : null) ?? document.body;
62
+ if (t.parentElement !== host) host.appendChild(t);
63
+ active = anchor instanceof HTMLElement ? anchor : null;
64
+ renderTip(t, text);
65
+ t.hidden = false;
66
+ t.classList.remove("show");
67
+ void t.offsetWidth;
68
+ place(anchor);
69
+ t.classList.add("show");
70
+ }
71
+ function hide() {
72
+ if (timer) {
73
+ window.clearTimeout(timer);
74
+ timer = 0;
75
+ }
76
+ active = null;
77
+ if (tip) {
78
+ tip.hidden = true;
79
+ tip.classList.remove("show");
80
+ }
81
+ }
82
+ function schedule(anchor) {
83
+ if (anchor === active) return;
84
+ if (timer) window.clearTimeout(timer);
85
+ timer = window.setTimeout(() => show(anchor), SHOW_DELAY);
86
+ }
87
+ function ensureTooltipHost() {
88
+ initTooltips();
89
+ }
90
+ function checkTooltipHost() {
91
+ if (checked) return;
92
+ checked = true;
93
+ setTimeout(() => {
94
+ if (inited || typeof document === "undefined") return;
95
+ if (document.querySelector("[data-tip]")) {
96
+ console.warn("app-shell: something set `data-tip` but no tooltip host is mounted, so those rollovers will not draw. Call initTooltips() once per window.");
97
+ }
98
+ }, 0);
99
+ }
100
+ var tipAncestor = (n) => n instanceof Element ? n.closest("[data-tip]") : null;
101
+ function initTooltips(opts = {}) {
102
+ if (opts.suppressed !== void 0) suppressed = opts.suppressed;
103
+ if (inited) return;
104
+ inited = true;
105
+ document.addEventListener("pointerover", (e) => {
106
+ const el2 = tipAncestor(e.target);
107
+ if (el2) schedule(el2);
108
+ });
109
+ document.addEventListener("pointerout", (e) => {
110
+ const el2 = tipAncestor(e.target);
111
+ if (!el2) return;
112
+ const to = e.relatedTarget;
113
+ if (to instanceof Node && el2.contains(to)) return;
114
+ hide();
115
+ });
116
+ document.addEventListener("focusin", (e) => {
117
+ const el2 = tipAncestor(e.target);
118
+ if (el2) schedule(el2);
119
+ });
120
+ document.addEventListener("focusout", () => hide());
121
+ document.addEventListener("pointerdown", () => hide(), true);
122
+ document.addEventListener("scroll", () => {
123
+ if (active) hide();
124
+ }, true);
125
+ document.addEventListener("keydown", (e) => {
126
+ if (e.key === "Escape") hide();
127
+ });
128
+ window.addEventListener("blur", () => hide());
129
+ }
130
+
131
+ // src/icons.ts
132
+ var icon = {
133
+ // --- dismissing and revealing ---------------------------------------------
134
+ /** Close a panel, a dialog, a popover. Patterpad's, twelve times over. */
135
+ close: "\u2715",
136
+ /** The overflow menu on a document or a row: "more about this". */
137
+ more: "\u22EF",
138
+ /** Collapsed disclosure, pointing at what would open. */
139
+ collapsed: "\u25B8",
140
+ /** Expanded disclosure. */
141
+ expanded: "\u25BE",
142
+ /** A dropdown's affordance, where it is not a real <select>. */
143
+ dropdown: "\u25BE",
144
+ // --- moving through things ------------------------------------------------
145
+ /** Back / previous, in a breadcrumb or a stepper. */
146
+ back: "\u2039",
147
+ /** Forward / next. */
148
+ forward: "\u203A",
149
+ /** Up one, in a list. */
150
+ up: "\u2191",
151
+ /** Down one. */
152
+ down: "\u2193",
153
+ // --- state ----------------------------------------------------------------
154
+ /** Done, applied, agreed. */
155
+ tick: "\u2713",
156
+ /** Somebody else holds this: the version-control lock. */
157
+ locked: "\u2298",
158
+ /** Read-only on disk with no other holder: the resting state of everything
159
+ * in a lock-based working copy that nobody has checked out yet. */
160
+ readOnly: "\u25CB",
161
+ /** YOU hold this one: checked out / opened by the current user, still yours to
162
+ * edit. Shares its glyph with `note`, which the table permits: an entry is a
163
+ * MEANING, and two meanings may draw the same shape. */
164
+ checkedOut: "\u270E",
165
+ /** Tracked, with local changes not yet committed. */
166
+ modified: "\u25CF",
167
+ /** Not in version control yet: new since the last commit. */
168
+ untracked: "+",
169
+ /** Something needs attention but is not fatal. */
170
+ warning: "\u203C",
171
+ /** A live thing: the running position, an active marker. */
172
+ dot: "\u25CF",
173
+ /** Play, in the sense of "run this". */
174
+ play: "\u25B6",
175
+ /** Start the thing again from the top: a play session, a sweep. Named for the
176
+ * act, not the arrow, which is the table's rule. */
177
+ restart: "\u27F2",
178
+ /** The three ways a collection can be shown. Named by what you get rather than
179
+ * by the glyph, so a second app can pick the same three without copying
180
+ * somebody's taste in symbols. */
181
+ viewCards: "\u25A6",
182
+ viewTable: "\u2630",
183
+ viewNode: "\u25C8",
184
+ // --- handling -------------------------------------------------------------
185
+ /** The drag grip on a reorderable row. */
186
+ grip: "\u283F",
187
+ /** Add one of whatever this list holds. */
188
+ add: "+",
189
+ // --- annotation -----------------------------------------------------------
190
+ /** Documentation notes: the pencil. Both apps had chosen it independently,
191
+ * which is the happiest way for a vocabulary entry to arrive. */
192
+ note: "\u270E",
193
+ /** A comment thread. Speech, not a pencil: documentation states the reason,
194
+ * comments are the conversation, and the two must never look alike. */
195
+ comment: "\u275D"
196
+ };
197
+ var STROKE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.571" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">';
198
+ var FILL = '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">';
199
+ var stroked = (inner) => `${STROKE}${inner}</svg>`;
200
+ var filled = (inner) => `${FILL}${inner}</svg>`;
201
+ var DISC = filled('<circle cx="12" cy="12" r="6"/>');
202
+ var DISC_LARGE = filled('<circle cx="12" cy="12" r="9"/>');
203
+ var DRAWN = {
204
+ // --- dismissing and revealing ---------------------------------------------
205
+ /** Close a panel, a dialog, a popover. (x) */
206
+ close: stroked('<path d="M18 6 6 18"/><path d="m6 6 12 12"/>'),
207
+ /** The overflow menu on a document or a row: "more about this". (ellipsis) */
208
+ more: stroked('<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>'),
209
+ /** Collapsed disclosure, pointing at what would open. (chevron-right) */
210
+ collapsed: stroked('<path d="m9 18 6-6-6-6"/>'),
211
+ /** Expanded disclosure. (chevron-down) */
212
+ expanded: stroked('<path d="m6 9 6 6 6-6"/>'),
213
+ /** A dropdown's affordance, where it is not a real <select>. (chevron-down) */
214
+ dropdown: stroked('<path d="m6 9 6 6 6-6"/>'),
215
+ // --- moving through things ------------------------------------------------
216
+ /** Back / previous, in a breadcrumb or a stepper. (chevron-left) */
217
+ back: stroked('<path d="m15 18-6-6 6-6"/>'),
218
+ /** Forward / next. (chevron-right) */
219
+ forward: stroked('<path d="m9 18 6-6-6-6"/>'),
220
+ /** Up one, in a list. (arrow-up) */
221
+ up: stroked('<path d="m5 12 7-7 7 7"/><path d="M12 19V5"/>'),
222
+ /** Down one. (arrow-down) */
223
+ down: stroked('<path d="M12 5v14"/><path d="m19 12-7 7-7-7"/>'),
224
+ /** History back. (arrow-left) */
225
+ arrowLeft: stroked('<path d="m12 19-7-7 7-7"/><path d="M19 12H5"/>'),
226
+ /** History forward; "go there". (arrow-right) */
227
+ arrowRight: stroked('<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>'),
228
+ // --- state ----------------------------------------------------------------
229
+ /** Done, applied, agreed. (check) */
230
+ tick: stroked('<path d="M20 6 9 17l-5-5"/>'),
231
+ /** Somebody else holds this: the version-control lock. (lock) */
232
+ locked: stroked('<rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>'),
233
+ /** Read-only on disk with no other holder. (lock-open) An open lock rather
234
+ * than a struck-out eye: this state is not a prohibition. Nobody holds the
235
+ * file, saving checks it out, and it is the resting state of everything the
236
+ * author has not touched yet under a lock-based VCS. eye-off would say "you
237
+ * cannot", which is false here; the open lock says "not held", beside
238
+ * `locked`, which is the shut one, so the two read as a pair. */
239
+ readOnly: stroked('<rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 9.9-1"/>'),
240
+ /** YOU hold this one: checked out, still yours to edit. (pencil) */
241
+ checkedOut: stroked('<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/>'),
242
+ /** Tracked, with local changes not yet committed. (a disc; see DISC) */
243
+ modified: DISC,
244
+ /** Not in version control yet: new since the last commit. (plus) */
245
+ untracked: stroked('<path d="M5 12h14"/><path d="M12 5v14"/>'),
246
+ /** Something needs attention but is not fatal. (triangle-alert) */
247
+ warning: stroked('<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/>'),
248
+ /** A live thing: the running position, an active marker. (a disc) */
249
+ dot: DISC,
250
+ /** Play, in the sense of "run this". (play) */
251
+ play: stroked('<path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z"/>'),
252
+ /** Start the thing again from the top: a play session, a sweep. (rotate-ccw) */
253
+ restart: stroked('<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/>'),
254
+ /** The three ways a collection can be shown, named by what you get. */
255
+ /** Cards. (layout-grid) */
256
+ viewCards: stroked('<rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="14" y="3" rx="1"/><rect width="7" height="7" x="14" y="14" rx="1"/><rect width="7" height="7" x="3" y="14" rx="1"/>'),
257
+ /** A table. (rows-3: a ruled box, which is a table. `list` is bullets, and
258
+ * the view is not a list.) */
259
+ viewTable: stroked('<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M21 9H3"/><path d="M21 15H3"/>'),
260
+ /** A node graph. (waypoints: four nodes on edges reads as a graph.
261
+ * `git-fork` reads as version control, which this family also draws, and
262
+ * a picture that means two things is the fault this table exists to
263
+ * prevent.) */
264
+ viewNode: stroked('<path d="m10.586 5.414-5.172 5.172"/><path d="m18.586 13.414-5.172 5.172"/><path d="M6 12h12"/><circle cx="12" cy="20" r="2"/><circle cx="12" cy="4" r="2"/><circle cx="20" cy="12" r="2"/><circle cx="4" cy="12" r="2"/>'),
265
+ /** Record: the disc, in whatever colour the button is. (a disc; see DISC_LARGE) */
266
+ record: DISC_LARGE,
267
+ /** Sound: play with audio. (volume-2: the cone and two waves. Patterpad's
268
+ * hand-drawn one had a single wave, which at 14px reads as quiet rather
269
+ * than as sound.) */
270
+ speaker: stroked('<path d="M11 4.702a.705.705 0 0 0-1.203-.498L6.413 7.587A1.4 1.4 0 0 1 5.416 8H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2.416a1.4 1.4 0 0 1 .997.413l3.383 3.384A.705.705 0 0 0 11 19.298z"/><path d="M16 9a5 5 0 0 1 0 6"/><path d="M19.364 18.364a9 9 0 0 0 0-12.728"/>'),
271
+ // --- handling -------------------------------------------------------------
272
+ /** The drag grip on a reorderable row. (grip-vertical) */
273
+ grip: stroked('<circle cx="9" cy="12" r="1"/><circle cx="9" cy="5" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="15" cy="19" r="1"/>'),
274
+ /** Add one of whatever this list holds. (plus) */
275
+ add: stroked('<path d="M5 12h14"/><path d="M12 5v14"/>'),
276
+ /** Copy this to the clipboard. (copy: the front sheet whole, the back
277
+ * sheet showing round two of its sides) */
278
+ copy: stroked('<rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>'),
279
+ /** Connect: to a server, a live link, a running game. (plug) */
280
+ connect: stroked('<path d="M12 22v-5"/><path d="M15 8V2"/><path d="M17 8a1 1 0 0 1 1 1v4a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1z"/><path d="M9 8V2"/>'),
281
+ /** Search. (search) */
282
+ search: stroked('<path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/>'),
283
+ /** Keep this window on top. (pin) */
284
+ pin: stroked('<path d="M12 17v5"/><path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"/>'),
285
+ /** Settings. (settings) */
286
+ settings: stroked('<path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915"/><circle cx="12" cy="12" r="3"/>'),
287
+ // --- annotation -----------------------------------------------------------
288
+ /** Documentation notes: a document with lines on it. (file-text) The glyph
289
+ * table drew a pencil and shared it with `checkedOut`; the drawn set gives
290
+ * the two meanings two shapes, and notes get the document, which is what
291
+ * the shell's own noteFilled/noteOutline pair had always drawn. */
292
+ note: stroked('<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/>'),
293
+ /** A comment thread. Speech, not a document: documentation states the
294
+ * reason, comments are the conversation, and the two must never look
295
+ * alike. (message-square) */
296
+ comment: stroked('<path d="M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"/>')
297
+ };
298
+ ({
299
+ ...DRAWN});
300
+ var ICON_SIZE = 14;
301
+ var templates = /* @__PURE__ */ new Map();
302
+ function iconNode(name, size = ICON_SIZE) {
303
+ let tpl = templates.get(name);
304
+ if (!tpl) {
305
+ const holder = document.createElement("template");
306
+ holder.innerHTML = DRAWN[name];
307
+ tpl = holder.content.firstElementChild;
308
+ tpl.setAttribute("data-icon", name);
309
+ templates.set(name, tpl);
310
+ }
311
+ const node = tpl.cloneNode(true);
312
+ node.setAttribute("width", String(size));
313
+ node.setAttribute("height", String(size));
314
+ return node;
315
+ }
316
+ var BY_GLYPH = /* @__PURE__ */ new Map();
317
+ for (const [name, glyph] of Object.entries(icon)) {
318
+ if (!BY_GLYPH.has(glyph)) BY_GLYPH.set(glyph, name);
319
+ }
320
+ function iconNameOfGlyph(glyph) {
321
+ return BY_GLYPH.get(glyph);
322
+ }
323
+ function isIconName(s) {
324
+ return Object.prototype.hasOwnProperty.call(DRAWN, s);
325
+ }
326
+
327
+ // src/dom.ts
328
+ function el(tag, classNameOrProps, ...children) {
329
+ const node = document.createElement(tag);
330
+ const props = typeof classNameOrProps === "string" ? { className: classNameOrProps } : classNameOrProps ?? {};
331
+ if (props.className) node.className = props.className;
332
+ if (props.text !== void 0 && props.text !== null) node.textContent = props.text;
333
+ if (props.title !== void 0) node.title = props.title;
334
+ if (props.tip !== void 0) {
335
+ node.dataset["tip"] = props.tip;
336
+ node.setAttribute("aria-label", props.tip);
337
+ ensureTooltipHost();
338
+ }
339
+ if (props.onClick) node.addEventListener("click", props.onClick);
340
+ for (const child of children) {
341
+ if (child === null || child === void 0) continue;
342
+ node.append(child);
343
+ }
344
+ checkTooltipHost();
345
+ return node;
346
+ }
347
+ function iconBtn(name, title, onClick, disabled = false, danger = false) {
348
+ const b = el("button", `shell-icon${danger ? " danger" : ""}`);
349
+ b.type = "button";
350
+ b.dataset["tip"] = title;
351
+ b.setAttribute("aria-label", title);
352
+ const word = isIconName(name) ? name : iconNameOfGlyph(name);
353
+ if (word) b.append(iconNode(word));
354
+ else b.textContent = name;
355
+ b.disabled = disabled;
356
+ b.addEventListener("click", onClick);
357
+ return b;
358
+ }
359
+ function moveItem(arr, i, delta) {
360
+ const j = i + delta;
361
+ if (j < 0 || j >= arr.length) return false;
362
+ [arr[i], arr[j]] = [arr[j], arr[i]];
363
+ return true;
364
+ }
365
+ var CHIP_ICON = 10;
366
+ function tagChips(holder, onChange) {
367
+ const wrap = el("div", "shell-tags");
368
+ const input = el("input", "shell-tag-input");
369
+ input.type = "text";
370
+ input.placeholder = "Add value";
371
+ input.spellcheck = false;
372
+ const makeChip = (v) => {
373
+ const chip = el("span", "shell-tag", v);
374
+ const x = el("button", "shell-tag-x", iconNode("close", CHIP_ICON));
375
+ x.type = "button";
376
+ x.dataset["tip"] = `Remove ${v}`;
377
+ x.setAttribute("aria-label", `Remove ${v}`);
378
+ x.addEventListener("click", () => {
379
+ holder.values = (holder.values ?? []).filter((o) => o !== v);
380
+ chip.remove();
381
+ onChange?.();
382
+ });
383
+ chip.append(x);
384
+ return chip;
385
+ };
386
+ const commit = () => {
387
+ const v = input.value.trim();
388
+ if (v && !(holder.values ?? []).includes(v)) {
389
+ (holder.values ??= []).push(v);
390
+ wrap.insertBefore(makeChip(v), input);
391
+ onChange?.();
392
+ }
393
+ input.value = "";
394
+ };
395
+ input.addEventListener("keydown", (e) => {
396
+ if (e.key === "Enter" || e.key === ",") {
397
+ e.preventDefault();
398
+ commit();
399
+ }
400
+ });
401
+ input.addEventListener("blur", commit);
402
+ for (const v of holder.values ?? []) wrap.append(makeChip(v));
403
+ wrap.append(input);
404
+ return wrap;
405
+ }
406
+
407
+ // src/value-control.ts
408
+ function fallbackText(decl) {
409
+ const d = decl.default;
410
+ if (d !== void 0 && d !== null) {
411
+ if (typeof d === "boolean") return d ? "True" : "False";
412
+ if (Array.isArray(d)) return d.length === 0 ? "none" : d.join(", ");
413
+ return d === "" ? "none" : String(d);
414
+ }
415
+ switch (decl.type) {
416
+ case "boolean":
417
+ return "False";
418
+ case "number":
419
+ return "0";
420
+ case "enum":
421
+ return decl.values?.[0] ?? "none";
422
+ case "quality":
423
+ return decl.stages?.[0] ?? "first stage";
424
+ case "flags":
425
+ return "none";
426
+ default:
427
+ return "empty";
428
+ }
429
+ }
430
+ var defaultLabel = (decl) => `default (${fallbackText(decl)})`;
431
+ function rungStrip(opts) {
432
+ const make = opts.doc === void 0 ? el : (tag, props) => {
433
+ const node = opts.doc.createElement(tag);
434
+ node.className = props.className;
435
+ if (props.text !== void 0) node.textContent = props.text;
436
+ if (props.tip !== void 0) {
437
+ node.title = props.tip;
438
+ node.setAttribute("aria-label", props.tip);
439
+ }
440
+ if (props.onClick) node.addEventListener("click", props.onClick);
441
+ if (tag === "button") node.type = "button";
442
+ return node;
443
+ };
444
+ const strip = make("span", { className: "seg shell-rungs" });
445
+ const decl = opts.decl ?? { type: "quality", stages: opts.stages };
446
+ const unset = opts.value === void 0;
447
+ const fallback = typeof decl.default === "string" && opts.stages.includes(decl.default) ? decl.default : opts.stages[0];
448
+ const readOnly = opts.onPick === void 0;
449
+ if (opts.onClear) {
450
+ strip.append(make("button", {
451
+ className: `seg-opt shell-rung shell-rung-default${unset ? " on" : ""}`,
452
+ text: defaultLabel(decl),
453
+ tip: unset ? "Not set here: the declared default holds" : "Clear the value, back to the default",
454
+ onClick: () => {
455
+ if (!unset) opts.onClear();
456
+ }
457
+ }));
458
+ }
459
+ for (const stage of opts.stages) {
460
+ const current = opts.value === stage;
461
+ const falls = unset && stage === fallback;
462
+ const rung = make(readOnly ? "span" : "button", {
463
+ className: `seg-opt shell-rung${current ? " on" : ""}${falls ? " fallback" : ""}`,
464
+ text: stage,
465
+ tip: current ? "The current stage" : falls ? "The stage it falls back to" : readOnly ? stage : opts.pickTip?.(stage) ?? `Set to "${stage}"`,
466
+ ...readOnly ? {} : { onClick: () => {
467
+ if (!current) opts.onPick(stage);
468
+ } }
469
+ });
470
+ strip.append(rung);
471
+ }
472
+ if (!unset && !opts.stages.includes(opts.value)) {
473
+ strip.append(make("span", {
474
+ className: "seg-opt shell-rung stray on",
475
+ text: opts.value,
476
+ tip: `"${opts.value}" is not one of the stages: pick the one it belongs at now`
477
+ }));
478
+ }
479
+ return strip;
480
+ }
481
+ var fallbackFlags = (decl) => Array.isArray(decl.default) ? decl.default.filter((v) => typeof v === "string") : [];
482
+ function fieldValueProblem(decl, value) {
483
+ if (value === void 0 || value === null || value === "") return "";
484
+ switch (decl.type) {
485
+ case "number":
486
+ return typeof value === "number" && Number.isFinite(value) ? "" : "Not a number.";
487
+ case "boolean":
488
+ return typeof value === "boolean" ? "" : "Not True or False.";
489
+ case "enum":
490
+ return decl.values && !decl.values.includes(String(value)) ? `"${String(value)}" is not one of the values.` : "";
491
+ case "quality":
492
+ return decl.stages && !decl.stages.includes(String(value)) ? `"${String(value)}" is not one of the stages.` : "";
493
+ case "flags": {
494
+ if (!Array.isArray(value)) return "Not a list of flags.";
495
+ const strays = decl.values ? value.filter((v) => !decl.values.includes(String(v))) : [];
496
+ if (strays.length === 0) return "";
497
+ return strays.length === 1 ? `"${String(strays[0])}" is not one of the values.` : `${strays.map((s) => `"${String(s)}"`).join(", ")} are not among the values.`;
498
+ }
499
+ default:
500
+ return "";
501
+ }
502
+ }
503
+ var isTextual = (type) => type !== "boolean" && type !== "enum" && type !== "flags" && type !== "quality";
504
+ var controlShape = (o) => JSON.stringify([
505
+ o.decl.type,
506
+ o.decl.values ?? null,
507
+ o.decl.stages ?? null,
508
+ o.decl.default ?? null,
509
+ o.onCommit === void 0,
510
+ o.disabled === true,
511
+ o.controlClassName ?? "",
512
+ o.decl.type === "quality" && (o.decl.stages?.length ?? 0) === 0
513
+ ]);
514
+ function fieldRow(initial) {
515
+ let opts = initial;
516
+ const row = el("div", `shell-fieldrow${opts.className ? ` ${opts.className}` : ""}`);
517
+ const caption = el("span", `shell-fieldrow-label${opts.labelClassName ? ` ${opts.labelClassName}` : ""}`);
518
+ caption.id = `shell-fr-${Math.random().toString(36).slice(2, 9)}`;
519
+ const body = el("div", "shell-fieldrow-body");
520
+ const purpose = el("p", "shell-fieldrow-purpose");
521
+ const problem = el("p", "shell-fieldrow-problem");
522
+ problem.setAttribute("role", "status");
523
+ row.append(caption, body);
524
+ let shape = "";
525
+ let show2 = () => {
526
+ };
527
+ let typing = "";
528
+ const commit = (v) => {
529
+ opts.onCommit?.(v);
530
+ };
531
+ const editable = () => opts.onCommit !== void 0 && opts.disabled !== true;
532
+ const ctl = (base) => opts.controlClassName ? `${base.replace(/^field /, "")} ${opts.controlClassName}` : base;
533
+ const labelledBy = (node) => {
534
+ node.setAttribute("aria-labelledby", caption.id);
535
+ const described = [purpose.id, problem.id].filter(Boolean).join(" ");
536
+ if (described) node.setAttribute("aria-describedby", described);
537
+ };
538
+ purpose.id = `${caption.id}-p`;
539
+ problem.id = `${caption.id}-m`;
540
+ const sayProblem = () => {
541
+ const words = [typing || fieldValueProblem(opts.decl, opts.value), opts.problem ?? ""].filter((w) => w !== "");
542
+ problem.textContent = words.join(" ");
543
+ problem.hidden = words.length === 0;
544
+ row.classList.toggle("has-problem", words.length > 0);
545
+ row.classList.toggle("shell-fieldrow-textual", isTextual(opts.decl.type));
546
+ };
547
+ const build = () => {
548
+ const decl = opts.decl;
549
+ typing = "";
550
+ if (decl.type === "quality" && (decl.stages?.length ?? 0) > 0) {
551
+ const holder = el("span", "shell-fieldrow-rungs");
552
+ show2 = (value) => {
553
+ holder.replaceChildren(rungStrip({
554
+ stages: decl.stages,
555
+ value: typeof value === "string" && value !== "" ? value : void 0,
556
+ decl,
557
+ ...editable() ? { onPick: (stage) => {
558
+ opts = { ...opts, value: stage };
559
+ show2(stage);
560
+ sayProblem();
561
+ commit(stage);
562
+ }, onClear: () => {
563
+ opts = { ...opts, value: void 0 };
564
+ show2(void 0);
565
+ sayProblem();
566
+ commit(void 0);
567
+ } } : {}
568
+ }));
569
+ const strip = holder.firstElementChild;
570
+ if (strip) {
571
+ strip.setAttribute("role", "group");
572
+ labelledBy(strip);
573
+ }
574
+ };
575
+ return holder;
576
+ }
577
+ if (decl.type === "boolean" || decl.type === "enum") {
578
+ const sel = el("select", ctl("field shell-fieldrow-select"));
579
+ labelledBy(sel);
580
+ sel.disabled = !editable();
581
+ const choices = decl.type === "boolean" ? [["true", "True"], ["false", "False"]] : (decl.values ?? []).map((v) => [v, v]);
582
+ show2 = (value) => {
583
+ const current = value === void 0 || value === null || value === "" ? "" : String(value);
584
+ sel.replaceChildren();
585
+ const add = (v, label) => {
586
+ const o = el("option", void 0, label);
587
+ o.value = v;
588
+ sel.append(o);
589
+ };
590
+ add("", defaultLabel(decl));
591
+ for (const [v, label] of choices) add(v, label);
592
+ if (current !== "" && !choices.some(([v]) => v === current)) add(current, current);
593
+ sel.value = current;
594
+ };
595
+ sel.addEventListener("change", () => {
596
+ const raw = sel.value;
597
+ const v = raw === "" ? void 0 : decl.type === "boolean" ? raw === "true" : raw;
598
+ opts = { ...opts, value: v };
599
+ sayProblem();
600
+ commit(v);
601
+ });
602
+ return sel;
603
+ }
604
+ if (decl.type === "flags" && (decl.values?.length ?? 0) > 0) {
605
+ const holder = el("span", "shell-flags");
606
+ holder.setAttribute("role", "group");
607
+ labelledBy(holder);
608
+ show2 = (value) => {
609
+ const unset = !Array.isArray(value);
610
+ const held = unset ? fallbackFlags(decl) : value.map(String);
611
+ holder.replaceChildren();
612
+ const chip = (text, cls, pressed, tip2, onClick) => {
613
+ const b = el(onClick && editable() ? "button" : "span", { className: `shell-flag${cls}`, text, tip: tip2 });
614
+ b.setAttribute("aria-label", `${text}: ${tip2}`);
615
+ if (b instanceof HTMLButtonElement) {
616
+ b.type = "button";
617
+ b.addEventListener("click", onClick);
618
+ }
619
+ if (pressed !== void 0) b.setAttribute("aria-pressed", String(pressed));
620
+ holder.append(b);
621
+ };
622
+ const set = (next) => {
623
+ opts = { ...opts, value: next };
624
+ show2(next);
625
+ sayProblem();
626
+ commit(next);
627
+ };
628
+ chip(
629
+ defaultLabel(decl),
630
+ ` shell-flag-default${unset ? " on" : ""}`,
631
+ void 0,
632
+ unset ? "Not set here: the declared default holds" : "Clear the value, back to the default",
633
+ unset ? void 0 : () => set(void 0)
634
+ );
635
+ for (const v of decl.values) {
636
+ const on = held.includes(v);
637
+ chip(
638
+ v,
639
+ `${on ? " on" : ""}${on && unset ? " fallback" : ""}`,
640
+ on,
641
+ on ? `Unset ${v}` : `Set ${v}`,
642
+ () => set(on ? held.filter((x) => x !== v) : decl.values.filter((x) => x === v || held.includes(x)).concat(held.filter((x) => !decl.values.includes(x))))
643
+ );
644
+ }
645
+ for (const v of held.filter((x) => !decl.values.includes(x))) {
646
+ chip(v, " stray on", true, `"${v}" is not one of the values: click to drop it`, () => set(held.filter((x) => x !== v)));
647
+ }
648
+ };
649
+ return holder;
650
+ }
651
+ if (decl.type === "flags") {
652
+ const holder = el("span", "shell-fieldrow-chips");
653
+ show2 = (value) => {
654
+ const box = { values: Array.isArray(value) ? value.map(String) : [] };
655
+ const chips = tagChips(box, () => {
656
+ const next = box.values.length > 0 ? [...box.values] : void 0;
657
+ opts = { ...opts, value: next };
658
+ sayProblem();
659
+ commit(next);
660
+ });
661
+ const input = chips.querySelector("input");
662
+ if (input) {
663
+ labelledBy(input);
664
+ input.placeholder = Array.isArray(value) ? "Add value" : defaultLabel(decl);
665
+ input.disabled = !editable();
666
+ }
667
+ chips.querySelectorAll("button").forEach((b) => {
668
+ b.disabled = !editable();
669
+ });
670
+ holder.replaceChildren(chips);
671
+ };
672
+ return holder;
673
+ }
674
+ const multiline = decl.type === "multiline";
675
+ const field = multiline ? el("textarea", ctl("field shell-fieldrow-text")) : el("input", ctl("field shell-fieldrow-text"));
676
+ if (field instanceof HTMLInputElement) {
677
+ field.type = "text";
678
+ if (decl.type === "number") field.inputMode = "decimal";
679
+ } else field.rows = 3;
680
+ labelledBy(field);
681
+ field.placeholder = defaultLabel(decl);
682
+ field.disabled = !editable();
683
+ const asText = (value) => value === void 0 || value === null ? "" : Array.isArray(value) ? value.join(", ") : String(value);
684
+ const read = (raw) => {
685
+ if (raw.trim() === "") return {};
686
+ if (decl.type !== "number") return { value: raw };
687
+ const n = Number(raw.trim());
688
+ return Number.isFinite(n) ? { value: n } : { bad: "Not a number." };
689
+ };
690
+ let atFocus = "";
691
+ show2 = (value) => {
692
+ if (field.ownerDocument.activeElement === field || keeper.restoring(field) || keeper.holds(row)) return;
693
+ field.value = asText(value);
694
+ };
695
+ const take = () => {
696
+ const r = read(field.value);
697
+ typing = r.bad ?? "";
698
+ if (r.bad === void 0) opts = { ...opts, value: r.value };
699
+ sayProblem();
700
+ if (r.bad === void 0) commit(r.value);
701
+ };
702
+ field.addEventListener("focus", () => {
703
+ if (!keeper.restoring(field)) atFocus = field.value;
704
+ });
705
+ field.addEventListener("input", take);
706
+ field.addEventListener("keydown", (e) => {
707
+ if (e.key !== "Escape" || field.value === atFocus) return;
708
+ field.value = atFocus;
709
+ take();
710
+ });
711
+ return field;
712
+ };
713
+ const draw = (next) => {
714
+ const decl = next.decl;
715
+ const label = next.label ?? decl.name;
716
+ if (typeof label === "string") caption.textContent = label;
717
+ else caption.replaceChildren(label);
718
+ caption.className = `shell-fieldrow-label${next.labelClassName ? ` ${next.labelClassName}` : ""}`;
719
+ row.className = `shell-fieldrow${next.className ? ` ${next.className}` : ""}`;
720
+ const nextShape = controlShape(next);
721
+ opts = next;
722
+ if (nextShape !== shape) {
723
+ shape = nextShape;
724
+ body.replaceChildren(build());
725
+ }
726
+ show2(next.value);
727
+ purpose.textContent = decl.purpose ?? "";
728
+ if (decl.purpose) body.append(purpose);
729
+ else purpose.remove();
730
+ body.append(problem);
731
+ sayProblem();
732
+ };
733
+ draw(initial);
734
+ keeper.watch(row);
735
+ return { el: row, update: draw };
736
+ }
737
+ var keeper = /* @__PURE__ */ (() => {
738
+ const memos = /* @__PURE__ */ new WeakMap();
739
+ const restoringNow = /* @__PURE__ */ new WeakSet();
740
+ const redrawn = /* @__PURE__ */ new WeakMap();
741
+ const put = (row) => {
742
+ const memo = memos.get(row);
743
+ if (!memo) return;
744
+ const doc = row.ownerDocument;
745
+ const active2 = doc.activeElement;
746
+ if (!row.isConnected || !memo.field.isConnected || !row.contains(memo.field)) return;
747
+ if (active2 && active2 !== doc.body && !row.contains(active2)) {
748
+ memos.delete(row);
749
+ return;
750
+ }
751
+ if (active2 === memo.field && memo.field.matches(":focus")) return;
752
+ restoringNow.add(memo.field);
753
+ try {
754
+ memo.field.focus({ preventScroll: true });
755
+ if (!(memo.field instanceof HTMLSelectElement) && memo.start !== null) memo.field.setSelectionRange(memo.start, memo.end ?? memo.start);
756
+ } finally {
757
+ restoringNow.delete(memo.field);
758
+ }
759
+ };
760
+ return {
761
+ watch(row) {
762
+ const note = (e) => {
763
+ const t = e.target;
764
+ if (!(t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement || t instanceof HTMLSelectElement)) return;
765
+ const text = !(t instanceof HTMLSelectElement);
766
+ memos.set(row, { field: t, start: text ? t.selectionStart : null, end: text ? t.selectionEnd : null });
767
+ };
768
+ row.addEventListener("focusin", note);
769
+ row.addEventListener("input", note);
770
+ row.addEventListener("keyup", note);
771
+ row.addEventListener("mouseup", note);
772
+ row.addEventListener("focusout", (e) => {
773
+ const at = performance.now();
774
+ const late = e.relatedTarget === null && at - (redrawn.get(row) ?? -Infinity) < 250;
775
+ if (late && memos.has(row)) {
776
+ queueMicrotask(() => put(row));
777
+ return;
778
+ }
779
+ queueMicrotask(() => {
780
+ if ((redrawn.get(row) ?? -Infinity) >= at) {
781
+ put(row);
782
+ return;
783
+ }
784
+ if (row.isConnected && !row.contains(row.ownerDocument.activeElement)) memos.delete(row);
785
+ });
786
+ });
787
+ },
788
+ /** Give the focus back to the row, if the author was in it and has not gone anywhere else since. */
789
+ restore(row) {
790
+ if (!memos.has(row)) return;
791
+ redrawn.set(row, performance.now());
792
+ queueMicrotask(() => put(row));
793
+ },
794
+ restoring: (field) => restoringNow.has(field),
795
+ /** Forget the row's focus if the author has since gone somewhere else (before the row is redrawn). */
796
+ settle(row) {
797
+ const active2 = row.ownerDocument.activeElement;
798
+ if (active2 && active2 !== row.ownerDocument.body && !row.contains(active2)) memos.delete(row);
799
+ },
800
+ /** The row the author is in (before a redraw takes it out). */
801
+ holds: (row) => memos.has(row)
802
+ };
803
+ })();
804
+ var fieldRowFocus = {
805
+ settle: (row) => keeper.settle(row),
806
+ restore: (row) => keeper.restore(row)
807
+ };
808
+
809
+ // src/value-rows.ts
810
+ var SCALARS = /* @__PURE__ */ new Set(["text", "multiline", "number", "boolean", "enum", "flags", "quality", "string"]);
811
+ var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
812
+ var nameOf = (decl) => decl.label ?? decl.name;
813
+ var asDecl = (d, name) => ({ ...d, name: d.name ?? name });
814
+ function ownProblems(decl, value, adapter) {
815
+ if (value === void 0) return "";
816
+ const exists = adapter ? (kind, id) => adapter.nodes(kind).some((n) => n.id === id) : void 0;
817
+ return extensions.valueProblems(decl, value, exists ? { exists } : {}).filter((p) => p.path === "" && p.severity === "error" && p.code !== "not-a-value" && p.code !== "not-a-stage").map((p) => `${p.message.replace(/^the value/, "The value")}.`).join(" ");
818
+ }
819
+ function valueRow(initial) {
820
+ const decl = initial.decl;
821
+ if (SCALARS.has(decl.type)) return scalarRow(initial);
822
+ if (decl.type === "reference") return referenceRow(initial);
823
+ return structuredRow(initial);
824
+ }
825
+ function scalarRow(initial) {
826
+ const map = (o) => {
827
+ const range = ownProblems(o.decl, o.value, o.adapter);
828
+ const problem = [range, o.problem ?? ""].filter((w) => w !== "").join(" ");
829
+ return {
830
+ decl: { ...o.decl, ...o.decl.help !== void 0 && o.decl.purpose === void 0 ? { purpose: o.decl.help } : {} },
831
+ value: o.value,
832
+ ...o.onCommit ? { onCommit: (v) => o.onCommit(v) } : {},
833
+ ...o.disabled !== void 0 ? { disabled: o.disabled } : {},
834
+ ...problem !== "" ? { problem } : {},
835
+ label: o.label ?? nameOf(o.decl),
836
+ ...o.className !== void 0 ? { className: o.className } : {},
837
+ ...o.labelClassName !== void 0 ? { labelClassName: o.labelClassName } : {},
838
+ ...o.controlClassName !== void 0 ? { controlClassName: o.controlClassName } : {}
839
+ };
840
+ };
841
+ const handle = fieldRow(map(initial));
842
+ return { el: handle.el, update: (o) => handle.update(map(o)) };
843
+ }
844
+ function referenceRow(initial) {
845
+ let opts = initial;
846
+ if (!opts.adapter) {
847
+ const inner = scalarRow({ ...opts, decl: { ...opts.decl, type: "text" } });
848
+ return { el: inner.el, update: (o) => {
849
+ opts = o;
850
+ inner.update({ ...o, decl: { ...o.decl, type: "text" } });
851
+ } };
852
+ }
853
+ const row = el("div", "shell-fieldrow shell-valuerow-ref");
854
+ const caption = el("span", "shell-fieldrow-label");
855
+ const body = el("div", "shell-fieldrow-body");
856
+ const sel = el("select", "field shell-fieldrow-select");
857
+ const help = el("p", "shell-fieldrow-purpose");
858
+ const problem = el("p", "shell-fieldrow-problem");
859
+ problem.setAttribute("role", "status");
860
+ body.append(sel, help, problem);
861
+ row.append(caption, body);
862
+ sel.addEventListener("change", () => {
863
+ const v = sel.value === "" ? void 0 : sel.value;
864
+ opts = { ...opts, value: v };
865
+ draw(opts);
866
+ opts.onCommit?.(v);
867
+ });
868
+ const draw = (o) => {
869
+ opts = o;
870
+ row.className = `shell-fieldrow shell-valuerow-ref${o.className ? ` ${o.className}` : ""}`;
871
+ const label = o.label ?? nameOf(o.decl);
872
+ if (typeof label === "string") caption.textContent = label;
873
+ else caption.replaceChildren(label);
874
+ if (o.controlClassName) sel.className = `shell-fieldrow-select ${o.controlClassName}`;
875
+ const nodes = o.adapter.nodes(o.decl.to ?? "");
876
+ const held = typeof o.value === "string" ? o.value : "";
877
+ sel.replaceChildren();
878
+ const add = (v, text) => {
879
+ const opt = el("option", void 0, text);
880
+ opt.value = v;
881
+ sel.append(opt);
882
+ };
883
+ add("", "default (none)");
884
+ for (const n of nodes) add(n.id, n.label);
885
+ if (held !== "" && !nodes.some((n) => n.id === held)) add(held, `${held} (gone)`);
886
+ sel.value = held;
887
+ sel.disabled = o.onCommit === void 0 || o.disabled === true;
888
+ help.textContent = o.decl.help ?? o.decl.purpose ?? "";
889
+ help.hidden = help.textContent === "";
890
+ const words = [ownProblems(o.decl, o.value, o.adapter), o.problem ?? ""].filter((w) => w !== "").join(" ");
891
+ problem.textContent = words;
892
+ problem.hidden = words === "";
893
+ row.classList.toggle("has-problem", words !== "");
894
+ };
895
+ draw(initial);
896
+ return { el: row, update: draw };
897
+ }
898
+ function structuredRow(initial) {
899
+ let opts = initial;
900
+ const row = el("div", "shell-fieldrow shell-valuerow");
901
+ const caption = el("span", "shell-fieldrow-label");
902
+ const body = el("div", "shell-fieldrow-body shell-valuerow-parts");
903
+ const help = el("p", "shell-fieldrow-purpose");
904
+ const problem = el("p", "shell-fieldrow-problem");
905
+ problem.setAttribute("role", "status");
906
+ const adder = el("div", "shell-valuerow-add");
907
+ row.append(caption, body);
908
+ const parts = /* @__PURE__ */ new Map();
909
+ let kept = [];
910
+ let addKey = "";
911
+ const editable = () => opts.onCommit !== void 0 && opts.disabled !== true;
912
+ const commit = (v) => {
913
+ opts = { ...opts, value: v };
914
+ draw(opts);
915
+ opts.onCommit?.(v);
916
+ };
917
+ const part = (key, o, controls = []) => {
918
+ const had = parts.get(key);
919
+ if (had && had.handle.el.dataset["type"] === o.decl.type) {
920
+ fieldRowFocus.settle(had.handle.el);
921
+ had.handle.update(o);
922
+ had.tools.replaceChildren(...controls);
923
+ kept.push(had.handle.el);
924
+ return had.holder;
925
+ }
926
+ const handle = valueRow(o);
927
+ handle.el.dataset["type"] = o.decl.type;
928
+ const holder = el("div", "shell-valuerow-part");
929
+ const tools = el("span", "shell-valuerow-controls");
930
+ tools.append(...controls);
931
+ holder.append(handle.el, tools);
932
+ parts.set(key, { key, handle, holder, tools });
933
+ return holder;
934
+ };
935
+ const draw = (o) => {
936
+ opts = o;
937
+ kept = [];
938
+ const decl = o.decl;
939
+ row.className = `shell-fieldrow shell-valuerow shell-valuerow-${decl.type}${o.className ? ` ${o.className}` : ""}`;
940
+ const label = o.label ?? nameOf(decl);
941
+ if (typeof label === "string") caption.textContent = label;
942
+ else caption.replaceChildren(label);
943
+ const used = /* @__PURE__ */ new Set();
944
+ const rows = [];
945
+ const child = (key, childDecl, value, onCommit, controls, childLabel, extraProblem) => {
946
+ used.add(key);
947
+ rows.push(part(key, {
948
+ decl: childDecl,
949
+ value,
950
+ ...editable() ? { onCommit } : {},
951
+ ...o.adapter ? { adapter: o.adapter } : {},
952
+ ...o.disabled !== void 0 ? { disabled: o.disabled } : {},
953
+ ...childLabel !== void 0 ? { label: childLabel } : {},
954
+ ...extraProblem ? { problem: extraProblem } : {},
955
+ ...o.controlClassName !== void 0 ? { controlClassName: o.controlClassName } : {}
956
+ }, controls));
957
+ };
958
+ if (decl.type === "record") {
959
+ const value = isObject(o.value) ? o.value : void 0;
960
+ const required = value === void 0 ? [] : extensions.valueProblems(decl, value).filter((p) => p.code === "required");
961
+ for (const f of decl.fields ?? []) {
962
+ const missing = required.find((p) => p.path === f.name);
963
+ child(`f:${f.name}`, f, value?.[f.name], (v) => {
964
+ const next = { ...isObject(opts.value) ? opts.value : {} };
965
+ if (v === void 0) delete next[f.name];
966
+ else next[f.name] = v;
967
+ commit(Object.keys(next).length > 0 ? next : void 0);
968
+ }, void 0, void 0, missing ? "Required." : void 0);
969
+ }
970
+ } else if (decl.type === "map" && decl.of) {
971
+ const value = isObject(o.value) ? o.value : {};
972
+ const keyNodes = decl.key?.type === "reference" && o.adapter ? o.adapter.nodes(decl.key.to ?? "") : void 0;
973
+ const keyLabel = (k) => keyNodes?.find((n) => n.id === k)?.label ?? k;
974
+ for (const k of Object.keys(value)) {
975
+ const remove = editable() ? [iconBtn("close", `Remove ${keyLabel(k)}`, () => {
976
+ const next = { ...isObject(opts.value) ? opts.value : {} };
977
+ delete next[k];
978
+ commit(Object.keys(next).length > 0 ? next : void 0);
979
+ })] : [];
980
+ const gone = keyNodes !== void 0 && !keyNodes.some((n) => n.id === k) ? `${keyLabel(k)} is not here any more.` : void 0;
981
+ child(`k:${k}`, asDecl(decl.of, k), value[k], (v) => {
982
+ const next = { ...isObject(opts.value) ? opts.value : {} };
983
+ next[k] = v === void 0 ? extensions.defaultOf(decl.of) : v;
984
+ commit(next);
985
+ }, remove, keyLabel(k), gone);
986
+ }
987
+ adder.replaceChildren();
988
+ if (editable()) {
989
+ const add = (k) => {
990
+ if (k === "" || Object.prototype.hasOwnProperty.call(value, k)) return;
991
+ commit({ ...value, [k]: extensions.defaultOf(decl.of) });
992
+ };
993
+ if (keyNodes !== void 0) {
994
+ const pick = el("select", "field shell-valuerow-key");
995
+ const first = el("option", void 0, "Add...");
996
+ first.value = "";
997
+ pick.append(first);
998
+ for (const n of keyNodes.filter((n2) => !Object.prototype.hasOwnProperty.call(value, n2.id))) {
999
+ const opt = el("option", void 0, n.label);
1000
+ opt.value = n.id;
1001
+ pick.append(opt);
1002
+ }
1003
+ pick.addEventListener("change", () => add(pick.value));
1004
+ adder.append(pick);
1005
+ } else {
1006
+ const input = el("input", "field shell-valuerow-key");
1007
+ input.type = "text";
1008
+ input.placeholder = "New key";
1009
+ input.value = addKey;
1010
+ input.addEventListener("input", () => {
1011
+ addKey = input.value;
1012
+ });
1013
+ const go = () => {
1014
+ const k = input.value.trim();
1015
+ addKey = "";
1016
+ add(k);
1017
+ };
1018
+ input.addEventListener("keydown", (e) => {
1019
+ if (e.key === "Enter") go();
1020
+ });
1021
+ adder.append(input, iconBtn("add", "Add", go));
1022
+ }
1023
+ }
1024
+ } else if (decl.type === "list" && decl.of) {
1025
+ const value = Array.isArray(o.value) ? o.value : [];
1026
+ const idOf = (item, i) => decl.id !== void 0 && isObject(item) && typeof item[decl.id] === "string" ? `id:${item[decl.id]}` : `i:${i}`;
1027
+ value.forEach((item, i) => {
1028
+ const set = (next) => commit(next.length > 0 ? next : void 0);
1029
+ const controls = editable() ? [
1030
+ iconBtn("up", "Move up", () => {
1031
+ const next = [...value];
1032
+ if (moveItem(next, i, -1)) set(next);
1033
+ }, i === 0),
1034
+ iconBtn("down", "Move down", () => {
1035
+ const next = [...value];
1036
+ if (moveItem(next, i, 1)) set(next);
1037
+ }, i === value.length - 1),
1038
+ iconBtn("close", "Remove", () => set(value.filter((_, j) => j !== i)), false, true)
1039
+ ] : [];
1040
+ const itemLabel = decl.id !== void 0 && isObject(item) && typeof item[decl.id] === "string" ? item[decl.id] : `${i + 1}`;
1041
+ child(idOf(item, i), asDecl(decl.of, itemLabel), item, (v) => {
1042
+ const next = [...Array.isArray(opts.value) ? opts.value : []];
1043
+ next[i] = v === void 0 ? extensions.defaultOf(decl.of) : v;
1044
+ set(next);
1045
+ }, controls, itemLabel);
1046
+ });
1047
+ adder.replaceChildren();
1048
+ if (editable()) {
1049
+ adder.append(iconBtn("add", "Add", () => {
1050
+ const fresh = extensions.defaultOf(decl.of);
1051
+ if (decl.id !== void 0 && isObject(fresh)) {
1052
+ const taken = new Set(value.map((x) => isObject(x) ? x[decl.id] : void 0));
1053
+ let n = value.length + 1;
1054
+ while (taken.has(`item-${n}`)) n++;
1055
+ fresh[decl.id] = `item-${n}`;
1056
+ }
1057
+ commit([...value, fresh]);
1058
+ }));
1059
+ }
1060
+ }
1061
+ for (const key of [...parts.keys()]) if (!used.has(key)) parts.delete(key);
1062
+ body.replaceChildren(...rows);
1063
+ if (decl.type !== "record" && editable()) body.append(adder);
1064
+ help.textContent = decl.help ?? decl.purpose ?? "";
1065
+ if (help.textContent !== "") body.append(help);
1066
+ const words = [ownProblems(decl, o.value, o.adapter), o.problem ?? ""].filter((w) => w !== "").join(" ");
1067
+ problem.textContent = words;
1068
+ problem.hidden = words === "";
1069
+ body.append(problem);
1070
+ row.classList.toggle("has-problem", words !== "");
1071
+ for (const k of kept) fieldRowFocus.restore(k);
1072
+ };
1073
+ draw(initial);
1074
+ return { el: row, update: draw };
1075
+ }
1076
+ function valueRows() {
1077
+ const rows = /* @__PURE__ */ new Map();
1078
+ return {
1079
+ row(key, opts) {
1080
+ const had = rows.get(key);
1081
+ if (had && had.type === opts.decl.type) {
1082
+ had.used = true;
1083
+ fieldRowFocus.settle(had.handle.el);
1084
+ had.handle.update(opts);
1085
+ fieldRowFocus.restore(had.handle.el);
1086
+ return had.handle.el;
1087
+ }
1088
+ const handle = valueRow(opts);
1089
+ rows.set(key, { handle, type: opts.decl.type, used: true });
1090
+ return handle.el;
1091
+ },
1092
+ sweep() {
1093
+ for (const [key, entry] of rows) {
1094
+ if (!entry.used) rows.delete(key);
1095
+ else entry.used = false;
1096
+ }
1097
+ }
1098
+ };
1099
+ }
1100
+
1101
+ exports.valueRow = valueRow;
1102
+ exports.valueRows = valueRows;
1103
+ //# sourceMappingURL=value-rows.cjs.map
1104
+ //# sourceMappingURL=value-rows.cjs.map