@wildwinter/app-shell 0.49.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.
Files changed (46) hide show
  1. package/README.md +37 -0
  2. package/dist/{chunk-FGAPO4N5.js → chunk-B56FWJ5Y.js} +21 -12
  3. package/dist/chunk-B56FWJ5Y.js.map +1 -0
  4. package/dist/chunk-ELEDQRZ7.js +435 -0
  5. package/dist/chunk-ELEDQRZ7.js.map +1 -0
  6. package/dist/controls.css +107 -0
  7. package/dist/controls.css.map +1 -1
  8. package/dist/history.cjs +194 -0
  9. package/dist/history.cjs.map +1 -0
  10. package/dist/history.d.cts +111 -0
  11. package/dist/history.d.ts +111 -0
  12. package/dist/history.js +188 -0
  13. package/dist/history.js.map +1 -0
  14. package/dist/index.cjs +368 -9
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +2 -2
  17. package/dist/index.d.ts +2 -2
  18. package/dist/index.js +2 -2
  19. package/dist/{property-list-BPGU3rhF.d.cts → property-list-DlpxIkv6.d.cts} +9 -0
  20. package/dist/{property-list-BPGU3rhF.d.ts → property-list-DlpxIkv6.d.ts} +9 -0
  21. package/dist/property-list.cjs +18 -9
  22. package/dist/property-list.cjs.map +1 -1
  23. package/dist/property-list.d.cts +1 -1
  24. package/dist/property-list.d.ts +1 -1
  25. package/dist/property-list.js +2 -2
  26. package/dist/value-control.cjs +584 -0
  27. package/dist/value-control.cjs.map +1 -1
  28. package/dist/value-control.d.cts +61 -1
  29. package/dist/value-control.d.ts +61 -1
  30. package/dist/value-control.js +1 -1
  31. package/dist/value-rows.cjs +1104 -0
  32. package/dist/value-rows.cjs.map +1 -0
  33. package/dist/value-rows.d.cts +71 -0
  34. package/dist/value-rows.d.ts +71 -0
  35. package/dist/value-rows.js +298 -0
  36. package/dist/value-rows.js.map +1 -0
  37. package/dist/window-guard.cjs +74 -0
  38. package/dist/window-guard.cjs.map +1 -0
  39. package/dist/window-guard.d.cts +32 -0
  40. package/dist/window-guard.d.ts +32 -0
  41. package/dist/window-guard.js +67 -0
  42. package/dist/window-guard.js.map +1 -0
  43. package/package.json +26 -4
  44. package/dist/chunk-EAE7UWQ5.js +0 -84
  45. package/dist/chunk-EAE7UWQ5.js.map +0 -1
  46. package/dist/chunk-FGAPO4N5.js.map +0 -1
@@ -1 +1 @@
1
- export { D as DefaultControlOptions, P as PROPERTY_TYPES, b as PropertyDeclLike, c as PropertyListHandle, d as PropertyListOptions, e as PropertyListType, i as defaultControl, m as mountPropertyList } from './property-list-BPGU3rhF.cjs';
1
+ export { D as DefaultControlOptions, P as PROPERTY_TYPES, b as PropertyDeclLike, c as PropertyListHandle, d as PropertyListOptions, e as PropertyListType, i as defaultControl, m as mountPropertyList } from './property-list-DlpxIkv6.cjs';
@@ -1 +1 @@
1
- export { D as DefaultControlOptions, P as PROPERTY_TYPES, b as PropertyDeclLike, c as PropertyListHandle, d as PropertyListOptions, e as PropertyListType, i as defaultControl, m as mountPropertyList } from './property-list-BPGU3rhF.js';
1
+ export { D as DefaultControlOptions, P as PROPERTY_TYPES, b as PropertyDeclLike, c as PropertyListHandle, d as PropertyListOptions, e as PropertyListType, i as defaultControl, m as mountPropertyList } from './property-list-DlpxIkv6.js';
@@ -1,5 +1,5 @@
1
- export { PROPERTY_TYPES, defaultControl, mountPropertyList } from './chunk-FGAPO4N5.js';
2
- import './chunk-EAE7UWQ5.js';
1
+ export { PROPERTY_TYPES, defaultControl, mountPropertyList } from './chunk-B56FWJ5Y.js';
2
+ import './chunk-ELEDQRZ7.js';
3
3
  import './chunk-W4WC4FO5.js';
