@wildwinter/app-shell 0.38.0 → 0.40.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 (168) hide show
  1. package/LICENSES.md +62 -0
  2. package/README.md +8 -0
  3. package/dist/about.css +17 -38
  4. package/dist/about.css.map +1 -1
  5. package/dist/anchored.css +9 -9
  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-DS4XXAZS.js +56 -0
  16. package/dist/chunk-DS4XXAZS.js.map +1 -0
  17. package/dist/chunk-EHR2KBZC.js +82 -0
  18. package/dist/chunk-EHR2KBZC.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-OPZPFOPS.js +66 -0
  22. package/dist/chunk-OPZPFOPS.js.map +1 -0
  23. package/dist/chunk-P4G4ERG6.js +60 -0
  24. package/dist/chunk-P4G4ERG6.js.map +1 -0
  25. package/dist/chunk-P6WLAEKZ.js +71 -0
  26. package/dist/chunk-P6WLAEKZ.js.map +1 -0
  27. package/dist/chunk-R3NXPHRZ.js +534 -0
  28. package/dist/chunk-R3NXPHRZ.js.map +1 -0
  29. package/dist/chunk-SDOKZTAR.js +87 -0
  30. package/dist/chunk-SDOKZTAR.js.map +1 -0
  31. package/dist/chunk-SEWJLUOD.js +70 -0
  32. package/dist/chunk-SEWJLUOD.js.map +1 -0
  33. package/dist/chunk-UG2P777F.js +587 -0
  34. package/dist/chunk-UG2P777F.js.map +1 -0
  35. package/dist/chunk-YRFJLJUH.js +55 -0
  36. package/dist/chunk-YRFJLJUH.js.map +1 -0
  37. package/dist/chunk-YWMSQFXX.js +146 -0
  38. package/dist/chunk-YWMSQFXX.js.map +1 -0
  39. package/dist/comments.css +16 -14
  40. package/dist/comments.css.map +1 -1
  41. package/dist/confirm.css +2 -55
  42. package/dist/confirm.css.map +1 -1
  43. package/dist/context-menu.cjs +250 -0
  44. package/dist/context-menu.cjs.map +1 -0
  45. package/dist/context-menu.css +64 -0
  46. package/dist/context-menu.css.map +1 -0
  47. package/dist/context-menu.d.cts +25 -0
  48. package/dist/context-menu.d.ts +25 -0
  49. package/dist/context-menu.js +5 -0
  50. package/dist/context-menu.js.map +1 -0
  51. package/dist/controls.css +214 -0
  52. package/dist/controls.css.map +1 -0
  53. package/dist/dialog.cjs +222 -0
  54. package/dist/dialog.cjs.map +1 -0
  55. package/dist/dialog.css +56 -0
  56. package/dist/dialog.css.map +1 -0
  57. package/dist/dialog.d.cts +28 -0
  58. package/dist/dialog.d.ts +28 -0
  59. package/dist/dialog.js +5 -0
  60. package/dist/dialog.js.map +1 -0
  61. package/dist/floating.cjs +83 -0
  62. package/dist/floating.cjs.map +1 -0
  63. package/dist/floating.d.cts +25 -0
  64. package/dist/floating.d.ts +25 -0
  65. package/dist/floating.js +4 -0
  66. package/dist/floating.js.map +1 -0
  67. package/dist/identity.css +15 -50
  68. package/dist/identity.css.map +1 -1
  69. package/dist/index.cjs +1553 -260
  70. package/dist/index.cjs.map +1 -1
  71. package/dist/index.d.cts +385 -152
  72. package/dist/index.d.ts +385 -152
  73. package/dist/index.js +264 -823
  74. package/dist/index.js.map +1 -1
  75. package/dist/job.css +24 -1
  76. package/dist/job.css.map +1 -1
  77. package/dist/keys.cjs +155 -0
  78. package/dist/keys.cjs.map +1 -0
  79. package/dist/keys.css +41 -0
  80. package/dist/keys.css.map +1 -0
  81. package/dist/keys.d.cts +57 -0
  82. package/dist/keys.d.ts +57 -0
  83. package/dist/keys.js +3 -0
  84. package/dist/keys.js.map +1 -0
  85. package/dist/link-status.cjs +430 -0
  86. package/dist/link-status.cjs.map +1 -0
  87. package/dist/link-status.css +76 -0
  88. package/dist/link-status.css.map +1 -0
  89. package/dist/link-status.d.cts +53 -0
  90. package/dist/link-status.d.ts +53 -0
  91. package/dist/link-status.js +5 -0
  92. package/dist/link-status.js.map +1 -0
  93. package/dist/menu.cjs +62 -1
  94. package/dist/menu.cjs.map +1 -1
  95. package/dist/menu.d.cts +88 -1
  96. package/dist/menu.d.ts +88 -1
  97. package/dist/menu.js +1 -1
  98. package/dist/notes-editor.css +9 -52
  99. package/dist/notes-editor.css.map +1 -1
  100. package/dist/pane-shell.css +23 -5
  101. package/dist/pane-shell.css.map +1 -1
  102. package/dist/problems.cjs +61 -0
  103. package/dist/problems.cjs.map +1 -0
  104. package/dist/problems.d.cts +57 -0
  105. package/dist/problems.d.ts +57 -0
  106. package/dist/problems.js +3 -0
  107. package/dist/problems.js.map +1 -0
  108. package/dist/property-list-D3VZCJzi.d.cts +162 -0
  109. package/dist/property-list-D3VZCJzi.d.ts +162 -0
  110. package/dist/property-list.cjs +784 -0
  111. package/dist/property-list.cjs.map +1 -0
  112. package/dist/property-list.d.cts +1 -0
  113. package/dist/property-list.d.ts +1 -0
  114. package/dist/property-list.js +4 -0
  115. package/dist/property-list.js.map +1 -0
  116. package/dist/save.css +1 -1
  117. package/dist/save.css.map +1 -1
  118. package/dist/settings.css +62 -18
  119. package/dist/settings.css.map +1 -1
  120. package/dist/stale.css +4 -1
  121. package/dist/stale.css.map +1 -1
  122. package/dist/stepper.css +47 -3
  123. package/dist/stepper.css.map +1 -1
  124. package/dist/toast.css +5 -5
  125. package/dist/toast.css.map +1 -1
  126. package/dist/tokens.css +20 -0
  127. package/dist/tokens.css.map +1 -1
  128. package/dist/tool-window.cjs +63 -0
  129. package/dist/tool-window.cjs.map +1 -1
  130. package/dist/tool-window.css +8 -4
  131. package/dist/tool-window.css.map +1 -1
  132. package/dist/tool-window.d.cts +67 -1
  133. package/dist/tool-window.d.ts +67 -1
  134. package/dist/tool-window.js +63 -1
  135. package/dist/tool-window.js.map +1 -1
  136. package/dist/tooltip.css +3 -3
  137. package/dist/tooltip.css.map +1 -1
  138. package/dist/updater-view.cjs +283 -0
  139. package/dist/updater-view.cjs.map +1 -0
  140. package/dist/updater-view.d.cts +23 -0
  141. package/dist/updater-view.d.ts +23 -0
  142. package/dist/updater-view.js +6 -0
  143. package/dist/updater-view.js.map +1 -0
  144. package/dist/updater.cjs +12 -16
  145. package/dist/updater.cjs.map +1 -1
  146. package/dist/updater.css +44 -0
  147. package/dist/updater.css.map +1 -0
  148. package/dist/updater.js +12 -16
  149. package/dist/updater.js.map +1 -1
  150. package/dist/util.cjs +78 -0
  151. package/dist/util.cjs.map +1 -0
  152. package/dist/util.d.cts +37 -0
  153. package/dist/util.d.ts +37 -0
  154. package/dist/util.js +3 -0
  155. package/dist/util.js.map +1 -0
  156. package/dist/vc.css +4 -3
  157. package/dist/vc.css.map +1 -1
  158. package/dist/welcome.cjs +215 -0
  159. package/dist/welcome.cjs.map +1 -0
  160. package/dist/welcome.css +112 -0
  161. package/dist/welcome.css.map +1 -0
  162. package/dist/welcome.d.cts +55 -0
  163. package/dist/welcome.d.ts +55 -0
  164. package/dist/welcome.js +4 -0
  165. package/dist/welcome.js.map +1 -0
  166. package/package.json +72 -2
  167. package/dist/chunk-QDPFU4WX.js +0 -43
  168. package/dist/chunk-QDPFU4WX.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,241 +1,22 @@
