@wildwinter/app-shell 0.38.0 → 0.39.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 (144) hide show
  1. package/LICENSES.md +62 -0
  2. package/README.md +8 -0
  3. package/dist/about.css +13 -34
  4. package/dist/about.css.map +1 -1
  5. package/dist/anchored.css +5 -5
  6. package/dist/anchored.css.map +1 -1
  7. package/dist/app-store.cjs +15 -0
  8. package/dist/app-store.cjs.map +1 -1
  9. package/dist/app-store.d.cts +25 -1
  10. package/dist/app-store.d.ts +25 -1
  11. package/dist/app-store.js +14 -1
  12. package/dist/app-store.js.map +1 -1
  13. package/dist/chunk-2ECJUTSX.js +97 -0
  14. package/dist/chunk-2ECJUTSX.js.map +1 -0
  15. package/dist/chunk-2NMVIM54.js +55 -0
  16. package/dist/chunk-2NMVIM54.js.map +1 -0
  17. package/dist/chunk-C6UVQYKI.js +70 -0
  18. package/dist/chunk-C6UVQYKI.js.map +1 -0
  19. package/dist/chunk-IU7EY6BI.js +26 -0
  20. package/dist/chunk-IU7EY6BI.js.map +1 -0
  21. package/dist/chunk-K2AOLQW4.js +82 -0
  22. package/dist/chunk-K2AOLQW4.js.map +1 -0
  23. package/dist/chunk-KGAMP5LW.js +87 -0
  24. package/dist/chunk-KGAMP5LW.js.map +1 -0
  25. package/dist/chunk-L5HOBEKL.js +534 -0
  26. package/dist/chunk-L5HOBEKL.js.map +1 -0
  27. package/dist/chunk-P4G4ERG6.js +60 -0
  28. package/dist/chunk-P4G4ERG6.js.map +1 -0
  29. package/dist/chunk-P6WLAEKZ.js +71 -0
  30. package/dist/chunk-P6WLAEKZ.js.map +1 -0
  31. package/dist/chunk-S4KWQLYN.js +66 -0
  32. package/dist/chunk-S4KWQLYN.js.map +1 -0
  33. package/dist/chunk-XZYZXY7I.js +554 -0
  34. package/dist/chunk-XZYZXY7I.js.map +1 -0
  35. package/dist/comments.css +5 -3
  36. package/dist/comments.css.map +1 -1
  37. package/dist/confirm.css +1 -54
  38. package/dist/confirm.css.map +1 -1
  39. package/dist/context-menu.cjs +250 -0
  40. package/dist/context-menu.cjs.map +1 -0
  41. package/dist/context-menu.css +64 -0
  42. package/dist/context-menu.css.map +1 -0
  43. package/dist/context-menu.d.cts +25 -0
  44. package/dist/context-menu.d.ts +25 -0
  45. package/dist/context-menu.js +5 -0
  46. package/dist/context-menu.js.map +1 -0
  47. package/dist/controls.css +155 -0
  48. package/dist/controls.css.map +1 -0
  49. package/dist/dialog.cjs +222 -0
  50. package/dist/dialog.cjs.map +1 -0
  51. package/dist/dialog.css +55 -0
  52. package/dist/dialog.css.map +1 -0
  53. package/dist/dialog.d.cts +28 -0
  54. package/dist/dialog.d.ts +28 -0
  55. package/dist/dialog.js +5 -0
  56. package/dist/dialog.js.map +1 -0
  57. package/dist/floating.cjs +83 -0
  58. package/dist/floating.cjs.map +1 -0
  59. package/dist/floating.d.cts +25 -0
  60. package/dist/floating.d.ts +25 -0
  61. package/dist/floating.js +4 -0
  62. package/dist/floating.js.map +1 -0
  63. package/dist/identity.css +15 -50
  64. package/dist/identity.css.map +1 -1
  65. package/dist/index.cjs +1307 -258
  66. package/dist/index.cjs.map +1 -1
  67. package/dist/index.d.cts +349 -149
  68. package/dist/index.d.ts +349 -149
  69. package/dist/index.js +259 -822
  70. package/dist/index.js.map +1 -1
  71. package/dist/job.css +23 -0
  72. package/dist/job.css.map +1 -1
  73. package/dist/link-status.cjs +430 -0
  74. package/dist/link-status.cjs.map +1 -0
  75. package/dist/link-status.css +76 -0
  76. package/dist/link-status.css.map +1 -0
  77. package/dist/link-status.d.cts +53 -0
  78. package/dist/link-status.d.ts +53 -0
  79. package/dist/link-status.js +5 -0
  80. package/dist/link-status.js.map +1 -0
  81. package/dist/menu.cjs +62 -1
  82. package/dist/menu.cjs.map +1 -1
  83. package/dist/menu.d.cts +88 -1
  84. package/dist/menu.d.ts +88 -1
  85. package/dist/menu.js +1 -1
  86. package/dist/notes-editor.css +3 -46
  87. package/dist/notes-editor.css.map +1 -1
  88. package/dist/pane-shell.css +16 -3
  89. package/dist/pane-shell.css.map +1 -1
  90. package/dist/property-list-D3VZCJzi.d.cts +162 -0
  91. package/dist/property-list-D3VZCJzi.d.ts +162 -0
  92. package/dist/property-list.cjs +784 -0
  93. package/dist/property-list.cjs.map +1 -0
  94. package/dist/property-list.d.cts +1 -0
  95. package/dist/property-list.d.ts +1 -0
  96. package/dist/property-list.js +4 -0
  97. package/dist/property-list.js.map +1 -0
  98. package/dist/settings.css +52 -8
  99. package/dist/settings.css.map +1 -1
  100. package/dist/stale.css +3 -0
  101. package/dist/stale.css.map +1 -1
  102. package/dist/stepper.css +35 -0
  103. package/dist/stepper.css.map +1 -1
  104. package/dist/tokens.css +19 -0
  105. package/dist/tokens.css.map +1 -1
  106. package/dist/tool-window.cjs +63 -0
  107. package/dist/tool-window.cjs.map +1 -1
  108. package/dist/tool-window.css +6 -3
  109. package/dist/tool-window.css.map +1 -1
  110. package/dist/tool-window.d.cts +67 -1
  111. package/dist/tool-window.d.ts +67 -1
  112. package/dist/tool-window.js +63 -1
  113. package/dist/tool-window.js.map +1 -1
  114. package/dist/updater-view.cjs +283 -0
  115. package/dist/updater-view.cjs.map +1 -0
  116. package/dist/updater-view.d.cts +23 -0
  117. package/dist/updater-view.d.ts +23 -0
  118. package/dist/updater-view.js +6 -0
  119. package/dist/updater-view.js.map +1 -0
  120. package/dist/updater.cjs +12 -16
  121. package/dist/updater.cjs.map +1 -1
  122. package/dist/updater.css +44 -0
  123. package/dist/updater.css.map +1 -0
  124. package/dist/updater.js +12 -16
  125. package/dist/updater.js.map +1 -1
  126. package/dist/util.cjs +78 -0
  127. package/dist/util.cjs.map +1 -0
  128. package/dist/util.d.cts +37 -0
  129. package/dist/util.d.ts +37 -0
  130. package/dist/util.js +3 -0
  131. package/dist/util.js.map +1 -0
  132. package/dist/vc.css +3 -2
  133. package/dist/vc.css.map +1 -1
  134. package/dist/welcome.cjs +215 -0
  135. package/dist/welcome.cjs.map +1 -0
  136. package/dist/welcome.css +112 -0
  137. package/dist/welcome.css.map +1 -0
  138. package/dist/welcome.d.cts +55 -0
  139. package/dist/welcome.d.ts +55 -0
  140. package/dist/welcome.js +4 -0
  141. package/dist/welcome.js.map +1 -0
  142. package/package.json +58 -2
  143. package/dist/chunk-QDPFU4WX.js +0 -43
  144. package/dist/chunk-QDPFU4WX.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,241 +1,19 @@
1
+ export { mountWelcome } from './chunk-C6UVQYKI.js';
2
+ export { feedUpdaterDownloadProgress, showUpdaterDialog } from './chunk-S4KWQLYN.js';
3
+ export { linkAddress, linkStatusClass, linkStatusTip, mountLinkStatus } from './chunk-KGAMP5LW.js';
4
+ import { isEditableTarget } from './chunk-P6WLAEKZ.js';
5
+ export { copyWithFeedback, debounce, formatCount, isEditableTarget, plural, relativeTime } from './chunk-P6WLAEKZ.js';
6
+ export { EDIT_MENU, FILE_MENU, GO_MENU, PANE_MENU, PLAY_MENU, PUBLISH_MENU, REVIEW_MENU, VIEW_MENU } from './chunk-2ECJUTSX.js';
7
+ export { PROPERTY_NAME_HINT, PROPERTY_TYPES, RESERVED_PROPERTY_NAMES, bindPropertyName, bindPropertyRef, defaultControl, dupGuard, expandableRow, firstIllegalPropertyName, focusNewRow, isCaseOnlyPropertyName, isValidPropertyName, mountPropertyList, mountSettingsDialog, propertyNameProblem, propertyNameify, propertyRefProblem, revalidatePropertyRefs, revealRow, revealRowWhenReady } from './chunk-L5HOBEKL.js';
8
+ import { dialogFrame } from './chunk-2NMVIM54.js';
9
+ export { dialogFrame } from './chunk-2NMVIM54.js';
10
+ export { createFloating } from './chunk-P4G4ERG6.js';
1
11
  export { JOB_PROGRESS } from './chunk-MDGOIT3O.js';