4
4
  //# sourceMappingURL=property-list.js.map
5
5
  //# sourceMappingURL=property-list.js.map
@@ -124,6 +124,194 @@ function initTooltips(opts = {}) {
124
124
  window.addEventListener("blur", () => hide());
125
125
  }
126
126
 
127
+ // src/icons.ts
128
+ var icon = {
129
+ // --- dismissing and revealing ---------------------------------------------
130
+ /** Close a panel, a dialog, a popover. Patterpad's, twelve times over. */
131
+ close: "\u2715",
132
+ /** The overflow menu on a document or a row: "more about this". */
133
+ more: "\u22EF",
134
+ /** Collapsed disclosure, pointing at what would open. */
135
+ collapsed: "\u25B8",
136
+ /** Expanded disclosure. */
137
+ expanded: "\u25BE",
138
+ /** A dropdown's affordance, where it is not a real <select>. */
139
+ dropdown: "\u25BE",
140
+ // --- moving through things ------------------------------------------------
141
+ /** Back / previous, in a breadcrumb or a stepper. */
142
+ back: "\u2039",
143
+ /** Forward / next. */
144
+ forward: "\u203A",
145
+ /** Up one, in a list. */
146
+ up: "\u2191",
147
+ /** Down one. */
148
+ down: "\u2193",
149
+ // --- state ----------------------------------------------------------------
150
+ /** Done, applied, agreed. */
151
+ tick: "\u2713",
152
+ /** Somebody else holds this: the version-control lock. */
153
+ locked: "\u2298",
154
+ /** Read-only on disk with no other holder: the resting state of everything
155
+ * in a lock-based working copy that nobody has checked out yet. */
156
+ readOnly: "\u25CB",
157
+ /** YOU hold this one: checked out / opened by the current user, still yours to
158
+ * edit. Shares its glyph with `note`, which the table permits: an entry is a
159
+ * MEANING, and two meanings may draw the same shape. */
160
+ checkedOut: "\u270E",
161
+ /** Tracked, with local changes not yet committed. */
162
+ modified: "\u25CF",
163
+ /** Not in version control yet: new since the last commit. */
164
+ untracked: "+",
165
+ /** Something needs attention but is not fatal. */
166
+ warning: "\u203C",
167
+ /** A live thing: the running position, an active marker. */
168
+ dot: "\u25CF",
169
+ /** Play, in the sense of "run this". */
170
+ play: "\u25B6",
171
+ /** Start the thing again from the top: a play session, a sweep. Named for the
172
+ * act, not the arrow, which is the table's rule. */
173
+ restart: "\u27F2",
174
+ /** The three ways a collection can be shown. Named by what you get rather than
175
+ * by the glyph, so a second app can pick the same three without copying
176
+ * somebody's taste in symbols. */
177
+ viewCards: "\u25A6",
178
+ viewTable: "\u2630",
179
+ viewNode: "\u25C8",
180
+ // --- handling -------------------------------------------------------------
181
+ /** The drag grip on a reorderable row. */
182
+ grip: "\u283F",
183
+ /** Add one of whatever this list holds. */
184
+ add: "+",
185
+ // --- annotation -----------------------------------------------------------
186
+ /** Documentation notes: the pencil. Both apps had chosen it independently,
187
+ * which is the happiest way for a vocabulary entry to arrive. */
188
+ note: "\u270E",
189
+ /** A comment thread. Speech, not a pencil: documentation states the reason,
190
+ * comments are the conversation, and the two must never look alike. */
191
+ comment: "\u275D"
192
+ };
193
+ 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">';
194
+ var FILL = '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">';
195
+ var stroked = (inner) => `${STROKE}${inner}</svg>`;
196
+ var filled = (inner) => `${FILL}${inner}</svg>`;
197
+ var DISC = filled('<circle cx="12" cy="12" r="6"/>');
198
+ var DISC_LARGE = filled('<circle cx="12" cy="12" r="9"/>');
199
+ var DRAWN = {
200
+ // --- dismissing and revealing ---------------------------------------------
201
+ /** Close a panel, a dialog, a popover. (x) */
202
+ close: stroked('<path d="M18 6 6 18"/><path d="m6 6 12 12"/>'),
203
+ /** The overflow menu on a document or a row: "more about this". (ellipsis) */
204
+ more: stroked('<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>'),
205
+ /** Collapsed disclosure, pointing at what would open. (chevron-right) */
206
+ collapsed: stroked('<path d="m9 18 6-6-6-6"/>'),
207
+ /** Expanded disclosure. (chevron-down) */
208
+ expanded: stroked('<path d="m6 9 6 6 6-6"/>'),
209
+ /** A dropdown's affordance, where it is not a real <select>. (chevron-down) */
210
+ dropdown: stroked('<path d="m6 9 6 6 6-6"/>'),
211
+ // --- moving through things ------------------------------------------------
212
+ /** Back / previous, in a breadcrumb or a stepper. (chevron-left) */
213
+ back: stroked('<path d="m15 18-6-6 6-6"/>'),
214
+ /** Forward / next. (chevron-right) */
215
+ forward: stroked('<path d="m9 18 6-6-6-6"/>'),
216
+ /** Up one, in a list. (arrow-up) */
217
+ up: stroked('<path d="m5 12 7-7 7 7"/><path d="M12 19V5"/>'),
218
+ /** Down one. (arrow-down) */
219
+ down: stroked('<path d="M12 5v14"/><path d="m19 12-7 7-7-7"/>'),
220
+ /** History back. (arrow-left) */
221
+ arrowLeft: stroked('<path d="m12 19-7-7 7-7"/><path d="M19 12H5"/>'),
222
+ /** History forward; "go there". (arrow-right) */
223
+ arrowRight: stroked('<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>'),
224
+ // --- state ----------------------------------------------------------------
225
+ /** Done, applied, agreed. (check) */
226
+ tick: stroked('<path d="M20 6 9 17l-5-5"/>'),
227
+ /** Somebody else holds this: the version-control lock. (lock) */
228
+ 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"/>'),
229
+ /** Read-only on disk with no other holder. (lock-open) An open lock rather
230
+ * than a struck-out eye: this state is not a prohibition. Nobody holds the
231
+ * file, saving checks it out, and it is the resting state of everything the
232
+ * author has not touched yet under a lock-based VCS. eye-off would say "you
233
+ * cannot", which is false here; the open lock says "not held", beside
234
+ * `locked`, which is the shut one, so the two read as a pair. */
235
+ 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"/>'),
236
+ /** YOU hold this one: checked out, still yours to edit. (pencil) */
237
+ 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"/>'),
238
+ /** Tracked, with local changes not yet committed. (a disc; see DISC) */
239
+ modified: DISC,
240
+ /** Not in version control yet: new since the last commit. (plus) */
241
+ untracked: stroked('<path d="M5 12h14"/><path d="M12 5v14"/>'),
242
+ /** Something needs attention but is not fatal. (triangle-alert) */
243
+ 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"/>'),
244
+ /** A live thing: the running position, an active marker. (a disc) */
245
+ dot: DISC,
246
+ /** Play, in the sense of "run this". (play) */
247
+ 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"/>'),
248
+ /** Start the thing again from the top: a play session, a sweep. (rotate-ccw) */
249
+ 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"/>'),
250
+ /** The three ways a collection can be shown, named by what you get. */
251
+ /** Cards. (layout-grid) */
252
+ 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"/>'),
253
+ /** A table. (rows-3: a ruled box, which is a table. `list` is bullets, and
254
+ * the view is not a list.) */
255
+ viewTable: stroked('<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M21 9H3"/><path d="M21 15H3"/>'),
256
+ /** A node graph. (waypoints: four nodes on edges reads as a graph.
257
+ * `git-fork` reads as version control, which this family also draws, and
258
+ * a picture that means two things is the fault this table exists to
259
+ * prevent.) */
260
+ 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"/>'),
261
+ /** Record: the disc, in whatever colour the button is. (a disc; see DISC_LARGE) */
262
+ record: DISC_LARGE,
263
+ /** Sound: play with audio. (volume-2: the cone and two waves. Patterpad's
264
+ * hand-drawn one had a single wave, which at 14px reads as quiet rather
265
+ * than as sound.) */
266
+ 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"/>'),
267
+ // --- handling -------------------------------------------------------------
268
+ /** The drag grip on a reorderable row. (grip-vertical) */
269
+ 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"/>'),
270
+ /** Add one of whatever this list holds. (plus) */
271
+ add: stroked('<path d="M5 12h14"/><path d="M12 5v14"/>'),
272
+ /** Copy this to the clipboard. (copy: the front sheet whole, the back
273
+ * sheet showing round two of its sides) */
274
+ 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"/>'),
275
+ /** Connect: to a server, a live link, a running game. (plug) */
276
+ 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"/>'),
277
+ /** Search. (search) */
278
+ search: stroked('<path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/>'),
279
+ /** Keep this window on top. (pin) */
280
+ 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"/>'),
281
+ /** Settings. (settings) */
282
+ 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"/>'),
283
+ // --- annotation -----------------------------------------------------------
284
+ /** Documentation notes: a document with lines on it. (file-text) The glyph
285
+ * table drew a pencil and shared it with `checkedOut`; the drawn set gives
286
+ * the two meanings two shapes, and notes get the document, which is what
287
+ * the shell's own noteFilled/noteOutline pair had always drawn. */
288
+ 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"/>'),
289
+ /** A comment thread. Speech, not a document: documentation states the
290
+ * reason, comments are the conversation, and the two must never look
291
+ * alike. (message-square) */
292
+ 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"/>')
293
+ };
294
+ var ICON_SIZE = 14;
295
+ var templates = /* @__PURE__ */ new Map();
296
+ function iconNode(name, size = ICON_SIZE) {
297
+ let tpl = templates.get(name);
298
+ if (!tpl) {
299
+ const holder = document.createElement("template");
300
+ holder.innerHTML = DRAWN[name];
301
+ tpl = holder.content.firstElementChild;
302
+ tpl.setAttribute("data-icon", name);
303
+ templates.set(name, tpl);
304
+ }
305
+ const node = tpl.cloneNode(true);
306
+ node.setAttribute("width", String(size));
307
+ node.setAttribute("height", String(size));
308
+ return node;
309
+ }
310
+ var BY_GLYPH = /* @__PURE__ */ new Map();
311
+ for (const [name, glyph] of Object.entries(icon)) {
312
+ if (!BY_GLYPH.has(glyph)) BY_GLYPH.set(glyph, name);
313
+ }
314
+
127
315
  // src/dom.ts
