@wildwinter/app-shell 0.37.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 +21 -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 +1322 -258
  66. package/dist/index.cjs.map +1 -1
  67. package/dist/index.d.cts +349 -131
  68. package/dist/index.d.ts +349 -131
  69. package/dist/index.js +259 -808
  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 +63 -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,175 +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
- const line = el("div", "set-rowline");
474
- if (opts.details && opts.details.length) {
475
- const details = el("div", "set-details");
476
- details.hidden = true;
477
- for (const d of opts.details) details.append(d);
478
- const toggle = el("button", "set-expand");
479
- toggle.type = "button";
480
- toggle.dataset.tip = "More";
481
- toggle.setAttribute("aria-label", "More");
482
- toggle.setAttribute("aria-expanded", "false");
483
- toggle.addEventListener("click", () => {
484
- const open = details.hidden;
485
- details.hidden = !open;
486
- toggle.setAttribute("aria-expanded", String(open));
487
- toggle.classList.toggle("open", open);
488
- });
489
- line.append(toggle);
490
- opts.line.forEach((c) => line.append(c));
491
- row.append(line, details);
492
- } else {
493
- opts.line.forEach((c) => line.append(c));
494
- row.append(line);
495
- }
496
- return row;
497
- }
498
- function focusNewRow(listEl) {
499
- const last = listEl?.lastElementChild;
500
- if (!(last instanceof HTMLElement)) return;
501
- last.scrollIntoView({ block: "nearest" });
502
- const name = last.querySelector("input.set-name") ?? last.querySelector("input");
503
- name?.focus();
504
- }
505
- function dupGuard() {
506
- const items = [];
507
- const check = () => {
508
- const counts = /* @__PURE__ */ new Map();
509
- for (const it of items) {
510
- const k = it.key().trim().toLowerCase();
511
- if (k) counts.set(k, (counts.get(k) ?? 0) + 1);
512
- }
513
- let any = false;
514
- for (const it of items) {
515
- const k = it.key().trim().toLowerCase();
516
- const dup = !!k && (counts.get(k) ?? 0) > 1;
517
- it.input.classList.toggle("invalid", dup);
518
- if (dup) {
519
- it.input.title = "Duplicate name. Names must be unique.";
520
- any = true;
521
- } else if (it.input.title === "Duplicate name. Names must be unique.") it.input.removeAttribute("title");
522
- }
523
- return any;
524
- };
525
- return {
526
- reset() {
527
- items.length = 0;
528
- },
529
- track(input, key = () => input.value, also) {
530
- items.push({ input, key });
531
- input.addEventListener("input", check);
532
- for (const a of also ?? []) a.addEventListener("input", check);
533
- },
534
- check,
535
- firstDuplicate() {
536
- check();
537
- return items.find((it) => it.input.classList.contains("invalid"))?.input ?? null;
538
- }
539
- };
540
- }
541
- function mountSettingsDialog(opts) {
542
- const dialog = el("dialog", "settings-dialog");
543
- const form = el("form", "settings-form");
544
- form.method = "dialog";
545
- const tabs = el("nav", "settings-tabs");
546
- const panels = el("div", "settings-panels");
547
- const error = el("p", "settings-error");
548
- error.hidden = true;
549
- const cancel = el("button", "settings-cancel", "Cancel");
550
- cancel.type = "button";
551
- cancel.value = "cancel";
552
- const save = el("button", "settings-save primary", "Save");
553
- save.value = "save";
554
- const panelFor = /* @__PURE__ */ new Map();
555
- const tabFor = /* @__PURE__ */ new Map();
556
- const handles = /* @__PURE__ */ new Map();
557
- let active2 = opts.sections[0]?.id ?? "";
558
- let lastGroup;
559
- for (const s of opts.sections) {
560
- if (s.group && s.group !== lastGroup) {
561
- tabs.append(el("div", "settings-group", s.group));
562
- lastGroup = s.group;
563
- }
564
- const tab = el("button", "settings-tab", s.label);
565
- tab.type = "button";
566
- tab.dataset["tab"] = s.id;
567
- tab.addEventListener("click", () => showTab(s.id));
568
- tabs.append(tab);
569
- tabFor.set(s.id, tab);
570
- const panel = el("section", "settings-panel");
571
- panel.dataset["panel"] = s.id;
572
- panel.hidden = true;
573
- panels.append(panel);
574
- panelFor.set(s.id, panel);
575
- }
576
- function showTab(id) {
577
- active2 = id;
578
- for (const [sid, t] of tabFor) t.classList.toggle("active", sid === id);
579
- for (const [sid, p] of panelFor) p.hidden = sid !== id;
580
- }
581
- form.append(
582
- el("h2", "settings-title", opts.title),
583
- el("div", "settings-body", tabs, panels),
584
- el("div", "settings-actions", error, cancel, save)
585
- );
586
- dialog.append(form);
587
- document.body.append(dialog);
588
- form.addEventListener("keydown", (e) => {
589
- if (e.key === "Enter" && e.target?.tagName === "INPUT") e.preventDefault();
590
- });
591
- cancel.addEventListener("click", () => dialog.close("cancel"));
592
- save.addEventListener("click", (e) => {
593
- e.preventDefault();
594
- error.hidden = true;
595
- for (const s of opts.sections) {
596
- const bad = handles.get(s.id)?.firstInvalid?.();
597
- if (bad) {
598
- showTab(s.id);
599
- bad.focus();
600
- error.textContent = "Fix the highlighted fields first.";
601
- error.hidden = false;
602
- return;
603
- }
604
- }
605
- void (async () => {
606
- await opts.onSave();
607
- dialog.close("save");
608
- })();
609
- });
610
- return {
611
- open(tabId) {
612
- handles.clear();
613
- for (const s of opts.sections) {
614
- const panel = panelFor.get(s.id);
615
- panel.replaceChildren();
616
- handles.set(s.id, s.mount(panel));
617
- }
618
- error.hidden = true;
619
- showTab(tabId ?? active2 ?? opts.sections[0]?.id ?? "");
620
- dialog.showModal();
621
- },
622
- destroy() {
623
- dialog.remove();
624
- }
625
- };
626
- }
627
-
628
196
  // src/tool-window-web.ts