2
- export { EDIT_MENU, GO_MENU, PANE_MENU } from './chunk-QDPFU4WX.js';
3
-
4
- // src/tooltip.ts
5
- var B_OPEN = "";
6
- var B_CLOSE = "";
7
- function tipBold(s) {
8
- return `${B_OPEN}${s}${B_CLOSE}`;
9
- }
10
- function renderTip(host, text) {
11
- host.textContent = "";
12
- const re = /\u0002([^\u0003]*)\u0003/g;
13
- let last = 0;
14
- let m;
15
- while ((m = re.exec(text)) !== null) {
16
- if (m.index > last) host.append(text.slice(last, m.index));
17
- const strong = document.createElement("strong");
18
- strong.textContent = m[1] ?? "";
19
- host.append(strong);
20
- last = m.index + m[0].length;
21
- }
22
- if (last < text.length) host.append(text.slice(last));
23
- }
24
- var inited = false;
25
- var checked = false;
26
- var suppressed;
27
- var SHOW_DELAY = 350;
28
- var EDGE = 6;
29
- var GAP = 7;
30
- var tip = null;
31
- var timer = 0;
32
- var active = null;
33
- var activeKey = null;
34
- function ensureEl() {
35
- if (tip) return tip;
36
- const el2 = document.createElement("div");
37
- el2.className = "tooltip";
38
- el2.setAttribute("role", "tooltip");
39
- el2.hidden = true;
40
- document.body.appendChild(el2);
41
- tip = el2;
42
- return el2;
43
- }
44
- var rectOf = (anchor) => anchor instanceof HTMLElement ? anchor.getBoundingClientRect() : anchor;
45
- function place(anchor) {
46
- const t = ensureEl();
47
- const r = rectOf(anchor);
48
- const tr = t.getBoundingClientRect();
49
- let top = r.top - tr.height - GAP;
50
- if (top < EDGE) top = r.top + r.height + GAP;
51
- let left = r.left + r.width / 2 - tr.width / 2;
52
- left = Math.max(EDGE, Math.min(left, window.innerWidth - tr.width - EDGE));
53
- t.style.left = `${Math.round(left)}px`;
54
- t.style.top = `${Math.round(top)}px`;
55
- }
56
- function show(anchor) {
57
- const text = anchor.dataset["tip"];
58
- if (!text) return;
59
- paint(anchor, text);
60
- }
61
- function paint(anchor, text) {
62
- if (suppressed?.()) return;
63
- const t = ensureEl();
64
- const host = (anchor instanceof HTMLElement ? anchor.closest("dialog[open]") : null) ?? document.body;
65
- if (t.parentElement !== host) host.appendChild(t);
66
- active = anchor instanceof HTMLElement ? anchor : null;
67
- renderTip(t, text);
68
- t.hidden = false;
69
- t.classList.remove("show");
70
- void t.offsetWidth;
71
- place(anchor);
72
- t.classList.add("show");
73
- }
74
- function hide() {
75
- if (timer) {
76
- window.clearTimeout(timer);
77
- timer = 0;
78
- }
79
- active = null;
80
- activeKey = null;
81
- if (tip) {
82
- tip.hidden = true;
83
- tip.classList.remove("show");
84
- }
85
- }
86
- function schedule(anchor) {
87
- if (anchor === active) return;
88
- if (timer) window.clearTimeout(timer);
89
- timer = window.setTimeout(() => show(anchor), SHOW_DELAY);
90
- }
91
- function tipAt(key, rect, text) {
92
- if (key === activeKey) {
93
- if (tip && !tip.hidden) {
94
- renderTip(tip, text);
95
- place(rect);
96
- }
97
- return;
98
- }
99
- hide();
100
- activeKey = key;
101
- timer = window.setTimeout(() => {
102
- timer = 0;
103
- paint(rect, text);
104
- }, SHOW_DELAY);
105
- }
106
- function hideTip() {
107
- hide();
108
- }
109
- function ensureTooltipHost() {
110
- initTooltips();
111
- }
112
- function checkTooltipHost() {
113
- if (checked) return;
114
- checked = true;
115
- setTimeout(() => {
116
- if (inited || typeof document === "undefined") return;
117
- if (document.querySelector("[data-tip]")) {
118
- 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.");
119
- }
120
- }, 0);
121
- }
122
- var tipAncestor = (n) => n instanceof Element ? n.closest("[data-tip]") : null;
123
- function initTooltips(opts = {}) {
124
- if (opts.suppressed !== void 0) suppressed = opts.suppressed;
125
- if (inited) return;
126
- inited = true;
127
- document.addEventListener("pointerover", (e) => {
128
- const el2 = tipAncestor(e.target);
129
- if (el2) schedule(el2);
130
- });
131
- document.addEventListener("pointerout", (e) => {
132
- const el2 = tipAncestor(e.target);
133
- if (!el2) return;
134
- const to = e.relatedTarget;
135
- if (to instanceof Node && el2.contains(to)) return;
136
- hide();
137
- });
138
- document.addEventListener("focusin", (e) => {
139
- const el2 = tipAncestor(e.target);
140
- if (el2) schedule(el2);
141
- });
142
- document.addEventListener("focusout", () => hide());
143
- document.addEventListener("pointerdown", () => hide(), true);
144
- document.addEventListener("scroll", () => {
145
- if (active) hide();
146
- }, true);
147
- document.addEventListener("keydown", (e) => {
148
- if (e.key === "Escape") hide();
149
- });
150
- window.addEventListener("blur", () => hide());
151
- }
152
-
153
- // src/dom.ts
154
- function el(tag, classNameOrProps, ...children) {
155
- const node2 = document.createElement(tag);
156
- const props = typeof classNameOrProps === "string" ? { className: classNameOrProps } : classNameOrProps ?? {};
157
- if (props.className) node2.className = props.className;
158
- if (props.text !== void 0 && props.text !== null) node2.textContent = props.text;
159
- if (props.title !== void 0) node2.title = props.title;
160
- if (props.tip !== void 0) {
161
- node2.dataset["tip"] = props.tip;
162
- node2.setAttribute("aria-label", props.tip);
163
- ensureTooltipHost();
164
- }
165
- if (props.onClick) node2.addEventListener("click", props.onClick);
166
- for (const child of children) {
167
- if (child === null || child === void 0) continue;
168
- node2.append(child);
169
- }
170
- checkTooltipHost();
171
- return node2;
172
- }
173
- function iconBtn(glyph, title, onClick, disabled = false, danger = false) {
174
- const b = el("button", `shell-icon${danger ? " danger" : ""}`);
175
- b.type = "button";
176
- b.textContent = glyph;
177
- b.dataset["tip"] = title;
178
- b.setAttribute("aria-label", title);
179
- b.disabled = disabled;
180
- b.addEventListener("click", onClick);
181
- return b;
182
- }
183
- function labelled(label2, control) {
184
- const target = control.matches("input, select, textarea") ? control : control.querySelector("input:not([type=button]):not([type=submit]), select, textarea");
185
- const w = el(target ? "label" : "div", "shell-labelled");
186
- w.append(el("span", "shell-fieldcap", label2), control);
187
- if (target) {
188
- if (!target.id) target.id = `shell-f-${Math.random().toString(36).slice(2, 9)}`;
189
- w.htmlFor = target.id;
190
- }
191
- return w;
192
- }
193
- function moveItem(arr, i, delta) {
194
- const j = i + delta;
195
- if (j < 0 || j >= arr.length) return false;
196
- [arr[i], arr[j]] = [arr[j], arr[i]];
197
- return true;
198
- }
199
- function tagChips(holder, onChange) {
200
- const wrap = el("div", "shell-tags");
201
- const input = el("input", "shell-tag-input");
202
- input.type = "text";
203
- input.placeholder = "Add value";
204
- input.spellcheck = false;
205
- const makeChip = (v) => {
206
- const chip = el("span", "shell-tag", v);
207
- const x = el("button", "shell-tag-x", "\u2715");
208
- x.type = "button";
209
- x.dataset["tip"] = `remove ${v}`;
210
- x.setAttribute("aria-label", `remove ${v}`);
211
- x.addEventListener("click", () => {
212
- holder.values = (holder.values ?? []).filter((o) => o !== v);
213
- chip.remove();
214
- onChange?.();
215
- });
216
- chip.append(x);
217
- return chip;
218
- };
219
- const commit = () => {
220
- const v = input.value.trim();
221
- if (v && !(holder.values ?? []).includes(v)) {
222
- (holder.values ??= []).push(v);
223
- wrap.insertBefore(makeChip(v), input);
224
- onChange?.();
225
- }
226
- input.value = "";
227
- };
228
- input.addEventListener("keydown", (e) => {
229
- if (e.key === "Enter" || e.key === ",") {
230
- e.preventDefault();
231
- commit();
232
- }
233
- });
234
- input.addEventListener("blur", commit);
235
- for (const v of holder.values ?? []) wrap.append(makeChip(v));
236
- wrap.append(input);
237
- return wrap;
238
- }
12
+ export { openContextMenu, openPopover } from './chunk-K2AOLQW4.js';
13
+ import { ensureTooltipHost, el, iconNode, labelled, icon } from './chunk-XZYZXY7I.js';
14
+ export { ICON_NAMES, ICON_SIZE, el, hideTip, icon, iconBtn, iconHtml, iconNameOfGlyph, iconNode, iconSvg, initTooltips, isIconName, labelled, moveItem, stageChips, tagChips, tipAt, tipBold, wireReorder } from './chunk-XZYZXY7I.js';
15
+ import { closeWithExit } from './chunk-IU7EY6BI.js';
16
+ export { closeWithExit } from './chunk-IU7EY6BI.js';
239
17
 
240
18
  // src/colour.ts
241
19
  var PALETTE_SIZE = 12;
@@ -257,79 +35,6 @@ function colourFor(name) {
257
35
  return PALETTE[colourIndex(name)];
258
36
  }
259
37
 
