@wildwinter/app-shell 0.46.5 → 0.48.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 (56) hide show
  1. package/LICENSES.md +5 -4
  2. package/dist/{chunk-J6RSONXM.js → chunk-62KWJPRP.js} +4 -4
  3. package/dist/{chunk-J6RSONXM.js.map → chunk-62KWJPRP.js.map} +1 -1
  4. package/dist/{chunk-WKPKBAXS.js → chunk-C245WRPF.js} +4 -4
  5. package/dist/{chunk-WKPKBAXS.js.map → chunk-C245WRPF.js.map} +1 -1
  6. package/dist/{chunk-R3NXPHRZ.js → chunk-CRUILFAT.js} +28 -6
  7. package/dist/chunk-CRUILFAT.js.map +1 -0
  8. package/dist/{chunk-EHR2KBZC.js → chunk-OMG6A5YB.js} +3 -3
  9. package/dist/{chunk-EHR2KBZC.js.map → chunk-OMG6A5YB.js.map} +1 -1
  10. package/dist/{chunk-2ECJUTSX.js → chunk-T25WXWPB.js} +3 -2
  11. package/dist/chunk-T25WXWPB.js.map +1 -0
  12. package/dist/{chunk-B2TC6Q5J.js → chunk-UQTUCOGV.js} +3 -3
  13. package/dist/{chunk-B2TC6Q5J.js.map → chunk-UQTUCOGV.js.map} +1 -1
  14. package/dist/{chunk-YRFJLJUH.js → chunk-VXEFIL43.js} +3 -3
  15. package/dist/{chunk-YRFJLJUH.js.map → chunk-VXEFIL43.js.map} +1 -1
  16. package/dist/{chunk-UG2P777F.js → chunk-W4WC4FO5.js} +102 -30
  17. package/dist/chunk-W4WC4FO5.js.map +1 -0
  18. package/dist/{chunk-CUFREPJ5.js → chunk-XSS7KM6F.js} +4 -4
  19. package/dist/{chunk-CUFREPJ5.js.map → chunk-XSS7KM6F.js.map} +1 -1
  20. package/dist/context-menu.cjs.map +1 -1
  21. package/dist/context-menu.js +2 -2
  22. package/dist/dialog.cjs.map +1 -1
  23. package/dist/dialog.js +2 -2
  24. package/dist/index.cjs +146 -36
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.cts +97 -14
  27. package/dist/index.d.ts +97 -14
  28. package/dist/index.js +31 -17
  29. package/dist/index.js.map +1 -1
  30. package/dist/kit-gallery.cjs.map +1 -1
  31. package/dist/kit-gallery.js +3 -3
  32. package/dist/link-status.cjs +7 -0
  33. package/dist/link-status.cjs.map +1 -1
  34. package/dist/link-status.js +2 -2
  35. package/dist/menu.cjs +1 -0
  36. package/dist/menu.cjs.map +1 -1
  37. package/dist/menu.d.cts +8 -4
  38. package/dist/menu.d.ts +8 -4
  39. package/dist/menu.js +1 -1
  40. package/dist/{property-list-D3VZCJzi.d.cts → property-list-BgZlkcbz.d.cts} +19 -2
  41. package/dist/{property-list-D3VZCJzi.d.ts → property-list-BgZlkcbz.d.ts} +19 -2
  42. package/dist/property-list.cjs +93 -27
  43. package/dist/property-list.cjs.map +1 -1
  44. package/dist/property-list.d.cts +1 -1
  45. package/dist/property-list.d.ts +1 -1
  46. package/dist/property-list.js +2 -2
  47. package/dist/settings.css +78 -0
  48. package/dist/settings.css.map +1 -1
  49. package/dist/updater-view.cjs.map +1 -1
  50. package/dist/updater-view.js +3 -3
  51. package/dist/welcome.cjs.map +1 -1
  52. package/dist/welcome.js +4 -4
  53. package/package.json +1 -1
  54. package/dist/chunk-2ECJUTSX.js.map +0 -1
  55. package/dist/chunk-R3NXPHRZ.js.map +0 -1
  56. package/dist/chunk-UG2P777F.js.map +0 -1
@@ -78,8 +78,14 @@ interface SettingsDialogOptions {
78
78
  sections: SettingsSection[];
79
79
  /** Fired when Save is clicked and every section validates. The app reads its
80
80
  * own section state and persists. May be async; the dialog stays open until
81
- * it resolves, then closes. */
82
- onSave: () => void | Promise<void>;
81
+ * it resolves, then closes.
82
+ *
83
+ * Return `false` (or a Promise of it) when the save was REFUSED, a version
84
+ * control lock say, and the dialog stays open with the author's edits in it
85
+ * for another try; saying why is the app's (a toast, usually). A throw, or a
86
+ * rejected Promise, is treated the same way rather than surfacing as an
87
+ * unhandled rejection. Anything else closes the dialog, as it always has. */
88
+ onSave: () => void | boolean | Promise<void | boolean>;
83
89
  }