1
+ export { mountWelcome } from './chunk-SEWJLUOD.js';
2
+ export { feedUpdaterDownloadProgress, showUpdaterDialog } from './chunk-OPZPFOPS.js';
3
+ import { isKeyCombo, tipWithKey } from './chunk-YWMSQFXX.js';
4
+ export { hintBar, isKeyCombo, keyHint, keyLabel, keyLegends, keyPlatform, setKeyPlatform, tipWithKey } from './chunk-YWMSQFXX.js';
5
+ export { linkAddress, linkStatusClass, linkStatusTip, mountLinkStatus } from './chunk-SDOKZTAR.js';
6
+ import { isEditableTarget } from './chunk-P6WLAEKZ.js';
7
+ export { copyWithFeedback, debounce, formatCount, isEditableTarget, plural, relativeTime } from './chunk-P6WLAEKZ.js';
8
+ export { EDIT_MENU, FILE_MENU, GO_MENU, PANE_MENU, PLAY_MENU, PUBLISH_MENU, REVIEW_MENU, VIEW_MENU } from './chunk-2ECJUTSX.js';
9
+ export { defaultProblemCopy, describeProblem, problemLine, problemName } from './chunk-DS4XXAZS.js';
10
+ 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-R3NXPHRZ.js';
11
+ import { dialogFrame } from './chunk-YRFJLJUH.js';
12
+ export { dialogFrame } from './chunk-YRFJLJUH.js';
13
+ export { createFloating } from './chunk-P4G4ERG6.js';
1
14
  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
- }
15
+ export { openContextMenu, openPopover } from './chunk-EHR2KBZC.js';
16
+ import { ensureTooltipHost, el, iconNode, labelled, icon } from './chunk-UG2P777F.js';
17
+ export { ICON_NAMES, ICON_SIZE, breadcrumb, el, hideTip, icon, iconBtn, iconHtml, iconNameOfGlyph, iconNode, iconSvg, initTooltips, isIconName, labelled, metaLine, moveItem, stageChips, tagChips, tipAt, tipBold, wireReorder } from './chunk-UG2P777F.js';
18
+ import { closeWithExit } from './chunk-IU7EY6BI.js';
19
+ export { closeWithExit } from './chunk-IU7EY6BI.js';
239
20
 
240
21
  // src/colour.ts
241
22
  var PALETTE_SIZE = 12;
@@ -257,79 +38,6 @@ function colourFor(name) {
257
38
  return PALETTE[colourIndex(name)];
258
39
  }
259
40
 
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
41
  // src/pane-shell.ts
334
42
  var DEFAULT_MAX = (w) => Math.min(640, w - 420);
335
43
  var openVar = { nav: "--nav-open-w", inspector: "--insp-open-w" };
@@ -342,6 +50,8 @@ function mountPaneShell(host, opts) {
342
50
  inspector: offered("inspector") && (opts.initial?.open?.inspector ?? true)
343
51
  };
344
52
  const width = { ...opts.initial?.width ?? {} };