629
- 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>`;
630
197
  function pinButton(opts) {
631
198
  const b = el("button", "swin-pin");
632
199
  b.type = "button";
633
- b.innerHTML = PIN_ICON;
200
+ b.append(iconNode("pin"));
634
201
  let pinned = opts.pinned;
635
202
  const reflect = () => {
636
203
  b.classList.toggle("on", pinned);
637
204
  b.setAttribute("aria-pressed", String(pinned));
638
- 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";
639
206
  b.dataset["tip"] = label2;
640
207
  b.setAttribute("aria-label", label2);
641
208
  };
@@ -650,66 +217,91 @@ function pinButton(opts) {
650
217
  reflect();
651
218
  } };
652
219
  }
653
-
654
- // src/exit.ts
655
- function closeWithExit(element, done) {
656
- element.classList.add("closing");
657
- const seconds = parseFloat(getComputedStyle(element).animationDuration) || 0;
658
- if (seconds === 0) {
659
- done();
660
- 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();
661
240
  }
662
- let fired = false;
663
- const finish = () => {
664
- if (fired) return;
665
- fired = true;
666
- element.removeEventListener("animationend", onEnd);
667
- clearTimeout(timer3);
668
- done();
669
- };
670
- const onEnd = (e) => {
671
- 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;
672
263
  };
673
- element.addEventListener("animationend", onEnd);
674
- 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
+ } };
675
274
  }
676
275
 
677
276
  // src/confirm.ts
678
277
  function confirmDialog(opts) {
679
278
  return new Promise((resolve) => {
680
- const cancel = el("button", "confirm-btn cancel", "Cancel");
681
- const danger = el("button", "confirm-btn danger", opts.confirmLabel);
682
- cancel.type = "button";
683
- danger.type = "button";
684
- const dlg = el(
685
- "dialog",
686
- "confirm-dialog",
687
- el("div", "confirm-title", opts.title),
688
- el("div", "confirm-body", opts.body),
689
- el("div", "confirm-actions", cancel, danger)
690
- );
691
- dlg.setAttribute("aria-label", opts.title);
692
- document.body.append(dlg);
693
279
  let done = false;
694
280
  const finish = (ok) => {
695
281
  if (done) return;
696
282
  done = true;
697
283
  resolve(ok);
698
- closeWithExit(dlg, () => {
699
- dlg.close();
700
- dlg.remove();
701
- });
284
+ frame.close();
702
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);
703
299
  cancel.addEventListener("click", () => finish(false));
704
300
  danger.addEventListener("click", () => finish(true));
705
- dlg.addEventListener("cancel", (e) => {
706
- e.preventDefault();
707
- finish(false);
301
+ frame.dialog.addEventListener("mousedown", (e) => {
302
+ if (e.target === frame.dialog) finish(false);
708
303
  });
709
- dlg.addEventListener("mousedown", (e) => {
710
- if (e.target === dlg) finish(false);
711
- });
712
- dlg.showModal();
304
+ frame.open();
713
305
  cancel.focus();
714
306
  });
715
307
  }
@@ -717,18 +309,16 @@ function confirmDialog(opts) {
717
309
  // src/about.ts
718
310
  function showAbout(opts) {
719
311
  return new Promise((resolve) => {
720
- 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}`);
721
314
  if (opts.wordmark !== void 0) {
722
315
  const mark = el("div", "shell-about-mark");
723
316
  mark.innerHTML = opts.wordmark;
724
- dialog.append(mark);
317
+ frame.dialog.prepend(mark);
725
318
  }