260
- // src/icons.ts
261
- var icon = {
262
- // --- dismissing and revealing ---------------------------------------------
263
- /** Close a panel, a dialog, a popover. Patterpad's, twelve times over. */
264
- close: "\u2715",
265
- /** The overflow menu on a document or a row: "more about this". */
266
- more: "\u22EF",
267
- /** Collapsed disclosure, pointing at what would open. */
268
- collapsed: "\u25B8",
269
- /** Expanded disclosure. */
270
- expanded: "\u25BE",
271
- /** A dropdown's affordance, where it is not a real <select>. */
272
- dropdown: "\u25BE",
273
- // --- moving through things ------------------------------------------------
274
- /** Back / previous, in a breadcrumb or a stepper. */
275
- back: "\u2039",
276
- /** Forward / next. */
277
- forward: "\u203A",
278
- /** Up one, in a list. */
279
- up: "\u2191",
280
- /** Down one. */
281
- down: "\u2193",
282
- // --- state ----------------------------------------------------------------
283
- /** Done, applied, agreed. */
284
- tick: "\u2713",
285
- /** Somebody else holds this: the version-control lock. */
286
- locked: "\u2298",
287
- /** Read-only on disk with no other holder: the resting state of everything
288
- * in a lock-based working copy that nobody has checked out yet. */
289
- readOnly: "\u25CB",
290
- /** YOU hold this one: checked out / opened by the current user, still yours to
291
- * edit. Shares its glyph with `note`, which the table permits: an entry is a
292
- * MEANING, and two meanings may draw the same shape. */
293
- checkedOut: "\u270E",
294
- /** Tracked, with local changes not yet committed. */
295
- modified: "\u25CF",
296
- /** Not in version control yet: new since the last commit. */
297
- untracked: "+",
298
- /** Something needs attention but is not fatal. */
299
- warning: "\u203C",
300
- /** A live thing: the running position, an active marker. */
301
- dot: "\u25CF",
302
- /** Play, in the sense of "run this". */
303
- play: "\u25B6",
304
- /** Start the thing again from the top: a play session, a sweep. Named for the
305
- * act, not the arrow, which is the table's rule. */
306
- restart: "\u27F2",
307
- /** The three ways a collection can be shown. Named by what you get rather than
308
- * by the glyph, so a second app can pick the same three without copying
309
- * somebody's taste in symbols. */
310
- viewCards: "\u25A6",
311
- viewTable: "\u2630",
312
- viewNode: "\u25C8",
313
- // --- handling -------------------------------------------------------------
314
- /** The drag grip on a reorderable row. */
315
- grip: "\u283F",
316
- /** Add one of whatever this list holds. */
317
- add: "+",
318
- // --- annotation -----------------------------------------------------------
319
- /** Documentation notes: the pencil. Both apps had chosen it independently,
320
- * which is the happiest way for a vocabulary entry to arrive. */
321
- note: "\u270E",
322
- /** A comment thread. Speech, not a pencil: documentation states the reason,
323
- * comments are the conversation, and the two must never look alike. */
324
- comment: "\u275D"
325
- };
326
- var iconSvg = {
327
- /** A document with lines: notes exist on this thing. */
328
- noteFilled: '<svg viewBox="0 0 16 16" width="13" height="13" fill="currentColor" fill-rule="evenodd" aria-hidden="true"><path d="M3.4 1.4h5.3L12.6 5.3v8.1a1.2 1.2 0 0 1-1.2 1.2H3.4a1.2 1.2 0 0 1-1.2-1.2V2.6A1.2 1.2 0 0 1 3.4 1.4ZM4.9 6.8h6.2v1.1H4.9Zm0 2.1h6.2v1.1H4.9Zm0 2.1h3.7v1.1H4.9Z"/></svg>',
329
- /** The same document, empty: nothing written here yet. */
330
- noteOutline: '<svg viewBox="0 0 16 16" width="13" height="13" fill="none" stroke="currentColor" stroke-width="1.1" stroke-linejoin="round" stroke-linecap="round" aria-hidden="true"><path d="M3.4 2h4.9l3.9 3.9v7.5a1 1 0 0 1-1 1H3.4a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1Z"/><path d="M8.3 2.2v3.8h3.8"/><path d="M4.9 8h6.2M4.9 10h6.2M4.9 12h3.7" stroke-width="0.95"/></svg>'
331
- };
332
-
333
38
  // src/pane-shell.ts
334
39
  var DEFAULT_MAX = (w) => Math.min(640, w - 420);
335
40
  var openVar = { nav: "--nav-open-w", inspector: "--insp-open-w" };
@@ -342,6 +47,8 @@ function mountPaneShell(host, opts) {
342
47
  inspector: offered("inspector") && (opts.initial?.open?.inspector ?? true)
343
48
  };
344
49
  const width = { ...opts.initial?.width ?? {} };