84
90
  interface SettingsDialog {
85
91
  /** Open (optionally on a given tab id). Sections re-mount from current data. */
@@ -143,6 +149,17 @@ interface PropertyListOptions<T extends PropertyDeclLike> extends DefaultControl
143
149
  extraDetails?: (decl: T, index: number) => (HTMLElement | null)[];
144
150
  /** Offer the Purpose field. Default true. */
145
151
  purpose?: boolean;
152
+ /**
153
+ * A quality's stage was renamed in place (the declaration's own default
154
+ * already follows it). Given, the rename is handed here INSTEAD of
155
+ * `onChange`, so the caller can carry every value that names the stage in
156
+ * the same edit as the declaration; absent, it is an ordinary change.
157
+ */
158
+ onStageRename?: (decl: T, from: string, to: string) => void;
159
+ /** A quality's stages were reordered (after `onChange`). `note` is a quiet
160
+ * line under the stages for the caller to fill, such as how many gates
161
+ * compare against the ladder. */
162
+ onStageReorder?: (decl: T, note: HTMLElement) => void;
146
163
  }
147
164
  interface PropertyListHandle extends SettingsSectionHandle {
148
165
  /** Both faults, for the Save gate: a duplicate first, then an illegal name. */
@@ -78,8 +78,14 @@ interface SettingsDialogOptions {
78
78
  sections: SettingsSection[];
79
79
  /** Fired when Save is clicked and every section validates. The app reads its
80
80
  * own section state and persists. May be async; the dialog stays open until
81
- * it resolves, then closes. */
82
- onSave: () => void | Promise<void>;
81
+ * it resolves, then closes.
82
+ *
83
+ * Return `false` (or a Promise of it) when the save was REFUSED, a version
84
+ * control lock say, and the dialog stays open with the author's edits in it
85
+ * for another try; saying why is the app's (a toast, usually). A throw, or a
86
+ * rejected Promise, is treated the same way rather than surfacing as an
87
+ * unhandled rejection. Anything else closes the dialog, as it always has. */
88
+ onSave: () => void | boolean | Promise<void | boolean>;
83
89
  }
84
90
  interface SettingsDialog {
85
91
  /** Open (optionally on a given tab id). Sections re-mount from current data. */
@@ -143,6 +149,17 @@ interface PropertyListOptions<T extends PropertyDeclLike> extends DefaultControl
143
149
  extraDetails?: (decl: T, index: number) => (HTMLElement | null)[];
144
150
  /** Offer the Purpose field. Default true. */
145
151
  purpose?: boolean;
152
+ /**
153
+ * A quality's stage was renamed in place (the declaration's own default
154
+ * already follows it). Given, the rename is handed here INSTEAD of
155
+ * `onChange`, so the caller can carry every value that names the stage in
156
+ * the same edit as the declaration; absent, it is an ordinary change.
157
+ */
158
+ onStageRename?: (decl: T, from: string, to: string) => void;
159
+ /** A quality's stages were reordered (after `onChange`). `note` is a quiet
160
+ * line under the stages for the caller to fill, such as how many gates
161
+ * compare against the ladder. */
162
+ onStageReorder?: (decl: T, note: HTMLElement) => void;
146
163
  }
147
164
  interface PropertyListHandle extends SettingsSectionHandle {
148
165
  /** Both faults, for the Save gate: a duplicate first, then an illegal name. */
@@ -260,11 +260,18 @@ var DRAWN = {
260
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
261
  /** Record: the disc, in whatever colour the button is. (a disc; see DISC_LARGE) */
262
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"/>'),
263
267
  // --- handling -------------------------------------------------------------
264
268
  /** The drag grip on a reorderable row. (grip-vertical) */
265
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"/>'),
266
270
  /** Add one of whatever this list holds. (plus) */
267
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"/>'),
268
275
  /** Connect: to a server, a live link, a running game. (plug) */
269
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"/>'),
270
277
  /** Search. (search) */
@@ -345,16 +352,17 @@ function iconBtn(name, title, onClick, disabled = false, danger = false) {
345
352
  b.addEventListener("click", onClick);
346
353
  return b;
347
354
  }
348
- function labelled(label, control) {
355
+ function labelled(label, control, hint) {
349
356
  const target = control.matches("input, select, textarea") ? control : control.querySelector("input:not([type=button]):not([type=submit]), select, textarea");
350
357
  const w = el(target ? "label" : "div", "shell-labelled");
351
358
  w.append(el("span", "shell-fieldcap", label), control);
352
359
  if (target) {
353
- if (!target.id) target.id = `shell-f-${Math.random().toString(36).slice(2, 9)}`;
360
+ if (!target.id) target.id = uid();
354
361
  w.htmlFor = target.id;
355
362
  }
356
363
  return w;
357
364
  }
365
+ var uid = () => `shell-f-${Math.random().toString(36).slice(2, 9)}`;
358
366
  function moveItem(arr, i, delta) {
359
367
  const j = i + delta;
360
368
  if (j < 0 || j >= arr.length) return false;
@@ -402,7 +410,7 @@ function tagChips(holder, onChange) {
402
410
  wrap.append(input);
403
411
  return wrap;
404
412
  }
405
- function stageChips(holder, onChange) {
413
+ function stageChips(holder, onChange, opts = {}) {
406
414
  const wrap = el("div", "shell-tags shell-stages");
407
415
  const input = el("input", "shell-tag-input");
408
416
  input.type = "text";
@@ -417,35 +425,79 @@ function stageChips(holder, onChange) {
417
425
  b.addEventListener("click", onClick);
418
426
  return b;
419
427
  };
428
+ const nameLabel = (chip, v, i) => {
429
+ const label = el("span", "shell-tag-label", `${i + 1}. ${v}`);
430
+ label.setAttribute("role", "button");
431
+ label.tabIndex = 0;
432
+ label.dataset["tip"] = `Rename ${v}`;
433
+ const edit = () => {
434
+ const field = el("input", "shell-tag-input shell-stage-rename");
435
+ field.type = "text";
436
+ field.value = v;
437
+ field.spellcheck = false;
438
+ field.setAttribute("aria-label", `Rename ${v}`);
439
+ let done = false;
440
+ const finish = (keep) => {
441
+ if (done) return;
442
+ done = true;
443
+ const to = field.value.trim();
444
+ const stages = holder.stages ?? [];
445
+ if (keep && to !== "" && to !== v && !stages.includes(to)) {
446
+ stages[stages.indexOf(v)] = to;
447
+ rebuild();
448
+ if (opts.onRename) opts.onRename(v, to);
449
+ else onChange?.();
450
+ } else {
451
+ rebuild();
452
+ }
453
+ };
454
+ field.addEventListener("keydown", (e) => {
455
+ if (e.key === "Enter") {
456
+ e.preventDefault();
457
+ finish(true);
458
+ } else if (e.key === "Escape") {
459
+ e.preventDefault();
460
+ e.stopPropagation();
461
+ finish(false);
462
+ }
463
+ });
464
+ field.addEventListener("blur", () => finish(true));
465
+ label.replaceWith(field);
466
+ chip.classList.add("editing");
467
+ field.focus();
468
+ field.select();
469
+ };
470
+ label.addEventListener("click", (e) => {
471
+ e.preventDefault();
472
+ edit();
473
+ });
474
+ label.addEventListener("keydown", (e) => {
475
+ if (e.key === "Enter" || e.key === " ") {
476
+ e.preventDefault();
477
+ edit();
478
+ }
479
+ });
480
+ return label;
481
+ };
482
+ const moved = () => {
483
+ rebuild();
484
+ onChange?.();
485
+ opts.onReorder?.();
486
+ };
420
487
  const rebuild = () => {
421
488
  for (const c of Array.from(wrap.children)) if (c !== input) c.remove();
422
489
  const stages = holder.stages ?? [];
423
490
  stages.forEach((v, i) => {
424
- const chip = el("span", "shell-tag", `${i + 1}. ${v}`);
491
+ const chip = el("span", "shell-tag");
425
492
  chip.dataset["stage"] = v;
426
493
  chip.append(
427
- control(
428
- "back",
429
- `Move ${v} earlier`,
430
- i === 0,
431
- () => {
432
- if (moveItem(stages, i, -1)) {
433
- rebuild();
434
- onChange?.();
435
- }
436
- }
437
- ),
438
- control(
439
- "forward",
440
- `Move ${v} later`,
441
- i === stages.length - 1,
442
- () => {
443
- if (moveItem(stages, i, 1)) {
444
- rebuild();
445
- onChange?.();
446
- }
447
- }
448
- )
494
+ nameLabel(chip, v, i),
495
+ control("back", `Move ${v} earlier`, i === 0, () => {
496
+ if (moveItem(stages, i, -1)) moved();
497
+ }),
498
+ control("forward", `Move ${v} later`, i === stages.length - 1, () => {
499
+ if (moveItem(stages, i, 1)) moved();
500
+ })
449
501
  );
450
502
  const x = el("button", "shell-tag-x", iconNode("close", CHIP_ICON));
451
503
  x.type = "button";
@@ -736,7 +788,21 @@ function mountPropertyList(host, decls, opts = {}) {
736
788
  }, false, true);
737
789
  const details = keep(opts.extraDetails?.(p, i));
738
790
  if (p.type === "enum" || p.type === "flags") details.push(labelled("Values", tagChips(p, refreshDefault)));
739
- if (p.type === "quality") details.push(labelled("Stages (in order)", stageChips(p, refreshDefault)));
791
+ if (p.type === "quality") {
792
+ const note = el("p", "shell-stage-note");
793
+ const stages = stageChips(p, refreshDefault, {
794
+ onRename: (from, to) => {
795
+ if (p.default === from) p.default = to;
796
+ const fresh = defaultControl(p, changed, opts);
797
+ def.replaceWith(fresh);
798
+ def = fresh;
799
+ if (opts.onStageRename) opts.onStageRename(p, from, to);
800
+ else changed();
801
+ },
802
+ onReorder: () => opts.onStageReorder?.(p, note)
803
+ });
804
+ details.push(labelled("Stages (in order)", stages), note);
805
+ }
740
806
  if (opts.purpose !== false) {
741
807
  const purpose = el("input", "field set-purpose");
742
808
  purpose.type = "text";