@wildwinter/app-shell 0.38.0 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/LICENSES.md +62 -0
  2. package/README.md +8 -0
  3. package/dist/about.css +13 -34
  4. package/dist/about.css.map +1 -1
  5. package/dist/anchored.css +5 -5
  6. package/dist/anchored.css.map +1 -1
  7. package/dist/app-store.cjs +15 -0
  8. package/dist/app-store.cjs.map +1 -1
  9. package/dist/app-store.d.cts +25 -1
  10. package/dist/app-store.d.ts +25 -1
  11. package/dist/app-store.js +14 -1
  12. package/dist/app-store.js.map +1 -1
  13. package/dist/chunk-2ECJUTSX.js +97 -0
  14. package/dist/chunk-2ECJUTSX.js.map +1 -0
  15. package/dist/chunk-2NMVIM54.js +55 -0
  16. package/dist/chunk-2NMVIM54.js.map +1 -0
  17. package/dist/chunk-C6UVQYKI.js +70 -0
  18. package/dist/chunk-C6UVQYKI.js.map +1 -0
  19. package/dist/chunk-IU7EY6BI.js +26 -0
  20. package/dist/chunk-IU7EY6BI.js.map +1 -0
  21. package/dist/chunk-K2AOLQW4.js +82 -0
  22. package/dist/chunk-K2AOLQW4.js.map +1 -0
  23. package/dist/chunk-KGAMP5LW.js +87 -0
  24. package/dist/chunk-KGAMP5LW.js.map +1 -0
  25. package/dist/chunk-L5HOBEKL.js +534 -0
  26. package/dist/chunk-L5HOBEKL.js.map +1 -0
  27. package/dist/chunk-P4G4ERG6.js +60 -0
  28. package/dist/chunk-P4G4ERG6.js.map +1 -0
  29. package/dist/chunk-P6WLAEKZ.js +71 -0
  30. package/dist/chunk-P6WLAEKZ.js.map +1 -0
  31. package/dist/chunk-S4KWQLYN.js +66 -0
  32. package/dist/chunk-S4KWQLYN.js.map +1 -0
  33. package/dist/chunk-XZYZXY7I.js +554 -0
  34. package/dist/chunk-XZYZXY7I.js.map +1 -0
  35. package/dist/comments.css +5 -3
  36. package/dist/comments.css.map +1 -1
  37. package/dist/confirm.css +1 -54
  38. package/dist/confirm.css.map +1 -1
  39. package/dist/context-menu.cjs +250 -0
  40. package/dist/context-menu.cjs.map +1 -0
  41. package/dist/context-menu.css +64 -0
  42. package/dist/context-menu.css.map +1 -0
  43. package/dist/context-menu.d.cts +25 -0
  44. package/dist/context-menu.d.ts +25 -0
  45. package/dist/context-menu.js +5 -0
  46. package/dist/context-menu.js.map +1 -0
  47. package/dist/controls.css +155 -0
  48. package/dist/controls.css.map +1 -0
  49. package/dist/dialog.cjs +222 -0
  50. package/dist/dialog.cjs.map +1 -0
  51. package/dist/dialog.css +55 -0
  52. package/dist/dialog.css.map +1 -0
  53. package/dist/dialog.d.cts +28 -0
  54. package/dist/dialog.d.ts +28 -0
  55. package/dist/dialog.js +5 -0
  56. package/dist/dialog.js.map +1 -0
  57. package/dist/floating.cjs +83 -0
  58. package/dist/floating.cjs.map +1 -0
  59. package/dist/floating.d.cts +25 -0
  60. package/dist/floating.d.ts +25 -0
  61. package/dist/floating.js +4 -0
  62. package/dist/floating.js.map +1 -0
  63. package/dist/identity.css +15 -50
  64. package/dist/identity.css.map +1 -1
  65. package/dist/index.cjs +1307 -258
  66. package/dist/index.cjs.map +1 -1
  67. package/dist/index.d.cts +349 -149
  68. package/dist/index.d.ts +349 -149
  69. package/dist/index.js +259 -822
  70. package/dist/index.js.map +1 -1
  71. package/dist/job.css +23 -0
  72. package/dist/job.css.map +1 -1
  73. package/dist/link-status.cjs +430 -0
  74. package/dist/link-status.cjs.map +1 -0
  75. package/dist/link-status.css +76 -0
  76. package/dist/link-status.css.map +1 -0
  77. package/dist/link-status.d.cts +53 -0
  78. package/dist/link-status.d.ts +53 -0
  79. package/dist/link-status.js +5 -0
  80. package/dist/link-status.js.map +1 -0
  81. package/dist/menu.cjs +62 -1
  82. package/dist/menu.cjs.map +1 -1
  83. package/dist/menu.d.cts +88 -1
  84. package/dist/menu.d.ts +88 -1
  85. package/dist/menu.js +1 -1
  86. package/dist/notes-editor.css +3 -46
  87. package/dist/notes-editor.css.map +1 -1
  88. package/dist/pane-shell.css +16 -3
  89. package/dist/pane-shell.css.map +1 -1
  90. package/dist/property-list-D3VZCJzi.d.cts +162 -0
  91. package/dist/property-list-D3VZCJzi.d.ts +162 -0
  92. package/dist/property-list.cjs +784 -0
  93. package/dist/property-list.cjs.map +1 -0
  94. package/dist/property-list.d.cts +1 -0
  95. package/dist/property-list.d.ts +1 -0
  96. package/dist/property-list.js +4 -0
  97. package/dist/property-list.js.map +1 -0
  98. package/dist/settings.css +52 -8
  99. package/dist/settings.css.map +1 -1
  100. package/dist/stale.css +3 -0
  101. package/dist/stale.css.map +1 -1
  102. package/dist/stepper.css +35 -0
  103. package/dist/stepper.css.map +1 -1
  104. package/dist/tokens.css +19 -0
  105. package/dist/tokens.css.map +1 -1
  106. package/dist/tool-window.cjs +63 -0
  107. package/dist/tool-window.cjs.map +1 -1
  108. package/dist/tool-window.css +6 -3
  109. package/dist/tool-window.css.map +1 -1
  110. package/dist/tool-window.d.cts +67 -1
  111. package/dist/tool-window.d.ts +67 -1
  112. package/dist/tool-window.js +63 -1
  113. package/dist/tool-window.js.map +1 -1
  114. package/dist/updater-view.cjs +283 -0
  115. package/dist/updater-view.cjs.map +1 -0
  116. package/dist/updater-view.d.cts +23 -0
  117. package/dist/updater-view.d.ts +23 -0
  118. package/dist/updater-view.js +6 -0
  119. package/dist/updater-view.js.map +1 -0
  120. package/dist/updater.cjs +12 -16
  121. package/dist/updater.cjs.map +1 -1
  122. package/dist/updater.css +44 -0
  123. package/dist/updater.css.map +1 -0
  124. package/dist/updater.js +12 -16
  125. package/dist/updater.js.map +1 -1
  126. package/dist/util.cjs +78 -0
  127. package/dist/util.cjs.map +1 -0
  128. package/dist/util.d.cts +37 -0
  129. package/dist/util.d.ts +37 -0
  130. package/dist/util.js +3 -0
  131. package/dist/util.js.map +1 -0
  132. package/dist/vc.css +3 -2
  133. package/dist/vc.css.map +1 -1
  134. package/dist/welcome.cjs +215 -0
  135. package/dist/welcome.cjs.map +1 -0
  136. package/dist/welcome.css +112 -0
  137. package/dist/welcome.css.map +1 -0
  138. package/dist/welcome.d.cts +55 -0
  139. package/dist/welcome.d.ts +55 -0
  140. package/dist/welcome.js +4 -0
  141. package/dist/welcome.js.map +1 -0
  142. package/package.json +58 -2
  143. package/dist/chunk-QDPFU4WX.js +0 -43
  144. package/dist/chunk-QDPFU4WX.js.map +0 -1