726
- dialog.append(
727
- el("div", "shell-about-name", opts.appName),
728
- el("div", "shell-about-version", `Version ${opts.version}`)
729
- );
730
- if (opts.blurb !== void 0) dialog.append(el("p", "shell-about-blurb", opts.blurb));
731
- 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));
732
322
  if (opts.links?.length) {
733
323
  const row = el("div", "shell-about-links");
734
324
  for (const link of opts.links) {
@@ -741,24 +331,13 @@ function showAbout(opts) {
741
331
  a.addEventListener("click", () => opts.onOpenLink?.(link.url));
742
332
  row.append(a);
743
333
  }
744
- dialog.append(row);
334
+ frame.body.append(row);
745
335
  }
746
- const close = el("button", "shell-about-close", "Close");
336
+ const close = el("button", "btn shell-about-close", "Close");
747
337
  close.type = "button";
748
- dialog.append(el("div", "shell-about-actions", close));
749
- dialog.setAttribute("aria-label", `About ${opts.appName}`);
750
- document.body.append(dialog);
751
- let done = false;
752
- const finish = () => {
753
- if (done) return;
754
- done = true;
755
- dialog.close();
756
- dialog.remove();
757
- resolve();
758
- };
759
- close.addEventListener("click", finish);
760
- dialog.addEventListener("close", finish);
761
- dialog.showModal();
338
+ close.addEventListener("click", () => frame.close());
339
+ frame.actions.append(close);
340
+ frame.open();
762
341
  close.focus();
763
342
  });
764
343
  }
@@ -773,7 +352,7 @@ function openAnchoredPanel(opts) {
773
352
  if (prev.anchor === opts.anchor) return null;
774
353
  }
775
354
  const panel = el("div", `shell-anchored ${opts.className ?? ""}`.trim());
776
- const closeBtn = el("button", "shell-anchored-close", "\u2715");
355
+ const closeBtn = el("button", "shell-anchored-close", iconNode("close", 12));
777
356
  closeBtn.type = "button";
778
357
  closeBtn.setAttribute("aria-label", "Close");
779
358
  const body = el("div", "shell-anchored-body");
@@ -781,7 +360,7 @@ function openAnchoredPanel(opts) {
781
360
  el("div", "shell-anchored-head", el("span", "shell-anchored-title", opts.title), closeBtn),
782
361
  body
783
362
  );
784
- document.body.append(panel);
363
+ (opts.host ?? document.body).append(panel);
785
364
  placeAnchored(panel, opts.anchor, opts.width, opts.prefer ?? "below", opts.keepClear ?? []);
786
365
  let closed = false;