53
+ const held = { nav: false, inspector: false };
54
+ const showing = (side) => open[side] && !held[side];
345
55
  const el2 = (tag, cls) => {
346
56
  const n = document.createElement(tag);
347
57
  if (cls) n.className = cls;
@@ -372,18 +82,29 @@ function mountPaneShell(host, opts) {
372
82
  host.append(root);
373
83
  if (!offered("nav")) navToggle.remove();
374
84
  if (!offered("inspector")) inspToggle.remove();
85
+ const defaultTip = (side, isOpen) => {
86
+ const text = `${isOpen ? "Hide" : "Show"} ${cfg[side].label ?? side}`;
87
+ const hint = cfg[side].shortcutHint;
88
+ if (!hint) return text;
89
+ return isKeyCombo(hint) ? tipWithKey(text, hint) : `${text} (${hint})`;
90
+ };
91
+ const tipFor = opts.tipFor ?? defaultTip;
375
92
  function applyToggleGlyph(side, button) {
376
93
  const isOpen = open[side];
377
- button.textContent = side === "nav" ? isOpen ? "\u2039" : "\u203A" : isOpen ? "\u203A" : "\u2039";
378
- const noun = cfg[side].label ?? side;
379
- const hint = cfg[side].shortcutHint ? ` (${cfg[side].shortcutHint})` : "";
380
- button.title = `${isOpen ? "Hide" : "Show"} ${noun}${hint}`;
381
- button.setAttribute("aria-label", button.title);
94
+ button.replaceChildren(iconNode(side === "nav" ? isOpen ? "back" : "forward" : isOpen ? "forward" : "back"));
95
+ const tip = tipFor(side, isOpen);
96
+ button.dataset["tip"] = tip;
97
+ button.setAttribute("aria-label", tip);
382
98
  button.setAttribute("aria-pressed", String(isOpen));
383
99
  }
100
+ ensureTooltipHost();
384
101
  function apply() {
385
- panes.classList.toggle(collapseClass.nav, !open.nav);
386
- panes.classList.toggle(collapseClass.inspector, !open.inspector);
102
+ for (const side of ["nav", "inspector"]) {
103
+ const collapsed = !showing(side);
104
+ panes.classList.toggle(collapseClass[side], collapsed);
105
+ const also = opts.collapseAlso?.[side];
106
+ if (also) panes.classList.toggle(also, collapsed);
107
+ }
387
108
  panes.style.setProperty(openVar.nav, width.nav !== void 0 ? `${width.nav}px` : cfg.nav.defaultWidth);
388
109
  panes.style.setProperty(openVar.inspector, width.inspector !== void 0 ? `${width.inspector}px` : cfg.inspector.defaultWidth);
389
110
  applyToggleGlyph("nav", navToggle);
@@ -402,6 +123,11 @@ function mountPaneShell(host, opts) {
402
123
  function togglePane(side) {
403
124
  if (offered(side)) setPaneOpen(side, !open[side]);
404
125
  }
126
+ function holdClosed(side, next) {
127
+ if (held[side] === next) return;
128
+ held[side] = next;
129
+ apply();
130
+ }
405
131
  function resetWidths() {
406
132
  delete width.nav;
407
133
  delete width.inspector;
@@ -411,9 +137,11 @@ function mountPaneShell(host, opts) {
411
137
  navToggle.addEventListener("click", () => togglePane("nav"));
412
138
  inspToggle.addEventListener("click", () => togglePane("inspector"));
413
139
  function beginResize(side, startEvent) {
414
- if (cfg[side].resizable === false || !open[side]) return;
140
+ if (cfg[side].resizable === false || !showing(side)) return;
415
141
  startEvent.preventDefault();
416
142
  const pane = side === "nav" ? navPane : inspPane;
143
+ const seam = side === "nav" ? navResizer : inspResizer;
144
+ seam.classList.add("dragging");
417
145
  const min = cfg[side].minWidth ?? 160;
418
146
  const max = cfg[side].maxWidth ?? DEFAULT_MAX;
419
147
  const startX = startEvent.clientX;
@@ -429,6 +157,7 @@ function mountPaneShell(host, opts) {
429
157
  window.removeEventListener("pointermove", onMove);
430
158
  window.removeEventListener("pointerup", onUp);
431
159
  document.body.classList.remove("pane-resizing");
160
+ seam.classList.remove("dragging");
432
161
  width[side] = latest;
433
162
  persist();
434
163
  };
@@ -458,7 +187,8 @@ function mountPaneShell(host, opts) {
458
187
  inspector: inspInner,
459
188
  togglePane,
460
189
  setPaneOpen,
461
- isOpen: (s) => open[s],
190
+ isOpen: showing,
191
+ holdClosed,
462
192
  resetWidths,
463
193
  state: snapshot,
464
194
  destroy: () => {
@@ -467,189 +197,16 @@ function mountPaneShell(host, opts) {
467
197
  };
468
198
  }
469
199
 
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
200
  // 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
201
  function pinButton(opts) {
645
202
  const b = el("button", "swin-pin");
646
203
  b.type = "button";
647
- b.innerHTML = PIN_ICON;
204
+ b.append(iconNode("pin"));
648
205
  let pinned = opts.pinned;
649
206
  const reflect = () => {
650
207
  b.classList.toggle("on", pinned);
651
208
  b.setAttribute("aria-pressed", String(pinned));
652
- const label2 = pinned ? "Pinned on top: click to unpin" : "Click to keep on top";
209
+ const label2 = pinned ? "Pinned on top. Click to unpin." : "Keep on top";
653
210
  b.dataset["tip"] = label2;
654
211
  b.setAttribute("aria-label", label2);
655
212
  };
@@ -664,66 +221,91 @@ function pinButton(opts) {
664
221
  reflect();
665
222
  } };
666
223
  }
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;
224
+ var escSlot;
225
+ var escWired = false;
226
+ function wireEsc() {
227
+ if (escWired) return;
228
+ escWired = true;
229
+ window.addEventListener("keydown", (e) => {
230
+ if (e.key !== "Escape" || !escSlot) return;
231
+ if (e.defaultPrevented) return;
232
+ if (!escSlot.fromFields && isEditableTarget(e.target)) return;
233
+ if (escSlot.onEscape?.() === true) return;
234
+ escSlot.onClose();
235
+ });
236
+ }
237
+ function toolWindowHead(opts) {
238
+ const keep = (parts) => (parts ?? []).filter((p) => p !== null);
239
+ const pin = opts.pin ?? pinButton({ pinned: opts.pinned ?? true, onToggle: opts.onPin ?? (() => {
240
+ }) });
241
+ if (opts.esc !== false) {
242
+ escSlot = { onClose: opts.onClose, onEscape: opts.onEscape, fromFields: opts.esc === "always" };
243
+ wireEsc();
675
244
  }
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();
245
+ return el(
246
+ "header",
247
+ { className: `swin-head${opts.className ? ` ${opts.className}` : ""}` },
248
+ ...opts.title !== void 0 ? [el("span", { className: "swin-title", text: opts.title })] : [],
249
+ ...opts.tabs ? [opts.tabs] : [],
250
+ ...keep(opts.lead),
251
+ el("span", { className: "swin-spacer" }),
252
+ ...keep(opts.trail),
253
+ pin.el,
254
+ el("button", { className: "swin-close", tip: "Close (Esc)", onClick: opts.onClose }, iconNode("close"))
255
+ );
256
+ }
257
+ function followButton(opts) {
258
+ const what = opts.what ?? "line";
259
+ const b = el("button", "btn swin-follow", "Follow in the editor");
260
+ b.type = "button";
261
+ let on = opts.on;
262
+ const reflect = () => {
263
+ b.classList.toggle("on", on);
264
+ b.setAttribute("aria-pressed", String(on));
265
+ const label2 = on ? `The editor opens each ${what} as it plays. Click to stop.` : `Open each ${what} in the editor as it plays`;
266
+ b.dataset["tip"] = label2;
686
267
  };
687
- element.addEventListener("animationend", onEnd);
688
- const timer3 = setTimeout(finish, seconds * 1e3 + 120);
268
+ reflect();
269
+ b.addEventListener("click", () => {
270
+ on = !on;
271
+ reflect();
272
+ opts.onToggle(on);
273
+ });
274
+ return { el: b, set: (next) => {
275
+ on = next;
276
+ reflect();
277
+ } };
689
278
  }
690
279
 
691
280
  // src/confirm.ts
692
281
  function confirmDialog(opts) {
693
282
  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
283
  let done = false;
708
284
  const finish = (ok) => {
709
285
  if (done) return;
710
286
  done = true;
711
287
  resolve(ok);
712
- closeWithExit(dlg, () => {
713
- dlg.close();
714
- dlg.remove();
715
- });
288
+ frame.close();
716
289
  };
290
+ const frame = dialogFrame({
291
+ title: opts.title,
292
+ className: "confirm-dialog",
293
+ onClose: () => finish(false)
294
+ // Esc, or closed by someone else
295
+ });
296
+ const cancel = el("button", "btn confirm-btn cancel", "Cancel");
297
+ const danger = el("button", "btn danger confirm-btn", opts.confirmLabel);
298
+ cancel.type = "button";
299
+ danger.type = "button";
300
+ if (opts.body !== void 0) frame.body.append(el("div", "confirm-body", opts.body));
301
+ if (opts.bodyNode) frame.body.append(opts.bodyNode);
302
+ frame.actions.append(cancel, danger);
717
303
  cancel.addEventListener("click", () => finish(false));
718
304
  danger.addEventListener("click", () => finish(true));
719
- dlg.addEventListener("cancel", (e) => {
720
- e.preventDefault();
721
- finish(false);
722
- });
723
- dlg.addEventListener("mousedown", (e) => {
724
- if (e.target === dlg) finish(false);
305
+ frame.dialog.addEventListener("mousedown", (e) => {
306
+ if (e.target === frame.dialog) finish(false);
725
307
  });
726
- dlg.showModal();
308
+ frame.open();
727
309
  cancel.focus();
728
310
  });
729
311
  }
@@ -731,18 +313,16 @@ function confirmDialog(opts) {
731
313
  // src/about.ts
732
314
  function showAbout(opts) {
733
315
  return new Promise((resolve) => {
734
- const dialog = el("dialog", "shell-about");
316
+ const frame = dialogFrame({ title: opts.appName, className: "shell-about", onClose: () => resolve() });
317
+ frame.dialog.setAttribute("aria-label", `About ${opts.appName}`);
735
318
  if (opts.wordmark !== void 0) {
736
319
  const mark = el("div", "shell-about-mark");
737
320
  mark.innerHTML = opts.wordmark;
738
- dialog.append(mark);
321
+ frame.dialog.prepend(mark);
739
322
  }
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));
323
+ frame.body.append(el("div", "shell-about-version", `Version ${opts.version}`));
324
+ if (opts.blurb !== void 0) frame.body.append(el("p", "shell-about-blurb", opts.blurb));
325
+ if (opts.credits !== void 0) frame.body.append(el("p", "shell-about-credits", opts.credits));
746
326
  if (opts.links?.length) {
747
327
  const row = el("div", "shell-about-links");
748
328
  for (const link of opts.links) {
@@ -755,24 +335,13 @@ function showAbout(opts) {
755
335
  a.addEventListener("click", () => opts.onOpenLink?.(link.url));
756
336
  row.append(a);
757
337
  }
758
- dialog.append(row);
338
+ frame.body.append(row);
759
339
  }
760
- const close = el("button", "shell-about-close", "Close");
340
+ const close = el("button", "btn shell-about-close", "Close");
761
341
  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();
342
+ close.addEventListener("click", () => frame.close());
343
+ frame.actions.append(close);
344
+ frame.open();
776
345
  close.focus();
777
346
  });
778
347
  }
@@ -787,7 +356,7 @@ function openAnchoredPanel(opts) {
787
356
  if (prev.anchor === opts.anchor) return null;
788
357
  }
789
358
  const panel = el("div", `shell-anchored ${opts.className ?? ""}`.trim());
790
- const closeBtn = el("button", "shell-anchored-close", "\u2715");
359
+ const closeBtn = el("button", "shell-anchored-close", iconNode("close", 12));
791
360
  closeBtn.type = "button";
792
361
  closeBtn.setAttribute("aria-label", "Close");
793
362
  const body = el("div", "shell-anchored-body");
@@ -795,7 +364,7 @@ function openAnchoredPanel(opts) {
795
364
  el("div", "shell-anchored-head", el("span", "shell-anchored-title", opts.title), closeBtn),
796
365
  body
797
366
  );
798
- document.body.append(panel);
367
+ (opts.host ?? document.body).append(panel);
799
368
  placeAnchored(panel, opts.anchor, opts.width, opts.prefer ?? "below", opts.keepClear ?? []);
800
369
  let closed = false;
801
370
  const close = () => {
@@ -835,7 +404,7 @@ function closeAnchoredPanel() {
835
404
  }
836
405
  function placeAnchored(panel, anchor, width, prefer, keepClear) {
837
406
  const a = anchor.getBoundingClientRect();
838
- const GAP2 = 10;
407
+ const GAP = 10;
839
408
  const CAP = window.innerHeight * 0.7;
840
409
  const MIN_H = 140;
841
410
  panel.style.width = `${width}px`;
@@ -843,7 +412,7 @@ function placeAnchored(panel, anchor, width, prefer, keepClear) {
843
412
  const centred = a.left + a.width / 2 - width / 2;
844
413
  panel.style.left = `${Math.round(Math.min(Math.max(8, centred), window.innerWidth - width - 8))}px`;
845
414
  } else {
846
- const leftOf = a.left - width - GAP2;
415
+ const leftOf = a.left - width - GAP;
847
416
  const alignedRight = Math.min(a.right - width, window.innerWidth - width - 8);
848
417
  panel.style.left = `${Math.round(prefer === "left" && leftOf >= 8 ? leftOf : Math.max(8, prefer === "left" ? a.left : alignedRight))}px`;
849
418
  }
@@ -864,15 +433,15 @@ function placeAnchored(panel, anchor, width, prefer, keepClear) {
864
433
  return;
865
434
  }
866
435
  }
867
- const roomBelow = bottomLimit - (a.bottom + GAP2);
868
- const roomAbove = a.top - GAP2 - 8;
436
+ const roomBelow = bottomLimit - (a.bottom + GAP);
437
+ const roomAbove = a.top - GAP - 8;
869
438
  if (roomAbove > roomBelow) {
870
439
  panel.style.top = "auto";
871
- panel.style.bottom = `${Math.round(window.innerHeight - (a.top - GAP2))}px`;
440
+ panel.style.bottom = `${Math.round(window.innerHeight - (a.top - GAP))}px`;
872
441
  panel.style.maxHeight = `${Math.round(Math.max(MIN_H, Math.min(CAP, roomAbove)))}px`;
873
442
  } else {
874
443
  panel.style.bottom = "auto";
875
- panel.style.top = `${Math.round(Math.max(8, a.bottom + GAP2))}px`;
444
+ panel.style.top = `${Math.round(Math.max(8, a.bottom + GAP))}px`;
876
445
  panel.style.maxHeight = `${Math.round(Math.max(MIN_H, Math.min(CAP, roomBelow)))}px`;
877
446
  }
878
447
  }
@@ -885,164 +454,6 @@ function isValidGameId(gameId) {
885
454
  return /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(gameId);
886
455
  }
887
456
 
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
457
  // src/id-editor.ts
1047
458
  function openGameIdEditor(opts) {
1048
459
  const slugify = opts.slugify ?? gameIdify;
@@ -1059,9 +470,9 @@ function openGameIdEditor(opts) {
1059
470
  const input = el("input", "shell-id-input");
1060
471
  input.type = "text";
1061
472
  input.value = opts.value;
1062
- input.placeholder = opts.derived || "<game-id>";
473
+ input.placeholder = opts.derived || "game-id";
1063
474
  input.spellcheck = false;
1064
- const hint = el("div", "shell-id-hint", "Lower case, digits, hyphens. Empty = auto from the name.");
475
+ const hint = el("div", "shell-id-hint", "Lower case letters, digits, and hyphens. Leave it empty to derive one from the name.");
1065
476
  const actions = el("div", "shell-id-actions");
1066
477
  const reset = el("button", "shell-id-reset", "Reset to auto");
1067
478
  const set = el("button", "shell-id-set", "Set");
@@ -1123,12 +534,13 @@ function foldVc(shards, keys) {
1123
534
  return out;
1124
535
  }
1125
536
  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" };
537
+ const badge = (name, cls, title) => ({ name, glyph: icon[name] ?? "", cls, title });
538
+ if (s?.lockedBy?.length) return badge("locked", "vc-locked", `Locked by ${s.lockedBy.join(", ")}`);
539
+ if (s?.outOfDate) return badge("down", "vc-stale", "Out of date. A newer version is on the server.");
540
+ if (s?.checkedOutByMe) return badge("checkedOut", "vc-mine", "Checked out by you");
541
+ if (s?.dirty) return badge("modified", "vc-dirty", "Modified. Local changes aren't committed yet.");
542
+ if (s?.untracked) return badge("untracked", "vc-new", "New. Not in version control yet.");
543
+ if (s && !s.writable) return badge("readOnly", "vc-frozen", "Read-only on disk. Saving checks it out.");
1132
544
  return null;
1133
545
  }
1134
546
  function paintVcBadges(root, shards) {
@@ -1136,7 +548,7 @@ function paintVcBadges(root, shards) {
1136
548
  host.querySelector(":scope > .vc-badge")?.remove();
1137
549
  const badge = vcBadgeFor(foldVc(shards, host.dataset["vc"]));
1138
550
  if (!badge) return;
1139
- const span = el("span", { className: `vc-badge ${badge.cls}`, text: badge.glyph, tip: badge.title });
551
+ const span = el("span", { className: `vc-badge ${badge.cls}`, tip: badge.title }, iconNode(badge.name, 12));
1140
552
  host.append(span);
1141
553
  });
1142
554
  }
@@ -1159,8 +571,8 @@ function lockNotice(holders) {
1159
571
  return el(
1160
572
  "div",
1161
573
  { 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.` })
574
+ el("span", { className: "vc-lock-glyph" }, iconNode("locked")),
575
+ el("span", { text: `Locked by ${holders.join(", ")}. Read-only until they release it.` })
1164
576
  );
1165
577
  }
1166
578
 
@@ -1168,41 +580,36 @@ function lockNotice(holders) {
1168
580
  function askIdentity(opts = {}) {
1169
581
  return new Promise((resolve) => {
1170
582
  const seed = opts.current ?? opts.suggested;
1171
- const name = el("input", "shell-ident-input");
583
+ const name = el("input", "field shell-ident-input");
1172
584
  name.placeholder = "Your name";
1173
585
  name.value = seed?.name ?? "";
1174
- const email = el("input", "shell-ident-input");
1175
- email.placeholder = "Email (optional)";
586
+ const email = el("input", "field shell-ident-input");
587
+ email.placeholder = "Optional";
1176
588
  email.type = "email";
1177
589
  email.value = seed?.email ?? "";
1178
- const skip = el("button", "shell-ident-btn", "Skip");
1179
- const save = el("button", "shell-ident-btn primary", "Save");
590
+ const skip = el("button", "btn shell-ident-btn", "Skip");
591
+ const save = el("button", "btn primary shell-ident-btn", opts.mode === "welcome" ? "Continue" : "Save");
1180
592
  skip.type = "button";
1181
593
  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
594
  let done = false;
1196
595
  const finish = (keep) => {
1197
596
  if (done) return;
1198
597
  done = true;
1199
598
  const typed = name.value.trim();
1200
599
  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();
600
+ const answer = keep && typed !== "" ? value === "" ? { name: typed } : { name: typed, email: value } : opts.neverAskAgainOnSkip === true ? { name: "" } : void 0;
1204
601
  resolve(answer);
602
+ frame.close();
1205
603
  };
604
+ const frame = dialogFrame({
605
+ title: titleFor(opts),
606
+ sub: subFor(opts),
607
+ className: "shell-ident",
608
+ onClose: () => finish(false)
609
+ // Escape, or closed by someone else: a skip
610
+ });
611
+ frame.body.append(el("div", "shell-ident-fields", labelled("Name", name), labelled("Email", email)));
612
+ frame.actions.append(skip, save);
1206
613
  skip.addEventListener("click", () => finish(false));
1207
614
  save.addEventListener("click", () => finish(true));
1208
615
  name.addEventListener("keydown", (e) => {
@@ -1211,11 +618,19 @@ function askIdentity(opts = {}) {
1211
618
  email.addEventListener("keydown", (e) => {
1212
619
  if (e.key === "Enter") finish(true);
1213
620
  });
1214
- dialog.addEventListener("close", () => finish(false));
1215
- dialog.showModal();
621
+ frame.open();
1216
622
  name.focus();
1217
623
  });
1218
624
  }
625
+ function titleFor(opts) {
626
+ if (opts.mode === "welcome") return opts.appName ? `Welcome to ${opts.appName}` : "Welcome";
627
+ if (opts.mode === "edit") return "User information";
628
+ return "Who is working here?";
629
+ }
630
+ function subFor(opts) {
631
+ const why = "Your name goes on the comments you write. It's kept in this app, not in the project.";
632
+ return opts.mode === "edit" ? why : `${why} You can change it later.`;
633
+ }
1219
634
 
1220
635
  // src/notes-editor.ts
1221
636
  var EDITOR_ONLY = "";
@@ -1228,10 +643,10 @@ var CLASS_LABEL = {
1228
643
  };
1229
644
  var label = (cls) => cls === EDITOR_ONLY ? "Note (editor-only)" : CLASS_LABEL[cls] ?? cls[0].toUpperCase() + cls.slice(1);
1230
645
  var PLACEHOLDER = {
1231
- vo: "direction for the voice actor / director",
1232
- loc: "context for translators"
646
+ vo: "Direction for the voice actor or director",
647
+ loc: "Context for translators"
1233
648
  };
1234
- var placeholder = (cls) => cls === EDITOR_ONLY ? "an internal note - never exported" : PLACEHOLDER[cls] ?? "intent / rationale - why this is here";
649
+ var placeholder = (cls) => cls === EDITOR_ONLY ? "An internal note, never exported" : PLACEHOLDER[cls] ?? "Why this is here";
1235
650
  function openNotesEditor(opts) {
1236
651
  const { notes } = opts;
1237
652
  const seed = /* @__PURE__ */ new Map();
@@ -1285,7 +700,7 @@ ${line.text}` : line.text);
1285
700
  { className: "shell-notes-class" },
1286
701
  el("label", "shell-notes-caption", label(cls))
1287
702
  );
1288
- const area = el("textarea", "shell-notes-text");
703
+ const area = el("textarea", "field shell-notes-text");
1289
704
  area.value = areas.get(cls)?.value ?? seed.get(cls) ?? "";
1290
705
  area.rows = 4;
1291
706
  area.placeholder = placeholder(cls);
@@ -1311,29 +726,24 @@ ${line.text}` : line.text);
1311
726
  }
1312
727
  };
1313
728
  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);
729
+ const done = el("button", "btn primary shell-notes-btn", "Done");
730
+ done.type = "button";
1324
731
  const before = JSON.stringify(notes.own);
1325
732
  let closed = false;
1326
- const close = () => {
733
+ const commit = () => {
1327
734
  if (closed) return;
1328
735
  closed = true;
1329
736
  const next = value();
1330
737
  if (JSON.stringify(next) !== before) opts.save(next);
1331
- dialog.close();
1332
- dialog.remove();
1333
738
  };
1334
- done.addEventListener("click", close);
1335
- dialog.addEventListener("close", close);
1336
- dialog.showModal();
739
+ const frame = dialogFrame({ title: `Notes: ${opts.subject}`, className: "shell-notes", onClose: commit });
740
+ frame.body.append(body);
741
+ frame.actions.append(done);
742
+ done.addEventListener("click", () => {
743
+ commit();
744
+ frame.close();
745
+ });
746
+ frame.open();
1337
747
  areas.get(shown[0] ?? EDITOR_ONLY)?.focus();
1338
748
  }
1339
749
 
@@ -1405,7 +815,7 @@ function openComments(opts) {
1405
815
  el("span", { className: "shell-cmt-ts", text: stamp(message.ts) })
1406
816
  );
1407
817
  if (opts.deleteMessage && message.deleted !== true) {
1408
- const bin = el("button", { className: "shell-cmt-del", text: "\u2715" });
818
+ const bin = el("button", { className: "shell-cmt-del" }, iconNode("close", 12));
1409
819
  bin.dataset.tip = "Delete this comment";
1410
820
  bin.setAttribute("aria-label", "Delete this comment");
1411
821
  bin.addEventListener("click", () => opts.deleteMessage?.(thread.id, index));
@@ -1470,37 +880,65 @@ var clock = (ms) => {
1470
880
  var estimable = (done, total, elapsedMs) => done > 0 && total > 0 && elapsedMs > 1e3 && done / total > 0.1;
1471
881
  function mountJobProgress(host, opts) {
1472
882
  const units = opts.units ?? "runs";
883
+ const pinned = opts.indeterminate === true;
884
+ const cancellableByDefault = opts.cancellable ?? opts.onCancel !== void 0;
1473
885
  const fill = el("i", { className: "job-fill" });
1474
886
  const bar = el("div", { className: "job-bar" }, fill);
887
+ bar.setAttribute("role", "progressbar");
888
+ bar.setAttribute("aria-valuemin", "0");
889
+ bar.setAttribute("aria-valuemax", "100");
1475
890
  const what = el("span", { className: "job-label" });
1476
891
  const count = el("span", { className: "job-count" });
1477
892
  const time = el("span", { className: "job-time" });
1478
893
  const cancel = el("button", { className: "job-cancel", text: "Cancel" });
894
+ cancel.type = "button";
1479
895
  const strip = el("div", { className: "job-strip" }, what, bar, count, time, cancel);
1480
896
  strip.hidden = true;
1481
897
  host.append(strip);
898
+ let runPinned = pinned;
899
+ let indeterminate = false;
900
+ const setMode = (band) => {
901
+ indeterminate = band;
902
+ strip.classList.toggle("indeterminate", band);
903
+ count.hidden = band;
904
+ if (band) {
905
+ fill.style.width = "";
906
+ bar.removeAttribute("aria-valuenow");
907
+ }
908
+ };
1482
909
  const view = {
1483
910
  element: strip,
1484
911
  get visible() {
1485
912
  return !strip.hidden;
1486
913
  },
1487
- begin(label2) {
914
+ get indeterminate() {
915
+ return indeterminate;
916
+ },
917
+ begin(label2, run) {
918
+ runPinned = run?.indeterminate ?? pinned;
919
+ const cancellable = run?.cancellable ?? cancellableByDefault;
1488
920
  what.textContent = label2 ?? "Running\u2026";
1489
921
  count.textContent = "";
1490
922
  time.textContent = "";
1491
- fill.style.width = "0%";
923
+ cancel.hidden = !cancellable;
1492
924
  cancel.disabled = false;
1493
925
  cancel.textContent = "Cancel";
926
+ setMode(true);
1494
927
  strip.hidden = false;
1495
928
  },
1496
929
  update(done, total, elapsedMs) {
1497
930
  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)) {
931
+ const band = runPinned || !(total > 0);
932
+ setMode(band);
933
+ if (!band) {
934
+ const pct = Math.min(100, done / total * 100);
935
+ fill.style.width = `${pct}%`;
936
+ bar.setAttribute("aria-valuenow", String(Math.round(pct)));
937
+ count.textContent = `${done} / ${total} ${units}`.trimEnd();
938
+ }
939
+ if (!band && estimable(done, total, elapsedMs)) {
1502
940
  const remaining = elapsedMs / done * (total - done);
1503
- time.textContent = `${clock(elapsedMs)} elapsed \xB7 about ${clock(remaining)} left`;
941
+ time.textContent = `${clock(elapsedMs)} elapsed, about ${clock(remaining)} left`;
1504
942
  } else {
1505
943
  time.textContent = clock(elapsedMs);
1506
944
  }
@@ -1512,8 +950,10 @@ function mountJobProgress(host, opts) {
1512
950
  cancel.addEventListener("click", () => {
1513
951
  cancel.disabled = true;
1514
952
  cancel.textContent = "Stopping\u2026";
1515
- opts.onCancel();
953
+ opts.onCancel?.();
1516
954
  });
955
+ cancel.hidden = !cancellableByDefault;
956
+ setMode(true);
1517
957
  return view;
1518
958
  }
1519
959
 
@@ -1525,7 +965,7 @@ function createSaveController(opts) {
1525
965
  let auto = true;
1526
966
  let dirty = false;
1527
967
  let dirtySince = 0;
1528
- let timer3;
968
+ let timer2;
1529
969
  let inFlight;
1530
970
  const setStatus = (next) => {
1531
971
  if (next === status) return;
@@ -1533,17 +973,17 @@ function createSaveController(opts) {
1533
973
  opts.onStatus?.(status);
1534
974
  };
1535
975
  const clearTimer = () => {
1536
- if (timer3 === void 0) return;
1537
- clearTimeout(timer3);
1538
- timer3 = void 0;
976
+ if (timer2 === void 0) return;
977
+ clearTimeout(timer2);
978
+ timer2 = void 0;
1539
979
  };
1540
980
  const arm = () => {
1541
981
  clearTimer();
1542
982
  if (!auto) return;
1543
983
  const deadline = dirtySince + maxWaitMs;
1544
984
  const wait = Math.max(0, Math.min(delayMs, deadline - Date.now()));
1545
- timer3 = setTimeout(() => {
1546
- timer3 = void 0;
985
+ timer2 = setTimeout(() => {
986
+ timer2 = void 0;
1547
987
  void run();
1548
988
  }, wait);
1549
989
  };
@@ -1636,18 +1076,21 @@ function staleBar(opts) {
1636
1076
  className: "stale-bar-msg",
1637
1077
  text: `${opts.subject} changed in the editor. ${opts.advice ?? "Restart to play the new version."}`
1638
1078
  }),
1639
- el("button", {
1640
- className: "stale-bar-go",
1641
- text: `${icon.restart} ${label2}`,
1642
- onClick: () => opts.onRestart()
1643
- })
1079
+ // The icon is a node beside the word, not a character inside it; the
1080
+ // button's flex gap sets the space (stale.css).
1081
+ el(
1082
+ "button",
1083
+ { className: "stale-bar-go", onClick: () => opts.onRestart() },
1084
+ iconNode("restart", 12),
1085
+ label2
1086
+ )
1644
1087
  );
1645
1088
  }
1646
1089
 
1647
1090
  // src/toast.ts
1648
1091
  var DURATION = { info: 4e3, ok: 4e3, error: 7e3 };
1649
1092
  var node;
1650
- var timer2 = 0;
1093
+ var timer = 0;
1651
1094
  var seq = 0;
1652
1095
  function mount() {
1653
1096
  if (node && node.isConnected) return node;
@@ -1664,8 +1107,8 @@ function toast(message, kind = "info", opts = {}) {
1664
1107
  t.textContent = message;
1665
1108
  t.className = `shell-toast ${kind}`;
1666
1109
  t.hidden = false;
1667
- window.clearTimeout(timer2);
1668
- timer2 = window.setTimeout(() => {
1110
+ window.clearTimeout(timer);
1111
+ timer = window.setTimeout(() => {
1669
1112
  closeWithExit(t, () => {
1670
1113
  if (seq === id) t.hidden = true;
1671
1114
  });
@@ -1673,7 +1116,7 @@ function toast(message, kind = "info", opts = {}) {
1673
1116
  }
1674
1117
  function dismissToast() {
1675
1118
  if (!node || node.hidden) return;
1676
- window.clearTimeout(timer2);
1119
+ window.clearTimeout(timer);
1677
1120
  const id = seq;
1678
1121
  const t = node;
1679
1122
  closeWithExit(t, () => {
@@ -1682,14 +1125,14 @@ function dismissToast() {
1682
1125
  }
1683
1126
 
1684
1127
  // src/stepper.ts
1128
+ var STEP_ICON = 12;
1685
1129
  function renderStepperBar(host, opts) {
1686
1130
  const { items } = opts;
1687
1131
  const close = opts.onClose === void 0 ? null : el("button", {
1688
1132
  className: "stepbar-nav stepbar-close",
1689
- text: icon.close,
1690
1133
  tip: opts.closeTip ?? "Close",
1691
1134
  onClick: opts.onClose
1692
- });
1135
+ }, iconNode("close", STEP_ICON));
1693
1136
  const actions = (opts.actions ?? []).filter((a) => a !== null && a !== void 0);
1694
1137
  if (items.length === 0) {
1695
1138
  if (opts.empty === void 0) {
@@ -1713,17 +1156,15 @@ function renderStepperBar(host, opts) {
1713
1156
  const nav = items.length > 1 ? [
1714
1157
  el("button", {
1715
1158
  className: "stepbar-nav",
1716
- text: icon.back,
1717
1159
  tip: opts.tips?.prev ?? "Previous",
1718
1160
  onClick: () => step(-1)
1719
- }),
1161
+ }, iconNode("back", STEP_ICON)),
1720
1162
  el("span", { className: "stepbar-of", text: `${index + 1}/${items.length}` }),
1721
1163
  el("button", {
1722
1164
  className: "stepbar-nav",
1723
- text: icon.forward,
1724
1165
  tip: opts.tips?.next ?? "Next",
1725
1166
  onClick: () => step(1)
1726
- })
1167
+ }, iconNode("forward", STEP_ICON))
1727
1168
  ] : [];
1728
1169
  host.replaceChildren(
1729
1170
  el("span", { className: `stepbar-count ${opts.tone ?? "muted"}`, text: String(items.length) }),
@@ -1732,7 +1173,7 @@ function renderStepperBar(host, opts) {
1732
1173
  "button",
1733
1174
  { className: "stepbar-cur", tip: opts.tips?.go ?? "Go to this", onClick: () => opts.onGo(index) },
1734
1175
  item.kind === void 0 ? null : el("span", { className: `stepbar-cat${item.kindClass ? ` ${item.kindClass}` : ""}`, text: item.kind }),
1735
- item.where === void 0 ? null : el("span", { className: "stepbar-where", text: item.where }),
1176
+ item.where === void 0 ? null : typeof item.where === "string" ? el("span", { className: "stepbar-where", text: item.where }) : el("span", "stepbar-where", item.where),
1736
1177
  el("span", { className: "stepbar-msg", text: item.text })
1737
1178
  ),
1738
1179
  ...actions,
@@ -1757,11 +1198,11 @@ function createNavHistory(opts) {
1757
1198
  }
1758
1199
  };
1759
1200
  return {
1760
- visit(place2) {
1201
+ visit(place) {
1761
1202
  ahead = [];
1762
1203
  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);
1204
+ if (top !== void 0 && opts.same(top, place)) past[past.length - 1] = place;
1205
+ else past.push(place);
1765
1206
  if (past.length > cap) past = past.slice(past.length - cap);
1766
1207
  },
1767
1208
  back(current2) {
@@ -1779,18 +1220,18 @@ function createNavHistory(opts) {
1779
1220
  };
1780
1221
  }
1781
1222
  function historyNav(onBack, onForward) {
1782
- const btn = (glyph, tip2, onClick) => {
1223
+ const btn = (name, tip, onClick) => {
1783
1224
  const b = document.createElement("button");
1784
1225
  b.type = "button";
1785
1226
  b.className = "shell-histnav-btn";
1786
- b.textContent = glyph;
1787
- b.dataset["tip"] = tip2;
1788
- b.setAttribute("aria-label", tip2);
1227
+ b.append(iconNode(name));
1228
+ b.dataset["tip"] = tip;
1229
+ b.setAttribute("aria-label", tip);
1789
1230
  b.addEventListener("click", onClick);
1790
1231
  return b;
1791
1232
  };
1792
- const back = btn("\u2190", "Back", onBack);
1793
- const forward = btn("\u2192", "Forward", onForward);
1233
+ const back = btn("arrowLeft", "Back", onBack);
1234
+ const forward = btn("arrowRight", "Forward", onForward);
1794
1235
  const el2 = document.createElement("span");
1795
1236
  el2.className = "shell-histnav";
1796
1237
  el2.append(back, forward);
@@ -1803,6 +1244,6 @@ function historyNav(onBack, onForward) {
1803
1244
  };
1804
1245
  }
1805
1246
 
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 };
1247
+ 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
1248
  //# sourceMappingURL=index.js.map
1808
1249
  //# sourceMappingURL=index.js.map