50
+ const held = { nav: false, inspector: false };
51
+ const showing = (side) => open[side] && !held[side];
345
52
  const el2 = (tag, cls) => {
346
53
  const n = document.createElement(tag);
347
54
  if (cls) n.className = cls;
@@ -372,18 +79,28 @@ function mountPaneShell(host, opts) {
372
79
  host.append(root);
373
80
  if (!offered("nav")) navToggle.remove();
374
81
  if (!offered("inspector")) inspToggle.remove();
375
- function applyToggleGlyph(side, button) {
376
- const isOpen = open[side];
377
- button.textContent = side === "nav" ? isOpen ? "\u2039" : "\u203A" : isOpen ? "\u203A" : "\u2039";
82
+ const defaultTip = (side, isOpen) => {
378
83
  const noun = cfg[side].label ?? side;
379
84
  const hint = cfg[side].shortcutHint ? ` (${cfg[side].shortcutHint})` : "";
380
- button.title = `${isOpen ? "Hide" : "Show"} ${noun}${hint}`;
381
- button.setAttribute("aria-label", button.title);
85
+ return `${isOpen ? "Hide" : "Show"} ${noun}${hint}`;
86
+ };
87
+ const tipFor = opts.tipFor ?? defaultTip;
88
+ function applyToggleGlyph(side, button) {
89
+ const isOpen = open[side];
90
+ button.replaceChildren(iconNode(side === "nav" ? isOpen ? "back" : "forward" : isOpen ? "forward" : "back"));
91
+ const tip = tipFor(side, isOpen);
92
+ button.dataset["tip"] = tip;
93
+ button.setAttribute("aria-label", tip);
382
94
  button.setAttribute("aria-pressed", String(isOpen));
383
95
  }
96
+ ensureTooltipHost();
384
97
  function apply() {
385
- panes.classList.toggle(collapseClass.nav, !open.nav);
386
- panes.classList.toggle(collapseClass.inspector, !open.inspector);
98
+ for (const side of ["nav", "inspector"]) {
99
+ const collapsed = !showing(side);
100
+ panes.classList.toggle(collapseClass[side], collapsed);
101
+ const also = opts.collapseAlso?.[side];
102
+ if (also) panes.classList.toggle(also, collapsed);
103
+ }
387
104
  panes.style.setProperty(openVar.nav, width.nav !== void 0 ? `${width.nav}px` : cfg.nav.defaultWidth);
388
105
  panes.style.setProperty(openVar.inspector, width.inspector !== void 0 ? `${width.inspector}px` : cfg.inspector.defaultWidth);
389
106
  applyToggleGlyph("nav", navToggle);
@@ -402,6 +119,11 @@ function mountPaneShell(host, opts) {
402
119
  function togglePane(side) {
403
120
  if (offered(side)) setPaneOpen(side, !open[side]);
404
121
  }
122
+ function holdClosed(side, next) {
123
+ if (held[side] === next) return;
124
+ held[side] = next;
125
+ apply();
126
+ }
405
127
  function resetWidths() {
406
128
  delete width.nav;
407
129
  delete width.inspector;
@@ -411,9 +133,11 @@ function mountPaneShell(host, opts) {
411
133
  navToggle.addEventListener("click", () => togglePane("nav"));
412
134
  inspToggle.addEventListener("click", () => togglePane("inspector"));
413
135
  function beginResize(side, startEvent) {
414
- if (cfg[side].resizable === false || !open[side]) return;
136
+ if (cfg[side].resizable === false || !showing(side)) return;
415
137
  startEvent.preventDefault();
416
138
  const pane = side === "nav" ? navPane : inspPane;
139
+ const seam = side === "nav" ? navResizer : inspResizer;
140
+ seam.classList.add("dragging");
417
141
  const min = cfg[side].minWidth ?? 160;
418
142
  const max = cfg[side].maxWidth ?? DEFAULT_MAX;
419
143
  const startX = startEvent.clientX;
@@ -429,6 +153,7 @@ function mountPaneShell(host, opts) {
429
153
  window.removeEventListener("pointermove", onMove);
430
154
  window.removeEventListener("pointerup", onUp);
431
155
  document.body.classList.remove("pane-resizing");
156
+ seam.classList.remove("dragging");
432
157
  width[side] = latest;
433
158
  persist();
434
159
  };
@@ -458,7 +183,8 @@ function mountPaneShell(host, opts) {
458
183
  inspector: inspInner,
459
184
  togglePane,
460
185
  setPaneOpen,
461
- isOpen: (s) => open[s],
186
+ isOpen: showing,
187
+ holdClosed,
462
188
  resetWidths,
463
189
  state: snapshot,
464
190
  destroy: () => {
@@ -467,189 +193,16 @@ function mountPaneShell(host, opts) {
467
193
  };
468
194
  }
469
195
 
470
- // src/settings.ts
471
- function expandableRow(opts) {
472
- const row = el("div", "set-row");
473
- if (opts.name !== void 0) row.dataset.name = opts.name;
474
- const line = el("div", "set-rowline");
475
- if (opts.details && opts.details.length) {
476
- const details = el("div", "set-details");
477
- details.hidden = true;
478
- for (const d of opts.details) details.append(d);
479
- const toggle = el("button", "set-expand");
480
- toggle.type = "button";
481
- toggle.dataset.tip = "More";
482
- toggle.setAttribute("aria-label", "More");
483
- toggle.setAttribute("aria-expanded", "false");
484
- toggle.addEventListener("click", () => {
485
- const open = details.hidden;
486
- details.hidden = !open;
487
- toggle.setAttribute("aria-expanded", String(open));
488
- toggle.classList.toggle("open", open);
489
- });
490
- line.append(toggle);
491
- opts.line.forEach((c) => line.append(c));
492
- row.append(line, details);
493
- } else {
494
- opts.line.forEach((c) => line.append(c));
495
- row.append(line);
496
- }
497
- return row;
498
- }
499
- function revealRow(within, name) {
500
- const row = [...within.querySelectorAll(".set-row")].find((r) => r.dataset.name === name);
501
- if (!row) return false;
502
- const toggle = row.querySelector(".set-rowline > .set-expand");
503
- if (toggle && toggle.getAttribute("aria-expanded") !== "true") toggle.click();
504
- const still = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
505
- row.scrollIntoView({ block: "center", behavior: still ? "auto" : "smooth" });
506
- row.classList.remove("landed");
507
- void row.offsetWidth;
508
- row.classList.add("landed");
509
- row.addEventListener("animationend", () => row.classList.remove("landed"), { once: true });
510
- return true;
511
- }
512
- function focusNewRow(listEl) {
513
- const last = listEl?.lastElementChild;
514
- if (!(last instanceof HTMLElement)) return;
515
- last.scrollIntoView({ block: "nearest" });
516
- const name = last.querySelector("input.set-name") ?? last.querySelector("input");
517
- name?.focus();
518
- }
519
- function dupGuard() {
520
- const items = [];
521
- const check = () => {
522
- const counts = /* @__PURE__ */ new Map();
523
- for (const it of items) {
524
- const k = it.key().trim().toLowerCase();
525
- if (k) counts.set(k, (counts.get(k) ?? 0) + 1);
526
- }
527
- let any = false;
528
- for (const it of items) {
529
- const k = it.key().trim().toLowerCase();
530
- const dup = !!k && (counts.get(k) ?? 0) > 1;
531
- it.input.classList.toggle("invalid", dup);
532
- if (dup) {
533
- it.input.title = "Duplicate name. Names must be unique.";
534
- any = true;
535
- } else if (it.input.title === "Duplicate name. Names must be unique.") it.input.removeAttribute("title");
536
- }
537
- return any;
538
- };
539
- return {
540
- reset() {
541
- items.length = 0;
542
- },
543
- track(input, key = () => input.value, also) {
544
- items.push({ input, key });
545
- input.addEventListener("input", check);
546
- for (const a of also ?? []) a.addEventListener("input", check);
547
- },
548
- check,
549
- firstDuplicate() {
550
- check();
551
- return items.find((it) => it.input.classList.contains("invalid"))?.input ?? null;
552
- }
553
- };
554
- }
555
- function mountSettingsDialog(opts) {
556
- const dialog = el("dialog", "settings-dialog");
557
- const form = el("form", "settings-form");
558
- form.method = "dialog";
559
- const tabs = el("nav", "settings-tabs");
560
- const panels = el("div", "settings-panels");
561
- const error = el("p", "settings-error");
562
- error.hidden = true;
563
- const cancel = el("button", "settings-cancel", "Cancel");
564
- cancel.type = "button";
565
- cancel.value = "cancel";
566
- const save = el("button", "settings-save primary", "Save");
567
- save.value = "save";
568
- const panelFor = /* @__PURE__ */ new Map();
569
- const tabFor = /* @__PURE__ */ new Map();
570
- const handles = /* @__PURE__ */ new Map();
571
- let active2 = opts.sections[0]?.id ?? "";
572
- let lastGroup;
573
- for (const s of opts.sections) {
574
- if (s.group && s.group !== lastGroup) {
575
- tabs.append(el("div", "settings-group", s.group));
576
- lastGroup = s.group;
577
- }
578
- const tab = el("button", "settings-tab", s.label);
579
- tab.type = "button";
580
- tab.dataset["tab"] = s.id;
581
- tab.addEventListener("click", () => showTab(s.id));
582
- tabs.append(tab);
583
- tabFor.set(s.id, tab);
584
- const panel = el("section", "settings-panel");
585
- panel.dataset["panel"] = s.id;
586
- panel.hidden = true;
587
- panels.append(panel);
588
- panelFor.set(s.id, panel);
589
- }
590
- function showTab(id) {
591
- active2 = id;
592
- for (const [sid, t] of tabFor) t.classList.toggle("active", sid === id);
593
- for (const [sid, p] of panelFor) p.hidden = sid !== id;
594
- }
595
- form.append(
596
- el("h2", "settings-title", opts.title),
597
- el("div", "settings-body", tabs, panels),
598
- el("div", "settings-actions", error, cancel, save)
599
- );
600
- dialog.append(form);
601
- document.body.append(dialog);
602
- form.addEventListener("keydown", (e) => {
603
- if (e.key === "Enter" && e.target?.tagName === "INPUT") e.preventDefault();
604
- });
605
- cancel.addEventListener("click", () => dialog.close("cancel"));
606
- save.addEventListener("click", (e) => {
607
- e.preventDefault();
608
- error.hidden = true;
609
- for (const s of opts.sections) {
610
- const bad = handles.get(s.id)?.firstInvalid?.();
611
- if (bad) {
612
- showTab(s.id);
613
- bad.focus();
614
- error.textContent = "Fix the highlighted fields first.";
615
- error.hidden = false;
616
- return;
617
- }
618
- }
619
- void (async () => {
620
- await opts.onSave();
621
- dialog.close("save");
622
- })();
623
- });
624
- return {
625
- open(tabId) {
626
- handles.clear();
627
- for (const s of opts.sections) {
628
- const panel = panelFor.get(s.id);
629
- panel.replaceChildren();
630
- handles.set(s.id, s.mount(panel));
631
- }
632
- error.hidden = true;
633
- showTab(tabId ?? active2 ?? opts.sections[0]?.id ?? "");
634
- dialog.showModal();
635
- },
636
- destroy() {
637
- dialog.remove();
638
- }
639
- };
640
- }
641
-
642
196
  // src/tool-window-web.ts
643
- var PIN_ICON = `<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="17" x2="12" y2="22"/><path d="M9 10.8a2 2 0 0 1-1.1 1.8l-1.8.9A2 2 0 0 0 5 15.3V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.7a2 2 0 0 0-1.1-1.8l-1.8-.9A2 2 0 0 1 15 10.8V7a1 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"/></svg>`;
644
197
  function pinButton(opts) {
645
198
  const b = el("button", "swin-pin");
646
199
  b.type = "button";
647
- b.innerHTML = PIN_ICON;
200
+ b.append(iconNode("pin"));
648
201
  let pinned = opts.pinned;
649
202
  const reflect = () => {
650
203
  b.classList.toggle("on", pinned);
651
204
  b.setAttribute("aria-pressed", String(pinned));
652
- const label2 = pinned ? "Pinned on top: click to unpin" : "Click to keep on top";
205
+ const label2 = pinned ? "Pinned on top. Click to unpin." : "Keep on top";
653
206
  b.dataset["tip"] = label2;
654
207
  b.setAttribute("aria-label", label2);
655
208
  };
@@ -664,66 +217,91 @@ function pinButton(opts) {
664
217
  reflect();
665
218
  } };
666
219
  }
667
-
668
- // src/exit.ts
669
- function closeWithExit(element, done) {
670
- element.classList.add("closing");
671
- const seconds = parseFloat(getComputedStyle(element).animationDuration) || 0;
672
- if (seconds === 0) {
673
- done();
674
- return;
220
+ var escSlot;
221
+ var escWired = false;
222
+ function wireEsc() {
223
+ if (escWired) return;
224
+ escWired = true;
225
+ window.addEventListener("keydown", (e) => {
226
+ if (e.key !== "Escape" || !escSlot) return;
227
+ if (e.defaultPrevented) return;
228
+ if (!escSlot.fromFields && isEditableTarget(e.target)) return;
229
+ if (escSlot.onEscape?.() === true) return;
230
+ escSlot.onClose();
231
+ });
232
+ }
233
+ function toolWindowHead(opts) {
234
+ const keep = (parts) => (parts ?? []).filter((p) => p !== null);
235
+ const pin = opts.pin ?? pinButton({ pinned: opts.pinned ?? true, onToggle: opts.onPin ?? (() => {
236
+ }) });
237
+ if (opts.esc !== false) {
238
+ escSlot = { onClose: opts.onClose, onEscape: opts.onEscape, fromFields: opts.esc === "always" };
239
+ wireEsc();
675
240
  }
676
- let fired = false;
677
- const finish = () => {
678
- if (fired) return;
679
- fired = true;
680
- element.removeEventListener("animationend", onEnd);
681
- clearTimeout(timer3);
682
- done();
683
- };
684
- const onEnd = (e) => {
685
- if (e.target === element) finish();
241
+ return el(
242
+ "header",
243
+ { className: `swin-head${opts.className ? ` ${opts.className}` : ""}` },
244
+ ...opts.title !== void 0 ? [el("span", { className: "swin-title", text: opts.title })] : [],
245
+ ...opts.tabs ? [opts.tabs] : [],
246
+ ...keep(opts.lead),
247
+ el("span", { className: "swin-spacer" }),
248
+ ...keep(opts.trail),
249
+ pin.el,
250
+ el("button", { className: "swin-close", tip: "Close (Esc)", onClick: opts.onClose }, iconNode("close"))
251
+ );
252
+ }
253
+ function followButton(opts) {
254
+ const what = opts.what ?? "line";
255
+ const b = el("button", "btn swin-follow", "Follow in the editor");
256
+ b.type = "button";
257
+ let on = opts.on;
258
+ const reflect = () => {
259
+ b.classList.toggle("on", on);
260
+ b.setAttribute("aria-pressed", String(on));
261
+ const label2 = on ? `The editor opens each ${what} as it plays. Click to stop.` : `Open each ${what} in the editor as it plays`;
262
+ b.dataset["tip"] = label2;
686
263
  };
687
- element.addEventListener("animationend", onEnd);
688
- const timer3 = setTimeout(finish, seconds * 1e3 + 120);
264
+ reflect();
265
+ b.addEventListener("click", () => {
266
+ on = !on;
267
+ reflect();
268
+ opts.onToggle(on);
269
+ });
270
+ return { el: b, set: (next) => {
271
+ on = next;
272
+ reflect();
273
+ } };
689
274
  }
690
275
 
691
276
  // src/confirm.ts
692
277
  function confirmDialog(opts) {
693
278
  return new Promise((resolve) => {
694
- const cancel = el("button", "confirm-btn cancel", "Cancel");
695
- const danger = el("button", "confirm-btn danger", opts.confirmLabel);
696
- cancel.type = "button";
697
- danger.type = "button";
698
- const dlg = el(
699
- "dialog",
700
- "confirm-dialog",
701
- el("div", "confirm-title", opts.title),
702
- el("div", "confirm-body", opts.body),
703
- el("div", "confirm-actions", cancel, danger)
704
- );
705
- dlg.setAttribute("aria-label", opts.title);
706
- document.body.append(dlg);
707
279
  let done = false;
708
280
  const finish = (ok) => {
709
281
  if (done) return;
710
282
  done = true;
711
283
  resolve(ok);
712
- closeWithExit(dlg, () => {
713
- dlg.close();
714
- dlg.remove();
715
- });
284
+ frame.close();
716
285
  };
286
+ const frame = dialogFrame({
287
+ title: opts.title,
288
+ className: "confirm-dialog",
289
+ onClose: () => finish(false)
290
+ // Esc, or closed by someone else
291
+ });
292
+ const cancel = el("button", "btn confirm-btn cancel", "Cancel");
293
+ const danger = el("button", "btn danger confirm-btn", opts.confirmLabel);
294
+ cancel.type = "button";
295
+ danger.type = "button";
296
+ if (opts.body !== void 0) frame.body.append(el("div", "confirm-body", opts.body));
297
+ if (opts.bodyNode) frame.body.append(opts.bodyNode);
298
+ frame.actions.append(cancel, danger);
717
299
  cancel.addEventListener("click", () => finish(false));
718
300
  danger.addEventListener("click", () => finish(true));
719
- dlg.addEventListener("cancel", (e) => {
720
- e.preventDefault();
721
- finish(false);
301
+ frame.dialog.addEventListener("mousedown", (e) => {
302
+ if (e.target === frame.dialog) finish(false);
722
303
  });
723
- dlg.addEventListener("mousedown", (e) => {
724
- if (e.target === dlg) finish(false);
725
- });
726
- dlg.showModal();
304
+ frame.open();
727
305
  cancel.focus();
728
306
  });
729
307
  }
@@ -731,18 +309,16 @@ function confirmDialog(opts) {
731
309
  // src/about.ts
732
310
  function showAbout(opts) {
733
311
  return new Promise((resolve) => {
734
- const dialog = el("dialog", "shell-about");
312
+ const frame = dialogFrame({ title: opts.appName, className: "shell-about", onClose: () => resolve() });
313
+ frame.dialog.setAttribute("aria-label", `About ${opts.appName}`);
735
314
  if (opts.wordmark !== void 0) {
736
315
  const mark = el("div", "shell-about-mark");
737
316
  mark.innerHTML = opts.wordmark;
738
- dialog.append(mark);
317
+ frame.dialog.prepend(mark);
739
318
  }
740
- dialog.append(
741
- el("div", "shell-about-name", opts.appName),
742
- el("div", "shell-about-version", `Version ${opts.version}`)
743
- );
744
- if (opts.blurb !== void 0) dialog.append(el("p", "shell-about-blurb", opts.blurb));
745
- if (opts.credits !== void 0) dialog.append(el("p", "shell-about-credits", opts.credits));
319
+ frame.body.append(el("div", "shell-about-version", `Version ${opts.version}`));
320
+ if (opts.blurb !== void 0) frame.body.append(el("p", "shell-about-blurb", opts.blurb));
321
+ if (opts.credits !== void 0) frame.body.append(el("p", "shell-about-credits", opts.credits));
746
322
  if (opts.links?.length) {
747
323
  const row = el("div", "shell-about-links");
748
324
  for (const link of opts.links) {
@@ -755,24 +331,13 @@ function showAbout(opts) {
755
331
  a.addEventListener("click", () => opts.onOpenLink?.(link.url));
756
332
  row.append(a);
757
333
  }
758
- dialog.append(row);
334
+ frame.body.append(row);
759
335
  }
760
- const close = el("button", "shell-about-close", "Close");
336
+ const close = el("button", "btn shell-about-close", "Close");
761
337
  close.type = "button";
762
- dialog.append(el("div", "shell-about-actions", close));
763
- dialog.setAttribute("aria-label", `About ${opts.appName}`);
764
- document.body.append(dialog);
765
- let done = false;
766
- const finish = () => {
767
- if (done) return;
768
- done = true;
769
- dialog.close();
770
- dialog.remove();
771
- resolve();
772
- };
773
- close.addEventListener("click", finish);
774
- dialog.addEventListener("close", finish);
775
- dialog.showModal();
338
+ close.addEventListener("click", () => frame.close());
339
+ frame.actions.append(close);
340
+ frame.open();
776
341
  close.focus();
777
342
  });
778
343
  }
@@ -787,7 +352,7 @@ function openAnchoredPanel(opts) {
787
352
  if (prev.anchor === opts.anchor) return null;
788
353
  }
789
354
  const panel = el("div", `shell-anchored ${opts.className ?? ""}`.trim());
790
- const closeBtn = el("button", "shell-anchored-close", "\u2715");
355
+ const closeBtn = el("button", "shell-anchored-close", iconNode("close", 12));
791
356
  closeBtn.type = "button";
792
357
  closeBtn.setAttribute("aria-label", "Close");
793
358
  const body = el("div", "shell-anchored-body");
@@ -795,7 +360,7 @@ function openAnchoredPanel(opts) {
795
360
  el("div", "shell-anchored-head", el("span", "shell-anchored-title", opts.title), closeBtn),
796
361
  body
797
362
  );
798
- document.body.append(panel);
363
+ (opts.host ?? document.body).append(panel);
799
364
  placeAnchored(panel, opts.anchor, opts.width, opts.prefer ?? "below", opts.keepClear ?? []);
800
365
  let closed = false;
801
366
  const close = () => {
@@ -835,7 +400,7 @@ function closeAnchoredPanel() {
835
400
  }
836
401
  function placeAnchored(panel, anchor, width, prefer, keepClear) {
837
402
  const a = anchor.getBoundingClientRect();
838
- const GAP2 = 10;
403
+ const GAP = 10;
839
404
  const CAP = window.innerHeight * 0.7;
840
405
  const MIN_H = 140;
841
406
  panel.style.width = `${width}px`;
@@ -843,7 +408,7 @@ function placeAnchored(panel, anchor, width, prefer, keepClear) {
843
408
  const centred = a.left + a.width / 2 - width / 2;
844
409
  panel.style.left = `${Math.round(Math.min(Math.max(8, centred), window.innerWidth - width - 8))}px`;
845
410
  } else {
846
- const leftOf = a.left - width - GAP2;
411
+ const leftOf = a.left - width - GAP;
847
412
  const alignedRight = Math.min(a.right - width, window.innerWidth - width - 8);
848
413
  panel.style.left = `${Math.round(prefer === "left" && leftOf >= 8 ? leftOf : Math.max(8, prefer === "left" ? a.left : alignedRight))}px`;
849
414
  }
@@ -864,15 +429,15 @@ function placeAnchored(panel, anchor, width, prefer, keepClear) {
864
429
  return;
865
430
  }
866
431
  }
867
- const roomBelow = bottomLimit - (a.bottom + GAP2);
868
- const roomAbove = a.top - GAP2 - 8;
432
+ const roomBelow = bottomLimit - (a.bottom + GAP);
433
+ const roomAbove = a.top - GAP - 8;
869
434
  if (roomAbove > roomBelow) {
870
435
  panel.style.top = "auto";
871
- panel.style.bottom = `${Math.round(window.innerHeight - (a.top - GAP2))}px`;
436
+ panel.style.bottom = `${Math.round(window.innerHeight - (a.top - GAP))}px`;
872
437
  panel.style.maxHeight = `${Math.round(Math.max(MIN_H, Math.min(CAP, roomAbove)))}px`;
873
438
  } else {
874
439
  panel.style.bottom = "auto";
875
- panel.style.top = `${Math.round(Math.max(8, a.bottom + GAP2))}px`;
440
+ panel.style.top = `${Math.round(Math.max(8, a.bottom + GAP))}px`;
876
441
  panel.style.maxHeight = `${Math.round(Math.max(MIN_H, Math.min(CAP, roomBelow)))}px`;
877
442
  }
878
443
  }
@@ -885,164 +450,6 @@ function isValidGameId(gameId) {
885
450
  return /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(gameId);
886
451
  }
887
452
 
888
- // src/property-names.ts
889
- var RESERVED_PROPERTY_NAMES = ["true", "false", "and", "or", "not"];
890
- var PROPERTY_NAME_HINT = "Lower case letters, digits and underscores. Cannot start with a digit. No hyphens or spaces.";
891
- function propertyNameify(text) {
892
- const trimmed = text.trim();
893
- const deliberateLeading = trimmed.startsWith("_");
894
- let out = trimmed.toLowerCase().replace(/['’]/g, "").replace(/[^a-z0-9_]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
895
- if (out === "") return "";
896
- if (deliberateLeading || /^[0-9]/.test(out)) out = `_${out}`;
897
- if (RESERVED_PROPERTY_NAMES.includes(out)) out = `${out}_`;
898
- return out;
899
- }
900
- function isValidPropertyName(name) {
901
- return /^[a-z_][a-z0-9_]*$/.test(name) && !RESERVED_PROPERTY_NAMES.includes(name);
902
- }
903
- function isCaseOnlyPropertyName(name) {
904
- return !isValidPropertyName(name) && isValidPropertyName(name.toLowerCase());
905
- }
906
-
907
- // src/property-name-field.ts
908
- var refSyncs = /* @__PURE__ */ new WeakMap();
909
- function propertyNameProblem(name) {
910
- if (isValidPropertyName(name)) return void 0;
911
- const suggestion = propertyNameify(name);
912
- const tail = suggestion ? ` Try "${suggestion}", or press Tab.` : "";
913
- if (RESERVED_PROPERTY_NAMES.includes(name.toLowerCase())) {
914
- return `Cannot be used: "${name.toLowerCase()}" is a keyword in expressions.${tail}`;
915
- }
916
- if (name.includes("-")) return `Cannot be used: a hyphen reads as subtraction in an expression.${tail}`;
917
- if (/^[0-9]/.test(name)) return `Cannot be used: a name cannot start with a digit.${tail}`;
918
- if (name !== name.toLowerCase()) {
919
- return `Cannot be used: expressions fold names, so this would look for "${name.toLowerCase()}".${tail}`;
920
- }
921
- return `Cannot be used: only lower case letters, digits and underscores.${tail}`;
922
- }
923
- function bindPropertyName(input, onCommit, opts = {}) {
924
- const hint = opts.hint;
925
- const sync = () => {
926
- const v = input.value.trim();
927
- const problem = v === "" ? void 0 : propertyNameProblem(v);
928
- input.classList.toggle("illegal", !!problem);
929
- if (problem) input.title = problem;
930
- else if (input.title.startsWith("Cannot be used")) {
931
- if (hint) input.title = hint;
932
- else input.removeAttribute("title");
933
- }
934
- };
935
- input.addEventListener("input", () => {
936
- onCommit(input.value);
937
- sync();
938
- });
939
- input.addEventListener("keydown", (e) => {
940
- if (e.key !== "Tab" || e.shiftKey) return;
941
- const typed = input.value.trim();
942
- if (typed === "" || isValidPropertyName(typed)) return;
943
- const coerced = propertyNameify(typed);
944
- if (!coerced || coerced === input.value) return;
945
- e.preventDefault();
946
- input.value = coerced;
947
- onCommit(coerced);
948
- sync();
949
- });
950
- sync();
951
- }
952
- function firstIllegalPropertyName(host) {
953
- return host.querySelector("input.illegal");
954
- }
955
- function nearest(name, known) {
956
- const n = name.toLowerCase();
957
- const byPrefix = known.filter((k) => k.toLowerCase().startsWith(n) || n.startsWith(k.toLowerCase()));
958
- if (byPrefix.length === 1) return byPrefix[0];
959
- let best;
960
- let bestScore = 3;
961
- for (const k of known) {
962
- const a = k.toLowerCase();
963
- if (Math.abs(a.length - n.length) > 2) continue;
964
- let prev = Array.from({ length: n.length + 1 }, (_, i) => i);
965
- for (let i = 1; i <= a.length; i++) {
966
- const row = [i];
967
- for (let j = 1; j <= n.length; j++) {
968
- row[j] = Math.min(prev[j] + 1, row[j - 1] + 1, prev[j - 1] + (a[i - 1] === n[j - 1] ? 0 : 1));
969
- }
970
- prev = row;
971
- }
972
- const d = prev[n.length];
973
- if (d < bestScore) {
974
- bestScore = d;
975
- best = k;
976
- }
977
- }
978
- return best;
979
- }
980
- function propertyRefProblem(name, opts) {
981
- const scope = opts.scope ? `@${opts.scope}` : "that scope";
982
- const folded = name.toLowerCase();
983
- if (!isValidPropertyName(folded)) {
984
- const suggestion = propertyNameify(name);
985
- const tail = suggestion ? ` Try "${suggestion}", or press Tab.` : "";
986
- if (RESERVED_PROPERTY_NAMES.includes(folded)) return `Cannot be used: "${folded}" is a keyword in expressions.${tail}`;
987
- if (name.includes("-")) return `Cannot be used: a hyphen reads as subtraction in an expression.${tail}`;
988
- if (/^[0-9]/.test(name)) return `Cannot be used: a name cannot start with a digit.${tail}`;
989
- return `Cannot be used: only lower case letters, digits and underscores.${tail}`;
990
- }
991
- const known = opts.known();
992
- if (known.some((k) => k.toLowerCase() === folded)) return void 0;
993
- const near = nearest(folded, known);
994
- if (near) return `No ${scope} property is called "${name}". Did you mean "${near}"?`;
995
- if (known.length === 0) return `No ${scope} properties are declared yet. Declare one above first.`;
996
- const shown = known.slice(0, 3).map((k) => `"${k}"`).join(", ");
997
- const more = known.length > 3 ? `, and ${known.length - 3} more` : "";
998
- return `No ${scope} property is called "${name}". Declared: ${shown}${more}.`;
999
- }
1000
- function bindPropertyRef(input, onCommit, opts) {
1001
- const list = document.createElement("datalist");
1002
- list.id = `shell-propref-${Math.random().toString(36).slice(2, 9)}`;
1003
- input.setAttribute("list", list.id);
1004
- const attach = () => {
1005
- if (!list.isConnected && input.parentNode) input.after(list);
1006
- };
1007
- const sync = () => {
1008
- attach();
1009
- const known = opts.known();
1010
- list.replaceChildren(...known.map((k) => {
1011
- const o = document.createElement("option");
1012
- o.value = k;
1013
- return o;
1014
- }));
1015
- const v = input.value.trim();
1016
- const problem = v === "" ? void 0 : propertyRefProblem(v, opts);
1017
- input.classList.toggle("illegal", !!problem);
1018
- if (problem) input.title = problem;
1019
- else if (input.title.startsWith("Cannot be used") || input.title.startsWith("No ")) {
1020
- if (opts.hint) input.title = opts.hint;
1021
- else input.removeAttribute("title");
1022
- }
1023
- };
1024
- refSyncs.set(input, sync);
1025
- input.addEventListener("input", () => {
1026
- onCommit(input.value);
1027
- sync();
1028
- });
1029
- input.addEventListener("keydown", (e) => {
1030
- if (e.key !== "Tab" || e.shiftKey) return;
1031
- const typed = input.value.trim();
1032
- if (typed === "" || isValidPropertyName(typed.toLowerCase())) return;
1033
- const coerced = propertyNameify(typed);
1034
- if (!coerced || coerced === input.value) return;
1035
- e.preventDefault();
1036
- input.value = coerced;
1037
- onCommit(coerced);
1038
- sync();
1039
- });
1040
- sync();
1041
- }
1042
- function revalidatePropertyRefs(host) {
1043
- for (const input of host.querySelectorAll("input")) refSyncs.get(input)?.();
1044
- }
1045
-
1046
453
  // src/id-editor.ts
1047
454
  function openGameIdEditor(opts) {
1048
455
  const slugify = opts.slugify ?? gameIdify;
@@ -1059,9 +466,9 @@ function openGameIdEditor(opts) {
1059
466
  const input = el("input", "shell-id-input");
1060
467
  input.type = "text";
1061
468
  input.value = opts.value;
1062
- input.placeholder = opts.derived || "<game-id>";
469
+ input.placeholder = opts.derived || "game-id";
1063
470
  input.spellcheck = false;
1064
- const hint = el("div", "shell-id-hint", "Lower case, digits, hyphens. Empty = auto from the name.");
471
+ const hint = el("div", "shell-id-hint", "Lower case letters, digits, and hyphens. Leave it empty to derive one from the name.");
1065
472
  const actions = el("div", "shell-id-actions");
1066
473
  const reset = el("button", "shell-id-reset", "Reset to auto");
1067
474
  const set = el("button", "shell-id-set", "Set");
@@ -1123,12 +530,13 @@ function foldVc(shards, keys) {
1123
530
  return out;
1124
531
  }
1125
532
  function vcBadgeFor(s) {
1126
- if (s?.lockedBy?.length) return { glyph: icon.locked, cls: "vc-locked", title: `Locked by ${s.lockedBy.join(", ")}` };
1127
- if (s?.outOfDate) return { glyph: icon.down, cls: "vc-stale", title: "Out of date - a newer version is on the server" };
1128
- if (s?.checkedOutByMe) return { glyph: icon.checkedOut, cls: "vc-mine", title: "Checked out by you" };
1129
- if (s?.dirty) return { glyph: icon.modified, cls: "vc-dirty", title: "Modified - uncommitted local changes" };
1130
- if (s?.untracked) return { glyph: icon.untracked, cls: "vc-new", title: "New - not yet committed" };
1131
- if (s && !s.writable) return { glyph: icon.readOnly, cls: "vc-frozen", title: "Read-only on disk - saving checks it out" };
533
+ const badge = (name, cls, title) => ({ name, glyph: icon[name] ?? "", cls, title });
534
+ if (s?.lockedBy?.length) return badge("locked", "vc-locked", `Locked by ${s.lockedBy.join(", ")}`);
535
+ if (s?.outOfDate) return badge("down", "vc-stale", "Out of date. A newer version is on the server.");
536
+ if (s?.checkedOutByMe) return badge("checkedOut", "vc-mine", "Checked out by you");
537
+ if (s?.dirty) return badge("modified", "vc-dirty", "Modified. Local changes aren't committed yet.");
538
+ if (s?.untracked) return badge("untracked", "vc-new", "New. Not in version control yet.");
539
+ if (s && !s.writable) return badge("readOnly", "vc-frozen", "Read-only on disk. Saving checks it out.");
1132
540
  return null;
1133
541
  }
1134
542
  function paintVcBadges(root, shards) {
@@ -1136,7 +544,7 @@ function paintVcBadges(root, shards) {
1136
544
  host.querySelector(":scope > .vc-badge")?.remove();
1137
545
  const badge = vcBadgeFor(foldVc(shards, host.dataset["vc"]));
1138
546
  if (!badge) return;
1139
- const span = el("span", { className: `vc-badge ${badge.cls}`, text: badge.glyph, tip: badge.title });
547
+ const span = el("span", { className: `vc-badge ${badge.cls}`, tip: badge.title }, iconNode(badge.name, 12));
1140
548
  host.append(span);
1141
549
  });
1142
550
  }
@@ -1159,8 +567,8 @@ function lockNotice(holders) {
1159
567
  return el(
1160
568
  "div",
1161
569
  { className: "vc-lock" },
1162
- el("span", { className: "vc-lock-glyph", text: icon.locked }),
1163
- el("span", { text: `Locked by ${holders.join(", ")} - read-only until they release it.` })
570
+ el("span", { className: "vc-lock-glyph" }, iconNode("locked")),
571
+ el("span", { text: `Locked by ${holders.join(", ")}. Read-only until they release it.` })
1164
572
  );
1165
573
  }
1166
574
 
@@ -1168,41 +576,36 @@ function lockNotice(holders) {
1168
576
  function askIdentity(opts = {}) {
1169
577
  return new Promise((resolve) => {
1170
578
  const seed = opts.current ?? opts.suggested;
1171
- const name = el("input", "shell-ident-input");
579
+ const name = el("input", "field shell-ident-input");
1172
580
  name.placeholder = "Your name";
1173
581
  name.value = seed?.name ?? "";
1174
- const email = el("input", "shell-ident-input");
1175
- email.placeholder = "Email (optional)";
582
+ const email = el("input", "field shell-ident-input");
583
+ email.placeholder = "Optional";
1176
584
  email.type = "email";
1177
585
  email.value = seed?.email ?? "";
1178
- const skip = el("button", "shell-ident-btn", "Skip");
1179
- const save = el("button", "shell-ident-btn primary", "Save");
586
+ const skip = el("button", "btn shell-ident-btn", "Skip");
587
+ const save = el("button", "btn primary shell-ident-btn", opts.mode === "welcome" ? "Continue" : "Save");
1180
588
  skip.type = "button";
1181
589
  save.type = "button";
1182
- const dialog = el(
1183
- "dialog",
1184
- "shell-ident",
1185
- el("div", "shell-ident-title", "Who is working here?"),
1186
- el(
1187
- "p",
1188
- "shell-ident-hint",
1189
- "Stamped on the comments you write. Kept in this app, never in the project, and changeable later."
1190
- ),
1191
- el("div", "shell-ident-fields", name, email),
1192
- el("div", "shell-ident-actions", skip, save)
1193
- );
1194
- document.body.append(dialog);
1195
590
  let done = false;
1196
591
  const finish = (keep) => {
1197
592
  if (done) return;
1198
593
  done = true;
1199
594
  const typed = name.value.trim();
1200
595
  const value = email.value.trim();
1201
- const answer = keep && typed !== "" ? value === "" ? { name: typed } : { name: typed, email: value } : void 0;
1202
- dialog.close();
1203
- dialog.remove();
596
+ const answer = keep && typed !== "" ? value === "" ? { name: typed } : { name: typed, email: value } : opts.neverAskAgainOnSkip === true ? { name: "" } : void 0;
1204
597
  resolve(answer);
598
+ frame.close();
1205
599
  };
600
+ const frame = dialogFrame({
601
+ title: titleFor(opts),
602
+ sub: subFor(opts),
603
+ className: "shell-ident",
604
+ onClose: () => finish(false)
605
+ // Escape, or closed by someone else: a skip
606
+ });
607
+ frame.body.append(el("div", "shell-ident-fields", labelled("Name", name), labelled("Email", email)));
608
+ frame.actions.append(skip, save);
1206
609
  skip.addEventListener("click", () => finish(false));
1207
610
  save.addEventListener("click", () => finish(true));
1208
611
  name.addEventListener("keydown", (e) => {
@@ -1211,11 +614,19 @@ function askIdentity(opts = {}) {
1211
614
  email.addEventListener("keydown", (e) => {
1212
615
  if (e.key === "Enter") finish(true);
1213
616
  });
1214
- dialog.addEventListener("close", () => finish(false));
1215
- dialog.showModal();
617
+ frame.open();
1216
618
  name.focus();
1217
619
  });
1218
620
  }
621
+ function titleFor(opts) {
622
+ if (opts.mode === "welcome") return opts.appName ? `Welcome to ${opts.appName}` : "Welcome";
623
+ if (opts.mode === "edit") return "User information";
624
+ return "Who is working here?";
625
+ }
626
+ function subFor(opts) {
627
+ const why = "Your name goes on the comments you write. It's kept in this app, not in the project.";
628
+ return opts.mode === "edit" ? why : `${why} You can change it later.`;
629
+ }
1219
630
 
1220
631
  // src/notes-editor.ts
1221
632
  var EDITOR_ONLY = "";
@@ -1228,10 +639,10 @@ var CLASS_LABEL = {
1228
639
  };
1229
640
  var label = (cls) => cls === EDITOR_ONLY ? "Note (editor-only)" : CLASS_LABEL[cls] ?? cls[0].toUpperCase() + cls.slice(1);
1230
641
  var PLACEHOLDER = {
1231
- vo: "direction for the voice actor / director",
1232
- loc: "context for translators"
642
+ vo: "Direction for the voice actor or director",
643
+ loc: "Context for translators"
1233
644
  };
1234
- var placeholder = (cls) => cls === EDITOR_ONLY ? "an internal note - never exported" : PLACEHOLDER[cls] ?? "intent / rationale - why this is here";
645
+ var placeholder = (cls) => cls === EDITOR_ONLY ? "An internal note, never exported" : PLACEHOLDER[cls] ?? "Why this is here";
1235
646
  function openNotesEditor(opts) {
1236
647
  const { notes } = opts;
1237
648
  const seed = /* @__PURE__ */ new Map();
@@ -1285,7 +696,7 @@ ${line.text}` : line.text);
1285
696
  { className: "shell-notes-class" },
1286
697
  el("label", "shell-notes-caption", label(cls))
1287
698
  );
1288
- const area = el("textarea", "shell-notes-text");
699
+ const area = el("textarea", "field shell-notes-text");
1289
700
  area.value = areas.get(cls)?.value ?? seed.get(cls) ?? "";
1290
701
  area.rows = 4;
1291
702
  area.placeholder = placeholder(cls);
@@ -1311,29 +722,24 @@ ${line.text}` : line.text);
1311
722
  }
1312
723
  };
1313
724
  render();
1314
- const done = el("button", "shell-notes-btn primary", "Done");
1315
- const dialog = el(
1316
- "dialog",
1317
- { className: "shell-notes" },
1318
- el("div", { className: "shell-notes-title", text: `Notes: ${opts.subject}` }),
1319
- body,
1320
- el("div", { className: "shell-notes-actions" }, done)
1321
- );
1322
- dialog.setAttribute("aria-label", `Notes: ${opts.subject}`);
1323
- document.body.append(dialog);
725
+ const done = el("button", "btn primary shell-notes-btn", "Done");
726
+ done.type = "button";
1324
727
  const before = JSON.stringify(notes.own);
1325
728
  let closed = false;
1326
- const close = () => {
729
+ const commit = () => {
1327
730
  if (closed) return;
1328
731
  closed = true;
1329
732
  const next = value();
1330
733
  if (JSON.stringify(next) !== before) opts.save(next);
1331
- dialog.close();
1332
- dialog.remove();
1333
734
  };
1334
- done.addEventListener("click", close);
1335
- dialog.addEventListener("close", close);
1336
- dialog.showModal();
735
+ const frame = dialogFrame({ title: `Notes: ${opts.subject}`, className: "shell-notes", onClose: commit });
736
+ frame.body.append(body);
737
+ frame.actions.append(done);
738
+ done.addEventListener("click", () => {
739
+ commit();
740
+ frame.close();
741
+ });
742
+ frame.open();
1337
743
  areas.get(shown[0] ?? EDITOR_ONLY)?.focus();
1338
744
  }
1339
745
 
@@ -1405,7 +811,7 @@ function openComments(opts) {
1405
811
  el("span", { className: "shell-cmt-ts", text: stamp(message.ts) })
1406
812
  );
1407
813
  if (opts.deleteMessage && message.deleted !== true) {
1408
- const bin = el("button", { className: "shell-cmt-del", text: "\u2715" });
814
+ const bin = el("button", { className: "shell-cmt-del" }, iconNode("close", 12));
1409
815
  bin.dataset.tip = "Delete this comment";
1410
816
  bin.setAttribute("aria-label", "Delete this comment");
1411
817
  bin.addEventListener("click", () => opts.deleteMessage?.(thread.id, index));
@@ -1470,37 +876,65 @@ var clock = (ms) => {
1470
876
  var estimable = (done, total, elapsedMs) => done > 0 && total > 0 && elapsedMs > 1e3 && done / total > 0.1;
1471
877
  function mountJobProgress(host, opts) {
1472
878
  const units = opts.units ?? "runs";
879
+ const pinned = opts.indeterminate === true;
880
+ const cancellableByDefault = opts.cancellable ?? opts.onCancel !== void 0;
1473
881
  const fill = el("i", { className: "job-fill" });
1474
882
  const bar = el("div", { className: "job-bar" }, fill);
883
+ bar.setAttribute("role", "progressbar");
884
+ bar.setAttribute("aria-valuemin", "0");
885
+ bar.setAttribute("aria-valuemax", "100");
1475
886
  const what = el("span", { className: "job-label" });
1476
887
  const count = el("span", { className: "job-count" });
1477
888
  const time = el("span", { className: "job-time" });
1478
889
  const cancel = el("button", { className: "job-cancel", text: "Cancel" });
890
+ cancel.type = "button";
1479
891
  const strip = el("div", { className: "job-strip" }, what, bar, count, time, cancel);
1480
892
  strip.hidden = true;
1481
893
  host.append(strip);
894
+ let runPinned = pinned;
895
+ let indeterminate = false;
896
+ const setMode = (band) => {
897
+ indeterminate = band;
898
+ strip.classList.toggle("indeterminate", band);
899
+ count.hidden = band;
900
+ if (band) {
901
+ fill.style.width = "";
902
+ bar.removeAttribute("aria-valuenow");
903
+ }
904
+ };
1482
905
  const view = {
1483
906
  element: strip,
1484
907
  get visible() {
1485
908
  return !strip.hidden;
1486
909
  },
1487
- begin(label2) {
910
+ get indeterminate() {
911
+ return indeterminate;
912
+ },
913
+ begin(label2, run) {
914
+ runPinned = run?.indeterminate ?? pinned;
915
+ const cancellable = run?.cancellable ?? cancellableByDefault;
1488
916
  what.textContent = label2 ?? "Running\u2026";
1489
917
  count.textContent = "";
1490
918
  time.textContent = "";
1491
- fill.style.width = "0%";
919
+ cancel.hidden = !cancellable;
1492
920
  cancel.disabled = false;
1493
921
  cancel.textContent = "Cancel";
922
+ setMode(true);
1494
923
  strip.hidden = false;
1495
924
  },
1496
925
  update(done, total, elapsedMs) {
1497
926
  if (strip.hidden) strip.hidden = false;
1498
- const pct = total > 0 ? Math.min(100, done / total * 100) : 0;
1499
- fill.style.width = `${pct}%`;
1500
- count.textContent = total > 0 ? `${done} / ${total} ${units}` : `${done} ${units}`;
1501
- if (estimable(done, total, elapsedMs)) {
927
+ const band = runPinned || !(total > 0);
928
+ setMode(band);
929
+ if (!band) {
930
+ const pct = Math.min(100, done / total * 100);
931
+ fill.style.width = `${pct}%`;
932
+ bar.setAttribute("aria-valuenow", String(Math.round(pct)));
933
+ count.textContent = `${done} / ${total} ${units}`.trimEnd();
934
+ }
935
+ if (!band && estimable(done, total, elapsedMs)) {
1502
936
  const remaining = elapsedMs / done * (total - done);
1503
- time.textContent = `${clock(elapsedMs)} elapsed \xB7 about ${clock(remaining)} left`;
937
+ time.textContent = `${clock(elapsedMs)} elapsed, about ${clock(remaining)} left`;
1504
938
  } else {
1505
939
  time.textContent = clock(elapsedMs);
1506
940
  }
@@ -1512,8 +946,10 @@ function mountJobProgress(host, opts) {
1512
946
  cancel.addEventListener("click", () => {
1513
947
  cancel.disabled = true;
1514
948
  cancel.textContent = "Stopping\u2026";
1515
- opts.onCancel();
949
+ opts.onCancel?.();
1516
950
  });
951
+ cancel.hidden = !cancellableByDefault;
952
+ setMode(true);
1517
953
  return view;
1518
954
  }
1519
955
 
@@ -1525,7 +961,7 @@ function createSaveController(opts) {
1525
961
  let auto = true;
1526
962
  let dirty = false;
1527
963
  let dirtySince = 0;
1528
- let timer3;
964
+ let timer2;
1529
965
  let inFlight;
1530
966
  const setStatus = (next) => {
1531
967
  if (next === status) return;
@@ -1533,17 +969,17 @@ function createSaveController(opts) {
1533
969
  opts.onStatus?.(status);
1534
970
  };
1535
971
  const clearTimer = () => {
1536
- if (timer3 === void 0) return;
1537
- clearTimeout(timer3);
1538
- timer3 = void 0;
972
+ if (timer2 === void 0) return;
973
+ clearTimeout(timer2);
974
+ timer2 = void 0;
1539
975
  };
1540
976
  const arm = () => {
1541
977
  clearTimer();
1542
978
  if (!auto) return;
1543
979
  const deadline = dirtySince + maxWaitMs;
1544
980
  const wait = Math.max(0, Math.min(delayMs, deadline - Date.now()));
1545
- timer3 = setTimeout(() => {
1546
- timer3 = void 0;
981
+ timer2 = setTimeout(() => {
982
+ timer2 = void 0;
1547
983
  void run();
1548
984
  }, wait);
1549
985
  };
@@ -1636,18 +1072,21 @@ function staleBar(opts) {
1636
1072
  className: "stale-bar-msg",
1637
1073
  text: `${opts.subject} changed in the editor. ${opts.advice ?? "Restart to play the new version."}`
1638
1074
  }),
1639
- el("button", {
1640
- className: "stale-bar-go",
1641
- text: `${icon.restart} ${label2}`,
1642
- onClick: () => opts.onRestart()
1643
- })
1075
+ // The icon is a node beside the word, not a character inside it; the
1076
+ // button's flex gap sets the space (stale.css).
1077
+ el(
1078
+ "button",
1079
+ { className: "stale-bar-go", onClick: () => opts.onRestart() },
1080
+ iconNode("restart", 12),
1081
+ label2
1082
+ )
1644
1083
  );
1645
1084
  }
1646
1085
 
1647
1086
  // src/toast.ts
1648
1087
  var DURATION = { info: 4e3, ok: 4e3, error: 7e3 };
1649
1088
  var node;
1650
- var timer2 = 0;
1089
+ var timer = 0;
1651
1090
  var seq = 0;
1652
1091
  function mount() {
1653
1092
  if (node && node.isConnected) return node;
@@ -1664,8 +1103,8 @@ function toast(message, kind = "info", opts = {}) {
1664
1103
  t.textContent = message;
1665
1104
  t.className = `shell-toast ${kind}`;
1666
1105
  t.hidden = false;
1667
- window.clearTimeout(timer2);
1668
- timer2 = window.setTimeout(() => {
1106
+ window.clearTimeout(timer);
1107
+ timer = window.setTimeout(() => {
1669
1108
  closeWithExit(t, () => {
1670
1109
  if (seq === id) t.hidden = true;
1671
1110
  });
@@ -1673,7 +1112,7 @@ function toast(message, kind = "info", opts = {}) {
1673
1112
  }
1674
1113
  function dismissToast() {
1675
1114
  if (!node || node.hidden) return;
1676
- window.clearTimeout(timer2);
1115
+ window.clearTimeout(timer);
1677
1116
  const id = seq;
1678
1117
  const t = node;
1679
1118
  closeWithExit(t, () => {
@@ -1682,14 +1121,14 @@ function dismissToast() {
1682
1121
  }
1683
1122
 
1684
1123
  // src/stepper.ts
1124
+ var STEP_ICON = 12;
1685
1125
  function renderStepperBar(host, opts) {
1686
1126
  const { items } = opts;
1687
1127
  const close = opts.onClose === void 0 ? null : el("button", {
1688
1128
  className: "stepbar-nav stepbar-close",
1689
- text: icon.close,
1690
1129
  tip: opts.closeTip ?? "Close",
1691
1130
  onClick: opts.onClose
1692
- });
1131
+ }, iconNode("close", STEP_ICON));
1693
1132
  const actions = (opts.actions ?? []).filter((a) => a !== null && a !== void 0);
1694
1133
  if (items.length === 0) {
1695
1134
  if (opts.empty === void 0) {
@@ -1713,17 +1152,15 @@ function renderStepperBar(host, opts) {
1713
1152
  const nav = items.length > 1 ? [
1714
1153
  el("button", {
1715
1154
  className: "stepbar-nav",
1716
- text: icon.back,
1717
1155
  tip: opts.tips?.prev ?? "Previous",
1718
1156
  onClick: () => step(-1)
1719
- }),
1157
+ }, iconNode("back", STEP_ICON)),
1720
1158
  el("span", { className: "stepbar-of", text: `${index + 1}/${items.length}` }),
1721
1159
  el("button", {
1722
1160
  className: "stepbar-nav",
1723
- text: icon.forward,
1724
1161
  tip: opts.tips?.next ?? "Next",
1725
1162
  onClick: () => step(1)
1726
- })
1163
+ }, iconNode("forward", STEP_ICON))
1727
1164
  ] : [];
1728
1165
  host.replaceChildren(
1729
1166
  el("span", { className: `stepbar-count ${opts.tone ?? "muted"}`, text: String(items.length) }),
@@ -1757,11 +1194,11 @@ function createNavHistory(opts) {
1757
1194
  }
1758
1195
  };
1759
1196
  return {
1760
- visit(place2) {
1197
+ visit(place) {
1761
1198
  ahead = [];
1762
1199
  const top = past[past.length - 1];
1763
- if (top !== void 0 && opts.same(top, place2)) past[past.length - 1] = place2;
1764
- else past.push(place2);
1200
+ if (top !== void 0 && opts.same(top, place)) past[past.length - 1] = place;
1201
+ else past.push(place);
1765
1202
  if (past.length > cap) past = past.slice(past.length - cap);
1766
1203
  },
1767
1204
  back(current2) {
@@ -1779,18 +1216,18 @@ function createNavHistory(opts) {
1779
1216
  };
1780
1217
  }
1781
1218
  function historyNav(onBack, onForward) {
1782
- const btn = (glyph, tip2, onClick) => {
1219
+ const btn = (name, tip, onClick) => {
1783
1220
  const b = document.createElement("button");
1784
1221
  b.type = "button";
1785
1222
  b.className = "shell-histnav-btn";
1786
- b.textContent = glyph;
1787
- b.dataset["tip"] = tip2;
1788
- b.setAttribute("aria-label", tip2);
1223
+ b.append(iconNode(name));
1224
+ b.dataset["tip"] = tip;
1225
+ b.setAttribute("aria-label", tip);
1789
1226
  b.addEventListener("click", onClick);
1790
1227
  return b;
1791
1228
  };
1792
- const back = btn("\u2190", "Back", onBack);
1793
- const forward = btn("\u2192", "Forward", onForward);
1229
+ const back = btn("arrowLeft", "Back", onBack);
1230
+ const forward = btn("arrowRight", "Forward", onForward);
1794
1231
  const el2 = document.createElement("span");
1795
1232
  el2.className = "shell-histnav";
1796
1233
  el2.append(back, forward);
@@ -1803,6 +1240,6 @@ function historyNav(onBack, onForward) {
1803
1240
  };
1804
1241
  }
1805
1242
 
1806
- export { PALETTE, PALETTE_SIZE, PROPERTY_NAME_HINT, RESERVED_PROPERTY_NAMES, askIdentity, bindPropertyName, bindPropertyRef, closeAnchoredPanel, closeWithExit, colourFor, colourIndex, confirmDialog, createNavHistory, createSaveController, dismissToast, dupGuard, el, expandableRow, firstIllegalPropertyName, focusNewRow, foldVc, gameIdify, hideTip, historyNav, icon, iconBtn, iconSvg, initTooltips, isCaseOnlyPropertyName, isValidGameId, isValidPropertyName, labelled, lockControls, lockNotice, mountJobProgress, mountPaneShell, mountSettingsDialog, moveItem, openAnchoredPanel, openComments, openGameIdEditor, openNotesEditor, paintVcBadges, pinButton, placeAnchored, propertyNameProblem, propertyNameify, propertyRefProblem, renderStepperBar, revalidatePropertyRefs, revealRow, saveIndicator, showAbout, staleBar, tagChips, tipAt, tipBold, toast, vcBadgeFor };
1243
+ export { PALETTE, PALETTE_SIZE, askIdentity, closeAnchoredPanel, colourFor, colourIndex, confirmDialog, createNavHistory, createSaveController, dismissToast, foldVc, followButton, gameIdify, historyNav, isValidGameId, lockControls, lockNotice, mountJobProgress, mountPaneShell, openAnchoredPanel, openComments, openGameIdEditor, openNotesEditor, paintVcBadges, pinButton, placeAnchored, renderStepperBar, saveIndicator, showAbout, staleBar, toast, toolWindowHead, vcBadgeFor };
1807
1244
  //# sourceMappingURL=index.js.map
1808
1245
  //# sourceMappingURL=index.js.map