@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.cjs CHANGED
@@ -149,6 +149,204 @@ function initTooltips(opts = {}) {
149
149
  window.addEventListener("blur", () => hide());
150
150
  }
151
151
 
152
+ // src/icons.ts
153
+ var icon = {
154
+ // --- dismissing and revealing ---------------------------------------------
155
+ /** Close a panel, a dialog, a popover. Patterpad's, twelve times over. */
156
+ close: "\u2715",
157
+ /** The overflow menu on a document or a row: "more about this". */
158
+ more: "\u22EF",
159
+ /** Collapsed disclosure, pointing at what would open. */
160
+ collapsed: "\u25B8",
161
+ /** Expanded disclosure. */
162
+ expanded: "\u25BE",
163
+ /** A dropdown's affordance, where it is not a real <select>. */
164
+ dropdown: "\u25BE",
165
+ // --- moving through things ------------------------------------------------
166
+ /** Back / previous, in a breadcrumb or a stepper. */
167
+ back: "\u2039",
168
+ /** Forward / next. */
169
+ forward: "\u203A",
170
+ /** Up one, in a list. */
171
+ up: "\u2191",
172
+ /** Down one. */
173
+ down: "\u2193",
174
+ // --- state ----------------------------------------------------------------
175
+ /** Done, applied, agreed. */
176
+ tick: "\u2713",
177
+ /** Somebody else holds this: the version-control lock. */
178
+ locked: "\u2298",
179
+ /** Read-only on disk with no other holder: the resting state of everything
180
+ * in a lock-based working copy that nobody has checked out yet. */
181
+ readOnly: "\u25CB",
182
+ /** YOU hold this one: checked out / opened by the current user, still yours to
183
+ * edit. Shares its glyph with `note`, which the table permits: an entry is a
184
+ * MEANING, and two meanings may draw the same shape. */
185
+ checkedOut: "\u270E",
186
+ /** Tracked, with local changes not yet committed. */
187
+ modified: "\u25CF",
188
+ /** Not in version control yet: new since the last commit. */
189
+ untracked: "+",
190
+ /** Something needs attention but is not fatal. */
191
+ warning: "\u203C",
192
+ /** A live thing: the running position, an active marker. */
193
+ dot: "\u25CF",
194
+ /** Play, in the sense of "run this". */
195
+ play: "\u25B6",
196
+ /** Start the thing again from the top: a play session, a sweep. Named for the
197
+ * act, not the arrow, which is the table's rule. */
198
+ restart: "\u27F2",
199
+ /** The three ways a collection can be shown. Named by what you get rather than
200
+ * by the glyph, so a second app can pick the same three without copying
201
+ * somebody's taste in symbols. */
202
+ viewCards: "\u25A6",
203
+ viewTable: "\u2630",
204
+ viewNode: "\u25C8",
205
+ // --- handling -------------------------------------------------------------
206
+ /** The drag grip on a reorderable row. */
207
+ grip: "\u283F",
208
+ /** Add one of whatever this list holds. */
209
+ add: "+",
210
+ // --- annotation -----------------------------------------------------------
211
+ /** Documentation notes: the pencil. Both apps had chosen it independently,
212
+ * which is the happiest way for a vocabulary entry to arrive. */
213
+ note: "\u270E",
214
+ /** A comment thread. Speech, not a pencil: documentation states the reason,
215
+ * comments are the conversation, and the two must never look alike. */
216
+ comment: "\u275D"
217
+ };
218
+ var STROKE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.571" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">';
219
+ var FILL = '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">';
220
+ var stroked = (inner) => `${STROKE}${inner}</svg>`;
221
+ var filled = (inner) => `${FILL}${inner}</svg>`;
222
+ var DISC = filled('<circle cx="12" cy="12" r="6"/>');
223
+ var DISC_LARGE = filled('<circle cx="12" cy="12" r="9"/>');
224
+ var DRAWN = {
225
+ // --- dismissing and revealing ---------------------------------------------
226
+ /** Close a panel, a dialog, a popover. (x) */
227
+ close: stroked('<path d="M18 6 6 18"/><path d="m6 6 12 12"/>'),
228
+ /** The overflow menu on a document or a row: "more about this". (ellipsis) */
229
+ more: stroked('<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>'),
230
+ /** Collapsed disclosure, pointing at what would open. (chevron-right) */
231
+ collapsed: stroked('<path d="m9 18 6-6-6-6"/>'),
232
+ /** Expanded disclosure. (chevron-down) */
233
+ expanded: stroked('<path d="m6 9 6 6 6-6"/>'),
234
+ /** A dropdown's affordance, where it is not a real <select>. (chevron-down) */
235
+ dropdown: stroked('<path d="m6 9 6 6 6-6"/>'),
236
+ // --- moving through things ------------------------------------------------
237
+ /** Back / previous, in a breadcrumb or a stepper. (chevron-left) */
238
+ back: stroked('<path d="m15 18-6-6 6-6"/>'),
239
+ /** Forward / next. (chevron-right) */
240
+ forward: stroked('<path d="m9 18 6-6-6-6"/>'),
241
+ /** Up one, in a list. (arrow-up) */
242
+ up: stroked('<path d="m5 12 7-7 7 7"/><path d="M12 19V5"/>'),
243
+ /** Down one. (arrow-down) */
244
+ down: stroked('<path d="M12 5v14"/><path d="m19 12-7 7-7-7"/>'),
245
+ /** History back. (arrow-left) */
246
+ arrowLeft: stroked('<path d="m12 19-7-7 7-7"/><path d="M19 12H5"/>'),
247
+ /** History forward; "go there". (arrow-right) */
248
+ arrowRight: stroked('<path d="M5 12h14"/><path d="m12 5 7 7-7 7"/>'),
249
+ // --- state ----------------------------------------------------------------
250
+ /** Done, applied, agreed. (check) */
251
+ tick: stroked('<path d="M20 6 9 17l-5-5"/>'),
252
+ /** Somebody else holds this: the version-control lock. (lock) */
253
+ locked: stroked('<rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>'),
254
+ /** Read-only on disk with no other holder. (lock-open) An open lock rather
255
+ * than a struck-out eye: this state is not a prohibition. Nobody holds the
256
+ * file, saving checks it out, and it is the resting state of everything the
257
+ * author has not touched yet under a lock-based VCS. eye-off would say "you
258
+ * cannot", which is false here; the open lock says "not held", beside
259
+ * `locked`, which is the shut one, so the two read as a pair. */
260
+ readOnly: stroked('<rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 9.9-1"/>'),
261
+ /** YOU hold this one: checked out, still yours to edit. (pencil) */
262
+ checkedOut: stroked('<path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/>'),
263
+ /** Tracked, with local changes not yet committed. (a disc; see DISC) */
264
+ modified: DISC,
265
+ /** Not in version control yet: new since the last commit. (plus) */
266
+ untracked: stroked('<path d="M5 12h14"/><path d="M12 5v14"/>'),
267
+ /** Something needs attention but is not fatal. (triangle-alert) */
268
+ warning: stroked('<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/>'),
269
+ /** A live thing: the running position, an active marker. (a disc) */
270
+ dot: DISC,
271
+ /** Play, in the sense of "run this". (play) */
272
+ play: stroked('<path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z"/>'),
273
+ /** Start the thing again from the top: a play session, a sweep. (rotate-ccw) */
274
+ restart: stroked('<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/>'),
275
+ /** The three ways a collection can be shown, named by what you get. */
276
+ /** Cards. (layout-grid) */
277
+ viewCards: stroked('<rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="14" y="3" rx="1"/><rect width="7" height="7" x="14" y="14" rx="1"/><rect width="7" height="7" x="3" y="14" rx="1"/>'),
278
+ /** A table. (rows-3: a ruled box, which is a table. `list` is bullets, and
279
+ * the view is not a list.) */
280
+ viewTable: stroked('<rect width="18" height="18" x="3" y="3" rx="2"/><path d="M21 9H3"/><path d="M21 15H3"/>'),
281
+ /** A node graph. (waypoints: four nodes on edges reads as a graph.
282
+ * `git-fork` reads as version control, which this family also draws, and
283
+ * a picture that means two things is the fault this table exists to
284
+ * prevent.) */
285
+ viewNode: stroked('<path d="m10.586 5.414-5.172 5.172"/><path d="m18.586 13.414-5.172 5.172"/><path d="M6 12h12"/><circle cx="12" cy="20" r="2"/><circle cx="12" cy="4" r="2"/><circle cx="20" cy="12" r="2"/><circle cx="4" cy="12" r="2"/>'),
286
+ /** Record: the disc, in whatever colour the button is. (a disc; see DISC_LARGE) */
287
+ record: DISC_LARGE,
288
+ // --- handling -------------------------------------------------------------
289
+ /** The drag grip on a reorderable row. (grip-vertical) */
290
+ grip: stroked('<circle cx="9" cy="12" r="1"/><circle cx="9" cy="5" r="1"/><circle cx="9" cy="19" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="15" cy="5" r="1"/><circle cx="15" cy="19" r="1"/>'),
291
+ /** Add one of whatever this list holds. (plus) */
292
+ add: stroked('<path d="M5 12h14"/><path d="M12 5v14"/>'),
293
+ /** Connect: to a server, a live link, a running game. (plug) */
294
+ connect: stroked('<path d="M12 22v-5"/><path d="M15 8V2"/><path d="M17 8a1 1 0 0 1 1 1v4a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V9a1 1 0 0 1 1-1z"/><path d="M9 8V2"/>'),
295
+ /** Search. (search) */
296
+ search: stroked('<path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/>'),
297
+ /** Keep this window on top. (pin) */
298
+ pin: stroked('<path d="M12 17v5"/><path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"/>'),
299
+ /** Settings. (settings) */
300
+ settings: stroked('<path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915"/><circle cx="12" cy="12" r="3"/>'),
301
+ // --- annotation -----------------------------------------------------------
302
+ /** Documentation notes: a document with lines on it. (file-text) The glyph
303
+ * table drew a pencil and shared it with `checkedOut`; the drawn set gives
304
+ * the two meanings two shapes, and notes get the document, which is what
305
+ * the shell's own noteFilled/noteOutline pair had always drawn. */
306
+ note: stroked('<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"/><path d="M14 2v5a1 1 0 0 0 1 1h5"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/>'),
307
+ /** A comment thread. Speech, not a document: documentation states the
308
+ * reason, comments are the conversation, and the two must never look
309
+ * alike. (message-square) */
310
+ comment: stroked('<path d="M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"/>')
311
+ };
312
+ var iconSvg = {
313
+ ...DRAWN,
314
+ /** A document with lines: notes exist on this thing. (legacy, 16 grid) */
315
+ 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>',
316
+ /** The same document, empty: nothing written here yet. (legacy, 16 grid) */
317
+ 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>'
318
+ };
319
+ var ICON_NAMES = Object.keys(DRAWN);
320
+ var ICON_SIZE = 14;
321
+ function iconHtml(name, size = ICON_SIZE) {
322
+ return DRAWN[name].replace("<svg ", `<svg width="${size}" height="${size}" data-icon="${name}" `);
323
+ }
324
+ var templates = /* @__PURE__ */ new Map();
325
+ function iconNode(name, size = ICON_SIZE) {
326
+ let tpl = templates.get(name);
327
+ if (!tpl) {
328
+ const holder = document.createElement("template");
329
+ holder.innerHTML = DRAWN[name];
330
+ tpl = holder.content.firstElementChild;
331
+ tpl.setAttribute("data-icon", name);
332
+ templates.set(name, tpl);
333
+ }
334
+ const node2 = tpl.cloneNode(true);
335
+ node2.setAttribute("width", String(size));
336
+ node2.setAttribute("height", String(size));
337
+ return node2;
338
+ }
339
+ var BY_GLYPH = /* @__PURE__ */ new Map();
340
+ for (const [name, glyph] of Object.entries(icon)) {
341
+ if (!BY_GLYPH.has(glyph)) BY_GLYPH.set(glyph, name);
342
+ }
343
+ function iconNameOfGlyph(glyph) {
344
+ return BY_GLYPH.get(glyph);
345
+ }
346
+ function isIconName(s) {
347
+ return Object.prototype.hasOwnProperty.call(DRAWN, s);
348
+ }
349
+
152
350
  // src/dom.ts