787
366
  const close = () => {
@@ -821,7 +400,7 @@ function closeAnchoredPanel() {
821
400
  }
822
401
  function placeAnchored(panel, anchor, width, prefer, keepClear) {
823
402
  const a = anchor.getBoundingClientRect();
824
- const GAP2 = 10;
403
+ const GAP = 10;
825
404
  const CAP = window.innerHeight * 0.7;
826
405
  const MIN_H = 140;
827
406
  panel.style.width = `${width}px`;
@@ -829,7 +408,7 @@ function placeAnchored(panel, anchor, width, prefer, keepClear) {
829
408
  const centred = a.left + a.width / 2 - width / 2;
830
409
  panel.style.left = `${Math.round(Math.min(Math.max(8, centred), window.innerWidth - width - 8))}px`;
831
410
  } else {
832
- const leftOf = a.left - width - GAP2;
411
+ const leftOf = a.left - width - GAP;
833
412
  const alignedRight = Math.min(a.right - width, window.innerWidth - width - 8);
834
413
  panel.style.left = `${Math.round(prefer === "left" && leftOf >= 8 ? leftOf : Math.max(8, prefer === "left" ? a.left : alignedRight))}px`;
835
414
  }
@@ -850,15 +429,15 @@ function placeAnchored(panel, anchor, width, prefer, keepClear) {
850
429
  return;
851
430
  }
852
431
  }
853
- const roomBelow = bottomLimit - (a.bottom + GAP2);
854
- const roomAbove = a.top - GAP2 - 8;
432
+ const roomBelow = bottomLimit - (a.bottom + GAP);
433
+ const roomAbove = a.top - GAP - 8;
855
434
  if (roomAbove > roomBelow) {
856
435
  panel.style.top = "auto";
857
- panel.style.bottom = `${Math.round(window.innerHeight - (a.top - GAP2))}px`;
436
+ panel.style.bottom = `${Math.round(window.innerHeight - (a.top - GAP))}px`;
858
437
  panel.style.maxHeight = `${Math.round(Math.max(MIN_H, Math.min(CAP, roomAbove)))}px`;
859
438
  } else {
860
439
  panel.style.bottom = "auto";
861
- 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`;
862
441
  panel.style.maxHeight = `${Math.round(Math.max(MIN_H, Math.min(CAP, roomBelow)))}px`;
863
442
  }
864
443
  }
@@ -871,164 +450,6 @@ function isValidGameId(gameId) {
871
450
  return /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(gameId);
872
451
  }
873
452
 
874
- // src/property-names.ts
875
- var RESERVED_PROPERTY_NAMES = ["true", "false", "and", "or", "not"];
876
- var PROPERTY_NAME_HINT = "Lower case letters, digits and underscores. Cannot start with a digit. No hyphens or spaces.";
877
- function propertyNameify(text) {
878
- const trimmed = text.trim();
879
- const deliberateLeading = trimmed.startsWith("_");
880
- let out = trimmed.toLowerCase().replace(/['’]/g, "").replace(/[^a-z0-9_]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "");
881
- if (out === "") return "";
882
- if (deliberateLeading || /^[0-9]/.test(out)) out = `_${out}`;
883
- if (RESERVED_PROPERTY_NAMES.includes(out)) out = `${out}_`;
884
- return out;
885
- }
886
- function isValidPropertyName(name) {
887
- return /^[a-z_][a-z0-9_]*$/.test(name) && !RESERVED_PROPERTY_NAMES.includes(name);
888
- }
889
- function isCaseOnlyPropertyName(name) {
890
- return !isValidPropertyName(name) && isValidPropertyName(name.toLowerCase());
891
- }
892
-
893
- // src/property-name-field.ts
894
- var refSyncs = /* @__PURE__ */ new WeakMap();
895
- function propertyNameProblem(name) {
896
- if (isValidPropertyName(name)) return void 0;
897
- const suggestion = propertyNameify(name);
898
- const tail = suggestion ? ` Try "${suggestion}", or press Tab.` : "";
899
- if (RESERVED_PROPERTY_NAMES.includes(name.toLowerCase())) {
900
- return `Cannot be used: "${name.toLowerCase()}" is a keyword in expressions.${tail}`;
901
- }
902
- if (name.includes("-")) return `Cannot be used: a hyphen reads as subtraction in an expression.${tail}`;
903
- if (/^[0-9]/.test(name)) return `Cannot be used: a name cannot start with a digit.${tail}`;
904
- if (name !== name.toLowerCase()) {
905
- return `Cannot be used: expressions fold names, so this would look for "${name.toLowerCase()}".${tail}`;
906
- }
907
- return `Cannot be used: only lower case letters, digits and underscores.${tail}`;
908
- }
909
- function bindPropertyName(input, onCommit, opts = {}) {
910
- const hint = opts.hint;
911
- const sync = () => {
912
- const v = input.value.trim();
913
- const problem = v === "" ? void 0 : propertyNameProblem(v);
914
- input.classList.toggle("illegal", !!problem);
915
- if (problem) input.title = problem;
916
- else if (input.title.startsWith("Cannot be used")) {
917
- if (hint) input.title = hint;
918
- else input.removeAttribute("title");
919
- }
920
- };
921
- input.addEventListener("input", () => {
922
- onCommit(input.value);
923
- sync();
924
- });
925
- input.addEventListener("keydown", (e) => {
926
- if (e.key !== "Tab" || e.shiftKey) return;
927
- const typed = input.value.trim();
928
- if (typed === "" || isValidPropertyName(typed)) return;
929
- const coerced = propertyNameify(typed);
930
- if (!coerced || coerced === input.value) return;
931
- e.preventDefault();
932
- input.value = coerced;
933
- onCommit(coerced);
934
- sync();
935
- });
936
- sync();
937
- }
938
- function firstIllegalPropertyName(host) {
939
- return host.querySelector("input.illegal");
940
- }
941
- function nearest(name, known) {
942
- const n = name.toLowerCase();
943
- const byPrefix = known.filter((k) => k.toLowerCase().startsWith(n) || n.startsWith(k.toLowerCase()));
944
- if (byPrefix.length === 1) return byPrefix[0];
945
- let best;
946
- let bestScore = 3;
947
- for (const k of known) {
948
- const a = k.toLowerCase();
949
- if (Math.abs(a.length - n.length) > 2) continue;
950
- let prev = Array.from({ length: n.length + 1 }, (_, i) => i);
951
- for (let i = 1; i <= a.length; i++) {
952
- const row = [i];
953
- for (let j = 1; j <= n.length; j++) {
954
- row[j] = Math.min(prev[j] + 1, row[j - 1] + 1, prev[j - 1] + (a[i - 1] === n[j - 1] ? 0 : 1));
955
- }
956
- prev = row;
957
- }
958
- const d = prev[n.length];
959
- if (d < bestScore) {
960
- bestScore = d;
961
- best = k;
962
- }
963
- }
964
- return best;
965
- }
966
- function propertyRefProblem(name, opts) {
967
- const scope = opts.scope ? `@${opts.scope}` : "that scope";
968
- const folded = name.toLowerCase();
969
- if (!isValidPropertyName(folded)) {
970
- const suggestion = propertyNameify(name);
971
- const tail = suggestion ? ` Try "${suggestion}", or press Tab.` : "";
972
- if (RESERVED_PROPERTY_NAMES.includes(folded)) return `Cannot be used: "${folded}" is a keyword in expressions.${tail}`;
973
- if (name.includes("-")) return `Cannot be used: a hyphen reads as subtraction in an expression.${tail}`;
974
- if (/^[0-9]/.test(name)) return `Cannot be used: a name cannot start with a digit.${tail}`;
975
- return `Cannot be used: only lower case letters, digits and underscores.${tail}`;
976
- }
977
- const known = opts.known();
978
- if (known.some((k) => k.toLowerCase() === folded)) return void 0;
979
- const near = nearest(folded, known);
980
- if (near) return `No ${scope} property is called "${name}". Did you mean "${near}"?`;
981
- if (known.length === 0) return `No ${scope} properties are declared yet. Declare one above first.`;
982
- const shown = known.slice(0, 3).map((k) => `"${k}"`).join(", ");
983
- const more = known.length > 3 ? `, and ${known.length - 3} more` : "";
984
- return `No ${scope} property is called "${name}". Declared: ${shown}${more}.`;
985
- }
986
- function bindPropertyRef(input, onCommit, opts) {
987
- const list = document.createElement("datalist");
988
- list.id = `shell-propref-${Math.random().toString(36).slice(2, 9)}`;
989
- input.setAttribute("list", list.id);
990
- const attach = () => {
991
- if (!list.isConnected && input.parentNode) input.after(list);
992
- };
993
- const sync = () => {
994
- attach();
995
- const known = opts.known();
996
- list.replaceChildren(...known.map((k) => {
997
- const o = document.createElement("option");
998
- o.value = k;
999
- return o;
1000
- }));
1001
- const v = input.value.trim();
1002
- const problem = v === "" ? void 0 : propertyRefProblem(v, opts);
1003
- input.classList.toggle("illegal", !!problem);
1004
- if (problem) input.title = problem;
1005
- else if (input.title.startsWith("Cannot be used") || input.title.startsWith("No ")) {
1006
- if (opts.hint) input.title = opts.hint;
1007
- else input.removeAttribute("title");
1008
- }
1009
- };
1010
- refSyncs.set(input, sync);
1011
- input.addEventListener("input", () => {
1012
- onCommit(input.value);
1013
- sync();
1014
- });
1015
- input.addEventListener("keydown", (e) => {
1016
- if (e.key !== "Tab" || e.shiftKey) return;
1017
- const typed = input.value.trim();
1018
- if (typed === "" || isValidPropertyName(typed.toLowerCase())) return;
1019
- const coerced = propertyNameify(typed);
1020
- if (!coerced || coerced === input.value) return;
1021
- e.preventDefault();
1022
- input.value = coerced;
1023
- onCommit(coerced);
1024
- sync();
1025
- });
1026
- sync();
1027
- }
1028
- function revalidatePropertyRefs(host) {
1029
- for (const input of host.querySelectorAll("input")) refSyncs.get(input)?.();
1030
- }
1031
-
1032
453
  // src/id-editor.ts
1033
454
  function openGameIdEditor(opts) {
1034
455
  const slugify = opts.slugify ?? gameIdify;
@@ -1045,9 +466,9 @@ function openGameIdEditor(opts) {
1045
466
  const input = el("input", "shell-id-input");
1046
467
  input.type = "text";
1047
468
  input.value = opts.value;
1048
- input.placeholder = opts.derived || "<game-id>";
469
+ input.placeholder = opts.derived || "game-id";
1049
470
  input.spellcheck = false;
1050
- 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.");
1051
472
  const actions = el("div", "shell-id-actions");
1052
473
  const reset = el("button", "shell-id-reset", "Reset to auto");
1053
474
  const set = el("button", "shell-id-set", "Set");
@@ -1109,12 +530,13 @@ function foldVc(shards, keys) {
1109
530
  return out;
1110
531
  }
1111
532
  function vcBadgeFor(s) {
1112
- if (s?.lockedBy?.length) return { glyph: icon.locked, cls: "vc-locked", title: `Locked by ${s.lockedBy.join(", ")}` };
1113
- if (s?.outOfDate) return { glyph: icon.down, cls: "vc-stale", title: "Out of date - a newer version is on the server" };
1114
- if (s?.checkedOutByMe) return { glyph: icon.checkedOut, cls: "vc-mine", title: "Checked out by you" };
1115
- if (s?.dirty) return { glyph: icon.modified, cls: "vc-dirty", title: "Modified - uncommitted local changes" };
1116
- if (s?.untracked) return { glyph: icon.untracked, cls: "vc-new", title: "New - not yet committed" };
1117
- 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.");
1118
540
  return null;
1119
541
  }
1120
542
  function paintVcBadges(root, shards) {
@@ -1122,7 +544,7 @@ function paintVcBadges(root, shards) {
1122
544
  host.querySelector(":scope > .vc-badge")?.remove();
1123
545
  const badge = vcBadgeFor(foldVc(shards, host.dataset["vc"]));
1124
546
  if (!badge) return;
1125
- 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));
1126
548
  host.append(span);
1127
549
  });
1128
550
  }
@@ -1145,8 +567,8 @@ function lockNotice(holders) {
1145
567
  return el(
1146
568
  "div",
1147
569
  { className: "vc-lock" },
1148
- el("span", { className: "vc-lock-glyph", text: icon.locked }),
1149
- 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.` })
1150
572
  );