package/dist/index.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,6 +746,38 @@ 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) {
@@ -525,6 +821,22 @@ function revealRow(within, name) {
525
821
  row.addEventListener("animationend", () => row.classList.remove("landed"), { once: true });
526
822
  return true;
527
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
+ }
528
840
  function focusNewRow(listEl) {
529
841
  const last = listEl?.lastElementChild;
530
842
  if (!(last instanceof HTMLElement)) return;
@@ -532,6 +844,7 @@ function focusNewRow(listEl) {
532
844
  const name = last.querySelector("input.set-name") ?? last.querySelector("input");
533
845
  name?.focus();
534
846
  }
847
+ var DUP_MESSAGE = "Another row already has this name.";
535
848
  function dupGuard() {
536
849
  const items = [];
537
850
  const check = () => {
@@ -546,9 +859,9 @@ function dupGuard() {
546
859
  const dup = !!k && (counts.get(k) ?? 0) > 1;
547
860
  it.input.classList.toggle("invalid", dup);
548
861
  if (dup) {
549
- it.input.title = "Duplicate name. Names must be unique.";
862
+ it.input.dataset["tip"] = DUP_MESSAGE;
550
863
  any = true;
551
- } 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"];
552
865
  }
553
866
  return any;
554
867
  };
@@ -594,7 +907,9 @@ function mountSettingsDialog(opts) {
594
907
  const tab = el("button", "settings-tab", s.label);
595
908
  tab.type = "button";
596
909
  tab.dataset["tab"] = s.id;
597
- tab.addEventListener("click", () => showTab(s.id));
910
+ tab.addEventListener("click", () => {
911
+ if (!tab.classList.contains("is-disabled")) showTab(s.id);
912
+ });
598
913
  tabs.append(tab);
599
914
  tabFor.set(s.id, tab);
600
915
  const panel = el("section", "settings-panel");
@@ -608,6 +923,25 @@ function mountSettingsDialog(opts) {
608
923
  for (const [sid, t] of tabFor) t.classList.toggle("active", sid === id);
609
924
  for (const [sid, p] of panelFor) p.hidden = sid !== id;
610
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
+ }
611
945
  form.append(
612
946
  el("h2", "settings-title", opts.title),
613
947
  el("div", "settings-body", tabs, panels),
@@ -623,14 +957,15 @@ function mountSettingsDialog(opts) {
623
957
  e.preventDefault();
624
958
  error.hidden = true;
625
959
  for (const s of opts.sections) {
626
- const bad = handles.get(s.id)?.firstInvalid?.();
627
- if (bad) {
628
- showTab(s.id);
629
- bad.focus();
630
- error.textContent = "Fix the highlighted fields first.";
631
- error.hidden = false;
632
- return;
633
- }
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;
634
969
  }
635
970
  void (async () => {
636
971
  await opts.onSave();
@@ -647,25 +982,94 @@ function mountSettingsDialog(opts) {
647
982
  }
648
983
  error.hidden = true;
649
984
  showTab(tabId ?? active2 ?? opts.sections[0]?.id ?? "");
985
+ refreshTabs();
650
986
  dialog.showModal();
651
987
  },
988
+ refreshTabs,
652
989
  destroy() {
653
990
  dialog.remove();
654
991
  }
655
992
  };
656
993
  }
657
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
+
658
1063
  // src/tool-window-web.ts
659
- 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>`;
660
1064
  function pinButton(opts) {
661
1065
  const b = el("button", "swin-pin");
662
1066
  b.type = "button";
663
- b.innerHTML = PIN_ICON;
1067
+ b.append(iconNode("pin"));
664
1068
  let pinned = opts.pinned;
665
1069
  const reflect = () => {
666
1070
  b.classList.toggle("on", pinned);
667
1071
  b.setAttribute("aria-pressed", String(pinned));
668
- 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";
669
1073
  b.dataset["tip"] = label2;
670
1074
  b.setAttribute("aria-label", label2);
671
1075
  };
@@ -680,6 +1084,61 @@ function pinButton(opts) {
680
1084
  reflect();
681
1085
  } };
682
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
+ }
683
1142
 
684
1143
  // src/exit.ts
685
1144
  function closeWithExit(element, done) {
@@ -704,61 +1163,232 @@ function closeWithExit(element, done) {
704
1163
  const timer3 = setTimeout(finish, seconds * 1e3 + 120);
705
1164
  }
706
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
+
707
1215
  // src/confirm.ts
708
1216
  function confirmDialog(opts) {
709
1217
  return new Promise((resolve) => {
710
- const cancel = el("button", "confirm-btn cancel", "Cancel");
711
- const danger = el("button", "confirm-btn danger", opts.confirmLabel);
712
- cancel.type = "button";
713
- danger.type = "button";
714
- const dlg = el(
715
- "dialog",
716
- "confirm-dialog",
717
- el("div", "confirm-title", opts.title),
718
- el("div", "confirm-body", opts.body),
719
- el("div", "confirm-actions", cancel, danger)
720
- );
721
- dlg.setAttribute("aria-label", opts.title);
722
- document.body.append(dlg);
723
1218
  let done = false;
724
1219
  const finish = (ok) => {
725
1220
  if (done) return;
726
1221
  done = true;
727
1222
  resolve(ok);
728
- closeWithExit(dlg, () => {
729
- dlg.close();
730
- dlg.remove();
731
- });
1223
+ frame.close();
732
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);
733
1238
  cancel.addEventListener("click", () => finish(false));
734
1239
  danger.addEventListener("click", () => finish(true));
735
- dlg.addEventListener("cancel", (e) => {
736
- e.preventDefault();
737
- finish(false);
1240
+ frame.dialog.addEventListener("mousedown", (e) => {
1241
+ if (e.target === frame.dialog) finish(false);
738
1242
  });
739
- dlg.addEventListener("mousedown", (e) => {
740
- if (e.target === dlg) finish(false);
741
- });
742
- dlg.showModal();
1243
+ frame.open();
743
1244
  cancel.focus();
744
1245
  });
745
1246
  }
746
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
+
747
1379
  // src/about.ts
748
1380
  function showAbout(opts) {
749
1381
  return new Promise((resolve) => {
750
- 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}`);
751
1384
  if (opts.wordmark !== void 0) {
752
1385
  const mark = el("div", "shell-about-mark");
753
1386
  mark.innerHTML = opts.wordmark;
754
- dialog.append(mark);
1387
+ frame.dialog.prepend(mark);
755
1388
  }
756
- dialog.append(
757
- el("div", "shell-about-name", opts.appName),
758
- el("div", "shell-about-version", `Version ${opts.version}`)
759
- );
760
- if (opts.blurb !== void 0) dialog.append(el("p", "shell-about-blurb", opts.blurb));
761
- 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));
762
1392
  if (opts.links?.length) {
763
1393
  const row = el("div", "shell-about-links");
764
1394
  for (const link of opts.links) {
@@ -771,39 +1401,28 @@ function showAbout(opts) {
771
1401
  a.addEventListener("click", () => opts.onOpenLink?.(link.url));
772
1402
  row.append(a);
773
1403
  }
774
- dialog.append(row);
1404
+ frame.body.append(row);
775
1405
  }
776
- const close = el("button", "shell-about-close", "Close");
1406
+ const close = el("button", "btn shell-about-close", "Close");
777
1407
  close.type = "button";
778
- dialog.append(el("div", "shell-about-actions", close));
779
- dialog.setAttribute("aria-label", `About ${opts.appName}`);
780
- document.body.append(dialog);
781
- let done = false;
782
- const finish = () => {
783
- if (done) return;
784
- done = true;
785
- dialog.close();
786
- dialog.remove();
787
- resolve();
788
- };
789
- close.addEventListener("click", finish);
790
- dialog.addEventListener("close", finish);
791
- dialog.showModal();
1408
+ close.addEventListener("click", () => frame.close());
1409
+ frame.actions.append(close);
1410
+ frame.open();
792
1411
  close.focus();
793
1412
  });
794
1413
  }