153
351
  function el(tag, classNameOrProps, ...children) {
154
352
  const node2 = document.createElement(tag);
@@ -169,12 +367,14 @@ function el(tag, classNameOrProps, ...children) {
169
367
  checkTooltipHost();
170
368
  return node2;
171
369
  }
172
- function iconBtn(glyph, title, onClick, disabled = false, danger = false) {
370
+ function iconBtn(name, title, onClick, disabled = false, danger = false) {
173
371
  const b = el("button", `shell-icon${danger ? " danger" : ""}`);
174
372
  b.type = "button";
175
- b.textContent = glyph;
176
373
  b.dataset["tip"] = title;
177
374
  b.setAttribute("aria-label", title);
375
+ const word = isIconName(name) ? name : iconNameOfGlyph(name);
376
+ if (word) b.append(iconNode(word));
377
+ else b.textContent = name;
178
378
  b.disabled = disabled;
179
379
  b.addEventListener("click", onClick);
180
380
  return b;
@@ -195,6 +395,7 @@ function moveItem(arr, i, delta) {
195
395
  [arr[i], arr[j]] = [arr[j], arr[i]];
196
396
  return true;
197
397
  }
398
+ var CHIP_ICON = 10;
198
399
  function tagChips(holder, onChange) {
199
400
  const wrap = el("div", "shell-tags");
200
401
  const input = el("input", "shell-tag-input");
@@ -203,10 +404,10 @@ function tagChips(holder, onChange) {
203
404
  input.spellcheck = false;
204
405
  const makeChip = (v) => {
205
406
  const chip = el("span", "shell-tag", v);
206
- const x = el("button", "shell-tag-x", "\u2715");
407
+ const x = el("button", "shell-tag-x", iconNode("close", CHIP_ICON));
207
408
  x.type = "button";
208
- x.dataset["tip"] = `remove ${v}`;
209
- x.setAttribute("aria-label", `remove ${v}`);
409
+ x.dataset["tip"] = `Remove ${v}`;
410
+ x.setAttribute("aria-label", `Remove ${v}`);
210
411
  x.addEventListener("click", () => {
211
412
  holder.values = (holder.values ?? []).filter((o) => o !== v);
212
413
  chip.remove();
@@ -235,6 +436,120 @@ function tagChips(holder, onChange) {
235
436
  wrap.append(input);
236
437
  return wrap;
237
438
  }
439
+ function stageChips(holder, onChange) {
440
+ const wrap = el("div", "shell-tags shell-stages");
441
+ const input = el("input", "shell-tag-input");
442
+ input.type = "text";
443
+ input.placeholder = "Add stage";
444
+ input.spellcheck = false;
445
+ const control = (name, tip2, disabled, onClick) => {
446
+ const b = el("button", "shell-tag-x shell-tag-move", iconNode(name, CHIP_ICON));
447
+ b.type = "button";
448
+ b.dataset["tip"] = tip2;
449
+ b.setAttribute("aria-label", tip2);
450
+ b.disabled = disabled;
451
+ b.addEventListener("click", onClick);
452
+ return b;
453
+ };
454
+ const rebuild = () => {
455
+ for (const c of Array.from(wrap.children)) if (c !== input) c.remove();
456
+ const stages = holder.stages ?? [];
457
+ stages.forEach((v, i) => {
458
+ const chip = el("span", "shell-tag", `${i + 1}. ${v}`);
459
+ chip.dataset["stage"] = v;
460
+ chip.append(
461
+ control(
462
+ "back",
463
+ `Move ${v} earlier`,
464
+ i === 0,
465
+ () => {
466
+ if (moveItem(stages, i, -1)) {
467
+ rebuild();
468
+ onChange?.();
469
+ }
470
+ }
471
+ ),
472
+ control(
473
+ "forward",
474
+ `Move ${v} later`,
475
+ i === stages.length - 1,
476
+ () => {
477
+ if (moveItem(stages, i, 1)) {
478
+ rebuild();
479
+ onChange?.();
480
+ }
481
+ }
482
+ )
483
+ );
484
+ const x = el("button", "shell-tag-x", iconNode("close", CHIP_ICON));
485
+ x.type = "button";
486
+ x.dataset["tip"] = `Remove ${v}`;
487
+ x.setAttribute("aria-label", `Remove ${v}`);
488
+ x.addEventListener("click", () => {
489
+ holder.stages = (holder.stages ?? []).filter((o) => o !== v);
490
+ rebuild();
491
+ onChange?.();
492
+ });
493
+ chip.append(x);
494
+ wrap.insertBefore(chip, input);
495
+ });
496
+ };
497
+ const commit = () => {
498
+ const v = input.value.trim();
499
+ if (v && !(holder.stages ?? []).includes(v)) {
500
+ (holder.stages ??= []).push(v);
501
+ rebuild();
502
+ onChange?.();
503
+ }
504
+ input.value = "";
505
+ };
506
+ input.addEventListener("keydown", (e) => {
507
+ if (e.key === "Enter" || e.key === ",") {
508
+ e.preventDefault();
509
+ commit();
510
+ }
511
+ });
512
+ input.addEventListener("blur", commit);
513
+ wrap.append(input);
514
+ rebuild();
515
+ return wrap;
516
+ }
517
+ var reorderDragId = null;
518
+ var clearDropMarks = (host) => host.querySelectorAll(".drop-before, .drop-after").forEach((e) => e.classList.remove("drop-before", "drop-after"));
519
+ function wireReorder(el2, id, axis, onMove) {
520
+ el2.draggable = true;
521
+ el2.dataset["reorderId"] = id;
522
+ const host = () => el2.parentElement ?? el2;
523
+ el2.addEventListener("dragstart", (e) => {
524
+ reorderDragId = id;
525
+ el2.classList.add("dragging");
526
+ if (e.dataTransfer) {
527
+ e.dataTransfer.effectAllowed = "move";
528
+ e.dataTransfer.setData("text/plain", id);
529
+ }
530
+ });
531
+ el2.addEventListener("dragend", () => {
532
+ reorderDragId = null;
533
+ el2.classList.remove("dragging");
534
+ clearDropMarks(host());
535
+ });
536
+ el2.addEventListener("dragover", (e) => {
537
+ if (!reorderDragId || reorderDragId === id) return;
538
+ e.preventDefault();
539
+ if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
540
+ const r = el2.getBoundingClientRect();
541
+ const before = axis === "y" ? e.clientY < r.top + r.height / 2 : e.clientX < r.left + r.width / 2;
542
+ clearDropMarks(host());
543
+ el2.classList.add(before ? "drop-before" : "drop-after");
544
+ });
545
+ el2.addEventListener("dragleave", () => el2.classList.remove("drop-before", "drop-after"));
546
+ el2.addEventListener("drop", (e) => {
547
+ e.preventDefault();
548
+ const before = el2.classList.contains("drop-before");
549
+ clearDropMarks(host());
550
+ if (reorderDragId && reorderDragId !== id) onMove(reorderDragId, before, id);
551
+ });
552
+ }
238
553
 
239
554
  // src/colour.ts
240
555
  var PALETTE_SIZE = 12;
@@ -256,79 +571,6 @@ function colourFor(name) {
256
571
  return PALETTE[colourIndex(name)];
257
572
  }
258
573
 
259
- // src/icons.ts
260
- var icon = {
261
- // --- dismissing and revealing ---------------------------------------------
262
- /** Close a panel, a dialog, a popover. Patterpad's, twelve times over. */
263
- close: "\u2715",
264
- /** The overflow menu on a document or a row: "more about this". */
265
- more: "\u22EF",
266
- /** Collapsed disclosure, pointing at what would open. */
267
- collapsed: "\u25B8",
268
- /** Expanded disclosure. */
269
- expanded: "\u25BE",
270
- /** A dropdown's affordance, where it is not a real <select>. */
271
- dropdown: "\u25BE",
272
- // --- moving through things ------------------------------------------------
273
- /** Back / previous, in a breadcrumb or a stepper. */
274
- back: "\u2039",
275
- /** Forward / next. */
276
- forward: "\u203A",
277
- /** Up one, in a list. */
278
- up: "\u2191",
279
- /** Down one. */
280
- down: "\u2193",
281
- // --- state ----------------------------------------------------------------
282
- /** Done, applied, agreed. */
283
- tick: "\u2713",
284
- /** Somebody else holds this: the version-control lock. */
285
- locked: "\u2298",
286
- /** Read-only on disk with no other holder: the resting state of everything
287
- * in a lock-based working copy that nobody has checked out yet. */
288
- readOnly: "\u25CB",
289
- /** YOU hold this one: checked out / opened by the current user, still yours to
290
- * edit. Shares its glyph with `note`, which the table permits: an entry is a
291
- * MEANING, and two meanings may draw the same shape. */
292
- checkedOut: "\u270E",
293
- /** Tracked, with local changes not yet committed. */
294
- modified: "\u25CF",
295
- /** Not in version control yet: new since the last commit. */
296
- untracked: "+",
297
- /** Something needs attention but is not fatal. */
298
- warning: "\u203C",
299
- /** A live thing: the running position, an active marker. */
300
- dot: "\u25CF",
301
- /** Play, in the sense of "run this". */
302
- play: "\u25B6",
303
- /** Start the thing again from the top: a play session, a sweep. Named for the
304
- * act, not the arrow, which is the table's rule. */
305
- restart: "\u27F2",
306
- /** The three ways a collection can be shown. Named by what you get rather than
307
- * by the glyph, so a second app can pick the same three without copying
308
- * somebody's taste in symbols. */
309
- viewCards: "\u25A6",
310
- viewTable: "\u2630",
311
- viewNode: "\u25C8",
312
- // --- handling -------------------------------------------------------------
313
- /** The drag grip on a reorderable row. */
314
- grip: "\u283F",
315
- /** Add one of whatever this list holds. */
316
- add: "+",
317
- // --- annotation -----------------------------------------------------------
318
- /** Documentation notes: the pencil. Both apps had chosen it independently,
319
- * which is the happiest way for a vocabulary entry to arrive. */
320
- note: "\u270E",
321
- /** A comment thread. Speech, not a pencil: documentation states the reason,
322
- * comments are the conversation, and the two must never look alike. */
323
- comment: "\u275D"
324
- };
325
- var iconSvg = {
326
- /** A document with lines: notes exist on this thing. */
327
- 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>',
328
- /** The same document, empty: nothing written here yet. */
329
- 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>'
330
- };
331
-
332
574
  // src/pane-shell.ts
333
575
  var DEFAULT_MAX = (w) => Math.min(640, w - 420);
334
576
  var openVar = { nav: "--nav-open-w", inspector: "--insp-open-w" };
@@ -341,6 +583,8 @@ function mountPaneShell(host, opts) {
341
583
  inspector: offered("inspector") && (opts.initial?.open?.inspector ?? true)
342
584
  };
343
585
  const width = { ...opts.initial?.width ?? {} };
586
+ const held = { nav: false, inspector: false };
587
+ const showing = (side) => open[side] && !held[side];
344
588
  const el2 = (tag, cls) => {
345
589
  const n = document.createElement(tag);
346
590
  if (cls) n.className = cls;
@@ -371,18 +615,28 @@ function mountPaneShell(host, opts) {
371
615
  host.append(root);
372
616
  if (!offered("nav")) navToggle.remove();
373
617
  if (!offered("inspector")) inspToggle.remove();
374
- function applyToggleGlyph(side, button) {
375
- const isOpen = open[side];
376
- button.textContent = side === "nav" ? isOpen ? "\u2039" : "\u203A" : isOpen ? "\u203A" : "\u2039";
618
+ const defaultTip = (side, isOpen) => {
377
619
  const noun = cfg[side].label ?? side;
378
620
  const hint = cfg[side].shortcutHint ? ` (${cfg[side].shortcutHint})` : "";
379
- button.title = `${isOpen ? "Hide" : "Show"} ${noun}${hint}`;
380
- button.setAttribute("aria-label", button.title);
621
+ return `${isOpen ? "Hide" : "Show"} ${noun}${hint}`;
622
+ };
623
+ const tipFor = opts.tipFor ?? defaultTip;
624
+ function applyToggleGlyph(side, button) {
625
+ const isOpen = open[side];
626
+ button.replaceChildren(iconNode(side === "nav" ? isOpen ? "back" : "forward" : isOpen ? "forward" : "back"));
627
+ const tip2 = tipFor(side, isOpen);
628
+ button.dataset["tip"] = tip2;
629
+ button.setAttribute("aria-label", tip2);
381
630
  button.setAttribute("aria-pressed", String(isOpen));
382
631
  }
632
+ ensureTooltipHost();
383
633
  function apply() {
384
- panes.classList.toggle(collapseClass.nav, !open.nav);
385
- panes.classList.toggle(collapseClass.inspector, !open.inspector);
634
+ for (const side of ["nav", "inspector"]) {
635
+ const collapsed = !showing(side);
636
+ panes.classList.toggle(collapseClass[side], collapsed);
637
+ const also = opts.collapseAlso?.[side];
638
+ if (also) panes.classList.toggle(also, collapsed);
639
+ }
386
640
  panes.style.setProperty(openVar.nav, width.nav !== void 0 ? `${width.nav}px` : cfg.nav.defaultWidth);
387
641
  panes.style.setProperty(openVar.inspector, width.inspector !== void 0 ? `${width.inspector}px` : cfg.inspector.defaultWidth);
388
642
  applyToggleGlyph("nav", navToggle);
@@ -401,6 +655,11 @@ function mountPaneShell(host, opts) {
401
655
  function togglePane(side) {
402
656
  if (offered(side)) setPaneOpen(side, !open[side]);
403
657
  }
658
+ function holdClosed(side, next) {
659
+ if (held[side] === next) return;
660
+ held[side] = next;
661
+ apply();
662
+ }
404
663
  function resetWidths() {
405
664
  delete width.nav;
406
665
  delete width.inspector;
@@ -410,9 +669,11 @@ function mountPaneShell(host, opts) {
410
669
  navToggle.addEventListener("click", () => togglePane("nav"));
411
670
  inspToggle.addEventListener("click", () => togglePane("inspector"));
412
671
  function beginResize(side, startEvent) {
413
- if (cfg[side].resizable === false || !open[side]) return;
672
+ if (cfg[side].resizable === false || !showing(side)) return;
414
673
  startEvent.preventDefault();
415
674
  const pane = side === "nav" ? navPane : inspPane;
675
+ const seam = side === "nav" ? navResizer : inspResizer;
676
+ seam.classList.add("dragging");
416
677
  const min = cfg[side].minWidth ?? 160;
417
678
  const max = cfg[side].maxWidth ?? DEFAULT_MAX;
418
679
  const startX = startEvent.clientX;
@@ -428,6 +689,7 @@ function mountPaneShell(host, opts) {
428
689
  window.removeEventListener("pointermove", onMove);
429
690
  window.removeEventListener("pointerup", onUp);
430
691
  document.body.classList.remove("pane-resizing");
692
+ seam.classList.remove("dragging");
431
693
  width[side] = latest;
432
694
  persist();
433
695
  };
@@ -457,7 +719,8 @@ function mountPaneShell(host, opts) {
457
719
  inspector: inspInner,
458
720
  togglePane,
459
721
  setPaneOpen,
460
- isOpen: (s) => open[s],
722
+ isOpen: showing,
723
+ holdClosed,
461
724
  resetWidths,
462
725
  state: snapshot,
463
726
  destroy: () => {
@@ -471,7 +734,8 @@ var EDIT_MENU = {
471
734
  undo: { label: "Undo", accelerator: "CmdOrCtrl+Z" },
472
735
  redo: { label: "Redo", accelerator: "Shift+CmdOrCtrl+Z" },
473
736
  duplicate: { label: "Duplicate", accelerator: "CmdOrCtrl+D" },
474
- find: { label: "Find\u2026", accelerator: "CmdOrCtrl+F" }
737
+ find: { label: "Find\u2026", accelerator: "CmdOrCtrl+F" },
738
+ replace: { label: "Replace\u2026", acceleratorMac: "Cmd+Alt+F", acceleratorOther: "Ctrl+H" }
475
739
  };
476
740
  var GO_MENU = {
477
741
  back: { label: "Back", acceleratorMac: "Ctrl+Cmd+Left", acceleratorOther: "Alt+Left" },
@@ -482,10 +746,43 @@ var PANE_MENU = {
482
746
  showInspector: { label: "Show Inspector", accelerator: "CmdOrCtrl+2" },
483
747
  resetView: { label: "Reset View" }
484
748
  };
749
+ var FILE_MENU = {
750
+ newProject: { label: "New Project\u2026", accelerator: "CmdOrCtrl+N" },
751
+ openProject: { label: "Open Project\u2026", accelerator: "CmdOrCtrl+O" },
752
+ openRecent: { label: "Open Recent" },
753
+ noRecents: { label: "No Recent Projects" },
754
+ clearRecents: { label: "Clear Recents" },
755
+ closeProject: { label: "Close Project" },
756
+ save: { label: "Save", accelerator: "CmdOrCtrl+S" },
757
+ saveAs: { label: "Save As\u2026", accelerator: "Shift+CmdOrCtrl+S" },
758
+ projectSettings: { label: "Project Settings\u2026", accelerator: "CmdOrCtrl+," }
759
+ };
760
+ var PLAY_MENU = {
761
+ liveLink: { label: "Live Link" }
762
+ };
763
+ var REVIEW_MENU = {
764
+ reviewFeedback: { label: "Review Feedback", accelerator: "CmdOrCtrl+Shift+R" },
765
+ nextFeedback: { label: "Next Feedback", accelerator: "F8" },
766
+ previousFeedback: { label: "Previous Feedback", accelerator: "Shift+F8" },
767
+ coverageTest: { label: "Coverage Test\u2026", accelerator: "Shift+CmdOrCtrl+C" },
768
+ findPropertyUsage: { label: "Find Property Usage\u2026" },
769
+ showResolvedComments: { label: "Show Resolved Comments" }
770
+ };
771
+ var PUBLISH_MENU = {
772
+ playableHtml: { label: "Publish Playable HTML\u2026" },
773
+ bundle: { label: "Publish Bundle", accelerator: "Shift+CmdOrCtrl+B" },
774
+ autoRebuild: { label: "Auto Rebuild" }
775
+ };
776
+ var VIEW_MENU = {
777
+ projectOverview: { label: "Project Overview" },
778
+ upALevel: { label: "Up a Level", accelerator: "CmdOrCtrl+[" },
779
+ colourTheme: { label: "Colour Theme" }
780
+ };
485
781
 
486
782
  // src/settings.ts
487
783
  function expandableRow(opts) {
488
784
  const row = el("div", "set-row");
785
+ if (opts.name !== void 0) row.dataset.name = opts.name;
489
786
  const line = el("div", "set-rowline");
490
787
  if (opts.details && opts.details.length) {
491
788
  const details = el("div", "set-details");
@@ -511,6 +808,35 @@ function expandableRow(opts) {
511
808
  }
512
809
  return row;
513
810
  }
811
+ function revealRow(within, name) {
812
+ const row = [...within.querySelectorAll(".set-row")].find((r) => r.dataset.name === name);
813
+ if (!row) return false;
814
+ const toggle = row.querySelector(".set-rowline > .set-expand");
815
+ if (toggle && toggle.getAttribute("aria-expanded") !== "true") toggle.click();
816
+ const still = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
817
+ row.scrollIntoView({ block: "center", behavior: still ? "auto" : "smooth" });
818
+ row.classList.remove("landed");
819
+ void row.offsetWidth;
820
+ row.classList.add("landed");
821
+ row.addEventListener("animationend", () => row.classList.remove("landed"), { once: true });
822
+ return true;
823
+ }
824
+ function revealRowWhenReady(within, name, tries = 12) {
825
+ return new Promise((resolve) => {
826
+ const attempt = (left) => {
827
+ if (revealRow(within, name)) {
828
+ resolve(true);
829
+ return;
830
+ }
831
+ if (left <= 0) {
832
+ resolve(false);
833
+ return;
834
+ }
835
+ requestAnimationFrame(() => attempt(left - 1));
836
+ };
837
+ attempt(tries);
838
+ });
839
+ }
514
840
  function focusNewRow(listEl) {
515
841
  const last = listEl?.lastElementChild;
516
842
  if (!(last instanceof HTMLElement)) return;
@@ -518,6 +844,7 @@ function focusNewRow(listEl) {
518
844
  const name = last.querySelector("input.set-name") ?? last.querySelector("input");
519
845
  name?.focus();
520
846
  }
847
+ var DUP_MESSAGE = "Another row already has this name.";
521
848
  function dupGuard() {
522
849
  const items = [];
523
850
  const check = () => {
@@ -532,9 +859,9 @@ function dupGuard() {
532
859
  const dup = !!k && (counts.get(k) ?? 0) > 1;
533
860
  it.input.classList.toggle("invalid", dup);
534
861
  if (dup) {
535
- it.input.title = "Duplicate name. Names must be unique.";
862
+ it.input.dataset["tip"] = DUP_MESSAGE;
536
863
  any = true;
537
- } else if (it.input.title === "Duplicate name. Names must be unique.") it.input.removeAttribute("title");
864
+ } else if (it.input.dataset["tip"] === DUP_MESSAGE) delete it.input.dataset["tip"];
538
865
  }
539
866
  return any;
540
867
  };
@@ -580,7 +907,9 @@ function mountSettingsDialog(opts) {
580
907
  const tab = el("button", "settings-tab", s.label);
581
908
  tab.type = "button";
582
909
  tab.dataset["tab"] = s.id;
583
- tab.addEventListener("click", () => showTab(s.id));
910
+ tab.addEventListener("click", () => {
911
+ if (!tab.classList.contains("is-disabled")) showTab(s.id);
912
+ });
584
913
  tabs.append(tab);
585
914
  tabFor.set(s.id, tab);
586
915
  const panel = el("section", "settings-panel");
@@ -594,6 +923,25 @@ function mountSettingsDialog(opts) {
594
923
  for (const [sid, t] of tabFor) t.classList.toggle("active", sid === id);
595
924
  for (const [sid, p] of panelFor) p.hidden = sid !== id;
596
925
  }
926
+ const usable = (s) => !tabFor.get(s.id)?.classList.contains("is-disabled");
927
+ function refreshTabs() {
928
+ for (const s of opts.sections) {
929
+ const tab = tabFor.get(s.id);
930
+ const reason = s.disabled?.() ?? null;
931
+ tab.classList.toggle("is-disabled", reason !== null);
932
+ if (reason !== null) {
933
+ tab.dataset["tip"] = reason;
934
+ tab.setAttribute("aria-disabled", "true");
935
+ } else {
936
+ delete tab.dataset["tip"];
937
+ tab.removeAttribute("aria-disabled");
938
+ }
939
+ }
940
+ if (!opts.sections.some((s) => s.id === active2 && usable(s))) {
941
+ const first = opts.sections.find(usable);
942
+ if (first) showTab(first.id);
943
+ }
944
+ }
597
945
  form.append(
598
946
  el("h2", "settings-title", opts.title),
599
947
  el("div", "settings-body", tabs, panels),
@@ -609,14 +957,15 @@ function mountSettingsDialog(opts) {
609
957
  e.preventDefault();
610
958
  error.hidden = true;
611
959
  for (const s of opts.sections) {
612
- const bad = handles.get(s.id)?.firstInvalid?.();
613
- if (bad) {
614
- showTab(s.id);
615
- bad.focus();
616
- error.textContent = "Fix the highlighted fields first.";
617
- error.hidden = false;
618
- return;
619
- }
960
+ const found = handles.get(s.id)?.firstInvalid?.();
961
+ if (!found) continue;
962
+ const bad = found instanceof HTMLElement ? found : found.el;
963
+ const message = (found instanceof HTMLElement ? void 0 : found.message) ?? bad.dataset["tip"];
964
+ showTab(s.id);
965
+ bad.focus();
966
+ error.textContent = message || "Fix the highlighted fields first.";
967
+ error.hidden = false;
968
+ return;
620
969
  }
621
970
  void (async () => {
622
971
  await opts.onSave();
@@ -633,25 +982,94 @@ function mountSettingsDialog(opts) {
633
982
  }
634
983
  error.hidden = true;
635
984
  showTab(tabId ?? active2 ?? opts.sections[0]?.id ?? "");
985
+ refreshTabs();
636
986
  dialog.showModal();
637
987
  },
988
+ refreshTabs,
638
989
  destroy() {
639
990
  dialog.remove();
640
991
  }
641
992
  };
642
993
  }
643
994
 
995
+ // src/util.ts
996
+ function plural(n, noun, pluralForm = `${noun}s`) {
997
+ return `${n} ${n === 1 ? noun : pluralForm}`;
998
+ }
999
+ function debounce(fn, ms) {
1000
+ let timer3;
1001
+ let pending;
1002
+ const run = () => {
1003
+ timer3 = void 0;
1004
+ if (pending === void 0) return;
1005
+ const args = pending;
1006
+ pending = void 0;
1007
+ fn(...args);
1008
+ };
1009
+ const debounced = ((...args) => {
1010
+ pending = args;
1011
+ if (timer3 !== void 0) clearTimeout(timer3);
1012
+ timer3 = setTimeout(run, ms);
1013
+ });
1014
+ debounced.cancel = () => {
1015
+ if (timer3 !== void 0) clearTimeout(timer3);
1016
+ timer3 = void 0;
1017
+ pending = void 0;
1018
+ };
1019
+ debounced.flush = () => {
1020
+ if (timer3 !== void 0) clearTimeout(timer3);
1021
+ run();
1022
+ };
1023
+ return debounced;
1024
+ }
1025
+ function isEditableTarget(t) {
1026
+ if (!(t instanceof Element)) return false;
1027
+ if (t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement || t instanceof HTMLSelectElement) return true;
1028
+ if (t instanceof HTMLElement && t.isContentEditable === true) return true;
1029
+ const attr = t.getAttribute("contenteditable");
1030
+ return attr !== null && attr !== "false";
1031
+ }
1032
+ function formatCount(n) {
1033
+ return n.toLocaleString();
1034
+ }
1035
+ var copyTimers = /* @__PURE__ */ new WeakMap();
1036
+ function copyWithFeedback(el2, text, ms = 1e3) {
1037
+ const write = typeof navigator !== "undefined" && navigator.clipboard ? navigator.clipboard.writeText(text).catch(() => {
1038
+ }) : Promise.resolve();
1039
+ el2.classList.add("copied");
1040
+ const prior = copyTimers.get(el2);
1041
+ if (prior !== void 0) clearTimeout(prior);
1042
+ copyTimers.set(el2, setTimeout(() => {
1043
+ el2.classList.remove("copied");
1044
+ copyTimers.delete(el2);
1045
+ }, ms));
1046
+ return write;
1047
+ }
1048
+ function relativeTime(date, now = Date.now()) {
1049
+ const then = date instanceof Date ? date.getTime() : new Date(date).getTime();
1050
+ if (!Number.isFinite(then)) return "";
1051
+ const mins = Math.floor((now - then) / 6e4);
1052
+ if (mins < 1) return "just now";
1053
+ if (mins < 60) return `${plural(mins, "minute")} ago`;
1054
+ const hours = Math.floor(mins / 60);
1055
+ if (hours === 1) return "an hour ago";
1056
+ if (hours < 24) return `${hours} hours ago`;
1057
+ const days = Math.floor(hours / 24);
1058
+ if (days === 1) return "yesterday";
1059
+ if (days < 7) return `${days} days ago`;
1060
+ return new Date(then).toLocaleDateString(void 0, { day: "numeric", month: "short", year: "numeric" });
1061
+ }
1062
+
644
1063
  // src/tool-window-web.ts
645
- 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>`;
646
1064
  function pinButton(opts) {
647
1065
  const b = el("button", "swin-pin");
648
1066
  b.type = "button";
649
- b.innerHTML = PIN_ICON;
1067
+ b.append(iconNode("pin"));
650
1068
  let pinned = opts.pinned;
651
1069
  const reflect = () => {
652
1070
  b.classList.toggle("on", pinned);
653
1071
  b.setAttribute("aria-pressed", String(pinned));
654
- const label2 = pinned ? "Pinned on top: click to unpin" : "Click to keep on top";
1072
+ const label2 = pinned ? "Pinned on top. Click to unpin." : "Keep on top";
655
1073
  b.dataset["tip"] = label2;
656
1074
  b.setAttribute("aria-label", label2);
657
1075
  };
@@ -666,6 +1084,61 @@ function pinButton(opts) {
666
1084
  reflect();
667
1085
  } };
668
1086
  }
1087
+ var escSlot;
1088
+ var escWired = false;
1089
+ function wireEsc() {
1090
+ if (escWired) return;
1091
+ escWired = true;
1092
+ window.addEventListener("keydown", (e) => {
1093
+ if (e.key !== "Escape" || !escSlot) return;
1094
+ if (e.defaultPrevented) return;
1095
+ if (!escSlot.fromFields && isEditableTarget(e.target)) return;
1096
+ if (escSlot.onEscape?.() === true) return;
1097
+ escSlot.onClose();
1098
+ });
1099
+ }
1100
+ function toolWindowHead(opts) {
1101
+ const keep = (parts) => (parts ?? []).filter((p) => p !== null);
1102
+ const pin = opts.pin ?? pinButton({ pinned: opts.pinned ?? true, onToggle: opts.onPin ?? (() => {
1103
+ }) });
1104
+ if (opts.esc !== false) {
1105
+ escSlot = { onClose: opts.onClose, onEscape: opts.onEscape, fromFields: opts.esc === "always" };
1106
+ wireEsc();
1107
+ }
1108
+ return el(
1109
+ "header",
1110
+ { className: `swin-head${opts.className ? ` ${opts.className}` : ""}` },
1111
+ ...opts.title !== void 0 ? [el("span", { className: "swin-title", text: opts.title })] : [],
1112
+ ...opts.tabs ? [opts.tabs] : [],
1113
+ ...keep(opts.lead),
1114
+ el("span", { className: "swin-spacer" }),
1115
+ ...keep(opts.trail),
1116
+ pin.el,
1117
+ el("button", { className: "swin-close", tip: "Close (Esc)", onClick: opts.onClose }, iconNode("close"))
1118
+ );
1119
+ }
1120
+ function followButton(opts) {
1121
+ const what = opts.what ?? "line";
1122
+ const b = el("button", "btn swin-follow", "Follow in the editor");
1123
+ b.type = "button";
1124
+ let on = opts.on;
1125
+ const reflect = () => {
1126
+ b.classList.toggle("on", on);
1127
+ b.setAttribute("aria-pressed", String(on));
1128
+ const label2 = on ? `The editor opens each ${what} as it plays. Click to stop.` : `Open each ${what} in the editor as it plays`;
1129
+ b.dataset["tip"] = label2;
1130
+ };
1131
+ reflect();
1132
+ b.addEventListener("click", () => {
1133
+ on = !on;
1134
+ reflect();
1135
+ opts.onToggle(on);
1136
+ });
1137
+ return { el: b, set: (next) => {
1138
+ on = next;
1139
+ reflect();
1140
+ } };
1141
+ }
669
1142
 
670
1143
  // src/exit.ts
671
1144
  function closeWithExit(element, done) {
@@ -690,61 +1163,232 @@ function closeWithExit(element, done) {
690
1163
  const timer3 = setTimeout(finish, seconds * 1e3 + 120);
691
1164
  }
692
1165
 
1166
+ // src/dialog.ts
1167
+ function dialogFrame(opts) {
1168
+ const dialog = el("dialog", `shell-dialog ${opts.className ?? ""}`.trim());
1169
+ const body = el("div", "shell-dialog-body");
1170
+ const actions = el("div", "shell-dialog-actions");
1171
+ dialog.append(el("h2", "shell-dialog-title", opts.title));
1172
+ if (opts.sub !== void 0) dialog.append(el("p", "shell-dialog-sub", opts.sub));
1173
+ dialog.append(body, actions);
1174
+ dialog.setAttribute("aria-label", opts.title);
1175
+ document.body.append(dialog);
1176
+ let state = "closed";
1177
+ let opener = null;
1178
+ const teardown = () => {
1179
+ dialog.remove();
1180
+ if (opener instanceof HTMLElement && opener.isConnected) opener.focus();
1181
+ opener = null;
1182
+ state = "closed";
1183
+ opts.onClose?.();
1184
+ };
1185
+ const close = () => {
1186
+ if (state !== "open") return;
1187
+ state = "closing";
1188
+ closeWithExit(dialog, () => {
1189
+ if (dialog.open) dialog.close();
1190
+ teardown();
1191
+ });
1192
+ };
1193
+ dialog.addEventListener("cancel", (e) => {
1194
+ e.preventDefault();
1195
+ close();
1196
+ });
1197
+ dialog.addEventListener("close", () => {
1198
+ if (state === "open") teardown();
1199
+ });
1200
+ return {
1201
+ dialog,
1202
+ body,
1203
+ actions,
1204
+ open() {
1205
+ if (state !== "closed") return;
1206
+ opener = document.activeElement;
1207
+ state = "open";
1208
+ dialog.showModal();
1209
+ },
1210
+ close,
1211
+ isOpen: () => state === "open"
1212
+ };
1213
+ }
1214
+
693
1215
  // src/confirm.ts
694
1216
  function confirmDialog(opts) {
695
1217
  return new Promise((resolve) => {
696
- const cancel = el("button", "confirm-btn cancel", "Cancel");
697
- const danger = el("button", "confirm-btn danger", opts.confirmLabel);
698
- cancel.type = "button";
699
- danger.type = "button";
700
- const dlg = el(
701
- "dialog",
702
- "confirm-dialog",
703
- el("div", "confirm-title", opts.title),
704
- el("div", "confirm-body", opts.body),
705
- el("div", "confirm-actions", cancel, danger)
706
- );
707
- dlg.setAttribute("aria-label", opts.title);
708
- document.body.append(dlg);
709
1218
  let done = false;
710
1219
  const finish = (ok) => {
711
1220
  if (done) return;
712
1221
  done = true;
713
1222
  resolve(ok);
714
- closeWithExit(dlg, () => {
715
- dlg.close();
716
- dlg.remove();
717
- });
1223
+ frame.close();
718
1224
  };
1225
+ const frame = dialogFrame({
1226
+ title: opts.title,
1227
+ className: "confirm-dialog",
1228
+ onClose: () => finish(false)
1229
+ // Esc, or closed by someone else
1230
+ });
1231
+ const cancel = el("button", "btn confirm-btn cancel", "Cancel");
1232
+ const danger = el("button", "btn danger confirm-btn", opts.confirmLabel);
1233
+ cancel.type = "button";
1234
+ danger.type = "button";
1235
+ if (opts.body !== void 0) frame.body.append(el("div", "confirm-body", opts.body));
1236
+ if (opts.bodyNode) frame.body.append(opts.bodyNode);
1237
+ frame.actions.append(cancel, danger);
719
1238
  cancel.addEventListener("click", () => finish(false));
720
1239
  danger.addEventListener("click", () => finish(true));
721
- dlg.addEventListener("cancel", (e) => {
722
- e.preventDefault();
723
- finish(false);
1240
+ frame.dialog.addEventListener("mousedown", (e) => {
1241
+ if (e.target === frame.dialog) finish(false);
724
1242
  });
725
- dlg.addEventListener("mousedown", (e) => {
726
- if (e.target === dlg) finish(false);
727
- });
728
- dlg.showModal();
1243
+ frame.open();
729
1244
  cancel.focus();
730
1245
  });
731
1246
  }
732
1247
 
1248
+ // src/floating.ts
1249
+ function followOnScroll(reposition) {
1250
+ const onMove = () => reposition();
1251
+ window.addEventListener("scroll", onMove, true);
1252
+ window.addEventListener("resize", onMove);
1253
+ return () => {
1254
+ window.removeEventListener("scroll", onMove, true);
1255
+ window.removeEventListener("resize", onMove);
1256
+ };
1257
+ }
1258
+ function createFloating(className, opts = {}) {
1259
+ const el2 = document.createElement("div");
1260
+ el2.className = className;
1261
+ el2.style.display = "none";
1262
+ (opts.host ?? document.body).appendChild(el2);
1263
+ let open = false;
1264
+ let detach = null;
1265
+ let outside = null;
1266
+ let closeToken = 0;
1267
+ const close = () => {
1268
+ if (!open) return;
1269
+ open = false;
1270
+ detach?.();
1271
+ detach = null;
1272
+ if (outside) {
1273
+ document.removeEventListener("mousedown", outside, true);
1274
+ outside = null;
1275
+ }
1276
+ const myToken = ++closeToken;
1277
+ closeWithExit(el2, () => {
1278
+ if (myToken !== closeToken) return;
1279
+ el2.style.display = "none";
1280
+ el2.replaceChildren();
1281
+ el2.classList.remove("closing");
1282
+ });
1283
+ };
1284
+ const show2 = (reposition) => {
1285
+ closeToken++;
1286
+ el2.classList.remove("closing");
1287
+ open = true;
1288
+ el2.style.display = "block";
1289
+ reposition();
1290
+ detach ??= followOnScroll(reposition);
1291
+ };
1292
+ const dismissOnOutside = (onOutside, isInside) => {
1293
+ if (outside) return;
1294
+ outside = (e) => {
1295
+ const t = e.target;
1296
+ if (!el2.contains(t) && !(isInside?.(t) ?? false)) onOutside();
1297
+ };
1298
+ document.addEventListener("mousedown", outside, true);
1299
+ };
1300
+ return { el: el2, show: show2, dismissOnOutside, close, isOpen: () => open };
1301
+ }
1302
+
1303
+ // src/context-menu.ts
1304
+ var EDGE2 = 6;
1305
+ var current = null;
1306
+ function floating(node2, onDismiss) {
1307
+ let gone = false;
1308
+ const dismiss = () => {
1309
+ if (gone) return;
1310
+ gone = true;
1311
+ if (current === dismiss) current = null;
1312
+ window.removeEventListener("pointerdown", onAway, true);
1313
+ window.removeEventListener("keydown", onKey, true);
1314
+ closeWithExit(node2, () => node2.remove());
1315
+ onDismiss?.();
1316
+ };
1317
+ const onAway = (e) => {
1318
+ if (!node2.contains(e.target)) dismiss();
1319
+ };
1320
+ const onKey = (e) => {
1321
+ if (e.key === "Escape") {
1322
+ e.preventDefault();
1323
+ dismiss();
1324
+ }
1325
+ };
1326
+ window.addEventListener("pointerdown", onAway, true);
1327
+ window.addEventListener("keydown", onKey, true);
1328
+ current?.();
1329
+ current = dismiss;
1330
+ return { dismiss };
1331
+ }
1332
+ function placeBelow(node2, anchor) {
1333
+ node2.style.left = `${anchor.left}px`;
1334
+ node2.style.top = `${anchor.bottom + 4}px`;
1335
+ const r = node2.getBoundingClientRect();
1336
+ if (r.right > window.innerWidth) node2.style.left = `${Math.max(EDGE2, window.innerWidth - r.width - EDGE2)}px`;
1337
+ if (r.bottom > window.innerHeight) node2.style.top = `${Math.max(EDGE2, anchor.top - r.height - 4)}px`;
1338
+ }
1339
+ function openPopover(anchor, build, onClose, opts = {}) {
1340
+ const pop = el("div", "popover");
1341
+ let closed = false;
1342
+ const finish = () => {
1343
+ if (closed) return;
1344
+ closed = true;
1345
+ onClose?.();
1346
+ };
1347
+ const { dismiss } = floating(pop, finish);
1348
+ pop.append(build(dismiss));
1349
+ (opts.host ?? document.body).append(pop);
1350
+ placeBelow(pop, anchor.getBoundingClientRect());
1351
+ }
1352
+ function openContextMenu(x, y, items) {
1353
+ const menu = el("div", "ctxmenu");
1354
+ menu.setAttribute("role", "menu");
1355
+ const { dismiss } = floating(menu);
1356
+ for (const item of items) {
1357
+ const button = el("button", {
1358
+ className: `ctxmenu-item${item.danger ? " danger" : ""}`,
1359
+ text: item.label,
1360
+ onClick: () => {
1361
+ if (item.disabled === true) return;
1362
+ dismiss();
1363
+ item.onClick();
1364
+ }
1365
+ });
1366
+ button.type = "button";
1367
+ button.setAttribute("role", "menuitem");
1368
+ if (item.disabled === true) button.disabled = true;
1369
+ menu.append(button);
1370
+ }
1371
+ menu.style.left = `${x}px`;
1372
+ menu.style.top = `${y}px`;
1373
+ document.body.append(menu);
1374
+ const r = menu.getBoundingClientRect();
1375
+ if (r.right > window.innerWidth) menu.style.left = `${Math.max(EDGE2, window.innerWidth - r.width - EDGE2)}px`;
1376
+ if (r.bottom > window.innerHeight) menu.style.top = `${Math.max(EDGE2, window.innerHeight - r.height - EDGE2)}px`;
1377
+ }
1378
+
733
1379
  // src/about.ts
734
1380
  function showAbout(opts) {
735
1381
  return new Promise((resolve) => {
736
- const dialog = el("dialog", "shell-about");
1382
+ const frame = dialogFrame({ title: opts.appName, className: "shell-about", onClose: () => resolve() });
1383
+ frame.dialog.setAttribute("aria-label", `About ${opts.appName}`);
737
1384
  if (opts.wordmark !== void 0) {
738
1385
  const mark = el("div", "shell-about-mark");
739
1386
  mark.innerHTML = opts.wordmark;
740
- dialog.append(mark);
1387
+ frame.dialog.prepend(mark);
741
1388
  }
742
- dialog.append(
743
- el("div", "shell-about-name", opts.appName),
744
- el("div", "shell-about-version", `Version ${opts.version}`)
745
- );
746
- if (opts.blurb !== void 0) dialog.append(el("p", "shell-about-blurb", opts.blurb));
747
- if (opts.credits !== void 0) dialog.append(el("p", "shell-about-credits", opts.credits));
1389
+ frame.body.append(el("div", "shell-about-version", `Version ${opts.version}`));
1390
+ if (opts.blurb !== void 0) frame.body.append(el("p", "shell-about-blurb", opts.blurb));
1391
+ if (opts.credits !== void 0) frame.body.append(el("p", "shell-about-credits", opts.credits));
748
1392
  if (opts.links?.length) {
749
1393
  const row = el("div", "shell-about-links");
750
1394
  for (const link of opts.links) {
@@ -757,39 +1401,28 @@ function showAbout(opts) {
757
1401
  a.addEventListener("click", () => opts.onOpenLink?.(link.url));
758
1402
  row.append(a);
759
1403
  }
760
- dialog.append(row);
1404
+ frame.body.append(row);
761
1405
  }
762
- const close = el("button", "shell-about-close", "Close");
1406
+ const close = el("button", "btn shell-about-close", "Close");
763
1407
  close.type = "button";
764
- dialog.append(el("div", "shell-about-actions", close));
765
- dialog.setAttribute("aria-label", `About ${opts.appName}`);
766
- document.body.append(dialog);
767
- let done = false;
768
- const finish = () => {
769
- if (done) return;
770
- done = true;
771
- dialog.close();
772
- dialog.remove();
773
- resolve();
774
- };
775
- close.addEventListener("click", finish);
776
- dialog.addEventListener("close", finish);
777
- dialog.showModal();
1408
+ close.addEventListener("click", () => frame.close());
1409
+ frame.actions.append(close);
1410
+ frame.open();
778
1411
  close.focus();
779
1412
  });
780
1413
  }
781
1414
 
782
1415
  // src/anchored.ts
783
- var current = null;
1416
+ var current2 = null;
784
1417
  function openAnchoredPanel(opts) {
785
- if (current) {
786
- const prev = current;
787
- current = null;
1418
+ if (current2) {
1419
+ const prev = current2;
1420
+ current2 = null;
788
1421
  prev.close();
789
1422
  if (prev.anchor === opts.anchor) return null;
790
1423
  }
791
1424
  const panel = el("div", `shell-anchored ${opts.className ?? ""}`.trim());
792
- const closeBtn = el("button", "shell-anchored-close", "\u2715");
1425
+ const closeBtn = el("button", "shell-anchored-close", iconNode("close", 12));
793
1426
  closeBtn.type = "button";
794
1427
  closeBtn.setAttribute("aria-label", "Close");
795
1428
  const body = el("div", "shell-anchored-body");
@@ -797,13 +1430,13 @@ function openAnchoredPanel(opts) {
797
1430
  el("div", "shell-anchored-head", el("span", "shell-anchored-title", opts.title), closeBtn),
798
1431
  body
799
1432
  );
800
- document.body.append(panel);
1433
+ (opts.host ?? document.body).append(panel);
801
1434
  placeAnchored(panel, opts.anchor, opts.width, opts.prefer ?? "below", opts.keepClear ?? []);
802
1435
  let closed = false;
803
1436
  const close = () => {
804
1437
  if (closed) return;
805
1438
  closed = true;
806
- if (current?.panel === panel) current = null;
1439
+ if (current2?.panel === panel) current2 = null;
807
1440
  document.removeEventListener("pointerdown", onDown, true);
808
1441
  document.removeEventListener("keydown", onKey, true);
809
1442
  closeWithExit(panel, () => {
@@ -827,12 +1460,12 @@ function openAnchoredPanel(opts) {
827
1460
  document.addEventListener("pointerdown", onDown, true);
828
1461
  document.addEventListener("keydown", onKey, true);
829
1462
  }, 0);
830
- current = { anchor: opts.anchor, panel, close };
1463
+ current2 = { anchor: opts.anchor, panel, close };
831
1464
  return { panel, body, close };
832
1465
  }
833
1466
  function closeAnchoredPanel() {
834
- const open = current;
835
- current = null;
1467
+ const open = current2;
1468
+ current2 = null;
836
1469
  open?.close();
837
1470
  }
838
1471
  function placeAnchored(panel, anchor, width, prefer, keepClear) {
@@ -889,7 +1522,7 @@ function isValidGameId(gameId) {
889
1522
 
890
1523
  // src/property-names.ts
891
1524
  var RESERVED_PROPERTY_NAMES = ["true", "false", "and", "or", "not"];
892
- var PROPERTY_NAME_HINT = "Lower case letters, digits and underscores. Cannot start with a digit. No hyphens or spaces.";
1525
+ var PROPERTY_NAME_HINT = "Lower case letters, digits, and underscores. It can't start with a digit. No hyphens or spaces.";
893
1526
  function propertyNameify(text) {
894
1527
  const trimmed = text.trim();
895
1528
  const deliberateLeading = trimmed.startsWith("_");
@@ -913,14 +1546,14 @@ function propertyNameProblem(name) {
913
1546
  const suggestion = propertyNameify(name);
914
1547
  const tail = suggestion ? ` Try "${suggestion}", or press Tab.` : "";
915
1548
  if (RESERVED_PROPERTY_NAMES.includes(name.toLowerCase())) {
916
- return `Cannot be used: "${name.toLowerCase()}" is a keyword in expressions.${tail}`;
1549
+ return `Can't be used. "${name.toLowerCase()}" is a keyword in expressions.${tail}`;
917
1550
  }
918
- if (name.includes("-")) return `Cannot be used: a hyphen reads as subtraction in an expression.${tail}`;
919
- if (/^[0-9]/.test(name)) return `Cannot be used: a name cannot start with a digit.${tail}`;
1551
+ if (name.includes("-")) return `Can't be used. A hyphen reads as subtraction in an expression.${tail}`;
1552
+ if (/^[0-9]/.test(name)) return `Can't be used. A name can't start with a digit.${tail}`;
920
1553
  if (name !== name.toLowerCase()) {
921
- return `Cannot be used: expressions fold names, so this would look for "${name.toLowerCase()}".${tail}`;
1554
+ return `Can't be used. Expressions fold names, so this would look for "${name.toLowerCase()}".${tail}`;
922
1555
  }
923
- return `Cannot be used: only lower case letters, digits and underscores.${tail}`;
1556
+ return `Can't be used. Only lower case letters, digits, and underscores.${tail}`;
924
1557
  }
925
1558
  function bindPropertyName(input, onCommit, opts = {}) {
926
1559
  const hint = opts.hint;
@@ -929,7 +1562,7 @@ function bindPropertyName(input, onCommit, opts = {}) {
929
1562
  const problem = v === "" ? void 0 : propertyNameProblem(v);
930
1563
  input.classList.toggle("illegal", !!problem);
931
1564
  if (problem) input.title = problem;
932
- else if (input.title.startsWith("Cannot be used")) {
1565
+ else if (input.title.startsWith("Can't be used")) {
933
1566
  if (hint) input.title = hint;
934
1567
  else input.removeAttribute("title");
935
1568
  }
@@ -985,10 +1618,10 @@ function propertyRefProblem(name, opts) {
985
1618
  if (!isValidPropertyName(folded)) {
986
1619
  const suggestion = propertyNameify(name);
987
1620
  const tail = suggestion ? ` Try "${suggestion}", or press Tab.` : "";
988
- if (RESERVED_PROPERTY_NAMES.includes(folded)) return `Cannot be used: "${folded}" is a keyword in expressions.${tail}`;
989
- if (name.includes("-")) return `Cannot be used: a hyphen reads as subtraction in an expression.${tail}`;
990
- if (/^[0-9]/.test(name)) return `Cannot be used: a name cannot start with a digit.${tail}`;
991
- return `Cannot be used: only lower case letters, digits and underscores.${tail}`;
1621
+ if (RESERVED_PROPERTY_NAMES.includes(folded)) return `Can't be used. "${folded}" is a keyword in expressions.${tail}`;
1622
+ if (name.includes("-")) return `Can't be used. A hyphen reads as subtraction in an expression.${tail}`;
1623
+ if (/^[0-9]/.test(name)) return `Can't be used. A name can't start with a digit.${tail}`;
1624
+ return `Can't be used. Only lower case letters, digits, and underscores.${tail}`;
992
1625
  }
993
1626
  const known = opts.known();
994
1627
  if (known.some((k) => k.toLowerCase() === folded)) return void 0;
@@ -997,7 +1630,7 @@ function propertyRefProblem(name, opts) {
997
1630
  if (known.length === 0) return `No ${scope} properties are declared yet. Declare one above first.`;
998
1631
  const shown = known.slice(0, 3).map((k) => `"${k}"`).join(", ");
999
1632
  const more = known.length > 3 ? `, and ${known.length - 3} more` : "";
1000
- return `No ${scope} property is called "${name}". Declared: ${shown}${more}.`;
1633
+ return `No ${scope} property is called "${name}". Pick one of ${shown}${more}.`;
1001
1634
  }
1002
1635
  function bindPropertyRef(input, onCommit, opts) {
1003
1636
  const list = document.createElement("datalist");
@@ -1018,7 +1651,7 @@ function bindPropertyRef(input, onCommit, opts) {
1018
1651
  const problem = v === "" ? void 0 : propertyRefProblem(v, opts);
1019
1652
  input.classList.toggle("illegal", !!problem);
1020
1653
  if (problem) input.title = problem;
1021
- else if (input.title.startsWith("Cannot be used") || input.title.startsWith("No ")) {
1654
+ else if (input.title.startsWith("Can't be used") || input.title.startsWith("No ")) {
1022
1655
  if (opts.hint) input.title = opts.hint;
1023
1656
  else input.removeAttribute("title");
1024
1657
  }
@@ -1061,9 +1694,9 @@ function openGameIdEditor(opts) {
1061
1694
  const input = el("input", "shell-id-input");
1062
1695
  input.type = "text";
1063
1696
  input.value = opts.value;
1064
- input.placeholder = opts.derived || "<game-id>";
1697
+ input.placeholder = opts.derived || "game-id";
1065
1698
  input.spellcheck = false;
1066
- const hint = el("div", "shell-id-hint", "Lower case, digits, hyphens. Empty = auto from the name.");
1699
+ const hint = el("div", "shell-id-hint", "Lower case letters, digits, and hyphens. Leave it empty to derive one from the name.");
1067
1700
  const actions = el("div", "shell-id-actions");
1068
1701
  const reset = el("button", "shell-id-reset", "Reset to auto");
1069
1702
  const set = el("button", "shell-id-set", "Set");
@@ -1125,12 +1758,13 @@ function foldVc(shards, keys) {
1125
1758
  return out;
1126
1759
  }
1127
1760
  function vcBadgeFor(s) {
1128
- if (s?.lockedBy?.length) return { glyph: icon.locked, cls: "vc-locked", title: `Locked by ${s.lockedBy.join(", ")}` };
1129
- if (s?.outOfDate) return { glyph: icon.down, cls: "vc-stale", title: "Out of date - a newer version is on the server" };
1130
- if (s?.checkedOutByMe) return { glyph: icon.checkedOut, cls: "vc-mine", title: "Checked out by you" };
1131
- if (s?.dirty) return { glyph: icon.modified, cls: "vc-dirty", title: "Modified - uncommitted local changes" };
1132
- if (s?.untracked) return { glyph: icon.untracked, cls: "vc-new", title: "New - not yet committed" };
1133
- if (s && !s.writable) return { glyph: icon.readOnly, cls: "vc-frozen", title: "Read-only on disk - saving checks it out" };
1761
+ const badge = (name, cls, title) => ({ name, glyph: icon[name] ?? "", cls, title });
1762
+ if (s?.lockedBy?.length) return badge("locked", "vc-locked", `Locked by ${s.lockedBy.join(", ")}`);
1763
+ if (s?.outOfDate) return badge("down", "vc-stale", "Out of date. A newer version is on the server.");
1764
+ if (s?.checkedOutByMe) return badge("checkedOut", "vc-mine", "Checked out by you");
1765
+ if (s?.dirty) return badge("modified", "vc-dirty", "Modified. Local changes aren't committed yet.");
1766
+ if (s?.untracked) return badge("untracked", "vc-new", "New. Not in version control yet.");
1767
+ if (s && !s.writable) return badge("readOnly", "vc-frozen", "Read-only on disk. Saving checks it out.");
1134
1768
  return null;
1135
1769
  }
1136
1770
  function paintVcBadges(root, shards) {
@@ -1138,7 +1772,7 @@ function paintVcBadges(root, shards) {
1138
1772
  host.querySelector(":scope > .vc-badge")?.remove();
1139
1773
  const badge = vcBadgeFor(foldVc(shards, host.dataset["vc"]));
1140
1774
  if (!badge) return;
1141
- const span = el("span", { className: `vc-badge ${badge.cls}`, text: badge.glyph, tip: badge.title });
1775
+ const span = el("span", { className: `vc-badge ${badge.cls}`, tip: badge.title }, iconNode(badge.name, 12));
1142
1776
  host.append(span);
1143
1777
  });
1144
1778
  }
@@ -1161,8 +1795,8 @@ function lockNotice(holders) {
1161
1795
  return el(
1162
1796
  "div",
1163
1797
  { className: "vc-lock" },
1164
- el("span", { className: "vc-lock-glyph", text: icon.locked }),
1165
- el("span", { text: `Locked by ${holders.join(", ")} - read-only until they release it.` })
1798
+ el("span", { className: "vc-lock-glyph" }, iconNode("locked")),
1799
+ el("span", { text: `Locked by ${holders.join(", ")}. Read-only until they release it.` })
1166
1800
  );
1167
1801
  }
1168
1802
 
@@ -1170,41 +1804,36 @@ function lockNotice(holders) {
1170
1804
  function askIdentity(opts = {}) {
1171
1805
  return new Promise((resolve) => {
1172
1806
  const seed = opts.current ?? opts.suggested;
1173
- const name = el("input", "shell-ident-input");
1807
+ const name = el("input", "field shell-ident-input");
1174
1808
  name.placeholder = "Your name";
1175
1809
  name.value = seed?.name ?? "";
1176
- const email = el("input", "shell-ident-input");
1177
- email.placeholder = "Email (optional)";
1810
+ const email = el("input", "field shell-ident-input");
1811
+ email.placeholder = "Optional";
1178
1812
  email.type = "email";
1179
1813
  email.value = seed?.email ?? "";
1180
- const skip = el("button", "shell-ident-btn", "Skip");
1181
- const save = el("button", "shell-ident-btn primary", "Save");
1814
+ const skip = el("button", "btn shell-ident-btn", "Skip");
1815
+ const save = el("button", "btn primary shell-ident-btn", opts.mode === "welcome" ? "Continue" : "Save");
1182
1816
  skip.type = "button";
1183
1817
  save.type = "button";
1184
- const dialog = el(
1185
- "dialog",
1186
- "shell-ident",
1187
- el("div", "shell-ident-title", "Who is working here?"),
1188
- el(
1189
- "p",
1190
- "shell-ident-hint",
1191
- "Stamped on the comments you write. Kept in this app, never in the project, and changeable later."
1192
- ),
1193
- el("div", "shell-ident-fields", name, email),
1194
- el("div", "shell-ident-actions", skip, save)
1195
- );
1196
- document.body.append(dialog);
1197
1818
  let done = false;
1198
1819
  const finish = (keep) => {
1199
1820
  if (done) return;
1200
1821
  done = true;
1201
1822
  const typed = name.value.trim();
1202
1823
  const value = email.value.trim();
1203
- const answer = keep && typed !== "" ? value === "" ? { name: typed } : { name: typed, email: value } : void 0;
1204
- dialog.close();
1205
- dialog.remove();
1824
+ const answer = keep && typed !== "" ? value === "" ? { name: typed } : { name: typed, email: value } : opts.neverAskAgainOnSkip === true ? { name: "" } : void 0;
1206
1825
  resolve(answer);
1826
+ frame.close();
1207
1827
  };
1828
+ const frame = dialogFrame({
1829
+ title: titleFor(opts),
1830
+ sub: subFor(opts),
1831
+ className: "shell-ident",
1832
+ onClose: () => finish(false)
1833
+ // Escape, or closed by someone else: a skip
1834
+ });
1835
+ frame.body.append(el("div", "shell-ident-fields", labelled("Name", name), labelled("Email", email)));
1836
+ frame.actions.append(skip, save);
1208
1837
  skip.addEventListener("click", () => finish(false));
1209
1838
  save.addEventListener("click", () => finish(true));
1210
1839
  name.addEventListener("keydown", (e) => {
@@ -1213,11 +1842,19 @@ function askIdentity(opts = {}) {
1213
1842
  email.addEventListener("keydown", (e) => {
1214
1843
  if (e.key === "Enter") finish(true);
1215
1844
  });
1216
- dialog.addEventListener("close", () => finish(false));
1217
- dialog.showModal();
1845
+ frame.open();
1218
1846
  name.focus();
1219
1847
  });
1220
1848
  }
1849
+ function titleFor(opts) {
1850
+ if (opts.mode === "welcome") return opts.appName ? `Welcome to ${opts.appName}` : "Welcome";
1851
+ if (opts.mode === "edit") return "User information";
1852
+ return "Who is working here?";
1853
+ }
1854
+ function subFor(opts) {
1855
+ const why = "Your name goes on the comments you write. It's kept in this app, not in the project.";
1856
+ return opts.mode === "edit" ? why : `${why} You can change it later.`;
1857
+ }
1221
1858
 
1222
1859
  // src/notes-editor.ts
1223
1860
  var EDITOR_ONLY = "";
@@ -1230,10 +1867,10 @@ var CLASS_LABEL = {
1230
1867
  };
1231
1868
  var label = (cls) => cls === EDITOR_ONLY ? "Note (editor-only)" : CLASS_LABEL[cls] ?? cls[0].toUpperCase() + cls.slice(1);
1232
1869
  var PLACEHOLDER = {
1233
- vo: "direction for the voice actor / director",
1234
- loc: "context for translators"
1870
+ vo: "Direction for the voice actor or director",
1871
+ loc: "Context for translators"
1235
1872
  };
1236
- var placeholder = (cls) => cls === EDITOR_ONLY ? "an internal note - never exported" : PLACEHOLDER[cls] ?? "intent / rationale - why this is here";
1873
+ var placeholder = (cls) => cls === EDITOR_ONLY ? "An internal note, never exported" : PLACEHOLDER[cls] ?? "Why this is here";
1237
1874
  function openNotesEditor(opts) {
1238
1875
  const { notes } = opts;
1239
1876
  const seed = /* @__PURE__ */ new Map();
@@ -1287,7 +1924,7 @@ ${line.text}` : line.text);
1287
1924
  { className: "shell-notes-class" },
1288
1925
  el("label", "shell-notes-caption", label(cls))
1289
1926
  );
1290
- const area = el("textarea", "shell-notes-text");
1927
+ const area = el("textarea", "field shell-notes-text");
1291
1928
  area.value = areas.get(cls)?.value ?? seed.get(cls) ?? "";
1292
1929
  area.rows = 4;
1293
1930
  area.placeholder = placeholder(cls);
@@ -1313,29 +1950,24 @@ ${line.text}` : line.text);
1313
1950
  }
1314
1951
  };
1315
1952
  render();
1316
- const done = el("button", "shell-notes-btn primary", "Done");
1317
- const dialog = el(
1318
- "dialog",
1319
- { className: "shell-notes" },
1320
- el("div", { className: "shell-notes-title", text: `Notes: ${opts.subject}` }),
1321
- body,
1322
- el("div", { className: "shell-notes-actions" }, done)
1323
- );
1324
- dialog.setAttribute("aria-label", `Notes: ${opts.subject}`);
1325
- document.body.append(dialog);
1953
+ const done = el("button", "btn primary shell-notes-btn", "Done");
1954
+ done.type = "button";
1326
1955
  const before = JSON.stringify(notes.own);
1327
1956
  let closed = false;
1328
- const close = () => {
1957
+ const commit = () => {
1329
1958
  if (closed) return;
1330
1959
  closed = true;
1331
1960
  const next = value();
1332
1961
  if (JSON.stringify(next) !== before) opts.save(next);
1333
- dialog.close();
1334
- dialog.remove();
1335
1962
  };
1336
- done.addEventListener("click", close);
1337
- dialog.addEventListener("close", close);
1338
- dialog.showModal();
1963
+ const frame = dialogFrame({ title: `Notes: ${opts.subject}`, className: "shell-notes", onClose: commit });
1964
+ frame.body.append(body);
1965
+ frame.actions.append(done);
1966
+ done.addEventListener("click", () => {
1967
+ commit();
1968
+ frame.close();
1969
+ });
1970
+ frame.open();
1339
1971
  areas.get(shown[0] ?? EDITOR_ONLY)?.focus();
1340
1972
  }
1341
1973
 
@@ -1407,7 +2039,7 @@ function openComments(opts) {
1407
2039
  el("span", { className: "shell-cmt-ts", text: stamp(message.ts) })
1408
2040
  );
1409
2041
  if (opts.deleteMessage && message.deleted !== true) {
1410
- const bin = el("button", { className: "shell-cmt-del", text: "\u2715" });
2042
+ const bin = el("button", { className: "shell-cmt-del" }, iconNode("close", 12));
1411
2043
  bin.dataset.tip = "Delete this comment";
1412
2044
  bin.setAttribute("aria-label", "Delete this comment");
1413
2045
  bin.addEventListener("click", () => opts.deleteMessage?.(thread.id, index));
@@ -1472,37 +2104,65 @@ var clock = (ms) => {
1472
2104
  var estimable = (done, total, elapsedMs) => done > 0 && total > 0 && elapsedMs > 1e3 && done / total > 0.1;
1473
2105
  function mountJobProgress(host, opts) {
1474
2106
  const units = opts.units ?? "runs";
2107
+ const pinned = opts.indeterminate === true;
2108
+ const cancellableByDefault = opts.cancellable ?? opts.onCancel !== void 0;
1475
2109
  const fill = el("i", { className: "job-fill" });
1476
2110
  const bar = el("div", { className: "job-bar" }, fill);
2111
+ bar.setAttribute("role", "progressbar");
2112
+ bar.setAttribute("aria-valuemin", "0");
2113
+ bar.setAttribute("aria-valuemax", "100");
1477
2114
  const what = el("span", { className: "job-label" });
1478
2115
  const count = el("span", { className: "job-count" });
1479
2116
  const time = el("span", { className: "job-time" });
1480
2117
  const cancel = el("button", { className: "job-cancel", text: "Cancel" });
2118
+ cancel.type = "button";
1481
2119
  const strip = el("div", { className: "job-strip" }, what, bar, count, time, cancel);
1482
2120
  strip.hidden = true;
1483
2121
  host.append(strip);
2122
+ let runPinned = pinned;
2123
+ let indeterminate = false;
2124
+ const setMode = (band) => {
2125
+ indeterminate = band;
2126
+ strip.classList.toggle("indeterminate", band);
2127
+ count.hidden = band;
2128
+ if (band) {
2129
+ fill.style.width = "";
2130
+ bar.removeAttribute("aria-valuenow");
2131
+ }
2132
+ };
1484
2133
  const view = {
1485
2134
  element: strip,
1486
2135
  get visible() {
1487
2136
  return !strip.hidden;
1488
2137
  },
1489
- begin(label2) {
2138
+ get indeterminate() {
2139
+ return indeterminate;
2140
+ },
2141
+ begin(label2, run) {
2142
+ runPinned = run?.indeterminate ?? pinned;
2143
+ const cancellable = run?.cancellable ?? cancellableByDefault;
1490
2144
  what.textContent = label2 ?? "Running\u2026";
1491
2145
  count.textContent = "";
1492
2146
  time.textContent = "";
1493
- fill.style.width = "0%";
2147
+ cancel.hidden = !cancellable;
1494
2148
  cancel.disabled = false;
1495
2149
  cancel.textContent = "Cancel";
2150
+ setMode(true);
1496
2151
  strip.hidden = false;
1497
2152
  },
1498
2153
  update(done, total, elapsedMs) {
1499
2154
  if (strip.hidden) strip.hidden = false;
1500
- const pct = total > 0 ? Math.min(100, done / total * 100) : 0;
1501
- fill.style.width = `${pct}%`;
1502
- count.textContent = total > 0 ? `${done} / ${total} ${units}` : `${done} ${units}`;
1503
- if (estimable(done, total, elapsedMs)) {
2155
+ const band = runPinned || !(total > 0);
2156
+ setMode(band);
2157
+ if (!band) {
2158
+ const pct = Math.min(100, done / total * 100);
2159
+ fill.style.width = `${pct}%`;
2160
+ bar.setAttribute("aria-valuenow", String(Math.round(pct)));
2161
+ count.textContent = `${done} / ${total} ${units}`.trimEnd();
2162
+ }
2163
+ if (!band && estimable(done, total, elapsedMs)) {
1504
2164
  const remaining = elapsedMs / done * (total - done);
1505
- time.textContent = `${clock(elapsedMs)} elapsed \xB7 about ${clock(remaining)} left`;
2165
+ time.textContent = `${clock(elapsedMs)} elapsed, about ${clock(remaining)} left`;
1506
2166
  } else {
1507
2167
  time.textContent = clock(elapsedMs);
1508
2168
  }
@@ -1514,8 +2174,10 @@ function mountJobProgress(host, opts) {
1514
2174
  cancel.addEventListener("click", () => {
1515
2175
  cancel.disabled = true;
1516
2176
  cancel.textContent = "Stopping\u2026";
1517
- opts.onCancel();
2177
+ opts.onCancel?.();
1518
2178
  });
2179
+ cancel.hidden = !cancellableByDefault;
2180
+ setMode(true);
1519
2181
  return view;
1520
2182
  }
1521
2183
 
@@ -1641,11 +2303,14 @@ function staleBar(opts) {
1641
2303
  className: "stale-bar-msg",
1642
2304
  text: `${opts.subject} changed in the editor. ${opts.advice ?? "Restart to play the new version."}`
1643
2305
  }),
1644
- el("button", {
1645
- className: "stale-bar-go",
1646
- text: `${icon.restart} ${label2}`,
1647
- onClick: () => opts.onRestart()
1648
- })
2306
+ // The icon is a node beside the word, not a character inside it; the
2307
+ // button's flex gap sets the space (stale.css).
2308
+ el(
2309
+ "button",
2310
+ { className: "stale-bar-go", onClick: () => opts.onRestart() },
2311
+ iconNode("restart", 12),
2312
+ label2
2313
+ )
1649
2314
  );
1650
2315
  }
1651
2316
 
@@ -1687,14 +2352,14 @@ function dismissToast() {
1687
2352
  }
1688
2353
 
1689
2354
  // src/stepper.ts
2355
+ var STEP_ICON = 12;
1690
2356
  function renderStepperBar(host, opts) {
1691
2357
  const { items } = opts;
1692
2358
  const close = opts.onClose === void 0 ? null : el("button", {
1693
2359
  className: "stepbar-nav stepbar-close",
1694
- text: icon.close,
1695
2360
  tip: opts.closeTip ?? "Close",
1696
2361
  onClick: opts.onClose
1697
- });
2362
+ }, iconNode("close", STEP_ICON));
1698
2363
  const actions = (opts.actions ?? []).filter((a) => a !== null && a !== void 0);
1699
2364
  if (items.length === 0) {
1700
2365
  if (opts.empty === void 0) {
@@ -1718,17 +2383,15 @@ function renderStepperBar(host, opts) {
1718
2383
  const nav = items.length > 1 ? [
1719
2384
  el("button", {
1720
2385
  className: "stepbar-nav",
1721
- text: icon.back,
1722
2386
  tip: opts.tips?.prev ?? "Previous",
1723
2387
  onClick: () => step(-1)
1724
- }),
2388
+ }, iconNode("back", STEP_ICON)),
1725
2389
  el("span", { className: "stepbar-of", text: `${index + 1}/${items.length}` }),
1726
2390
  el("button", {
1727
2391
  className: "stepbar-nav",
1728
- text: icon.forward,
1729
2392
  tip: opts.tips?.next ?? "Next",
1730
2393
  onClick: () => step(1)
1731
- })
2394
+ }, iconNode("forward", STEP_ICON))
1732
2395
  ] : [];
1733
2396
  host.replaceChildren(
1734
2397
  el("span", { className: `stepbar-count ${opts.tone ?? "muted"}`, text: String(items.length) }),
@@ -1745,19 +2408,383 @@ function renderStepperBar(host, opts) {
1745
2408
  );
1746
2409
  }
1747
2410
 
2411
+ // src/updater-view.ts
2412
+ var liveProgress = null;
2413
+ var mb = (n) => (n / (1024 * 1024)).toFixed(1);
2414
+ function feedUpdaterDownloadProgress(p) {
2415
+ if (!liveProgress) return;
2416
+ const pct = Math.max(0, Math.min(100, p.percent));
2417
+ liveProgress.bar.style.width = `${pct}%`;
2418
+ liveProgress.label.textContent = `${pct.toFixed(0)}%, ${mb(p.transferred)} of ${mb(p.total)} MB (${mb(p.bytesPerSecond)} MB/s)`;
2419
+ }
2420
+ function showUpdaterDialog(opts) {
2421
+ return new Promise((resolve) => {
2422
+ const defaultId = opts.defaultId ?? 0;
2423
+ const cancelId = opts.cancelId ?? opts.buttons.length - 1;
2424
+ let done = false;
2425
+ const finish = (idx) => {
2426
+ if (done) return;
2427
+ done = true;
2428
+ liveProgress = null;
2429
+ resolve(idx);
2430
+ frame.close();
2431
+ };
2432
+ const frame = dialogFrame({
2433
+ title: opts.message,
2434
+ className: "updater-dialog",
2435
+ // Esc is the cancel button, not a further answer: main is waiting on an index.
2436
+ onClose: () => finish(cancelId)
2437
+ });
2438
+ if (opts.detail) frame.body.append(el("p", "updater-detail", opts.detail));
2439
+ if (opts.progress) {
2440
+ const track = el("div", "updater-progress-track");
2441
+ const bar = el("div", "updater-progress-bar");
2442
+ track.append(bar);
2443
+ const label2 = el("p", "updater-progress-label", "Starting download\u2026");
2444
+ frame.body.append(track, label2);
2445
+ liveProgress = { bar, label: label2 };
2446
+ }
2447
+ if (opts.links?.length) {
2448
+ const row = el("p", "updater-links");
2449
+ for (const link of opts.links) {
2450
+ const a = el("a", void 0, link.label);
2451
+ a.href = link.url;
2452
+ a.addEventListener("click", (e) => {
2453
+ e.preventDefault();
2454
+ opts.openExternal?.(link.url);
2455
+ });
2456
+ row.append(a);
2457
+ }
2458
+ frame.body.append(row);
2459
+ }
2460
+ opts.buttons.forEach((label2, i) => {
2461
+ const b = el("button", i === defaultId ? "btn primary" : "btn", label2);
2462
+ b.type = "button";
2463
+ b.addEventListener("click", () => finish(i));
2464
+ frame.actions.append(b);
2465
+ if (i === defaultId) queueMicrotask(() => b.focus());
2466
+ });
2467
+ frame.open();
2468
+ });
2469
+ }
2470
+
2471
+ // src/welcome.ts
2472
+ function mountWelcome(host, opts) {
2473
+ const caption = (text) => el("h2", "welcome-caption", text);
2474
+ const group = (label2, ...body) => el("section", "welcome-group", caption(label2), ...body);
2475
+ const actions = el("div", "welcome-actions");
2476
+ for (const a of opts.actions) {
2477
+ const b = el("button", { className: `btn${a.primary ? " primary" : ""}`, text: a.label, onClick: a.onClick });
2478
+ b.type = "button";
2479
+ actions.append(b);
2480
+ }
2481
+ const groups = (opts.groups ?? []).map((g) => group(
2482
+ g.caption,
2483
+ g.note !== void 0 ? el("p", "welcome-note", g.note) : null,
2484
+ el("div", "welcome-list", ...g.items.map((item) => {
2485
+ const b = el(
2486
+ "button",
2487
+ { className: "welcome-item", onClick: item.onOpen },
2488
+ el("span", "welcome-item-name", item.name),
2489
+ item.hint !== void 0 ? el("span", "welcome-item-hint", item.hint) : null
2490
+ );
2491
+ b.type = "button";
2492
+ return b;
2493
+ }))
2494
+ ));
2495
+ const recentsHost = el("div", "welcome-recents-slot");
2496
+ const drawRecents = (recents) => {
2497
+ const shown = recents.slice(0, opts.maxRecents ?? 5);
2498
+ recentsHost.replaceChildren();
2499
+ if (shown.length === 0) return;
2500
+ recentsHost.append(group(
2501
+ opts.recentsCaption ?? "Recent",
2502
+ el("div", "welcome-recents", ...shown.map((r) => {
2503
+ const b = el(
2504
+ "button",
2505
+ { className: "welcome-recent", onClick: r.onOpen },
2506
+ el("span", "welcome-recent-name", r.name),
2507
+ el("span", "welcome-recent-path", r.path)
2508
+ );
2509
+ b.type = "button";
2510
+ return b;
2511
+ }))
2512
+ ));
2513
+ };
2514
+ drawRecents(opts.recents);
2515
+ const error = el("p", "welcome-error");
2516
+ const setError = (text) => {
2517
+ error.textContent = text ?? "";
2518
+ error.hidden = text === void 0 || text === "";
2519
+ };
2520
+ setError(opts.error);
2521
+ const root = el("div", "welcome", el(
2522
+ "div",
2523
+ "welcome-card",
2524
+ el("h1", "welcome-title", opts.title),
2525
+ el("p", "welcome-sub", opts.sub),
2526
+ actions,
2527
+ opts.tourLine ? el("div", "welcome-line", opts.tourLine) : null,
2528
+ ...groups,
2529
+ recentsHost,
2530
+ error
2531
+ ));
2532
+ host.replaceChildren(root);
2533
+ return { el: root, setRecents: drawRecents, setError };
2534
+ }
2535
+
2536
+ // src/link-status.ts
2537
+ function linkStatusClass(s) {
2538
+ if (s.state === "off" || s.state === "error") return "off";
2539
+ if (s.state === "listening") return "listening";
2540
+ return s.build === "stale" ? "stale" : "live";
2541
+ }
2542
+ function linkStatusTip(s) {
2543
+ switch (s.state) {
2544
+ case "off":
2545
+ return "Live link is off. Click to start listening.";
2546
+ case "error":
2547
+ return `Live link failed (${s.message}). Click to retry.`;
2548
+ case "listening":
2549
+ return "Live link is listening for a game. Click to stop.";
2550
+ case "connected": {
2551
+ const who = s.project !== void 0 ? ` to ${s.project}` : "";
2552
+ const build = s.build === "stale" ? " on a different build, so save or rebuild to re-sync" : s.build === "match" ? " and in sync" : "";
2553
+ const note = s.note ? ` ${s.note}` : "";
2554
+ return `Live link is connected${who}${build}.${note} Click to stop.`;
2555
+ }
2556
+ }
2557
+ }
2558
+ function linkAddress(s) {
2559
+ if (s.state !== "listening" && s.state !== "connected") return void 0;
2560
+ return s.address ?? `ws://127.0.0.1:${s.port}`;
2561
+ }
2562
+ function mountLinkStatus(host, opts) {
2563
+ const wrap = el("div", "linkstatus");
2564
+ wrap.hidden = true;
2565
+ const url = el("button", "linkstatus-url");
2566
+ url.type = "button";
2567
+ url.hidden = true;
2568
+ const toggle = el("button", "linkstatus-toggle off");
2569
+ toggle.type = "button";
2570
+ toggle.append(iconNode("connect"));
2571
+ if (opts.label !== void 0) wrap.append(el("span", "linkstatus-word", opts.label));
2572
+ wrap.append(...opts.lead ?? [], url, toggle);
2573
+ host.append(wrap);
2574
+ let status = { state: "off" };
2575
+ const render = () => {
2576
+ toggle.className = `linkstatus-toggle ${linkStatusClass(status)}`;
2577
+ const tip2 = linkStatusTip(status);
2578
+ toggle.dataset["tip"] = tip2;
2579
+ toggle.setAttribute("aria-label", tip2);
2580
+ const addr = linkAddress(status);
2581
+ if (addr !== void 0) {
2582
+ url.textContent = addr;
2583
+ url.dataset["tip"] = "Click to copy the live link address";
2584
+ url.hidden = false;
2585
+ } else url.hidden = true;
2586
+ };
2587
+ const apply = (s) => {
2588
+ status = s;
2589
+ render();
2590
+ };
2591
+ render();
2592
+ toggle.addEventListener("click", () => {
2593
+ const out = opts.onToggle(status);
2594
+ void Promise.resolve(out).then((next) => {
2595
+ if (next) apply(next);
2596
+ });
2597
+ });
2598
+ url.addEventListener("click", () => {
2599
+ const addr = url.textContent ?? "";
2600
+ if (!addr) return;
2601
+ void copyWithFeedback(url, addr);
2602
+ opts.onCopy?.(addr);
2603
+ });
2604
+ return {
2605
+ el: wrap,
2606
+ apply,
2607
+ status: () => status,
2608
+ setVisible(on) {
2609
+ wrap.hidden = !on;
2610
+ },
2611
+ toggle() {
2612
+ toggle.click();
2613
+ }
2614
+ };
2615
+ }
2616
+
2617
+ // src/property-list.ts
2618
+ var PROPERTY_TYPES = [
2619
+ ["number", "Number"],
2620
+ ["boolean", "True / False"],
2621
+ ["string", "Text"],
2622
+ ["enum", "List"],
2623
+ ["flags", "Flags"],
2624
+ ["quality", "Quality"]
2625
+ ];
2626
+ var option = (value, label2, selected) => {
2627
+ const o = el("option", void 0, label2);
2628
+ o.value = value;
2629
+ o.selected = selected;
2630
+ return o;
2631
+ };
2632
+ var NONE = "(none)";
2633
+ function defaultControl(p, onChange, opts = {}) {
2634
+ const typed = opts.typed === true;
2635
+ const write = (raw) => {
2636
+ if (raw === "") {
2637
+ if (typed) delete p.default;
2638
+ else p.default = "";
2639
+ } else if (typed && p.type === "boolean") p.default = raw === "true";
2640
+ else if (typed && p.type === "number") p.default = Number(raw);
2641
+ else p.default = raw;
2642
+ onChange?.();
2643
+ };
2644
+ const current3 = p.default === void 0 || p.default === null ? "" : String(p.default);
2645
+ if (p.type === "boolean") {
2646
+ const sel = el("select", "field set-default");
2647
+ sel.append(option("", NONE, current3 === ""), option("true", "True", current3 === "true"), option("false", "False", current3 === "false"));
2648
+ sel.addEventListener("change", () => write(sel.value));
2649
+ return sel;
2650
+ }
2651
+ if (p.type === "enum") {
2652
+ const sel = el("select", "field set-default");
2653
+ sel.append(option("", NONE, current3 === ""));
2654
+ for (const v of p.values ?? []) sel.append(option(v, v, current3 === v));
2655
+ sel.addEventListener("change", () => write(sel.value));
2656
+ return sel;
2657
+ }
2658
+ if (p.type === "quality") {
2659
+ const sel = el("select", "field set-default");
2660
+ sel.append(option("", "(first stage)", current3 === ""));
2661
+ for (const v of p.stages ?? []) sel.append(option(v, v, current3 === v));
2662
+ sel.addEventListener("change", () => write(sel.value));
2663
+ return sel;
2664
+ }
2665
+ if (p.type === "flags") {
2666
+ const s = el("span", "set-flagnote", "Starts empty");
2667
+ s.dataset["tip"] = "A flags property starts with none set.";
2668
+ return s;
2669
+ }
2670
+ const input = el("input", "field set-default");
2671
+ input.type = p.type === "number" ? "number" : "text";
2672
+ input.placeholder = "Default";
2673
+ input.value = current3;
2674
+ input.addEventListener("input", () => write(input.value));
2675
+ return input;
2676
+ }
2677
+ function mountPropertyList(host, decls, opts = {}) {
2678
+ const changed = () => opts.onChange?.();
2679
+ const guard = dupGuard();
2680
+ const types = opts.types ?? PROPERTY_TYPES;
2681
+ const list = el("div", "set-list");
2682
+ const keep = (parts) => (parts ?? []).filter((p) => p !== null);
2683
+ const row = (p, i) => {
2684
+ const name = el("input", "field set-name");
2685
+ name.type = "text";
2686
+ name.placeholder = "Property name";
2687
+ name.value = p.name;
2688
+ name.spellcheck = false;
2689
+ name.dataset["tip"] = PROPERTY_NAME_HINT;
2690
+ bindPropertyName(name, (v) => {
2691
+ p.name = v;
2692
+ changed();
2693
+ }, { hint: PROPERTY_NAME_HINT });
2694
+ guard.track(name);
2695
+ const type = el("select", "field set-type");
2696
+ for (const [v, label2] of types) type.append(option(v, label2, v === p.type));
2697
+ type.addEventListener("change", () => {
2698
+ p.type = type.value;
2699
+ if (opts.typed) delete p.default;
2700
+ else p.default = "";
2701
+ if (p.type === "enum" || p.type === "flags") p.values ??= [];
2702
+ else delete p.values;
2703
+ if (p.type === "quality") p.stages ??= [];
2704
+ else delete p.stages;
2705
+ render();
2706
+ changed();
2707
+ });
2708
+ let def = defaultControl(p, changed, opts);
2709
+ const refreshDefault = () => {
2710
+ const fresh = defaultControl(p, changed, opts);
2711
+ def.replaceWith(fresh);
2712
+ def = fresh;
2713
+ changed();
2714
+ };
2715
+ const up = iconBtn("up", "Move up", () => {
2716
+ if (moveItem(decls, i, -1)) {
2717
+ render();
2718
+ changed();
2719
+ }
2720
+ }, i === 0);
2721
+ const down = iconBtn("down", "Move down", () => {
2722
+ if (moveItem(decls, i, 1)) {
2723
+ render();
2724
+ changed();
2725
+ }
2726
+ }, i === decls.length - 1);
2727
+ const del = iconBtn("close", "Delete property", () => {
2728
+ decls.splice(i, 1);
2729
+ render();
2730
+ changed();
2731
+ }, false, true);
2732
+ const details = keep(opts.extraDetails?.(p, i));
2733
+ if (p.type === "enum" || p.type === "flags") details.push(labelled("Values", tagChips(p, refreshDefault)));
2734
+ if (p.type === "quality") details.push(labelled("Stages (in order)", stageChips(p, refreshDefault)));
2735
+ if (opts.purpose !== false) {
2736
+ const purpose = el("input", "field set-purpose");
2737
+ purpose.type = "text";
2738
+ purpose.placeholder = "What this property is for";
2739
+ purpose.value = p.purpose ?? "";
2740
+ purpose.addEventListener("input", () => {
2741
+ if (purpose.value.trim()) p.purpose = purpose.value;
2742
+ else delete p.purpose;
2743
+ changed();
2744
+ });
2745
+ details.push(labelled("Purpose", purpose));
2746
+ }
2747
+ return expandableRow({ line: [name, type, def, ...keep(opts.extraLine?.(p, i)), up, down, del], details, name: p.name });
2748
+ };
2749
+ const render = () => {
2750
+ guard.reset();
2751
+ list.replaceChildren();
2752
+ if (decls.length === 0) list.append(el("p", "empty", opts.emptyText ?? "No properties yet."));
2753
+ else decls.forEach((p, i) => list.append(row(p, i)));
2754
+ guard.check();
2755
+ };
2756
+ render();
2757
+ const add = el("button", "btn set-add", opts.addLabel ?? "+ Add property");
2758
+ add.type = "button";
2759
+ add.addEventListener("click", () => {
2760
+ decls.push(opts.newDecl ? opts.newDecl() : { name: "", type: types[0]?.[0] ?? "string", ...opts.typed ? {} : { default: "" } });
2761
+ render();
2762
+ changed();
2763
+ focusNewRow(list);
2764
+ });
2765
+ host.append(list, add);
2766
+ return {
2767
+ firstDuplicate: () => guard.firstDuplicate(),
2768
+ firstIllegalName: () => firstIllegalPropertyName(host),
2769
+ // The Save gate takes both faults; the field's own rollover says which.
2770
+ firstInvalid: () => guard.firstDuplicate() ?? firstIllegalPropertyName(host),
2771
+ render
2772
+ };
2773
+ }
2774
+
1748
2775
  // src/nav-history.ts
1749
2776
  function createNavHistory(opts) {
1750
2777
  const cap = opts.cap ?? 100;
1751
2778
  const usable = opts.usable ?? (() => true);
1752
2779
  let past = [];
1753
2780
  let ahead = [];
1754
- const step = (from, to, current2) => {
2781
+ const step = (from, to, current3) => {
1755
2782
  for (; ; ) {
1756
2783
  const p = from.pop();
1757
2784
  if (p === void 0) return void 0;
1758
2785
  if (!usable(p)) continue;
1759
- if (opts.same(p, current2)) continue;
1760
- to.push(current2);
2786
+ if (opts.same(p, current3)) continue;
2787
+ to.push(current3);
1761
2788
  return p;
1762
2789
  }
1763
2790
  };
@@ -1769,11 +2796,11 @@ function createNavHistory(opts) {
1769
2796
  else past.push(place2);
1770
2797
  if (past.length > cap) past = past.slice(past.length - cap);
1771
2798
  },
1772
- back(current2) {
1773
- return step(past, ahead, current2);
2799
+ back(current3) {
2800
+ return step(past, ahead, current3);
1774
2801
  },
1775
- forward(current2) {
1776
- return step(ahead, past, current2);
2802
+ forward(current3) {
2803
+ return step(ahead, past, current3);
1777
2804
  },
1778
2805
  canBack() {
1779
2806
  return past.length > 0;
@@ -1784,18 +2811,18 @@ function createNavHistory(opts) {
1784
2811
  };
1785
2812
  }
1786
2813
  function historyNav(onBack, onForward) {
1787
- const btn = (glyph, tip2, onClick) => {
2814
+ const btn = (name, tip2, onClick) => {
1788
2815
  const b = document.createElement("button");
1789
2816
  b.type = "button";
1790
2817
  b.className = "shell-histnav-btn";
1791
- b.textContent = glyph;
2818
+ b.append(iconNode(name));
1792
2819
  b.dataset["tip"] = tip2;
1793
2820
  b.setAttribute("aria-label", tip2);
1794
2821
  b.addEventListener("click", onClick);
1795
2822
  return b;
1796
2823
  };
1797
- const back = btn("\u2190", "Back", onBack);
1798
- const forward = btn("\u2192", "Forward", onForward);
2824
+ const back = btn("arrowLeft", "Back", onBack);
2825
+ const forward = btn("arrowRight", "Forward", onForward);
1799
2826
  const el2 = document.createElement("span");
1800
2827
  el2.className = "shell-histnav";
1801
2828
  el2.append(back, forward);
@@ -1809,13 +2836,21 @@ function historyNav(onBack, onForward) {
1809
2836
  }
1810
2837
 
1811
2838
  exports.EDIT_MENU = EDIT_MENU;
2839
+ exports.FILE_MENU = FILE_MENU;
1812
2840
  exports.GO_MENU = GO_MENU;
2841
+ exports.ICON_NAMES = ICON_NAMES;
2842
+ exports.ICON_SIZE = ICON_SIZE;
1813
2843
  exports.JOB_PROGRESS = JOB_PROGRESS;
1814
2844
  exports.PALETTE = PALETTE;
1815
2845
  exports.PALETTE_SIZE = PALETTE_SIZE;
1816
2846
  exports.PANE_MENU = PANE_MENU;
2847
+ exports.PLAY_MENU = PLAY_MENU;
1817
2848
  exports.PROPERTY_NAME_HINT = PROPERTY_NAME_HINT;
2849
+ exports.PROPERTY_TYPES = PROPERTY_TYPES;
2850
+ exports.PUBLISH_MENU = PUBLISH_MENU;
1818
2851
  exports.RESERVED_PROPERTY_NAMES = RESERVED_PROPERTY_NAMES;
2852
+ exports.REVIEW_MENU = REVIEW_MENU;
2853
+ exports.VIEW_MENU = VIEW_MENU;
1819
2854
  exports.askIdentity = askIdentity;
1820
2855
  exports.bindPropertyName = bindPropertyName;
1821
2856
  exports.bindPropertyRef = bindPropertyRef;
@@ -1824,51 +2859,80 @@ exports.closeWithExit = closeWithExit;
1824
2859
  exports.colourFor = colourFor;
1825
2860
  exports.colourIndex = colourIndex;
1826
2861
  exports.confirmDialog = confirmDialog;
2862
+ exports.copyWithFeedback = copyWithFeedback;
2863
+ exports.createFloating = createFloating;
1827
2864
  exports.createNavHistory = createNavHistory;
1828
2865
  exports.createSaveController = createSaveController;
2866
+ exports.debounce = debounce;
2867
+ exports.defaultControl = defaultControl;
2868
+ exports.dialogFrame = dialogFrame;
1829
2869
  exports.dismissToast = dismissToast;
1830
2870
  exports.dupGuard = dupGuard;
1831
2871
  exports.el = el;
1832
2872
  exports.expandableRow = expandableRow;
2873
+ exports.feedUpdaterDownloadProgress = feedUpdaterDownloadProgress;
1833
2874
  exports.firstIllegalPropertyName = firstIllegalPropertyName;
1834
2875
  exports.focusNewRow = focusNewRow;
1835
2876
  exports.foldVc = foldVc;
2877
+ exports.followButton = followButton;
2878
+ exports.formatCount = formatCount;
1836
2879
  exports.gameIdify = gameIdify;
1837
2880
  exports.hideTip = hideTip;
1838
2881
  exports.historyNav = historyNav;
1839
2882
  exports.icon = icon;
1840
2883
  exports.iconBtn = iconBtn;
2884
+ exports.iconHtml = iconHtml;
2885
+ exports.iconNameOfGlyph = iconNameOfGlyph;
2886
+ exports.iconNode = iconNode;
1841
2887
  exports.iconSvg = iconSvg;
1842
2888
  exports.initTooltips = initTooltips;
1843
2889
  exports.isCaseOnlyPropertyName = isCaseOnlyPropertyName;
2890
+ exports.isEditableTarget = isEditableTarget;
2891
+ exports.isIconName = isIconName;
1844
2892
  exports.isValidGameId = isValidGameId;
1845
2893
  exports.isValidPropertyName = isValidPropertyName;
1846
2894
  exports.labelled = labelled;
2895
+ exports.linkAddress = linkAddress;
2896
+ exports.linkStatusClass = linkStatusClass;
2897
+ exports.linkStatusTip = linkStatusTip;
1847
2898
  exports.lockControls = lockControls;
1848
2899
  exports.lockNotice = lockNotice;
1849
2900
  exports.mountJobProgress = mountJobProgress;
2901
+ exports.mountLinkStatus = mountLinkStatus;
1850
2902
  exports.mountPaneShell = mountPaneShell;
2903
+ exports.mountPropertyList = mountPropertyList;
1851
2904
  exports.mountSettingsDialog = mountSettingsDialog;
2905
+ exports.mountWelcome = mountWelcome;
1852
2906
  exports.moveItem = moveItem;
1853
2907
  exports.openAnchoredPanel = openAnchoredPanel;
1854
2908
  exports.openComments = openComments;
2909
+ exports.openContextMenu = openContextMenu;
1855
2910
  exports.openGameIdEditor = openGameIdEditor;
1856
2911
  exports.openNotesEditor = openNotesEditor;
2912
+ exports.openPopover = openPopover;
1857
2913
  exports.paintVcBadges = paintVcBadges;
1858
2914
  exports.pinButton = pinButton;
1859
2915
  exports.placeAnchored = placeAnchored;
2916
+ exports.plural = plural;
1860
2917
  exports.propertyNameProblem = propertyNameProblem;
1861
2918
  exports.propertyNameify = propertyNameify;
1862
2919
  exports.propertyRefProblem = propertyRefProblem;
2920
+ exports.relativeTime = relativeTime;
1863
2921
  exports.renderStepperBar = renderStepperBar;
1864
2922
  exports.revalidatePropertyRefs = revalidatePropertyRefs;
2923
+ exports.revealRow = revealRow;
2924
+ exports.revealRowWhenReady = revealRowWhenReady;
1865
2925
  exports.saveIndicator = saveIndicator;
1866
2926
  exports.showAbout = showAbout;
2927
+ exports.showUpdaterDialog = showUpdaterDialog;
2928
+ exports.stageChips = stageChips;
1867
2929
  exports.staleBar = staleBar;
1868
2930
  exports.tagChips = tagChips;
1869
2931
  exports.tipAt = tipAt;
1870
2932
  exports.tipBold = tipBold;
1871
2933
  exports.toast = toast;
2934
+ exports.toolWindowHead = toolWindowHead;
1872
2935
  exports.vcBadgeFor = vcBadgeFor;
2936
+ exports.wireReorder = wireReorder;
1873
2937
  //# sourceMappingURL=index.cjs.map
1874
2938
  //# sourceMappingURL=index.cjs.map