1151
573
  }
1152
574
 
@@ -1154,41 +576,36 @@ function lockNotice(holders) {
1154
576
  function askIdentity(opts = {}) {
1155
577
  return new Promise((resolve) => {
1156
578
  const seed = opts.current ?? opts.suggested;
1157
- const name = el("input", "shell-ident-input");
579
+ const name = el("input", "field shell-ident-input");
1158
580
  name.placeholder = "Your name";
1159
581
  name.value = seed?.name ?? "";
1160
- const email = el("input", "shell-ident-input");
1161
- email.placeholder = "Email (optional)";
582
+ const email = el("input", "field shell-ident-input");
583
+ email.placeholder = "Optional";
1162
584
  email.type = "email";
1163
585
  email.value = seed?.email ?? "";
1164
- const skip = el("button", "shell-ident-btn", "Skip");
1165
- 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");
1166
588
  skip.type = "button";
1167
589
  save.type = "button";
1168
- const dialog = el(
1169
- "dialog",
1170
- "shell-ident",
1171
- el("div", "shell-ident-title", "Who is working here?"),
1172
- el(
1173
- "p",
1174
- "shell-ident-hint",
1175
- "Stamped on the comments you write. Kept in this app, never in the project, and changeable later."
1176
- ),
1177
- el("div", "shell-ident-fields", name, email),
1178
- el("div", "shell-ident-actions", skip, save)
1179
- );
1180
- document.body.append(dialog);
1181
590
  let done = false;
1182
591
  const finish = (keep) => {
1183
592
  if (done) return;
1184
593
  done = true;
1185
594
  const typed = name.value.trim();
1186
595
  const value = email.value.trim();
1187
- const answer = keep && typed !== "" ? value === "" ? { name: typed } : { name: typed, email: value } : void 0;
1188
- dialog.close();
1189
- dialog.remove();
596
+ const answer = keep && typed !== "" ? value === "" ? { name: typed } : { name: typed, email: value } : opts.neverAskAgainOnSkip === true ? { name: "" } : void 0;
1190
597
  resolve(answer);
598
+ frame.close();
1191
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);
1192
609
  skip.addEventListener("click", () => finish(false));
1193
610
  save.addEventListener("click", () => finish(true));
1194
611
  name.addEventListener("keydown", (e) => {
@@ -1197,11 +614,19 @@ function askIdentity(opts = {}) {
1197
614
  email.addEventListener("keydown", (e) => {
1198
615
  if (e.key === "Enter") finish(true);
1199
616
  });
1200
- dialog.addEventListener("close", () => finish(false));
1201
- dialog.showModal();
617
+ frame.open();
1202
618
  name.focus();
1203
619
  });
1204
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
+ }
1205
630
 
1206
631
  // src/notes-editor.ts
1207
632
  var EDITOR_ONLY = "";
@@ -1214,10 +639,10 @@ var CLASS_LABEL = {
1214
639
  };
1215
640
  var label = (cls) => cls === EDITOR_ONLY ? "Note (editor-only)" : CLASS_LABEL[cls] ?? cls[0].toUpperCase() + cls.slice(1);
1216
641
  var PLACEHOLDER = {
1217
- vo: "direction for the voice actor / director",
1218
- loc: "context for translators"
642
+ vo: "Direction for the voice actor or director",
643
+ loc: "Context for translators"
1219
644
  };
1220
- 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";
1221
646
  function openNotesEditor(opts) {
1222
647
  const { notes } = opts;
1223
648
  const seed = /* @__PURE__ */ new Map();
@@ -1271,7 +696,7 @@ ${line.text}` : line.text);
1271
696
  { className: "shell-notes-class" },
1272
697
  el("label", "shell-notes-caption", label(cls))
1273
698
  );
1274
- const area = el("textarea", "shell-notes-text");
699
+ const area = el("textarea", "field shell-notes-text");
1275
700
  area.value = areas.get(cls)?.value ?? seed.get(cls) ?? "";
1276
701
  area.rows = 4;
1277
702
  area.placeholder = placeholder(cls);
@@ -1297,29 +722,24 @@ ${line.text}` : line.text);
1297
722
  }
1298
723
  };
1299
724
  render();
1300
- const done = el("button", "shell-notes-btn primary", "Done");
1301
- const dialog = el(
1302
- "dialog",
1303
- { className: "shell-notes" },
1304
- el("div", { className: "shell-notes-title", text: `Notes: ${opts.subject}` }),
1305
- body,
1306
- el("div", { className: "shell-notes-actions" }, done)
1307
- );
1308
- dialog.setAttribute("aria-label", `Notes: ${opts.subject}`);
1309
- document.body.append(dialog);
725
+ const done = el("button", "btn primary shell-notes-btn", "Done");
726
+ done.type = "button";
1310
727
  const before = JSON.stringify(notes.own);
1311
728
  let closed = false;
1312
- const close = () => {
729
+ const commit = () => {
1313
730
  if (closed) return;
1314
731
  closed = true;
1315
732
  const next = value();
1316
733
  if (JSON.stringify(next) !== before) opts.save(next);
1317
- dialog.close();
1318
- dialog.remove();
1319
734
  };
1320
- done.addEventListener("click", close);
1321
- dialog.addEventListener("close", close);
1322
- 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();
1323
743
  areas.get(shown[0] ?? EDITOR_ONLY)?.focus();
1324
744
  }
1325
745
 
@@ -1391,7 +811,7 @@ function openComments(opts) {
1391
811
  el("span", { className: "shell-cmt-ts", text: stamp(message.ts) })
1392
812
  );
1393
813
  if (opts.deleteMessage && message.deleted !== true) {
1394
- const bin = el("button", { className: "shell-cmt-del", text: "\u2715" });
814
+ const bin = el("button", { className: "shell-cmt-del" }, iconNode("close", 12));
1395
815
  bin.dataset.tip = "Delete this comment";
1396
816
  bin.setAttribute("aria-label", "Delete this comment");
1397
817
  bin.addEventListener("click", () => opts.deleteMessage?.(thread.id, index));
@@ -1456,37 +876,65 @@ var clock = (ms) => {
1456
876
  var estimable = (done, total, elapsedMs) => done > 0 && total > 0 && elapsedMs > 1e3 && done / total > 0.1;
1457
877
  function mountJobProgress(host, opts) {
1458
878
  const units = opts.units ?? "runs";
879
+ const pinned = opts.indeterminate === true;
880
+ const cancellableByDefault = opts.cancellable ?? opts.onCancel !== void 0;
1459
881
  const fill = el("i", { className: "job-fill" });
1460
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");
1461
886
  const what = el("span", { className: "job-label" });
1462
887
  const count = el("span", { className: "job-count" });
1463
888
  const time = el("span", { className: "job-time" });
1464
889
  const cancel = el("button", { className: "job-cancel", text: "Cancel" });
890
+ cancel.type = "button";
1465
891
  const strip = el("div", { className: "job-strip" }, what, bar, count, time, cancel);
1466
892
  strip.hidden = true;
1467
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
+ };
1468
905
  const view = {
1469
906
  element: strip,
1470
907
  get visible() {
1471
908
  return !strip.hidden;
1472
909
  },
1473
- begin(label2) {
910
+ get indeterminate() {
911
+ return indeterminate;
912
+ },
913
+ begin(label2, run) {
914
+ runPinned = run?.indeterminate ?? pinned;
915
+ const cancellable = run?.cancellable ?? cancellableByDefault;
1474
916
  what.textContent = label2 ?? "Running\u2026";
1475
917
  count.textContent = "";
1476
918
  time.textContent = "";
1477
- fill.style.width = "0%";
919
+ cancel.hidden = !cancellable;
1478
920
  cancel.disabled = false;
1479
921
  cancel.textContent = "Cancel";
922
+ setMode(true);
1480
923
  strip.hidden = false;
1481
924
  },
1482
925
  update(done, total, elapsedMs) {
1483
926
  if (strip.hidden) strip.hidden = false;
1484
- const pct = total > 0 ? Math.min(100, done / total * 100) : 0;
1485
- fill.style.width = `${pct}%`;
1486
- count.textContent = total > 0 ? `${done} / ${total} ${units}` : `${done} ${units}`;
1487
- 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)) {
1488
936
  const remaining = elapsedMs / done * (total - done);
1489
- time.textContent = `${clock(elapsedMs)} elapsed \xB7 about ${clock(remaining)} left`;
937
+ time.textContent = `${clock(elapsedMs)} elapsed, about ${clock(remaining)} left`;
1490
938
  } else {
1491
939
  time.textContent = clock(elapsedMs);
1492
940
  }
@@ -1498,8 +946,10 @@ function mountJobProgress(host, opts) {
1498
946
  cancel.addEventListener("click", () => {
1499
947
  cancel.disabled = true;
1500
948
  cancel.textContent = "Stopping\u2026";
1501
- opts.onCancel();
949
+ opts.onCancel?.();
1502
950
  });
951
+ cancel.hidden = !cancellableByDefault;
952
+ setMode(true);
1503
953
  return view;
1504
954
  }
1505
955
 
@@ -1511,7 +961,7 @@ function createSaveController(opts) {
1511
961
  let auto = true;
1512
962
  let dirty = false;
1513
963
  let dirtySince = 0;
1514
- let timer3;
964
+ let timer2;
1515
965
  let inFlight;
1516
966
  const setStatus = (next) => {
1517
967
  if (next === status) return;
@@ -1519,17 +969,17 @@ function createSaveController(opts) {
1519
969
  opts.onStatus?.(status);
1520
970
  };
1521
971
  const clearTimer = () => {
1522
- if (timer3 === void 0) return;
1523
- clearTimeout(timer3);
1524
- timer3 = void 0;
972
+ if (timer2 === void 0) return;
973
+ clearTimeout(timer2);
974
+ timer2 = void 0;
1525
975
  };
1526
976
  const arm = () => {
1527
977
  clearTimer();
1528
978
  if (!auto) return;
1529
979
  const deadline = dirtySince + maxWaitMs;
1530
980
  const wait = Math.max(0, Math.min(delayMs, deadline - Date.now()));
1531
- timer3 = setTimeout(() => {
1532
- timer3 = void 0;
981
+ timer2 = setTimeout(() => {
982
+ timer2 = void 0;
1533
983
  void run();
1534
984
  }, wait);
1535
985
  };
@@ -1622,18 +1072,21 @@ function staleBar(opts) {
1622
1072
  className: "stale-bar-msg",
1623
1073
  text: `${opts.subject} changed in the editor. ${opts.advice ?? "Restart to play the new version."}`
1624
1074
  }),
1625
- el("button", {
1626
- className: "stale-bar-go",
1627
- text: `${icon.restart} ${label2}`,
1628
- onClick: () => opts.onRestart()
1629
- })
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
+ )
1630
1083
  );
1631
1084
  }
1632
1085
 
1633
1086
  // src/toast.ts
1634
1087
  var DURATION = { info: 4e3, ok: 4e3, error: 7e3 };
1635
1088
  var node;
1636
- var timer2 = 0;
1089
+ var timer = 0;
1637
1090
  var seq = 0;
1638
1091
  function mount() {
1639
1092
  if (node && node.isConnected) return node;
@@ -1650,8 +1103,8 @@ function toast(message, kind = "info", opts = {}) {
1650
1103
  t.textContent = message;
1651
1104
  t.className = `shell-toast ${kind}`;
1652
1105
  t.hidden = false;
1653
- window.clearTimeout(timer2);
1654
- timer2 = window.setTimeout(() => {
1106
+ window.clearTimeout(timer);
1107
+ timer = window.setTimeout(() => {
1655
1108
  closeWithExit(t, () => {
1656
1109
  if (seq === id) t.hidden = true;
1657
1110
  });
@@ -1659,7 +1112,7 @@ function toast(message, kind = "info", opts = {}) {
1659
1112
  }
1660
1113
  function dismissToast() {
1661
1114
  if (!node || node.hidden) return;
1662
- window.clearTimeout(timer2);
1115
+ window.clearTimeout(timer);
1663
1116
  const id = seq;
1664
1117
  const t = node;
1665
1118
  closeWithExit(t, () => {
@@ -1668,14 +1121,14 @@ function dismissToast() {
1668
1121
  }
1669
1122
 
1670
1123
  // src/stepper.ts
1124
+ var STEP_ICON = 12;
1671
1125
  function renderStepperBar(host, opts) {
1672
1126
  const { items } = opts;
1673
1127
  const close = opts.onClose === void 0 ? null : el("button", {
1674
1128
  className: "stepbar-nav stepbar-close",
1675
- text: icon.close,
1676
1129
  tip: opts.closeTip ?? "Close",
1677
1130
  onClick: opts.onClose
1678
- });
1131
+ }, iconNode("close", STEP_ICON));
1679
1132
  const actions = (opts.actions ?? []).filter((a) => a !== null && a !== void 0);
1680
1133
  if (items.length === 0) {
1681
1134
  if (opts.empty === void 0) {
@@ -1699,17 +1152,15 @@ function renderStepperBar(host, opts) {
1699
1152
  const nav = items.length > 1 ? [
1700
1153
  el("button", {
1701
1154
  className: "stepbar-nav",
1702
- text: icon.back,
1703
1155
  tip: opts.tips?.prev ?? "Previous",
1704
1156
  onClick: () => step(-1)
1705
- }),
1157
+ }, iconNode("back", STEP_ICON)),
1706
1158
  el("span", { className: "stepbar-of", text: `${index + 1}/${items.length}` }),
1707
1159
  el("button", {
1708
1160
  className: "stepbar-nav",
1709
- text: icon.forward,
1710
1161
  tip: opts.tips?.next ?? "Next",
1711
1162
  onClick: () => step(1)
1712
- })
1163
+ }, iconNode("forward", STEP_ICON))
1713
1164
  ] : [];
1714
1165
  host.replaceChildren(
1715
1166
  el("span", { className: `stepbar-count ${opts.tone ?? "muted"}`, text: String(items.length) }),
@@ -1743,11 +1194,11 @@ function createNavHistory(opts) {
1743
1194
  }
1744
1195
  };
1745
1196
  return {
1746
- visit(place2) {
1197
+ visit(place) {
1747
1198
  ahead = [];
1748
1199
  const top = past[past.length - 1];
1749
- if (top !== void 0 && opts.same(top, place2)) past[past.length - 1] = place2;
1750
- else past.push(place2);
1200
+ if (top !== void 0 && opts.same(top, place)) past[past.length - 1] = place;
1201
+ else past.push(place);
1751
1202
  if (past.length > cap) past = past.slice(past.length - cap);
1752
1203
  },
1753
1204
  back(current2) {
@@ -1765,18 +1216,18 @@ function createNavHistory(opts) {
1765
1216
  };
1766
1217
  }
1767
1218
  function historyNav(onBack, onForward) {
1768
- const btn = (glyph, tip2, onClick) => {
1219
+ const btn = (name, tip, onClick) => {
1769
1220
  const b = document.createElement("button");
1770
1221
  b.type = "button";
1771
1222
  b.className = "shell-histnav-btn";
1772
- b.textContent = glyph;
1773
- b.dataset["tip"] = tip2;
1774
- b.setAttribute("aria-label", tip2);
1223
+ b.append(iconNode(name));
1224
+ b.dataset["tip"] = tip;
1225
+ b.setAttribute("aria-label", tip);
1775
1226
  b.addEventListener("click", onClick);
1776
1227
  return b;
1777
1228
  };
1778
- const back = btn("\u2190", "Back", onBack);
1779
- const forward = btn("\u2192", "Forward", onForward);
1229
+ const back = btn("arrowLeft", "Back", onBack);
1230
+ const forward = btn("arrowRight", "Forward", onForward);
1780
1231
  const el2 = document.createElement("span");
1781
1232
  el2.className = "shell-histnav";
1782
1233
  el2.append(back, forward);
@@ -1789,6 +1240,6 @@ function historyNav(onBack, onForward) {
1789
1240
  };
1790
1241
  }
1791
1242
 
1792
- 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, 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 };
1793
1244
  //# sourceMappingURL=index.js.map
1794
1245
  //# sourceMappingURL=index.js.map