795
1414
 
796
1415
  // src/anchored.ts
797
- var current = null;
1416
+ var current2 = null;
798
1417
  function openAnchoredPanel(opts) {
799
- if (current) {
800
- const prev = current;
801
- current = null;
1418
+ if (current2) {
1419
+ const prev = current2;
1420
+ current2 = null;
802
1421
  prev.close();
803
1422
  if (prev.anchor === opts.anchor) return null;
804
1423
  }
805
1424
  const panel = el("div", `shell-anchored ${opts.className ?? ""}`.trim());
806
- const closeBtn = el("button", "shell-anchored-close", "\u2715");
1425
+ const closeBtn = el("button", "shell-anchored-close", iconNode("close", 12));
807
1426
  closeBtn.type = "button";
808
1427
  closeBtn.setAttribute("aria-label", "Close");
809
1428
  const body = el("div", "shell-anchored-body");
@@ -811,13 +1430,13 @@ function openAnchoredPanel(opts) {
811
1430
  el("div", "shell-anchored-head", el("span", "shell-anchored-title", opts.title), closeBtn),
812
1431
  body
813
1432
  );
814
- document.body.append(panel);
1433
+ (opts.host ?? document.body).append(panel);
815
1434
  placeAnchored(panel, opts.anchor, opts.width, opts.prefer ?? "below", opts.keepClear ?? []);
816
1435
  let closed = false;
817
1436
  const close = () => {
818
1437
  if (closed) return;
819
1438
  closed = true;
820
- if (current?.panel === panel) current = null;
1439
+ if (current2?.panel === panel) current2 = null;
821
1440
  document.removeEventListener("pointerdown", onDown, true);
822
1441
  document.removeEventListener("keydown", onKey, true);
823
1442
  closeWithExit(panel, () => {
@@ -841,12 +1460,12 @@ function openAnchoredPanel(opts) {
841
1460
  document.addEventListener("pointerdown", onDown, true);
842
1461
  document.addEventListener("keydown", onKey, true);
843
1462
  }, 0);
844
- current = { anchor: opts.anchor, panel, close };
1463
+ current2 = { anchor: opts.anchor, panel, close };
845
1464
  return { panel, body, close };
846
1465
  }
847
1466
  function closeAnchoredPanel() {
848
- const open = current;
849
- current = null;
1467
+ const open = current2;
1468
+ current2 = null;
850
1469
  open?.close();
851
1470
  }
852
1471
  function placeAnchored(panel, anchor, width, prefer, keepClear) {
@@ -903,7 +1522,7 @@ function isValidGameId(gameId) {
903
1522
 
904
1523
  // src/property-names.ts
905
1524
  var RESERVED_PROPERTY_NAMES = ["true", "false", "and", "or", "not"];
906
- 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.";
907
1526
  function propertyNameify(text) {
908
1527
  const trimmed = text.trim();
909
1528
  const deliberateLeading = trimmed.startsWith("_");
@@ -927,14 +1546,14 @@ function propertyNameProblem(name) {
927
1546
  const suggestion = propertyNameify(name);
928
1547
  const tail = suggestion ? ` Try "${suggestion}", or press Tab.` : "";
929
1548
  if (RESERVED_PROPERTY_NAMES.includes(name.toLowerCase())) {
930
- 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}`;
931
1550
  }
932
- if (name.includes("-")) return `Cannot be used: a hyphen reads as subtraction in an expression.${tail}`;
933
- 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}`;
934
1553
  if (name !== name.toLowerCase()) {
935
- 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}`;
936
1555
  }
937
- 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}`;
938
1557
  }
939
1558
  function bindPropertyName(input, onCommit, opts = {}) {
940
1559
  const hint = opts.hint;
@@ -943,7 +1562,7 @@ function bindPropertyName(input, onCommit, opts = {}) {
943
1562
  const problem = v === "" ? void 0 : propertyNameProblem(v);
944
1563
  input.classList.toggle("illegal", !!problem);
945
1564
  if (problem) input.title = problem;
946
- else if (input.title.startsWith("Cannot be used")) {
1565
+ else if (input.title.startsWith("Can't be used")) {
947
1566
  if (hint) input.title = hint;
948
1567
  else input.removeAttribute("title");
949
1568
  }
@@ -999,10 +1618,10 @@ function propertyRefProblem(name, opts) {
999
1618
  if (!isValidPropertyName(folded)) {
1000
1619
  const suggestion = propertyNameify(name);
1001
1620
  const tail = suggestion ? ` Try "${suggestion}", or press Tab.` : "";
1002
- if (RESERVED_PROPERTY_NAMES.includes(folded)) return `Cannot be used: "${folded}" is a keyword in expressions.${tail}`;
1003
- if (name.includes("-")) return `Cannot be used: a hyphen reads as subtraction in an expression.${tail}`;
1004
- if (/^[0-9]/.test(name)) return `Cannot be used: a name cannot start with a digit.${tail}`;
1005
- 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}`;
1006
1625
  }
1007
1626
  const known = opts.known();
1008
1627
  if (known.some((k) => k.toLowerCase() === folded)) return void 0;
@@ -1011,7 +1630,7 @@ function propertyRefProblem(name, opts) {
1011
1630
  if (known.length === 0) return `No ${scope} properties are declared yet. Declare one above first.`;
1012
1631
  const shown = known.slice(0, 3).map((k) => `"${k}"`).join(", ");
1013
1632
  const more = known.length > 3 ? `, and ${known.length - 3} more` : "";
1014
- return `No ${scope} property is called "${name}". Declared: ${shown}${more}.`;
1633
+ return `No ${scope} property is called "${name}". Pick one of ${shown}${more}.`;
1015
1634
  }
1016
1635
  function bindPropertyRef(input, onCommit, opts) {
1017
1636
  const list = document.createElement("datalist");
@@ -1032,7 +1651,7 @@ function bindPropertyRef(input, onCommit, opts) {
1032
1651
  const problem = v === "" ? void 0 : propertyRefProblem(v, opts);
1033
1652
  input.classList.toggle("illegal", !!problem);
1034
1653
  if (problem) input.title = problem;
1035
- 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 ")) {
1036
1655
  if (opts.hint) input.title = opts.hint;
1037
1656
  else input.removeAttribute("title");
1038
1657
  }
@@ -1075,9 +1694,9 @@ function openGameIdEditor(opts) {
1075
1694
  const input = el("input", "shell-id-input");
1076
1695
  input.type = "text";
1077
1696
  input.value = opts.value;
1078
- input.placeholder = opts.derived || "<game-id>";
1697
+ input.placeholder = opts.derived || "game-id";
1079
1698
  input.spellcheck = false;
1080
- 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.");
1081
1700
  const actions = el("div", "shell-id-actions");
1082
1701
  const reset = el("button", "shell-id-reset", "Reset to auto");
1083
1702
  const set = el("button", "shell-id-set", "Set");
@@ -1139,12 +1758,13 @@ function foldVc(shards, keys) {
1139
1758
  return out;
1140
1759
  }
1141
1760
  function vcBadgeFor(s) {
1142
- if (s?.lockedBy?.length) return { glyph: icon.locked, cls: "vc-locked", title: `Locked by ${s.lockedBy.join(", ")}` };
1143
- if (s?.outOfDate) return { glyph: icon.down, cls: "vc-stale", title: "Out of date - a newer version is on the server" };
1144
- if (s?.checkedOutByMe) return { glyph: icon.checkedOut, cls: "vc-mine", title: "Checked out by you" };
1145
- if (s?.dirty) return { glyph: icon.modified, cls: "vc-dirty", title: "Modified - uncommitted local changes" };
1146
- if (s?.untracked) return { glyph: icon.untracked, cls: "vc-new", title: "New - not yet committed" };
1147
- 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.");
1148
1768
  return null;
1149
1769
  }
1150
1770
  function paintVcBadges(root, shards) {
@@ -1152,7 +1772,7 @@ function paintVcBadges(root, shards) {
1152
1772
  host.querySelector(":scope > .vc-badge")?.remove();
1153
1773
  const badge = vcBadgeFor(foldVc(shards, host.dataset["vc"]));
1154
1774
  if (!badge) return;
1155
- 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));
1156
1776
  host.append(span);
1157
1777
  });
1158
1778
  }
@@ -1175,8 +1795,8 @@ function lockNotice(holders) {
1175
1795
  return el(
1176
1796
  "div",
1177
1797
  { className: "vc-lock" },
1178
- el("span", { className: "vc-lock-glyph", text: icon.locked }),
1179
- 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.` })
1180
1800
  );
1181
1801
  }
1182
1802
 
@@ -1184,41 +1804,36 @@ function lockNotice(holders) {
1184
1804
  function askIdentity(opts = {}) {
1185
1805
  return new Promise((resolve) => {
1186
1806
  const seed = opts.current ?? opts.suggested;
1187
- const name = el("input", "shell-ident-input");
1807
+ const name = el("input", "field shell-ident-input");
1188
1808
  name.placeholder = "Your name";
1189
1809
  name.value = seed?.name ?? "";
1190
- const email = el("input", "shell-ident-input");
1191
- email.placeholder = "Email (optional)";
1810
+ const email = el("input", "field shell-ident-input");
1811
+ email.placeholder = "Optional";
1192
1812
  email.type = "email";
1193
1813
  email.value = seed?.email ?? "";
1194
- const skip = el("button", "shell-ident-btn", "Skip");
1195
- 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");
1196
1816
  skip.type = "button";
1197
1817
  save.type = "button";
1198
- const dialog = el(
1199
- "dialog",
1200
- "shell-ident",
1201
- el("div", "shell-ident-title", "Who is working here?"),
1202
- el(
1203
- "p",
1204
- "shell-ident-hint",
1205
- "Stamped on the comments you write. Kept in this app, never in the project, and changeable later."
1206
- ),
1207
- el("div", "shell-ident-fields", name, email),
1208
- el("div", "shell-ident-actions", skip, save)
1209
- );
1210
- document.body.append(dialog);
1211
1818
  let done = false;
1212
1819
  const finish = (keep) => {
1213
1820
  if (done) return;
1214
1821
  done = true;
1215
1822
  const typed = name.value.trim();
1216
1823
  const value = email.value.trim();
1217
- const answer = keep && typed !== "" ? value === "" ? { name: typed } : { name: typed, email: value } : void 0;
1218
- dialog.close();
1219
- dialog.remove();
1824
+ const answer = keep && typed !== "" ? value === "" ? { name: typed } : { name: typed, email: value } : opts.neverAskAgainOnSkip === true ? { name: "" } : void 0;
1220
1825
  resolve(answer);
1826
+ frame.close();
1221
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);
1222
1837
  skip.addEventListener("click", () => finish(false));
1223
1838
  save.addEventListener("click", () => finish(true));
1224
1839
  name.addEventListener("keydown", (e) => {
@@ -1227,11 +1842,19 @@ function askIdentity(opts = {}) {
1227
1842
  email.addEventListener("keydown", (e) => {
1228
1843
  if (e.key === "Enter") finish(true);
1229
1844
  });
1230
- dialog.addEventListener("close", () => finish(false));
1231
- dialog.showModal();
1845
+ frame.open();
1232
1846
  name.focus();
1233
1847
  });
1234
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
+ }
1235
1858
 
1236
1859
  // src/notes-editor.ts
1237
1860
  var EDITOR_ONLY = "";
@@ -1244,10 +1867,10 @@ var CLASS_LABEL = {
1244
1867
  };
1245
1868
  var label = (cls) => cls === EDITOR_ONLY ? "Note (editor-only)" : CLASS_LABEL[cls] ?? cls[0].toUpperCase() + cls.slice(1);
1246
1869
  var PLACEHOLDER = {
1247
- vo: "direction for the voice actor / director",
1248
- loc: "context for translators"
1870
+ vo: "Direction for the voice actor or director",
1871
+ loc: "Context for translators"
1249
1872
  };
1250
- 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";
1251
1874
  function openNotesEditor(opts) {
1252
1875
  const { notes } = opts;
1253
1876
  const seed = /* @__PURE__ */ new Map();
@@ -1301,7 +1924,7 @@ ${line.text}` : line.text);
1301
1924
  { className: "shell-notes-class" },
1302
1925
  el("label", "shell-notes-caption", label(cls))
1303
1926
  );
1304
- const area = el("textarea", "shell-notes-text");
1927
+ const area = el("textarea", "field shell-notes-text");
1305
1928
  area.value = areas.get(cls)?.value ?? seed.get(cls) ?? "";
1306
1929
  area.rows = 4;
1307
1930
  area.placeholder = placeholder(cls);
@@ -1327,29 +1950,24 @@ ${line.text}` : line.text);
1327
1950
  }
1328
1951
  };
1329
1952
  render();
1330
- const done = el("button", "shell-notes-btn primary", "Done");
1331
- const dialog = el(
1332
- "dialog",
1333
- { className: "shell-notes" },
1334
- el("div", { className: "shell-notes-title", text: `Notes: ${opts.subject}` }),
1335
- body,
1336
- el("div", { className: "shell-notes-actions" }, done)
1337
- );
1338
- dialog.setAttribute("aria-label", `Notes: ${opts.subject}`);
1339
- document.body.append(dialog);
1953
+ const done = el("button", "btn primary shell-notes-btn", "Done");
1954
+ done.type = "button";
1340
1955
  const before = JSON.stringify(notes.own);
1341
1956
  let closed = false;
1342
- const close = () => {
1957
+ const commit = () => {
1343
1958
  if (closed) return;
1344
1959
  closed = true;
1345
1960
  const next = value();
1346
1961
  if (JSON.stringify(next) !== before) opts.save(next);
1347
- dialog.close();
1348
- dialog.remove();
1349
1962
  };
1350
- done.addEventListener("click", close);
1351
- dialog.addEventListener("close", close);
1352
- 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();
1353
1971
  areas.get(shown[0] ?? EDITOR_ONLY)?.focus();
1354
1972
  }
1355
1973
 
@@ -1421,7 +2039,7 @@ function openComments(opts) {
1421
2039
  el("span", { className: "shell-cmt-ts", text: stamp(message.ts) })
1422
2040
  );
1423
2041
  if (opts.deleteMessage && message.deleted !== true) {
1424
- const bin = el("button", { className: "shell-cmt-del", text: "\u2715" });
2042
+ const bin = el("button", { className: "shell-cmt-del" }, iconNode("close", 12));
1425
2043
  bin.dataset.tip = "Delete this comment";
1426
2044
  bin.setAttribute("aria-label", "Delete this comment");
1427
2045
  bin.addEventListener("click", () => opts.deleteMessage?.(thread.id, index));
@@ -1486,37 +2104,65 @@ var clock = (ms) => {
1486
2104
  var estimable = (done, total, elapsedMs) => done > 0 && total > 0 && elapsedMs > 1e3 && done / total > 0.1;
1487
2105
  function mountJobProgress(host, opts) {
1488
2106
  const units = opts.units ?? "runs";
2107
+ const pinned = opts.indeterminate === true;
2108
+ const cancellableByDefault = opts.cancellable ?? opts.onCancel !== void 0;
1489
2109
  const fill = el("i", { className: "job-fill" });
1490
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");
1491
2114
  const what = el("span", { className: "job-label" });
1492
2115
  const count = el("span", { className: "job-count" });
1493
2116
  const time = el("span", { className: "job-time" });
1494
2117
  const cancel = el("button", { className: "job-cancel", text: "Cancel" });
2118
+ cancel.type = "button";
1495
2119
  const strip = el("div", { className: "job-strip" }, what, bar, count, time, cancel);
1496
2120
  strip.hidden = true;
1497
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
+ };
1498
2133
  const view = {
1499
2134
  element: strip,
1500
2135
  get visible() {
1501
2136
  return !strip.hidden;
1502
2137
  },
1503
- begin(label2) {
2138
+ get indeterminate() {
2139
+ return indeterminate;
2140
+ },
2141
+ begin(label2, run) {
2142
+ runPinned = run?.indeterminate ?? pinned;
2143
+ const cancellable = run?.cancellable ?? cancellableByDefault;
1504
2144
  what.textContent = label2 ?? "Running\u2026";
1505
2145
  count.textContent = "";
1506
2146
  time.textContent = "";
1507
- fill.style.width = "0%";
2147
+ cancel.hidden = !cancellable;
1508
2148
  cancel.disabled = false;
1509
2149
  cancel.textContent = "Cancel";
2150
+ setMode(true);
1510
2151
  strip.hidden = false;
1511
2152
  },
1512
2153
  update(done, total, elapsedMs) {
1513
2154
  if (strip.hidden) strip.hidden = false;
1514
- const pct = total > 0 ? Math.min(100, done / total * 100) : 0;
1515
- fill.style.width = `${pct}%`;
1516
- count.textContent = total > 0 ? `${done} / ${total} ${units}` : `${done} ${units}`;
1517
- 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)) {
1518
2164
  const remaining = elapsedMs / done * (total - done);
1519
- time.textContent = `${clock(elapsedMs)} elapsed \xB7 about ${clock(remaining)} left`;
2165
+ time.textContent = `${clock(elapsedMs)} elapsed, about ${clock(remaining)} left`;
1520
2166
  } else {
1521
2167
  time.textContent = clock(elapsedMs);
1522
2168
  }
@@ -1528,8 +2174,10 @@ function mountJobProgress(host, opts) {
1528
2174
  cancel.addEventListener("click", () => {
1529
2175
  cancel.disabled = true;
1530
2176
  cancel.textContent = "Stopping\u2026";
1531
- opts.onCancel();
2177
+ opts.onCancel?.();
1532
2178
  });
2179
+ cancel.hidden = !cancellableByDefault;
2180
+ setMode(true);
1533
2181
  return view;
1534
2182
  }
1535
2183
 
@@ -1655,11 +2303,14 @@ function staleBar(opts) {
1655
2303
  className: "stale-bar-msg",
1656
2304
  text: `${opts.subject} changed in the editor. ${opts.advice ?? "Restart to play the new version."}`
1657
2305
  }),
1658
- el("button", {
1659
- className: "stale-bar-go",
1660
- text: `${icon.restart} ${label2}`,
1661
- onClick: () => opts.onRestart()
1662
- })
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
+ )
1663
2314
  );
1664
2315
  }
1665
2316
 
@@ -1701,14 +2352,14 @@ function dismissToast() {
1701
2352
  }
1702
2353
 
1703
2354
  // src/stepper.ts
2355
+ var STEP_ICON = 12;
1704
2356
  function renderStepperBar(host, opts) {
1705
2357
  const { items } = opts;
1706
2358
  const close = opts.onClose === void 0 ? null : el("button", {
1707
2359
  className: "stepbar-nav stepbar-close",
1708
- text: icon.close,
1709
2360
  tip: opts.closeTip ?? "Close",
1710
2361
  onClick: opts.onClose
1711
- });
2362
+ }, iconNode("close", STEP_ICON));
1712
2363
  const actions = (opts.actions ?? []).filter((a) => a !== null && a !== void 0);
1713
2364
  if (items.length === 0) {
1714
2365
  if (opts.empty === void 0) {
@@ -1732,17 +2383,15 @@ function renderStepperBar(host, opts) {
1732
2383
  const nav = items.length > 1 ? [
1733
2384
  el("button", {
1734
2385
  className: "stepbar-nav",
1735
- text: icon.back,
1736
2386
  tip: opts.tips?.prev ?? "Previous",
1737
2387
  onClick: () => step(-1)
1738
- }),
2388
+ }, iconNode("back", STEP_ICON)),
1739
2389
  el("span", { className: "stepbar-of", text: `${index + 1}/${items.length}` }),
1740
2390
  el("button", {
1741
2391
  className: "stepbar-nav",
1742
- text: icon.forward,
1743
2392
  tip: opts.tips?.next ?? "Next",
1744
2393
  onClick: () => step(1)
1745
- })
2394
+ }, iconNode("forward", STEP_ICON))
1746
2395
  ] : [];
1747
2396
  host.replaceChildren(
1748
2397
  el("span", { className: `stepbar-count ${opts.tone ?? "muted"}`, text: String(items.length) }),
@@ -1759,19 +2408,383 @@ function renderStepperBar(host, opts) {
1759
2408
  );
1760
2409
  }
1761
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
+
1762
2775
  // src/nav-history.ts
1763
2776
  function createNavHistory(opts) {
1764
2777
  const cap = opts.cap ?? 100;
1765
2778
  const usable = opts.usable ?? (() => true);
1766
2779
  let past = [];
1767
2780
  let ahead = [];
1768
- const step = (from, to, current2) => {
2781
+ const step = (from, to, current3) => {
1769
2782
  for (; ; ) {
1770
2783
  const p = from.pop();
1771
2784
  if (p === void 0) return void 0;
1772
2785
  if (!usable(p)) continue;
1773
- if (opts.same(p, current2)) continue;
1774
- to.push(current2);
2786
+ if (opts.same(p, current3)) continue;
2787
+ to.push(current3);
1775
2788
  return p;
1776
2789
  }
1777
2790
  };
@@ -1783,11 +2796,11 @@ function createNavHistory(opts) {
1783
2796
  else past.push(place2);
1784
2797
  if (past.length > cap) past = past.slice(past.length - cap);
1785
2798
  },
1786
- back(current2) {
1787
- return step(past, ahead, current2);
2799
+ back(current3) {
2800
+ return step(past, ahead, current3);
1788
2801
  },
1789
- forward(current2) {
1790
- return step(ahead, past, current2);
2802
+ forward(current3) {
2803
+ return step(ahead, past, current3);
1791
2804
  },
1792
2805
  canBack() {
1793
2806
  return past.length > 0;
@@ -1798,18 +2811,18 @@ function createNavHistory(opts) {
1798
2811
  };
1799
2812
  }
1800
2813
  function historyNav(onBack, onForward) {
1801
- const btn = (glyph, tip2, onClick) => {
2814
+ const btn = (name, tip2, onClick) => {
1802
2815
  const b = document.createElement("button");
1803
2816
  b.type = "button";
1804
2817
  b.className = "shell-histnav-btn";
1805
- b.textContent = glyph;
2818
+ b.append(iconNode(name));
1806
2819
  b.dataset["tip"] = tip2;
1807
2820
  b.setAttribute("aria-label", tip2);
1808
2821
  b.addEventListener("click", onClick);
1809
2822
  return b;
1810
2823
  };
1811
- const back = btn("\u2190", "Back", onBack);
1812
- const forward = btn("\u2192", "Forward", onForward);
2824
+ const back = btn("arrowLeft", "Back", onBack);
2825
+ const forward = btn("arrowRight", "Forward", onForward);
1813
2826
  const el2 = document.createElement("span");
1814
2827
  el2.className = "shell-histnav";
1815
2828
  el2.append(back, forward);
@@ -1823,13 +2836,21 @@ function historyNav(onBack, onForward) {
1823
2836
  }
1824
2837
 
1825
2838
  exports.EDIT_MENU = EDIT_MENU;
2839
+ exports.FILE_MENU = FILE_MENU;
1826
2840
  exports.GO_MENU = GO_MENU;
2841
+ exports.ICON_NAMES = ICON_NAMES;
2842
+ exports.ICON_SIZE = ICON_SIZE;
1827
2843
  exports.JOB_PROGRESS = JOB_PROGRESS;
1828
2844
  exports.PALETTE = PALETTE;
1829
2845
  exports.PALETTE_SIZE = PALETTE_SIZE;
1830
2846
  exports.PANE_MENU = PANE_MENU;
2847
+ exports.PLAY_MENU = PLAY_MENU;
1831
2848
  exports.PROPERTY_NAME_HINT = PROPERTY_NAME_HINT;
2849
+ exports.PROPERTY_TYPES = PROPERTY_TYPES;
2850
+ exports.PUBLISH_MENU = PUBLISH_MENU;
1832
2851
  exports.RESERVED_PROPERTY_NAMES = RESERVED_PROPERTY_NAMES;
2852
+ exports.REVIEW_MENU = REVIEW_MENU;
2853
+ exports.VIEW_MENU = VIEW_MENU;
1833
2854
  exports.askIdentity = askIdentity;
1834
2855
  exports.bindPropertyName = bindPropertyName;
1835
2856
  exports.bindPropertyRef = bindPropertyRef;
@@ -1838,52 +2859,80 @@ exports.closeWithExit = closeWithExit;
1838
2859
  exports.colourFor = colourFor;
1839
2860
  exports.colourIndex = colourIndex;
1840
2861
  exports.confirmDialog = confirmDialog;
2862
+ exports.copyWithFeedback = copyWithFeedback;
2863
+ exports.createFloating = createFloating;
1841
2864
  exports.createNavHistory = createNavHistory;
1842
2865
  exports.createSaveController = createSaveController;
2866
+ exports.debounce = debounce;
2867
+ exports.defaultControl = defaultControl;
2868
+ exports.dialogFrame = dialogFrame;
1843
2869
  exports.dismissToast = dismissToast;
1844
2870
  exports.dupGuard = dupGuard;
1845
2871
  exports.el = el;
1846
2872
  exports.expandableRow = expandableRow;
2873
+ exports.feedUpdaterDownloadProgress = feedUpdaterDownloadProgress;
1847
2874
  exports.firstIllegalPropertyName = firstIllegalPropertyName;
1848
2875
  exports.focusNewRow = focusNewRow;
1849
2876
  exports.foldVc = foldVc;
2877
+ exports.followButton = followButton;
2878
+ exports.formatCount = formatCount;
1850
2879
  exports.gameIdify = gameIdify;
1851
2880
  exports.hideTip = hideTip;
1852
2881
  exports.historyNav = historyNav;
1853
2882
  exports.icon = icon;
1854
2883
  exports.iconBtn = iconBtn;
2884
+ exports.iconHtml = iconHtml;
2885
+ exports.iconNameOfGlyph = iconNameOfGlyph;
2886
+ exports.iconNode = iconNode;
1855
2887
  exports.iconSvg = iconSvg;
1856
2888
  exports.initTooltips = initTooltips;
1857
2889
  exports.isCaseOnlyPropertyName = isCaseOnlyPropertyName;
2890
+ exports.isEditableTarget = isEditableTarget;
2891
+ exports.isIconName = isIconName;
1858
2892
  exports.isValidGameId = isValidGameId;
1859
2893
  exports.isValidPropertyName = isValidPropertyName;
1860
2894
  exports.labelled = labelled;
2895
+ exports.linkAddress = linkAddress;
2896
+ exports.linkStatusClass = linkStatusClass;
2897
+ exports.linkStatusTip = linkStatusTip;
1861
2898
  exports.lockControls = lockControls;
1862
2899
  exports.lockNotice = lockNotice;
1863
2900
  exports.mountJobProgress = mountJobProgress;
2901
+ exports.mountLinkStatus = mountLinkStatus;
1864
2902
  exports.mountPaneShell = mountPaneShell;
2903
+ exports.mountPropertyList = mountPropertyList;
1865
2904
  exports.mountSettingsDialog = mountSettingsDialog;
2905
+ exports.mountWelcome = mountWelcome;
1866
2906
  exports.moveItem = moveItem;
1867
2907
  exports.openAnchoredPanel = openAnchoredPanel;
1868
2908
  exports.openComments = openComments;
2909
+ exports.openContextMenu = openContextMenu;
1869
2910
  exports.openGameIdEditor = openGameIdEditor;
1870
2911
  exports.openNotesEditor = openNotesEditor;
2912
+ exports.openPopover = openPopover;
1871
2913
  exports.paintVcBadges = paintVcBadges;
1872
2914
  exports.pinButton = pinButton;
1873
2915
  exports.placeAnchored = placeAnchored;
2916
+ exports.plural = plural;
1874
2917
  exports.propertyNameProblem = propertyNameProblem;
1875
2918
  exports.propertyNameify = propertyNameify;
1876
2919
  exports.propertyRefProblem = propertyRefProblem;
2920
+ exports.relativeTime = relativeTime;
1877
2921
  exports.renderStepperBar = renderStepperBar;
1878
2922
  exports.revalidatePropertyRefs = revalidatePropertyRefs;
1879
2923
  exports.revealRow = revealRow;
2924
+ exports.revealRowWhenReady = revealRowWhenReady;
1880
2925
  exports.saveIndicator = saveIndicator;
1881
2926
  exports.showAbout = showAbout;
2927
+ exports.showUpdaterDialog = showUpdaterDialog;
2928
+ exports.stageChips = stageChips;
1882
2929
  exports.staleBar = staleBar;
1883
2930
  exports.tagChips = tagChips;
1884
2931
  exports.tipAt = tipAt;
1885
2932
  exports.tipBold = tipBold;
1886
2933
  exports.toast = toast;
2934
+ exports.toolWindowHead = toolWindowHead;
1887
2935
  exports.vcBadgeFor = vcBadgeFor;
2936
+ exports.wireReorder = wireReorder;
1888
2937
  //# sourceMappingURL=index.cjs.map
1889
2938
  //# sourceMappingURL=index.cjs.map