128
316
  function el(tag, classNameOrProps, ...children) {
129
317
  const node = document.createElement(tag);
@@ -144,6 +332,47 @@ function el(tag, classNameOrProps, ...children) {
144
332
  checkTooltipHost();
145
333
  return node;
146
334
  }
335
+ var CHIP_ICON = 10;
336
+ function tagChips(holder, onChange) {
337
+ const wrap = el("div", "shell-tags");
338
+ const input = el("input", "shell-tag-input");
339
+ input.type = "text";
340
+ input.placeholder = "Add value";
341
+ input.spellcheck = false;
342
+ const makeChip = (v) => {
343
+ const chip = el("span", "shell-tag", v);
344
+ const x = el("button", "shell-tag-x", iconNode("close", CHIP_ICON));
345
+ x.type = "button";
346
+ x.dataset["tip"] = `Remove ${v}`;
347
+ x.setAttribute("aria-label", `Remove ${v}`);
348
+ x.addEventListener("click", () => {
349
+ holder.values = (holder.values ?? []).filter((o) => o !== v);
350
+ chip.remove();
351
+ onChange?.();
352
+ });
353
+ chip.append(x);
354
+ return chip;
355
+ };
356
+ const commit = () => {
357
+ const v = input.value.trim();
358
+ if (v && !(holder.values ?? []).includes(v)) {
359
+ (holder.values ??= []).push(v);
360
+ wrap.insertBefore(makeChip(v), input);
361
+ onChange?.();
362
+ }
363
+ input.value = "";
364
+ };
365
+ input.addEventListener("keydown", (e) => {
366
+ if (e.key === "Enter" || e.key === ",") {
367
+ e.preventDefault();
368
+ commit();
369
+ }
370
+ });
371
+ input.addEventListener("blur", commit);
372
+ for (const v of holder.values ?? []) wrap.append(makeChip(v));
373
+ wrap.append(input);
374
+ return wrap;
375
+ }
147
376
 
148
377
  // src/value-control.ts
149
378
  function fallbackText(decl) {
@@ -223,9 +452,364 @@ function rungStrip(opts) {
223
452
  }
224
453
  return strip;
225
454
  }
455
+ var fallbackFlags = (decl) => Array.isArray(decl.default) ? decl.default.filter((v) => typeof v === "string") : [];
456
+ function fieldValueProblem(decl, value) {
457
+ if (value === void 0 || value === null || value === "") return "";
458
+ switch (decl.type) {
459
+ case "number":
460
+ return typeof value === "number" && Number.isFinite(value) ? "" : "Not a number.";
461
+ case "boolean":
462
+ return typeof value === "boolean" ? "" : "Not True or False.";
463
+ case "enum":
464
+ return decl.values && !decl.values.includes(String(value)) ? `"${String(value)}" is not one of the values.` : "";
465
+ case "quality":
466
+ return decl.stages && !decl.stages.includes(String(value)) ? `"${String(value)}" is not one of the stages.` : "";
467
+ case "flags": {
468
+ if (!Array.isArray(value)) return "Not a list of flags.";
469
+ const strays = decl.values ? value.filter((v) => !decl.values.includes(String(v))) : [];
470
+ if (strays.length === 0) return "";
471
+ return strays.length === 1 ? `"${String(strays[0])}" is not one of the values.` : `${strays.map((s) => `"${String(s)}"`).join(", ")} are not among the values.`;
472
+ }
473
+ default:
474
+ return "";
475
+ }
476
+ }
477
+ var isTextual = (type) => type !== "boolean" && type !== "enum" && type !== "flags" && type !== "quality";
478
+ var controlShape = (o) => JSON.stringify([
479
+ o.decl.type,
480
+ o.decl.values ?? null,
481
+ o.decl.stages ?? null,
482
+ o.decl.default ?? null,
483
+ o.onCommit === void 0,
484
+ o.disabled === true,
485
+ o.controlClassName ?? "",
486
+ o.decl.type === "quality" && (o.decl.stages?.length ?? 0) === 0
487
+ ]);
488
+ function fieldRow(initial) {
489
+ let opts = initial;
490
+ const row = el("div", `shell-fieldrow${opts.className ? ` ${opts.className}` : ""}`);
491
+ const caption = el("span", `shell-fieldrow-label${opts.labelClassName ? ` ${opts.labelClassName}` : ""}`);
492
+ caption.id = `shell-fr-${Math.random().toString(36).slice(2, 9)}`;
493
+ const body = el("div", "shell-fieldrow-body");
494
+ const purpose = el("p", "shell-fieldrow-purpose");
495
+ const problem = el("p", "shell-fieldrow-problem");
496
+ problem.setAttribute("role", "status");
497
+ row.append(caption, body);
498
+ let shape = "";
499
+ let show2 = () => {
500
+ };
501
+ let typing = "";
502
+ const commit = (v) => {
503
+ opts.onCommit?.(v);
504
+ };
505
+ const editable = () => opts.onCommit !== void 0 && opts.disabled !== true;
506
+ const ctl = (base) => opts.controlClassName ? `${base.replace(/^field /, "")} ${opts.controlClassName}` : base;
507
+ const labelledBy = (node) => {
508
+ node.setAttribute("aria-labelledby", caption.id);
509
+ const described = [purpose.id, problem.id].filter(Boolean).join(" ");
510
+ if (described) node.setAttribute("aria-describedby", described);
511
+ };
512
+ purpose.id = `${caption.id}-p`;
513
+ problem.id = `${caption.id}-m`;
514
+ const sayProblem = () => {
515
+ const words = [typing || fieldValueProblem(opts.decl, opts.value), opts.problem ?? ""].filter((w) => w !== "");
516
+ problem.textContent = words.join(" ");
517
+ problem.hidden = words.length === 0;
518
+ row.classList.toggle("has-problem", words.length > 0);
519
+ row.classList.toggle("shell-fieldrow-textual", isTextual(opts.decl.type));
520
+ };
521
+ const build = () => {
522
+ const decl = opts.decl;
523
+ typing = "";
524
+ if (decl.type === "quality" && (decl.stages?.length ?? 0) > 0) {
525
+ const holder = el("span", "shell-fieldrow-rungs");
526
+ show2 = (value) => {
527
+ holder.replaceChildren(rungStrip({
528
+ stages: decl.stages,
529
+ value: typeof value === "string" && value !== "" ? value : void 0,
530
+ decl,
531
+ ...editable() ? { onPick: (stage) => {
532
+ opts = { ...opts, value: stage };
533
+ show2(stage);
534
+ sayProblem();
535
+ commit(stage);
536
+ }, onClear: () => {
537
+ opts = { ...opts, value: void 0 };
538
+ show2(void 0);
539
+ sayProblem();
540
+ commit(void 0);
541
+ } } : {}
542
+ }));
543
+ const strip = holder.firstElementChild;
544
+ if (strip) {
545
+ strip.setAttribute("role", "group");
546
+ labelledBy(strip);
547
+ }
548
+ };
549
+ return holder;
550
+ }
551
+ if (decl.type === "boolean" || decl.type === "enum") {
552
+ const sel = el("select", ctl("field shell-fieldrow-select"));
553
+ labelledBy(sel);
554
+ sel.disabled = !editable();
555
+ const choices = decl.type === "boolean" ? [["true", "True"], ["false", "False"]] : (decl.values ?? []).map((v) => [v, v]);
556
+ show2 = (value) => {
557
+ const current = value === void 0 || value === null || value === "" ? "" : String(value);
558
+ sel.replaceChildren();
559
+ const add = (v, label) => {
560
+ const o = el("option", void 0, label);
561
+ o.value = v;
562
+ sel.append(o);
563
+ };
564
+ add("", defaultLabel(decl));
565
+ for (const [v, label] of choices) add(v, label);
566
+ if (current !== "" && !choices.some(([v]) => v === current)) add(current, current);
567
+ sel.value = current;
568
+ };
569
+ sel.addEventListener("change", () => {
570
+ const raw = sel.value;
571
+ const v = raw === "" ? void 0 : decl.type === "boolean" ? raw === "true" : raw;
572
+ opts = { ...opts, value: v };
573
+ sayProblem();
574
+ commit(v);
575
+ });
576
+ return sel;
577
+ }
578
+ if (decl.type === "flags" && (decl.values?.length ?? 0) > 0) {
579
+ const holder = el("span", "shell-flags");
580
+ holder.setAttribute("role", "group");
581
+ labelledBy(holder);
582
+ show2 = (value) => {
583
+ const unset = !Array.isArray(value);
584
+ const held = unset ? fallbackFlags(decl) : value.map(String);
585
+ holder.replaceChildren();
586
+ const chip = (text, cls, pressed, tip2, onClick) => {
587
+ const b = el(onClick && editable() ? "button" : "span", { className: `shell-flag${cls}`, text, tip: tip2 });
588
+ b.setAttribute("aria-label", `${text}: ${tip2}`);
589
+ if (b instanceof HTMLButtonElement) {
590
+ b.type = "button";
591
+ b.addEventListener("click", onClick);
592
+ }
593
+ if (pressed !== void 0) b.setAttribute("aria-pressed", String(pressed));
594
+ holder.append(b);
595
+ };
596
+ const set = (next) => {
597
+ opts = { ...opts, value: next };
598
+ show2(next);
599
+ sayProblem();
600
+ commit(next);
601
+ };
602
+ chip(
603
+ defaultLabel(decl),
604
+ ` shell-flag-default${unset ? " on" : ""}`,
605
+ void 0,
606
+ unset ? "Not set here: the declared default holds" : "Clear the value, back to the default",
607
+ unset ? void 0 : () => set(void 0)
608
+ );
609
+ for (const v of decl.values) {
610
+ const on = held.includes(v);
611
+ chip(
612
+ v,
613
+ `${on ? " on" : ""}${on && unset ? " fallback" : ""}`,
614
+ on,
615
+ on ? `Unset ${v}` : `Set ${v}`,
616
+ () => set(on ? held.filter((x) => x !== v) : decl.values.filter((x) => x === v || held.includes(x)).concat(held.filter((x) => !decl.values.includes(x))))
617
+ );
618
+ }
619
+ for (const v of held.filter((x) => !decl.values.includes(x))) {
620
+ chip(v, " stray on", true, `"${v}" is not one of the values: click to drop it`, () => set(held.filter((x) => x !== v)));
621
+ }
622
+ };
623
+ return holder;
624
+ }
625
+ if (decl.type === "flags") {
626
+ const holder = el("span", "shell-fieldrow-chips");
627
+ show2 = (value) => {
628
+ const box = { values: Array.isArray(value) ? value.map(String) : [] };
629
+ const chips = tagChips(box, () => {
630
+ const next = box.values.length > 0 ? [...box.values] : void 0;
631
+ opts = { ...opts, value: next };
632
+ sayProblem();
633
+ commit(next);
634
+ });
635
+ const input = chips.querySelector("input");
636
+ if (input) {
637
+ labelledBy(input);
638
+ input.placeholder = Array.isArray(value) ? "Add value" : defaultLabel(decl);
639
+ input.disabled = !editable();
640
+ }
641
+ chips.querySelectorAll("button").forEach((b) => {
642
+ b.disabled = !editable();
643
+ });
644
+ holder.replaceChildren(chips);
645
+ };
646
+ return holder;
647
+ }
648
+ const multiline = decl.type === "multiline";
649
+ const field = multiline ? el("textarea", ctl("field shell-fieldrow-text")) : el("input", ctl("field shell-fieldrow-text"));
650
+ if (field instanceof HTMLInputElement) {
651
+ field.type = "text";
652
+ if (decl.type === "number") field.inputMode = "decimal";
653
+ } else field.rows = 3;
654
+ labelledBy(field);
655
+ field.placeholder = defaultLabel(decl);
656
+ field.disabled = !editable();
657
+ const asText = (value) => value === void 0 || value === null ? "" : Array.isArray(value) ? value.join(", ") : String(value);
658
+ const read = (raw) => {
659
+ if (raw.trim() === "") return {};
660
+ if (decl.type !== "number") return { value: raw };
661
+ const n = Number(raw.trim());
662
+ return Number.isFinite(n) ? { value: n } : { bad: "Not a number." };
663
+ };
664
+ let atFocus = "";
665
+ show2 = (value) => {
666
+ if (field.ownerDocument.activeElement === field || keeper.restoring(field) || keeper.holds(row)) return;
667
+ field.value = asText(value);
668
+ };
669
+ const take = () => {
670
+ const r = read(field.value);
671
+ typing = r.bad ?? "";
672
+ if (r.bad === void 0) opts = { ...opts, value: r.value };
673
+ sayProblem();
674
+ if (r.bad === void 0) commit(r.value);
675
+ };
676
+ field.addEventListener("focus", () => {
677
+ if (!keeper.restoring(field)) atFocus = field.value;
678
+ });
679
+ field.addEventListener("input", take);
680
+ field.addEventListener("keydown", (e) => {
681
+ if (e.key !== "Escape" || field.value === atFocus) return;
682
+ field.value = atFocus;
683
+ take();
684
+ });
685
+ return field;
686
+ };
687
+ const draw = (next) => {
688
+ const decl = next.decl;
689
+ const label = next.label ?? decl.name;
690
+ if (typeof label === "string") caption.textContent = label;
691
+ else caption.replaceChildren(label);
692
+ caption.className = `shell-fieldrow-label${next.labelClassName ? ` ${next.labelClassName}` : ""}`;
693
+ row.className = `shell-fieldrow${next.className ? ` ${next.className}` : ""}`;
694
+ const nextShape = controlShape(next);
695
+ opts = next;
696
+ if (nextShape !== shape) {
697
+ shape = nextShape;
698
+ body.replaceChildren(build());
699
+ }
700
+ show2(next.value);
701
+ purpose.textContent = decl.purpose ?? "";
702
+ if (decl.purpose) body.append(purpose);
703
+ else purpose.remove();
704
+ body.append(problem);
705
+ sayProblem();
706
+ };
707
+ draw(initial);
708
+ keeper.watch(row);
709
+ return { el: row, update: draw };
710
+ }
711
+ var keeper = /* @__PURE__ */ (() => {
712
+ const memos = /* @__PURE__ */ new WeakMap();
713
+ const restoringNow = /* @__PURE__ */ new WeakSet();
714
+ const redrawn = /* @__PURE__ */ new WeakMap();
715
+ const put = (row) => {
716
+ const memo = memos.get(row);
717
+ if (!memo) return;
718
+ const doc = row.ownerDocument;
719
+ const active2 = doc.activeElement;
720
+ if (!row.isConnected || !memo.field.isConnected || !row.contains(memo.field)) return;
721
+ if (active2 && active2 !== doc.body && !row.contains(active2)) {
722
+ memos.delete(row);
723
+ return;
724
+ }
725
+ if (active2 === memo.field && memo.field.matches(":focus")) return;
726
+ restoringNow.add(memo.field);
727
+ try {
728
+ memo.field.focus({ preventScroll: true });
729
+ if (!(memo.field instanceof HTMLSelectElement) && memo.start !== null) memo.field.setSelectionRange(memo.start, memo.end ?? memo.start);
730
+ } finally {
731
+ restoringNow.delete(memo.field);
732
+ }
733
+ };
734
+ return {
735
+ watch(row) {
736
+ const note = (e) => {
737
+ const t = e.target;
738
+ if (!(t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement || t instanceof HTMLSelectElement)) return;
739
+ const text = !(t instanceof HTMLSelectElement);
740
+ memos.set(row, { field: t, start: text ? t.selectionStart : null, end: text ? t.selectionEnd : null });
741
+ };
742
+ row.addEventListener("focusin", note);
743
+ row.addEventListener("input", note);
744
+ row.addEventListener("keyup", note);
745
+ row.addEventListener("mouseup", note);
746
+ row.addEventListener("focusout", (e) => {
747
+ const at = performance.now();
748
+ const late = e.relatedTarget === null && at - (redrawn.get(row) ?? -Infinity) < 250;
749
+ if (late && memos.has(row)) {
750
+ queueMicrotask(() => put(row));
751
+ return;
752
+ }
753
+ queueMicrotask(() => {
754
+ if ((redrawn.get(row) ?? -Infinity) >= at) {
755
+ put(row);
756
+ return;
757
+ }
758
+ if (row.isConnected && !row.contains(row.ownerDocument.activeElement)) memos.delete(row);
759
+ });
760
+ });
761
+ },
762
+ /** Give the focus back to the row, if the author was in it and has not gone anywhere else since. */
763
+ restore(row) {
764
+ if (!memos.has(row)) return;
765
+ redrawn.set(row, performance.now());
766
+ queueMicrotask(() => put(row));
767
+ },
768
+ restoring: (field) => restoringNow.has(field),
769
+ /** Forget the row's focus if the author has since gone somewhere else (before the row is redrawn). */
770
+ settle(row) {
771
+ const active2 = row.ownerDocument.activeElement;
772
+ if (active2 && active2 !== row.ownerDocument.body && !row.contains(active2)) memos.delete(row);
773
+ },
774
+ /** The row the author is in (before a redraw takes it out). */
775
+ holds: (row) => memos.has(row)
776
+ };
777
+ })();
778
+ var fieldRowFocus = {
779
+ settle: (row) => keeper.settle(row),
780
+ restore: (row) => keeper.restore(row)
781
+ };
782
+ function fieldRows() {
783
+ const rows = /* @__PURE__ */ new Map();
784
+ return {
785
+ row(key, opts) {
786
+ const had = rows.get(key);
787
+ if (had) {
788
+ had.used = true;
789
+ keeper.settle(had.handle.el);
790
+ had.handle.update(opts);
791
+ keeper.restore(had.handle.el);
792
+ return had.handle.el;
793
+ }
794
+ const handle = fieldRow(opts);
795
+ rows.set(key, { handle, used: true });
796
+ return handle.el;
797
+ },
798
+ sweep() {
799
+ for (const [key, entry] of rows) {
800
+ if (!entry.used) rows.delete(key);
801
+ else entry.used = false;
802
+ }
803
+ }
804
+ };
805
+ }
226
806
 
227
807
  exports.defaultLabel = defaultLabel;
228
808
  exports.fallbackText = fallbackText;
809
+ exports.fieldRow = fieldRow;
810
+ exports.fieldRowFocus = fieldRowFocus;
811
+ exports.fieldRows = fieldRows;
812
+ exports.fieldValueProblem = fieldValueProblem;
229
813
  exports.rungStrip = rungStrip;
230
814
  exports.stageGateNote = stageGateNote;
231
815
  //# sourceMappingURL=value-control.cjs.map