@wildwinter/app-shell 0.38.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/LICENSES.md +62 -0
  2. package/README.md +8 -0
  3. package/dist/about.css +17 -38
  4. package/dist/about.css.map +1 -1
  5. package/dist/anchored.css +9 -9
  6. package/dist/anchored.css.map +1 -1
  7. package/dist/app-store.cjs +15 -0
  8. package/dist/app-store.cjs.map +1 -1
  9. package/dist/app-store.d.cts +25 -1
  10. package/dist/app-store.d.ts +25 -1
  11. package/dist/app-store.js +14 -1
  12. package/dist/app-store.js.map +1 -1
  13. package/dist/chunk-2ECJUTSX.js +97 -0
  14. package/dist/chunk-2ECJUTSX.js.map +1 -0
  15. package/dist/chunk-DS4XXAZS.js +56 -0
  16. package/dist/chunk-DS4XXAZS.js.map +1 -0
  17. package/dist/chunk-EHR2KBZC.js +82 -0
  18. package/dist/chunk-EHR2KBZC.js.map +1 -0
  19. package/dist/chunk-IU7EY6BI.js +26 -0
  20. package/dist/chunk-IU7EY6BI.js.map +1 -0
  21. package/dist/chunk-OPZPFOPS.js +66 -0
  22. package/dist/chunk-OPZPFOPS.js.map +1 -0
  23. package/dist/chunk-P4G4ERG6.js +60 -0
  24. package/dist/chunk-P4G4ERG6.js.map +1 -0
  25. package/dist/chunk-P6WLAEKZ.js +71 -0
  26. package/dist/chunk-P6WLAEKZ.js.map +1 -0
  27. package/dist/chunk-R3NXPHRZ.js +534 -0
  28. package/dist/chunk-R3NXPHRZ.js.map +1 -0
  29. package/dist/chunk-SDOKZTAR.js +87 -0
  30. package/dist/chunk-SDOKZTAR.js.map +1 -0
  31. package/dist/chunk-SEWJLUOD.js +70 -0
  32. package/dist/chunk-SEWJLUOD.js.map +1 -0
  33. package/dist/chunk-UG2P777F.js +587 -0
  34. package/dist/chunk-UG2P777F.js.map +1 -0
  35. package/dist/chunk-YRFJLJUH.js +55 -0
  36. package/dist/chunk-YRFJLJUH.js.map +1 -0
  37. package/dist/chunk-YWMSQFXX.js +146 -0
  38. package/dist/chunk-YWMSQFXX.js.map +1 -0
  39. package/dist/comments.css +16 -14
  40. package/dist/comments.css.map +1 -1
  41. package/dist/confirm.css +2 -55
  42. package/dist/confirm.css.map +1 -1
  43. package/dist/context-menu.cjs +250 -0
  44. package/dist/context-menu.cjs.map +1 -0
  45. package/dist/context-menu.css +64 -0
  46. package/dist/context-menu.css.map +1 -0
  47. package/dist/context-menu.d.cts +25 -0
  48. package/dist/context-menu.d.ts +25 -0
  49. package/dist/context-menu.js +5 -0
  50. package/dist/context-menu.js.map +1 -0
  51. package/dist/controls.css +214 -0
  52. package/dist/controls.css.map +1 -0
  53. package/dist/dialog.cjs +222 -0
  54. package/dist/dialog.cjs.map +1 -0
  55. package/dist/dialog.css +56 -0
  56. package/dist/dialog.css.map +1 -0
  57. package/dist/dialog.d.cts +28 -0
  58. package/dist/dialog.d.ts +28 -0
  59. package/dist/dialog.js +5 -0
  60. package/dist/dialog.js.map +1 -0
  61. package/dist/floating.cjs +83 -0
  62. package/dist/floating.cjs.map +1 -0
  63. package/dist/floating.d.cts +25 -0
  64. package/dist/floating.d.ts +25 -0
  65. package/dist/floating.js +4 -0
  66. package/dist/floating.js.map +1 -0
  67. package/dist/identity.css +15 -50
  68. package/dist/identity.css.map +1 -1
  69. package/dist/index.cjs +1553 -260
  70. package/dist/index.cjs.map +1 -1
  71. package/dist/index.d.cts +385 -152
  72. package/dist/index.d.ts +385 -152
  73. package/dist/index.js +264 -823
  74. package/dist/index.js.map +1 -1
  75. package/dist/job.css +24 -1
  76. package/dist/job.css.map +1 -1
  77. package/dist/keys.cjs +155 -0
  78. package/dist/keys.cjs.map +1 -0
  79. package/dist/keys.css +41 -0
  80. package/dist/keys.css.map +1 -0
  81. package/dist/keys.d.cts +57 -0
  82. package/dist/keys.d.ts +57 -0
  83. package/dist/keys.js +3 -0
  84. package/dist/keys.js.map +1 -0
  85. package/dist/link-status.cjs +430 -0
  86. package/dist/link-status.cjs.map +1 -0
  87. package/dist/link-status.css +76 -0
  88. package/dist/link-status.css.map +1 -0
  89. package/dist/link-status.d.cts +53 -0
  90. package/dist/link-status.d.ts +53 -0
  91. package/dist/link-status.js +5 -0
  92. package/dist/link-status.js.map +1 -0
  93. package/dist/menu.cjs +62 -1
  94. package/dist/menu.cjs.map +1 -1
  95. package/dist/menu.d.cts +88 -1
  96. package/dist/menu.d.ts +88 -1
  97. package/dist/menu.js +1 -1
  98. package/dist/notes-editor.css +9 -52
  99. package/dist/notes-editor.css.map +1 -1
  100. package/dist/pane-shell.css +23 -5
  101. package/dist/pane-shell.css.map +1 -1
  102. package/dist/problems.cjs +61 -0
  103. package/dist/problems.cjs.map +1 -0
  104. package/dist/problems.d.cts +57 -0
  105. package/dist/problems.d.ts +57 -0
  106. package/dist/problems.js +3 -0
  107. package/dist/problems.js.map +1 -0
  108. package/dist/property-list-D3VZCJzi.d.cts +162 -0
  109. package/dist/property-list-D3VZCJzi.d.ts +162 -0
  110. package/dist/property-list.cjs +784 -0
  111. package/dist/property-list.cjs.map +1 -0
  112. package/dist/property-list.d.cts +1 -0
  113. package/dist/property-list.d.ts +1 -0
  114. package/dist/property-list.js +4 -0
  115. package/dist/property-list.js.map +1 -0
  116. package/dist/save.css +1 -1
  117. package/dist/save.css.map +1 -1
  118. package/dist/settings.css +62 -18
  119. package/dist/settings.css.map +1 -1
  120. package/dist/stale.css +4 -1
  121. package/dist/stale.css.map +1 -1
  122. package/dist/stepper.css +47 -3
  123. package/dist/stepper.css.map +1 -1
  124. package/dist/toast.css +5 -5
  125. package/dist/toast.css.map +1 -1
  126. package/dist/tokens.css +20 -0
  127. package/dist/tokens.css.map +1 -1
  128. package/dist/tool-window.cjs +63 -0
  129. package/dist/tool-window.cjs.map +1 -1
  130. package/dist/tool-window.css +8 -4
  131. package/dist/tool-window.css.map +1 -1
  132. package/dist/tool-window.d.cts +67 -1
  133. package/dist/tool-window.d.ts +67 -1
  134. package/dist/tool-window.js +63 -1
  135. package/dist/tool-window.js.map +1 -1
  136. package/dist/tooltip.css +3 -3
  137. package/dist/tooltip.css.map +1 -1
  138. package/dist/updater-view.cjs +283 -0
  139. package/dist/updater-view.cjs.map +1 -0
  140. package/dist/updater-view.d.cts +23 -0
  141. package/dist/updater-view.d.ts +23 -0
  142. package/dist/updater-view.js +6 -0
  143. package/dist/updater-view.js.map +1 -0
  144. package/dist/updater.cjs +12 -16
  145. package/dist/updater.cjs.map +1 -1
  146. package/dist/updater.css +44 -0
  147. package/dist/updater.css.map +1 -0
  148. package/dist/updater.js +12 -16
  149. package/dist/updater.js.map +1 -1
  150. package/dist/util.cjs +78 -0
  151. package/dist/util.cjs.map +1 -0
  152. package/dist/util.d.cts +37 -0
  153. package/dist/util.d.ts +37 -0
  154. package/dist/util.js +3 -0
  155. package/dist/util.js.map +1 -0
  156. package/dist/vc.css +4 -3
  157. package/dist/vc.css.map +1 -1
  158. package/dist/welcome.cjs +215 -0
  159. package/dist/welcome.cjs.map +1 -0
  160. package/dist/welcome.css +112 -0
  161. package/dist/welcome.css.map +1 -0
  162. package/dist/welcome.d.cts +55 -0
  163. package/dist/welcome.d.ts +55 -0
  164. package/dist/welcome.js +4 -0
  165. package/dist/welcome.js.map +1 -0
  166. package/package.json +72 -2
  167. package/dist/chunk-QDPFU4WX.js +0 -43
  168. package/dist/chunk-QDPFU4WX.js.map +0 -1
package/dist/index.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");
@@ -202,18 +403,18 @@ function tagChips(holder, onChange) {
202
403
  input.placeholder = "Add value";
203
404
  input.spellcheck = false;
204
405
  const makeChip = (v) => {
205
- const chip = el("span", "shell-tag", v);
206
- const x = el("button", "shell-tag-x", "\u2715");
406
+ const chip2 = el("span", "shell-tag", v);
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
- chip.remove();
413
+ chip2.remove();
213
414
  onChange?.();
214
415
  });
215
- chip.append(x);
216
- return chip;
416
+ chip2.append(x);
417
+ return chip2;
217
418
  };
218
419
  const commit = () => {
219
420
  const v = input.value.trim();
@@ -235,6 +436,153 @@ 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 chip2 = el("span", "shell-tag", `${i + 1}. ${v}`);
459
+ chip2.dataset["stage"] = v;
460
+ chip2.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
+ chip2.append(x);
494
+ wrap.insertBefore(chip2, 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
+ }
553
+ function metaLine(parts) {
554
+ const row = el("span", "shell-meta");
555
+ for (const part of parts) {
556
+ if (part === null || part === void 0 || part === "") continue;
557
+ const seg = el("span", "shell-meta-part");
558
+ seg.append(part);
559
+ row.append(seg);
560
+ }
561
+ return row;
562
+ }
563
+ function breadcrumb(parts) {
564
+ const trail = el("nav", "shell-crumbs");
565
+ trail.setAttribute("aria-label", "Where this is");
566
+ parts.forEach((part, i) => {
567
+ const crumb = typeof part === "string" ? { label: part } : part;
568
+ const last = i === parts.length - 1;
569
+ if (i > 0) {
570
+ const sep = iconNode("forward", 12);
571
+ sep.classList.add("shell-crumb-sep");
572
+ trail.append(sep);
573
+ }
574
+ if (crumb.onClick && !last) {
575
+ const b = el("button", { className: "shell-crumb shell-crumb-link", text: crumb.label, onClick: crumb.onClick });
576
+ b.type = "button";
577
+ trail.append(b);
578
+ } else {
579
+ const s = el("span", "shell-crumb", crumb.label);
580
+ if (last) s.setAttribute("aria-current", "location");
581
+ trail.append(s);
582
+ }
583
+ });
584
+ return trail;
585
+ }
238
586
 
239
587
  // src/colour.ts
240
588
  var PALETTE_SIZE = 12;
@@ -256,78 +604,148 @@ function colourFor(name) {
256
604
  return PALETTE[colourIndex(name)];
257
605
  }
258
606
 
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"
607
+ // src/keys.ts
608
+ var override;
609
+ function setKeyPlatform(platform) {
610
+ override = platform;
611
+ }
612
+ function keyPlatform() {
613
+ if (override) return override;
614
+ if (typeof navigator !== "undefined") {
615
+ const s = `${navigator.platform ?? ""} ${navigator.userAgent ?? ""}`;
616
+ if (/mac|iphone|ipad/i.test(s)) return "mac";
617
+ if (/win/i.test(s)) return "win";
618
+ if (s.trim()) return "linux";
619
+ }
620
+ const p = typeof process !== "undefined" ? process.platform : void 0;
621
+ return p === "darwin" ? "mac" : p === "win32" ? "win" : "linux";
622
+ }
623
+ var MAC_ORDER = ["ctrl", "alt", "shift", "mod"];
624
+ var OTHER_ORDER = ["mod", "ctrl", "alt", "shift"];
625
+ var MAC_MOD = { ctrl: "\u2303", alt: "\u2325", shift: "\u21E7", mod: "\u2318" };
626
+ var OTHER_MOD = { ctrl: "Ctrl", alt: "Alt", shift: "Shift", mod: "Ctrl" };
627
+ var MODIFIER_ALIASES = {
628
+ mod: "mod",
629
+ cmd: "mod",
630
+ command: "mod",
631
+ meta: "mod",
632
+ super: "mod",
633
+ ctrl: "ctrl",
634
+ control: "ctrl",
635
+ alt: "alt",
636
+ option: "alt",
637
+ opt: "alt",
638
+ shift: "shift"
324
639
  };
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>'
640
+ var NAMED = {
641
+ enter: { mac: "\u21A9", other: "Enter" },
642
+ return: { mac: "\u21A9", other: "Enter" },
643
+ esc: { mac: "esc", other: "Esc" },
644
+ escape: { mac: "esc", other: "Esc" },
645
+ up: { mac: "\u2191", other: "Up" },
646
+ down: { mac: "\u2193", other: "Down" },
647
+ left: { mac: "\u2190", other: "Left" },
648
+ right: { mac: "\u2192", other: "Right" },
649
+ backspace: { mac: "\u232B", other: "Backspace" },
650
+ delete: { mac: "\u2326", other: "Del" },
651
+ del: { mac: "\u2326", other: "Del" },
652
+ tab: { mac: "\u21E5", other: "Tab" },
653
+ space: { mac: "Space", other: "Space" },
654
+ home: { mac: "\u2196", other: "Home" },
655
+ end: { mac: "\u2198", other: "End" },
656
+ pageup: { mac: "\u21DE", other: "PgUp" },
657
+ pagedown: { mac: "\u21DF", other: "PgDn" }
330
658
  };
659
+ var PORTABLE_MODS = /* @__PURE__ */ new Set(["mod", "ctrl", "alt", "shift"]);
660
+ function parse(combo) {
661
+ const parts = combo.split("+");
662
+ const mods = [];
663
+ let key;
664
+ for (let i = 0; i < parts.length; i++) {
665
+ const raw = parts[i] ?? "";
666
+ if (raw === "" && i > 0) {
667
+ key = "+";
668
+ continue;
669
+ }
670
+ const mod = MODIFIER_ALIASES[raw.toLowerCase()];
671
+ if (mod) {
672
+ if (!mods.includes(mod)) mods.push(mod);
673
+ continue;
674
+ }
675
+ key = raw;
676
+ }
677
+ return { mods, key };
678
+ }
679
+ function isKeyCombo(value) {
680
+ const plus = value.length > 2 && value.endsWith("++");
681
+ const parts = (plus ? value.slice(0, -2) : value).split("+");
682
+ let keys = plus ? 1 : 0;
683
+ for (const raw of parts) {
684
+ if (raw === "") return false;
685
+ const word = raw.toLowerCase();
686
+ if (PORTABLE_MODS.has(word)) continue;
687
+ if (raw.length === 1 || word in NAMED) {
688
+ keys++;
689
+ continue;
690
+ }
691
+ return false;
692
+ }
693
+ return keys === 1;
694
+ }
695
+ function keyLegend(key, mac) {
696
+ const named = NAMED[key.toLowerCase()];
697
+ if (named) return mac ? named.mac : named.other;
698
+ return key.length === 1 ? key.toUpperCase() : key;
699
+ }
700
+ function keyLabel(combo, platform = keyPlatform()) {
701
+ const mac = platform === "mac";
702
+ const { mods, key } = parse(combo);
703
+ const order = mac ? MAC_ORDER : OTHER_ORDER;
704
+ const words = order.filter((m) => mods.includes(m)).map((m) => (mac ? MAC_MOD : OTHER_MOD)[m]);
705
+ const seen = /* @__PURE__ */ new Set();
706
+ const modWords = words.filter((w) => seen.has(w) ? false : (seen.add(w), true));
707
+ const legend = key === void 0 ? [] : [keyLegend(key, mac)];
708
+ return mac ? [...modWords, ...legend].join("") : [...modWords, ...legend].join("+");
709
+ }
710
+ function keyLegends(combo, platform = keyPlatform()) {
711
+ if (platform === "mac") return [keyLabel(combo, platform)];
712
+ const { mods, key } = parse(combo);
713
+ const seen = /* @__PURE__ */ new Set();
714
+ const words = OTHER_ORDER.filter((m) => mods.includes(m)).map((m) => OTHER_MOD[m]).filter((w) => seen.has(w) ? false : (seen.add(w), true));
715
+ return key === void 0 ? words : [...words, keyLegend(key, false)];
716
+ }
717
+ function chip(legend) {
718
+ const k = document.createElement("kbd");
719
+ k.className = "shell-kbd";
720
+ k.textContent = legend;
721
+ return k;
722
+ }
723
+ function keyHint(combo, opts = {}) {
724
+ const platform = opts.platform ?? keyPlatform();
725
+ const legends = combo.trim().split(/\s+/).flatMap((c) => keyLegends(c, platform));
726
+ if (legends.length === 1) return chip(legends[0]);
727
+ const group = document.createElement("span");
728
+ group.className = "shell-keys";
729
+ for (const legend of legends) group.append(chip(legend));
730
+ return group;
731
+ }
732
+ function hintBar(items, opts = {}) {
733
+ const bar = document.createElement("div");
734
+ bar.className = "shell-hintbar";
735
+ for (const item of items) {
736
+ const hint = document.createElement("span");
737
+ hint.className = "shell-hint";
738
+ const label2 = document.createElement("span");
739
+ label2.className = "shell-hint-label";
740
+ label2.textContent = item.label;
741
+ hint.append(keyHint(item.keys, opts), label2);
742
+ bar.append(hint);
743
+ }
744
+ return bar;
745
+ }
746
+ function tipWithKey(text, combo, platform = keyPlatform()) {
747
+ return `${text} (${keyLabel(combo, platform)})`;
748
+ }
331
749
 
332
750
  // src/pane-shell.ts
333
751
  var DEFAULT_MAX = (w) => Math.min(640, w - 420);
@@ -341,6 +759,8 @@ function mountPaneShell(host, opts) {
341
759
  inspector: offered("inspector") && (opts.initial?.open?.inspector ?? true)
342
760
  };
343
761
  const width = { ...opts.initial?.width ?? {} };
762
+ const held = { nav: false, inspector: false };
763
+ const showing = (side) => open[side] && !held[side];
344
764
  const el2 = (tag, cls) => {
345
765
  const n = document.createElement(tag);
346
766
  if (cls) n.className = cls;
@@ -371,18 +791,29 @@ function mountPaneShell(host, opts) {
371
791
  host.append(root);
372
792
  if (!offered("nav")) navToggle.remove();
373
793
  if (!offered("inspector")) inspToggle.remove();
794
+ const defaultTip = (side, isOpen) => {
795
+ const text = `${isOpen ? "Hide" : "Show"} ${cfg[side].label ?? side}`;
796
+ const hint = cfg[side].shortcutHint;
797
+ if (!hint) return text;
798
+ return isKeyCombo(hint) ? tipWithKey(text, hint) : `${text} (${hint})`;
799
+ };
800
+ const tipFor = opts.tipFor ?? defaultTip;
374
801
  function applyToggleGlyph(side, button) {
375
802
  const isOpen = open[side];
376
- button.textContent = side === "nav" ? isOpen ? "\u2039" : "\u203A" : isOpen ? "\u203A" : "\u2039";
377
- const noun = cfg[side].label ?? side;
378
- const hint = cfg[side].shortcutHint ? ` (${cfg[side].shortcutHint})` : "";
379
- button.title = `${isOpen ? "Hide" : "Show"} ${noun}${hint}`;
380
- button.setAttribute("aria-label", button.title);
803
+ button.replaceChildren(iconNode(side === "nav" ? isOpen ? "back" : "forward" : isOpen ? "forward" : "back"));
804
+ const tip2 = tipFor(side, isOpen);
805
+ button.dataset["tip"] = tip2;
806
+ button.setAttribute("aria-label", tip2);
381
807
  button.setAttribute("aria-pressed", String(isOpen));
382
808
  }
809
+ ensureTooltipHost();
383
810
  function apply() {
384
- panes.classList.toggle(collapseClass.nav, !open.nav);
385
- panes.classList.toggle(collapseClass.inspector, !open.inspector);
811
+ for (const side of ["nav", "inspector"]) {
812
+ const collapsed = !showing(side);
813
+ panes.classList.toggle(collapseClass[side], collapsed);
814
+ const also = opts.collapseAlso?.[side];
815
+ if (also) panes.classList.toggle(also, collapsed);
816
+ }
386
817
  panes.style.setProperty(openVar.nav, width.nav !== void 0 ? `${width.nav}px` : cfg.nav.defaultWidth);
387
818
  panes.style.setProperty(openVar.inspector, width.inspector !== void 0 ? `${width.inspector}px` : cfg.inspector.defaultWidth);
388
819
  applyToggleGlyph("nav", navToggle);
@@ -401,6 +832,11 @@ function mountPaneShell(host, opts) {
401
832
  function togglePane(side) {
402
833
  if (offered(side)) setPaneOpen(side, !open[side]);
403
834
  }
835
+ function holdClosed(side, next) {
836
+ if (held[side] === next) return;
837
+ held[side] = next;
838
+ apply();
839
+ }
404
840
  function resetWidths() {
405
841
  delete width.nav;
406
842
  delete width.inspector;
@@ -410,9 +846,11 @@ function mountPaneShell(host, opts) {
410
846
  navToggle.addEventListener("click", () => togglePane("nav"));
411
847
  inspToggle.addEventListener("click", () => togglePane("inspector"));
412
848
  function beginResize(side, startEvent) {
413
- if (cfg[side].resizable === false || !open[side]) return;
849
+ if (cfg[side].resizable === false || !showing(side)) return;
414
850
  startEvent.preventDefault();
415
851
  const pane = side === "nav" ? navPane : inspPane;
852
+ const seam = side === "nav" ? navResizer : inspResizer;
853
+ seam.classList.add("dragging");
416
854
  const min = cfg[side].minWidth ?? 160;
417
855
  const max = cfg[side].maxWidth ?? DEFAULT_MAX;
418
856
  const startX = startEvent.clientX;
@@ -428,6 +866,7 @@ function mountPaneShell(host, opts) {
428
866
  window.removeEventListener("pointermove", onMove);
429
867
  window.removeEventListener("pointerup", onUp);
430
868
  document.body.classList.remove("pane-resizing");
869
+ seam.classList.remove("dragging");
431
870
  width[side] = latest;
432
871
  persist();
433
872
  };
@@ -457,7 +896,8 @@ function mountPaneShell(host, opts) {
457
896
  inspector: inspInner,
458
897
  togglePane,
459
898
  setPaneOpen,
460
- isOpen: (s) => open[s],
899
+ isOpen: showing,
900
+ holdClosed,
461
901
  resetWidths,
462
902
  state: snapshot,
463
903
  destroy: () => {
@@ -471,7 +911,8 @@ var EDIT_MENU = {
471
911
  undo: { label: "Undo", accelerator: "CmdOrCtrl+Z" },
472
912
  redo: { label: "Redo", accelerator: "Shift+CmdOrCtrl+Z" },
473
913
  duplicate: { label: "Duplicate", accelerator: "CmdOrCtrl+D" },
474
- find: { label: "Find\u2026", accelerator: "CmdOrCtrl+F" }
914
+ find: { label: "Find\u2026", accelerator: "CmdOrCtrl+F" },
915
+ replace: { label: "Replace\u2026", acceleratorMac: "Cmd+Alt+F", acceleratorOther: "Ctrl+H" }
475
916
  };
476
917
  var GO_MENU = {
477
918
  back: { label: "Back", acceleratorMac: "Ctrl+Cmd+Left", acceleratorOther: "Alt+Left" },
@@ -482,6 +923,91 @@ var PANE_MENU = {
482
923
  showInspector: { label: "Show Inspector", accelerator: "CmdOrCtrl+2" },
483
924
  resetView: { label: "Reset View" }
484
925
  };
926
+ var FILE_MENU = {
927
+ newProject: { label: "New Project\u2026", accelerator: "CmdOrCtrl+N" },
928
+ openProject: { label: "Open Project\u2026", accelerator: "CmdOrCtrl+O" },
929
+ openRecent: { label: "Open Recent" },
930
+ noRecents: { label: "No Recent Projects" },
931
+ clearRecents: { label: "Clear Recents" },
932
+ closeProject: { label: "Close Project" },
933
+ save: { label: "Save", accelerator: "CmdOrCtrl+S" },
934
+ saveAs: { label: "Save As\u2026", accelerator: "Shift+CmdOrCtrl+S" },
935
+ projectSettings: { label: "Project Settings\u2026", accelerator: "CmdOrCtrl+," }
936
+ };
937
+ var PLAY_MENU = {
938
+ liveLink: { label: "Live Link" }
939
+ };
940
+ var REVIEW_MENU = {
941
+ reviewFeedback: { label: "Review Feedback", accelerator: "CmdOrCtrl+Shift+R" },
942
+ nextFeedback: { label: "Next Feedback", accelerator: "F8" },
943
+ previousFeedback: { label: "Previous Feedback", accelerator: "Shift+F8" },
944
+ coverageTest: { label: "Coverage Test\u2026", accelerator: "Shift+CmdOrCtrl+C" },
945
+ findPropertyUsage: { label: "Find Property Usage\u2026" },
946
+ showResolvedComments: { label: "Show Resolved Comments" }
947
+ };
948
+ var PUBLISH_MENU = {
949
+ playableHtml: { label: "Publish Playable HTML\u2026" },
950
+ bundle: { label: "Publish Bundle", accelerator: "Shift+CmdOrCtrl+B" },
951
+ autoRebuild: { label: "Auto Rebuild" }
952
+ };
953
+ var VIEW_MENU = {
954
+ projectOverview: { label: "Project Overview" },
955
+ upALevel: { label: "Up a Level", accelerator: "CmdOrCtrl+[" },
956
+ colourTheme: { label: "Colour Theme" }
957
+ };
958
+
959
+ // src/problems.ts
960
+ function problemName(p, dangling = false) {
961
+ if (p.title) return `\u201C${p.title}\u201D`;
962
+ if (dangling && p.where) return `(id ${p.where})`;
963
+ return "This";
964
+ }
965
+ function describeProblem(problem, table = {}) {
966
+ const entry = problem.code ? table[problem.code] : void 0;
967
+ if (entry) {
968
+ const copy = entry(problem);
969
+ const text = copy.dangling && !problem.title && problem.where && !copy.text.includes(`(id ${problem.where})`) ? `${copy.text} (id ${problem.where})` : copy.text;
970
+ return copy.next ? { text, next: copy.next } : { text };
971
+ }
972
+ const message = problem.message.replace(/\s*\[[^\]]*\]\s*$/, "").trim();
973
+ return { text: problem.title ? `${problem.title}: ${message}` : message };
974
+ }
975
+ function problemLine(problem, table) {
976
+ const { text, next } = describeProblem(problem, table);
977
+ return next ? `${text} ${next}` : text;
978
+ }
979
+ var defaultProblemCopy = {
980
+ "duplicate-gameid": (p) => ({
981
+ text: `The Game ID on ${problemName(p)} is already used elsewhere.`,
982
+ next: "Each one must be unique; change one of them."
983
+ }),
984
+ "invalid-gameid": (p) => ({
985
+ text: `The Game ID on ${problemName(p)} isn't valid.`,
986
+ next: "Use lowercase letters, digits and hyphens."
987
+ }),
988
+ "dangling-reference": (p) => ({
989
+ text: `${problemName(p, true)} points at something that no longer exists.`,
990
+ next: "Choose where it goes.",
991
+ dangling: true
992
+ }),
993
+ "unknown-property": (p) => ({
994
+ text: `${problemName(p)} uses a property that isn't set up yet.`,
995
+ next: "Declare it in the project settings, or fix the name."
996
+ }),
997
+ "missing-name": (p) => ({ text: `${problemName(p)} needs a name.` }),
998
+ "empty": (p) => ({
999
+ text: `${problemName(p)} is empty.`,
1000
+ next: "Add something inside it, or remove it."
1001
+ }),
1002
+ "stale-build": (p) => ({
1003
+ text: `Your playable build${p.path ? ` (${p.path})` : ""} is out of date.`,
1004
+ next: "It refreshes the next time you publish."
1005
+ }),
1006
+ "merge-conflict": (p) => ({
1007
+ text: `${p.path ?? "This file"} still has an unresolved merge conflict in it.`,
1008
+ next: "Resolve it in your version control tool, then reopen the project."
1009
+ })
1010
+ };
485
1011
 
486
1012
  // src/settings.ts
487
1013
  function expandableRow(opts) {
@@ -525,6 +1051,22 @@ function revealRow(within, name) {
525
1051
  row.addEventListener("animationend", () => row.classList.remove("landed"), { once: true });
526
1052
  return true;
527
1053
  }
1054
+ function revealRowWhenReady(within, name, tries = 12) {
1055
+ return new Promise((resolve) => {
1056
+ const attempt = (left) => {
1057
+ if (revealRow(within, name)) {
1058
+ resolve(true);
1059
+ return;
1060
+ }
1061
+ if (left <= 0) {
1062
+ resolve(false);
1063
+ return;
1064
+ }
1065
+ requestAnimationFrame(() => attempt(left - 1));
1066
+ };
1067
+ attempt(tries);
1068
+ });
1069
+ }
528
1070
  function focusNewRow(listEl) {
529
1071
  const last = listEl?.lastElementChild;
530
1072
  if (!(last instanceof HTMLElement)) return;
@@ -532,6 +1074,7 @@ function focusNewRow(listEl) {
532
1074
  const name = last.querySelector("input.set-name") ?? last.querySelector("input");
533
1075
  name?.focus();
534
1076
  }
1077
+ var DUP_MESSAGE = "Another row already has this name.";
535
1078
  function dupGuard() {
536
1079
  const items = [];
537
1080
  const check = () => {
@@ -546,9 +1089,9 @@ function dupGuard() {
546
1089
  const dup = !!k && (counts.get(k) ?? 0) > 1;
547
1090
  it.input.classList.toggle("invalid", dup);
548
1091
  if (dup) {
549
- it.input.title = "Duplicate name. Names must be unique.";
1092
+ it.input.dataset["tip"] = DUP_MESSAGE;
550
1093
  any = true;
551
- } else if (it.input.title === "Duplicate name. Names must be unique.") it.input.removeAttribute("title");
1094
+ } else if (it.input.dataset["tip"] === DUP_MESSAGE) delete it.input.dataset["tip"];
552
1095
  }
553
1096
  return any;
554
1097
  };
@@ -594,7 +1137,9 @@ function mountSettingsDialog(opts) {
594
1137
  const tab = el("button", "settings-tab", s.label);
595
1138
  tab.type = "button";
596
1139
  tab.dataset["tab"] = s.id;
597
- tab.addEventListener("click", () => showTab(s.id));
1140
+ tab.addEventListener("click", () => {
1141
+ if (!tab.classList.contains("is-disabled")) showTab(s.id);
1142
+ });
598
1143
  tabs.append(tab);
599
1144
  tabFor.set(s.id, tab);
600
1145
  const panel = el("section", "settings-panel");
@@ -608,6 +1153,25 @@ function mountSettingsDialog(opts) {
608
1153
  for (const [sid, t] of tabFor) t.classList.toggle("active", sid === id);
609
1154
  for (const [sid, p] of panelFor) p.hidden = sid !== id;
610
1155
  }
1156
+ const usable = (s) => !tabFor.get(s.id)?.classList.contains("is-disabled");
1157
+ function refreshTabs() {
1158
+ for (const s of opts.sections) {
1159
+ const tab = tabFor.get(s.id);
1160
+ const reason = s.disabled?.() ?? null;
1161
+ tab.classList.toggle("is-disabled", reason !== null);
1162
+ if (reason !== null) {
1163
+ tab.dataset["tip"] = reason;
1164
+ tab.setAttribute("aria-disabled", "true");
1165
+ } else {
1166
+ delete tab.dataset["tip"];
1167
+ tab.removeAttribute("aria-disabled");
1168
+ }
1169
+ }
1170
+ if (!opts.sections.some((s) => s.id === active2 && usable(s))) {
1171
+ const first = opts.sections.find(usable);
1172
+ if (first) showTab(first.id);
1173
+ }
1174
+ }
611
1175
  form.append(
612
1176
  el("h2", "settings-title", opts.title),
613
1177
  el("div", "settings-body", tabs, panels),
@@ -623,14 +1187,15 @@ function mountSettingsDialog(opts) {
623
1187
  e.preventDefault();
624
1188
  error.hidden = true;
625
1189
  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
- }
1190
+ const found = handles.get(s.id)?.firstInvalid?.();
1191
+ if (!found) continue;
1192
+ const bad = found instanceof HTMLElement ? found : found.el;
1193
+ const message = (found instanceof HTMLElement ? void 0 : found.message) ?? bad.dataset["tip"];
1194
+ showTab(s.id);
1195
+ bad.focus();
1196
+ error.textContent = message || "Fix the highlighted fields first.";
1197
+ error.hidden = false;
1198
+ return;
634
1199
  }
635
1200
  void (async () => {
636
1201
  await opts.onSave();
@@ -647,25 +1212,94 @@ function mountSettingsDialog(opts) {
647
1212
  }
648
1213
  error.hidden = true;
649
1214
  showTab(tabId ?? active2 ?? opts.sections[0]?.id ?? "");
1215
+ refreshTabs();
650
1216
  dialog.showModal();
651
1217
  },
1218
+ refreshTabs,
652
1219
  destroy() {
653
1220
  dialog.remove();
654
1221
  }
655
1222
  };
656
1223
  }
657
1224
 
1225
+ // src/util.ts
1226
+ function plural(n, noun, pluralForm = `${noun}s`) {
1227
+ return `${n} ${n === 1 ? noun : pluralForm}`;
1228
+ }
1229
+ function debounce(fn, ms) {
1230
+ let timer3;
1231
+ let pending;
1232
+ const run = () => {
1233
+ timer3 = void 0;
1234
+ if (pending === void 0) return;
1235
+ const args = pending;
1236
+ pending = void 0;
1237
+ fn(...args);
1238
+ };
1239
+ const debounced = ((...args) => {
1240
+ pending = args;
1241
+ if (timer3 !== void 0) clearTimeout(timer3);
1242
+ timer3 = setTimeout(run, ms);
1243
+ });
1244
+ debounced.cancel = () => {
1245
+ if (timer3 !== void 0) clearTimeout(timer3);
1246
+ timer3 = void 0;
1247
+ pending = void 0;
1248
+ };
1249
+ debounced.flush = () => {
1250
+ if (timer3 !== void 0) clearTimeout(timer3);
1251
+ run();
1252
+ };
1253
+ return debounced;
1254
+ }
1255
+ function isEditableTarget(t) {
1256
+ if (!(t instanceof Element)) return false;
1257
+ if (t instanceof HTMLInputElement || t instanceof HTMLTextAreaElement || t instanceof HTMLSelectElement) return true;
1258
+ if (t instanceof HTMLElement && t.isContentEditable === true) return true;
1259
+ const attr = t.getAttribute("contenteditable");
1260
+ return attr !== null && attr !== "false";
1261
+ }
1262
+ function formatCount(n) {
1263
+ return n.toLocaleString();
1264
+ }
1265
+ var copyTimers = /* @__PURE__ */ new WeakMap();
1266
+ function copyWithFeedback(el2, text, ms = 1e3) {
1267
+ const write = typeof navigator !== "undefined" && navigator.clipboard ? navigator.clipboard.writeText(text).catch(() => {
1268
+ }) : Promise.resolve();
1269
+ el2.classList.add("copied");
1270
+ const prior = copyTimers.get(el2);
1271
+ if (prior !== void 0) clearTimeout(prior);
1272
+ copyTimers.set(el2, setTimeout(() => {
1273
+ el2.classList.remove("copied");
1274
+ copyTimers.delete(el2);
1275
+ }, ms));
1276
+ return write;
1277
+ }
1278
+ function relativeTime(date, now = Date.now()) {
1279
+ const then = date instanceof Date ? date.getTime() : new Date(date).getTime();
1280
+ if (!Number.isFinite(then)) return "";
1281
+ const mins = Math.floor((now - then) / 6e4);
1282
+ if (mins < 1) return "just now";
1283
+ if (mins < 60) return `${plural(mins, "minute")} ago`;
1284
+ const hours = Math.floor(mins / 60);
1285
+ if (hours === 1) return "an hour ago";
1286
+ if (hours < 24) return `${hours} hours ago`;
1287
+ const days = Math.floor(hours / 24);
1288
+ if (days === 1) return "yesterday";
1289
+ if (days < 7) return `${days} days ago`;
1290
+ return new Date(then).toLocaleDateString(void 0, { day: "numeric", month: "short", year: "numeric" });
1291
+ }
1292
+
658
1293
  // 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
1294
  function pinButton(opts) {
661
1295
  const b = el("button", "swin-pin");
662
1296
  b.type = "button";
663
- b.innerHTML = PIN_ICON;
1297
+ b.append(iconNode("pin"));
664
1298
  let pinned = opts.pinned;
665
1299
  const reflect = () => {
666
1300
  b.classList.toggle("on", pinned);
667
1301
  b.setAttribute("aria-pressed", String(pinned));
668
- const label2 = pinned ? "Pinned on top: click to unpin" : "Click to keep on top";
1302
+ const label2 = pinned ? "Pinned on top. Click to unpin." : "Keep on top";
669
1303
  b.dataset["tip"] = label2;
670
1304
  b.setAttribute("aria-label", label2);
671
1305
  };
@@ -680,6 +1314,61 @@ function pinButton(opts) {
680
1314
  reflect();
681
1315
  } };
682
1316
  }
1317
+ var escSlot;
1318
+ var escWired = false;
1319
+ function wireEsc() {
1320
+ if (escWired) return;
1321
+ escWired = true;
1322
+ window.addEventListener("keydown", (e) => {
1323
+ if (e.key !== "Escape" || !escSlot) return;
1324
+ if (e.defaultPrevented) return;
1325
+ if (!escSlot.fromFields && isEditableTarget(e.target)) return;
1326
+ if (escSlot.onEscape?.() === true) return;
1327
+ escSlot.onClose();
1328
+ });
1329
+ }
1330
+ function toolWindowHead(opts) {
1331
+ const keep = (parts) => (parts ?? []).filter((p) => p !== null);
1332
+ const pin = opts.pin ?? pinButton({ pinned: opts.pinned ?? true, onToggle: opts.onPin ?? (() => {
1333
+ }) });
1334
+ if (opts.esc !== false) {
1335
+ escSlot = { onClose: opts.onClose, onEscape: opts.onEscape, fromFields: opts.esc === "always" };
1336
+ wireEsc();
1337
+ }
1338
+ return el(
1339
+ "header",
1340
+ { className: `swin-head${opts.className ? ` ${opts.className}` : ""}` },
1341
+ ...opts.title !== void 0 ? [el("span", { className: "swin-title", text: opts.title })] : [],
1342
+ ...opts.tabs ? [opts.tabs] : [],
1343
+ ...keep(opts.lead),
1344
+ el("span", { className: "swin-spacer" }),
1345
+ ...keep(opts.trail),
1346
+ pin.el,
1347
+ el("button", { className: "swin-close", tip: "Close (Esc)", onClick: opts.onClose }, iconNode("close"))
1348
+ );
1349
+ }
1350
+ function followButton(opts) {
1351
+ const what = opts.what ?? "line";
1352
+ const b = el("button", "btn swin-follow", "Follow in the editor");
1353
+ b.type = "button";
1354
+ let on = opts.on;
1355
+ const reflect = () => {
1356
+ b.classList.toggle("on", on);
1357
+ b.setAttribute("aria-pressed", String(on));
1358
+ const label2 = on ? `The editor opens each ${what} as it plays. Click to stop.` : `Open each ${what} in the editor as it plays`;
1359
+ b.dataset["tip"] = label2;
1360
+ };
1361
+ reflect();
1362
+ b.addEventListener("click", () => {
1363
+ on = !on;
1364
+ reflect();
1365
+ opts.onToggle(on);
1366
+ });
1367
+ return { el: b, set: (next) => {
1368
+ on = next;
1369
+ reflect();
1370
+ } };
1371
+ }
683
1372
 
684
1373
  // src/exit.ts
685
1374
  function closeWithExit(element, done) {
@@ -704,61 +1393,232 @@ function closeWithExit(element, done) {
704
1393
  const timer3 = setTimeout(finish, seconds * 1e3 + 120);
705
1394
  }
706
1395
 
1396
+ // src/dialog.ts
1397
+ function dialogFrame(opts) {
1398
+ const dialog = el("dialog", `shell-dialog ${opts.className ?? ""}`.trim());
1399
+ const body = el("div", "shell-dialog-body");
1400
+ const actions = el("div", "shell-dialog-actions");
1401
+ dialog.append(el("h2", "shell-dialog-title", opts.title));
1402
+ if (opts.sub !== void 0) dialog.append(el("p", "shell-dialog-sub", opts.sub));
1403
+ dialog.append(body, actions);
1404
+ dialog.setAttribute("aria-label", opts.title);
1405
+ document.body.append(dialog);
1406
+ let state = "closed";
1407
+ let opener = null;
1408
+ const teardown = () => {
1409
+ dialog.remove();
1410
+ if (opener instanceof HTMLElement && opener.isConnected) opener.focus();
1411
+ opener = null;
1412
+ state = "closed";
1413
+ opts.onClose?.();
1414
+ };
1415
+ const close = () => {
1416
+ if (state !== "open") return;
1417
+ state = "closing";
1418
+ closeWithExit(dialog, () => {
1419
+ if (dialog.open) dialog.close();
1420
+ teardown();
1421
+ });
1422
+ };
1423
+ dialog.addEventListener("cancel", (e) => {
1424
+ e.preventDefault();
1425
+ close();
1426
+ });
1427
+ dialog.addEventListener("close", () => {
1428
+ if (state === "open") teardown();
1429
+ });
1430
+ return {
1431
+ dialog,
1432
+ body,
1433
+ actions,
1434
+ open() {
1435
+ if (state !== "closed") return;
1436
+ opener = document.activeElement;
1437
+ state = "open";
1438
+ dialog.showModal();
1439
+ },
1440
+ close,
1441
+ isOpen: () => state === "open"
1442
+ };
1443
+ }
1444
+
707
1445
  // src/confirm.ts
708
1446
  function confirmDialog(opts) {
709
1447
  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
1448
  let done = false;
724
1449
  const finish = (ok) => {
725
1450
  if (done) return;
726
1451
  done = true;
727
1452
  resolve(ok);
728
- closeWithExit(dlg, () => {
729
- dlg.close();
730
- dlg.remove();
731
- });
1453
+ frame.close();
732
1454
  };
1455
+ const frame = dialogFrame({
1456
+ title: opts.title,
1457
+ className: "confirm-dialog",
1458
+ onClose: () => finish(false)
1459
+ // Esc, or closed by someone else
1460
+ });
1461
+ const cancel = el("button", "btn confirm-btn cancel", "Cancel");
1462
+ const danger = el("button", "btn danger confirm-btn", opts.confirmLabel);
1463
+ cancel.type = "button";
1464
+ danger.type = "button";
1465
+ if (opts.body !== void 0) frame.body.append(el("div", "confirm-body", opts.body));
1466
+ if (opts.bodyNode) frame.body.append(opts.bodyNode);
1467
+ frame.actions.append(cancel, danger);
733
1468
  cancel.addEventListener("click", () => finish(false));
734
1469
  danger.addEventListener("click", () => finish(true));
735
- dlg.addEventListener("cancel", (e) => {
736
- e.preventDefault();
737
- finish(false);
738
- });
739
- dlg.addEventListener("mousedown", (e) => {
740
- if (e.target === dlg) finish(false);
1470
+ frame.dialog.addEventListener("mousedown", (e) => {
1471
+ if (e.target === frame.dialog) finish(false);
741
1472
  });
742
- dlg.showModal();
1473
+ frame.open();
743
1474
  cancel.focus();
744
1475
  });
745
1476
  }
746
1477
 
1478
+ // src/floating.ts
1479
+ function followOnScroll(reposition) {
1480
+ const onMove = () => reposition();
1481
+ window.addEventListener("scroll", onMove, true);
1482
+ window.addEventListener("resize", onMove);
1483
+ return () => {
1484
+ window.removeEventListener("scroll", onMove, true);
1485
+ window.removeEventListener("resize", onMove);
1486
+ };
1487
+ }
1488
+ function createFloating(className, opts = {}) {
1489
+ const el2 = document.createElement("div");
1490
+ el2.className = className;
1491
+ el2.style.display = "none";
1492
+ (opts.host ?? document.body).appendChild(el2);
1493
+ let open = false;
1494
+ let detach = null;
1495
+ let outside = null;
1496
+ let closeToken = 0;
1497
+ const close = () => {
1498
+ if (!open) return;
1499
+ open = false;
1500
+ detach?.();
1501
+ detach = null;
1502
+ if (outside) {
1503
+ document.removeEventListener("mousedown", outside, true);
1504
+ outside = null;
1505
+ }
1506
+ const myToken = ++closeToken;
1507
+ closeWithExit(el2, () => {
1508
+ if (myToken !== closeToken) return;
1509
+ el2.style.display = "none";
1510
+ el2.replaceChildren();
1511
+ el2.classList.remove("closing");
1512
+ });
1513
+ };
1514
+ const show2 = (reposition) => {
1515
+ closeToken++;
1516
+ el2.classList.remove("closing");
1517
+ open = true;
1518
+ el2.style.display = "block";
1519
+ reposition();
1520
+ detach ??= followOnScroll(reposition);
1521
+ };
1522
+ const dismissOnOutside = (onOutside, isInside) => {
1523
+ if (outside) return;
1524
+ outside = (e) => {
1525
+ const t = e.target;
1526
+ if (!el2.contains(t) && !(isInside?.(t) ?? false)) onOutside();
1527
+ };
1528
+ document.addEventListener("mousedown", outside, true);
1529
+ };
1530
+ return { el: el2, show: show2, dismissOnOutside, close, isOpen: () => open };
1531
+ }
1532
+
1533
+ // src/context-menu.ts
1534
+ var EDGE2 = 6;
1535
+ var current = null;
1536
+ function floating(node2, onDismiss) {
1537
+ let gone = false;
1538
+ const dismiss = () => {
1539
+ if (gone) return;
1540
+ gone = true;
1541
+ if (current === dismiss) current = null;
1542
+ window.removeEventListener("pointerdown", onAway, true);
1543
+ window.removeEventListener("keydown", onKey, true);
1544
+ closeWithExit(node2, () => node2.remove());
1545
+ onDismiss?.();
1546
+ };
1547
+ const onAway = (e) => {
1548
+ if (!node2.contains(e.target)) dismiss();
1549
+ };
1550
+ const onKey = (e) => {
1551
+ if (e.key === "Escape") {
1552
+ e.preventDefault();
1553
+ dismiss();
1554
+ }
1555
+ };
1556
+ window.addEventListener("pointerdown", onAway, true);
1557
+ window.addEventListener("keydown", onKey, true);
1558
+ current?.();
1559
+ current = dismiss;
1560
+ return { dismiss };
1561
+ }
1562
+ function placeBelow(node2, anchor) {
1563
+ node2.style.left = `${anchor.left}px`;
1564
+ node2.style.top = `${anchor.bottom + 4}px`;
1565
+ const r = node2.getBoundingClientRect();
1566
+ if (r.right > window.innerWidth) node2.style.left = `${Math.max(EDGE2, window.innerWidth - r.width - EDGE2)}px`;
1567
+ if (r.bottom > window.innerHeight) node2.style.top = `${Math.max(EDGE2, anchor.top - r.height - 4)}px`;
1568
+ }
1569
+ function openPopover(anchor, build, onClose, opts = {}) {
1570
+ const pop = el("div", "popover");
1571
+ let closed = false;
1572
+ const finish = () => {
1573
+ if (closed) return;
1574
+ closed = true;
1575
+ onClose?.();
1576
+ };
1577
+ const { dismiss } = floating(pop, finish);
1578
+ pop.append(build(dismiss));
1579
+ (opts.host ?? document.body).append(pop);
1580
+ placeBelow(pop, anchor.getBoundingClientRect());
1581
+ }
1582
+ function openContextMenu(x, y, items) {
1583
+ const menu = el("div", "ctxmenu");
1584
+ menu.setAttribute("role", "menu");
1585
+ const { dismiss } = floating(menu);
1586
+ for (const item of items) {
1587
+ const button = el("button", {
1588
+ className: `ctxmenu-item${item.danger ? " danger" : ""}`,
1589
+ text: item.label,
1590
+ onClick: () => {
1591
+ if (item.disabled === true) return;
1592
+ dismiss();
1593
+ item.onClick();
1594
+ }
1595
+ });
1596
+ button.type = "button";
1597
+ button.setAttribute("role", "menuitem");
1598
+ if (item.disabled === true) button.disabled = true;
1599
+ menu.append(button);
1600
+ }
1601
+ menu.style.left = `${x}px`;
1602
+ menu.style.top = `${y}px`;
1603
+ document.body.append(menu);
1604
+ const r = menu.getBoundingClientRect();
1605
+ if (r.right > window.innerWidth) menu.style.left = `${Math.max(EDGE2, window.innerWidth - r.width - EDGE2)}px`;
1606
+ if (r.bottom > window.innerHeight) menu.style.top = `${Math.max(EDGE2, window.innerHeight - r.height - EDGE2)}px`;
1607
+ }
1608
+
747
1609
  // src/about.ts
748
1610
  function showAbout(opts) {
749
1611
  return new Promise((resolve) => {
750
- const dialog = el("dialog", "shell-about");
1612
+ const frame = dialogFrame({ title: opts.appName, className: "shell-about", onClose: () => resolve() });
1613
+ frame.dialog.setAttribute("aria-label", `About ${opts.appName}`);
751
1614
  if (opts.wordmark !== void 0) {
752
1615
  const mark = el("div", "shell-about-mark");
753
1616
  mark.innerHTML = opts.wordmark;
754
- dialog.append(mark);
1617
+ frame.dialog.prepend(mark);
755
1618
  }
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));
1619
+ frame.body.append(el("div", "shell-about-version", `Version ${opts.version}`));
1620
+ if (opts.blurb !== void 0) frame.body.append(el("p", "shell-about-blurb", opts.blurb));
1621
+ if (opts.credits !== void 0) frame.body.append(el("p", "shell-about-credits", opts.credits));
762
1622
  if (opts.links?.length) {
763
1623
  const row = el("div", "shell-about-links");
764
1624
  for (const link of opts.links) {
@@ -771,39 +1631,28 @@ function showAbout(opts) {
771
1631
  a.addEventListener("click", () => opts.onOpenLink?.(link.url));
772
1632
  row.append(a);
773
1633
  }
774
- dialog.append(row);
1634
+ frame.body.append(row);
775
1635
  }
776
- const close = el("button", "shell-about-close", "Close");
1636
+ const close = el("button", "btn shell-about-close", "Close");
777
1637
  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();
1638
+ close.addEventListener("click", () => frame.close());
1639
+ frame.actions.append(close);
1640
+ frame.open();
792
1641
  close.focus();
793
1642
  });
794
1643
  }
795
1644
 
796
1645
  // src/anchored.ts
797
- var current = null;
1646
+ var current2 = null;
798
1647
  function openAnchoredPanel(opts) {
799
- if (current) {
800
- const prev = current;
801
- current = null;
1648
+ if (current2) {
1649
+ const prev = current2;
1650
+ current2 = null;
802
1651
  prev.close();
803
1652
  if (prev.anchor === opts.anchor) return null;
804
1653
  }
805
1654
  const panel = el("div", `shell-anchored ${opts.className ?? ""}`.trim());
806
- const closeBtn = el("button", "shell-anchored-close", "\u2715");
1655
+ const closeBtn = el("button", "shell-anchored-close", iconNode("close", 12));
807
1656
  closeBtn.type = "button";
808
1657
  closeBtn.setAttribute("aria-label", "Close");
809
1658
  const body = el("div", "shell-anchored-body");
@@ -811,13 +1660,13 @@ function openAnchoredPanel(opts) {
811
1660
  el("div", "shell-anchored-head", el("span", "shell-anchored-title", opts.title), closeBtn),
812
1661
  body
813
1662
  );
814
- document.body.append(panel);
1663
+ (opts.host ?? document.body).append(panel);
815
1664
  placeAnchored(panel, opts.anchor, opts.width, opts.prefer ?? "below", opts.keepClear ?? []);
816
1665
  let closed = false;
817
1666
  const close = () => {
818
1667
  if (closed) return;
819
1668
  closed = true;
820
- if (current?.panel === panel) current = null;
1669
+ if (current2?.panel === panel) current2 = null;
821
1670
  document.removeEventListener("pointerdown", onDown, true);
822
1671
  document.removeEventListener("keydown", onKey, true);
823
1672
  closeWithExit(panel, () => {
@@ -841,12 +1690,12 @@ function openAnchoredPanel(opts) {
841
1690
  document.addEventListener("pointerdown", onDown, true);
842
1691
  document.addEventListener("keydown", onKey, true);
843
1692
  }, 0);
844
- current = { anchor: opts.anchor, panel, close };
1693
+ current2 = { anchor: opts.anchor, panel, close };
845
1694
  return { panel, body, close };
846
1695
  }
847
1696
  function closeAnchoredPanel() {
848
- const open = current;
849
- current = null;
1697
+ const open = current2;
1698
+ current2 = null;
850
1699
  open?.close();
851
1700
  }
852
1701
  function placeAnchored(panel, anchor, width, prefer, keepClear) {
@@ -903,7 +1752,7 @@ function isValidGameId(gameId) {
903
1752
 
904
1753
  // src/property-names.ts
905
1754
  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.";
1755
+ var PROPERTY_NAME_HINT = "Lower case letters, digits, and underscores. It can't start with a digit. No hyphens or spaces.";
907
1756
  function propertyNameify(text) {
908
1757
  const trimmed = text.trim();
909
1758
  const deliberateLeading = trimmed.startsWith("_");
@@ -927,14 +1776,14 @@ function propertyNameProblem(name) {
927
1776
  const suggestion = propertyNameify(name);
928
1777
  const tail = suggestion ? ` Try "${suggestion}", or press Tab.` : "";
929
1778
  if (RESERVED_PROPERTY_NAMES.includes(name.toLowerCase())) {
930
- return `Cannot be used: "${name.toLowerCase()}" is a keyword in expressions.${tail}`;
1779
+ return `Can't be used. "${name.toLowerCase()}" is a keyword in expressions.${tail}`;
931
1780
  }
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}`;
1781
+ if (name.includes("-")) return `Can't be used. A hyphen reads as subtraction in an expression.${tail}`;
1782
+ if (/^[0-9]/.test(name)) return `Can't be used. A name can't start with a digit.${tail}`;
934
1783
  if (name !== name.toLowerCase()) {
935
- return `Cannot be used: expressions fold names, so this would look for "${name.toLowerCase()}".${tail}`;
1784
+ return `Can't be used. Expressions fold names, so this would look for "${name.toLowerCase()}".${tail}`;
936
1785
  }
937
- return `Cannot be used: only lower case letters, digits and underscores.${tail}`;
1786
+ return `Can't be used. Only lower case letters, digits, and underscores.${tail}`;
938
1787
  }
939
1788
  function bindPropertyName(input, onCommit, opts = {}) {
940
1789
  const hint = opts.hint;
@@ -943,7 +1792,7 @@ function bindPropertyName(input, onCommit, opts = {}) {
943
1792
  const problem = v === "" ? void 0 : propertyNameProblem(v);
944
1793
  input.classList.toggle("illegal", !!problem);
945
1794
  if (problem) input.title = problem;
946
- else if (input.title.startsWith("Cannot be used")) {
1795
+ else if (input.title.startsWith("Can't be used")) {
947
1796
  if (hint) input.title = hint;
948
1797
  else input.removeAttribute("title");
949
1798
  }
@@ -999,10 +1848,10 @@ function propertyRefProblem(name, opts) {
999
1848
  if (!isValidPropertyName(folded)) {
1000
1849
  const suggestion = propertyNameify(name);
1001
1850
  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}`;
1851
+ if (RESERVED_PROPERTY_NAMES.includes(folded)) return `Can't be used. "${folded}" is a keyword in expressions.${tail}`;
1852
+ if (name.includes("-")) return `Can't be used. A hyphen reads as subtraction in an expression.${tail}`;
1853
+ if (/^[0-9]/.test(name)) return `Can't be used. A name can't start with a digit.${tail}`;
1854
+ return `Can't be used. Only lower case letters, digits, and underscores.${tail}`;
1006
1855
  }
1007
1856
  const known = opts.known();
1008
1857
  if (known.some((k) => k.toLowerCase() === folded)) return void 0;
@@ -1011,7 +1860,7 @@ function propertyRefProblem(name, opts) {
1011
1860
  if (known.length === 0) return `No ${scope} properties are declared yet. Declare one above first.`;
1012
1861
  const shown = known.slice(0, 3).map((k) => `"${k}"`).join(", ");
1013
1862
  const more = known.length > 3 ? `, and ${known.length - 3} more` : "";
1014
- return `No ${scope} property is called "${name}". Declared: ${shown}${more}.`;
1863
+ return `No ${scope} property is called "${name}". Pick one of ${shown}${more}.`;
1015
1864
  }
1016
1865
  function bindPropertyRef(input, onCommit, opts) {
1017
1866
  const list = document.createElement("datalist");
@@ -1032,7 +1881,7 @@ function bindPropertyRef(input, onCommit, opts) {
1032
1881
  const problem = v === "" ? void 0 : propertyRefProblem(v, opts);
1033
1882
  input.classList.toggle("illegal", !!problem);
1034
1883
  if (problem) input.title = problem;
1035
- else if (input.title.startsWith("Cannot be used") || input.title.startsWith("No ")) {
1884
+ else if (input.title.startsWith("Can't be used") || input.title.startsWith("No ")) {
1036
1885
  if (opts.hint) input.title = opts.hint;
1037
1886
  else input.removeAttribute("title");
1038
1887
  }
@@ -1075,9 +1924,9 @@ function openGameIdEditor(opts) {
1075
1924
  const input = el("input", "shell-id-input");
1076
1925
  input.type = "text";
1077
1926
  input.value = opts.value;
1078
- input.placeholder = opts.derived || "<game-id>";
1927
+ input.placeholder = opts.derived || "game-id";
1079
1928
  input.spellcheck = false;
1080
- const hint = el("div", "shell-id-hint", "Lower case, digits, hyphens. Empty = auto from the name.");
1929
+ const hint = el("div", "shell-id-hint", "Lower case letters, digits, and hyphens. Leave it empty to derive one from the name.");
1081
1930
  const actions = el("div", "shell-id-actions");
1082
1931
  const reset = el("button", "shell-id-reset", "Reset to auto");
1083
1932
  const set = el("button", "shell-id-set", "Set");
@@ -1139,12 +1988,13 @@ function foldVc(shards, keys) {
1139
1988
  return out;
1140
1989
  }
1141
1990
  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" };
1991
+ const badge = (name, cls, title) => ({ name, glyph: icon[name] ?? "", cls, title });
1992
+ if (s?.lockedBy?.length) return badge("locked", "vc-locked", `Locked by ${s.lockedBy.join(", ")}`);
1993
+ if (s?.outOfDate) return badge("down", "vc-stale", "Out of date. A newer version is on the server.");
1994
+ if (s?.checkedOutByMe) return badge("checkedOut", "vc-mine", "Checked out by you");
1995
+ if (s?.dirty) return badge("modified", "vc-dirty", "Modified. Local changes aren't committed yet.");
1996
+ if (s?.untracked) return badge("untracked", "vc-new", "New. Not in version control yet.");
1997
+ if (s && !s.writable) return badge("readOnly", "vc-frozen", "Read-only on disk. Saving checks it out.");
1148
1998
  return null;
1149
1999
  }
1150
2000
  function paintVcBadges(root, shards) {
@@ -1152,7 +2002,7 @@ function paintVcBadges(root, shards) {
1152
2002
  host.querySelector(":scope > .vc-badge")?.remove();
1153
2003
  const badge = vcBadgeFor(foldVc(shards, host.dataset["vc"]));
1154
2004
  if (!badge) return;
1155
- const span = el("span", { className: `vc-badge ${badge.cls}`, text: badge.glyph, tip: badge.title });
2005
+ const span = el("span", { className: `vc-badge ${badge.cls}`, tip: badge.title }, iconNode(badge.name, 12));
1156
2006
  host.append(span);
1157
2007
  });
1158
2008
  }
@@ -1175,8 +2025,8 @@ function lockNotice(holders) {
1175
2025
  return el(
1176
2026
  "div",
1177
2027
  { 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.` })
2028
+ el("span", { className: "vc-lock-glyph" }, iconNode("locked")),
2029
+ el("span", { text: `Locked by ${holders.join(", ")}. Read-only until they release it.` })
1180
2030
  );
1181
2031
  }
1182
2032
 
@@ -1184,41 +2034,36 @@ function lockNotice(holders) {
1184
2034
  function askIdentity(opts = {}) {
1185
2035
  return new Promise((resolve) => {
1186
2036
  const seed = opts.current ?? opts.suggested;
1187
- const name = el("input", "shell-ident-input");
2037
+ const name = el("input", "field shell-ident-input");
1188
2038
  name.placeholder = "Your name";
1189
2039
  name.value = seed?.name ?? "";
1190
- const email = el("input", "shell-ident-input");
1191
- email.placeholder = "Email (optional)";
2040
+ const email = el("input", "field shell-ident-input");
2041
+ email.placeholder = "Optional";
1192
2042
  email.type = "email";
1193
2043
  email.value = seed?.email ?? "";
1194
- const skip = el("button", "shell-ident-btn", "Skip");
1195
- const save = el("button", "shell-ident-btn primary", "Save");
2044
+ const skip = el("button", "btn shell-ident-btn", "Skip");
2045
+ const save = el("button", "btn primary shell-ident-btn", opts.mode === "welcome" ? "Continue" : "Save");
1196
2046
  skip.type = "button";
1197
2047
  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
2048
  let done = false;
1212
2049
  const finish = (keep) => {
1213
2050
  if (done) return;
1214
2051
  done = true;
1215
2052
  const typed = name.value.trim();
1216
2053
  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();
2054
+ const answer = keep && typed !== "" ? value === "" ? { name: typed } : { name: typed, email: value } : opts.neverAskAgainOnSkip === true ? { name: "" } : void 0;
1220
2055
  resolve(answer);
2056
+ frame.close();
1221
2057
  };
2058
+ const frame = dialogFrame({
2059
+ title: titleFor(opts),
2060
+ sub: subFor(opts),
2061
+ className: "shell-ident",
2062
+ onClose: () => finish(false)
2063
+ // Escape, or closed by someone else: a skip
2064
+ });
2065
+ frame.body.append(el("div", "shell-ident-fields", labelled("Name", name), labelled("Email", email)));
2066
+ frame.actions.append(skip, save);
1222
2067
  skip.addEventListener("click", () => finish(false));
1223
2068
  save.addEventListener("click", () => finish(true));
1224
2069
  name.addEventListener("keydown", (e) => {
@@ -1227,11 +2072,19 @@ function askIdentity(opts = {}) {
1227
2072
  email.addEventListener("keydown", (e) => {
1228
2073
  if (e.key === "Enter") finish(true);
1229
2074
  });
1230
- dialog.addEventListener("close", () => finish(false));
1231
- dialog.showModal();
2075
+ frame.open();
1232
2076
  name.focus();
1233
2077
  });
1234
2078
  }
2079
+ function titleFor(opts) {
2080
+ if (opts.mode === "welcome") return opts.appName ? `Welcome to ${opts.appName}` : "Welcome";
2081
+ if (opts.mode === "edit") return "User information";
2082
+ return "Who is working here?";
2083
+ }
2084
+ function subFor(opts) {
2085
+ const why = "Your name goes on the comments you write. It's kept in this app, not in the project.";
2086
+ return opts.mode === "edit" ? why : `${why} You can change it later.`;
2087
+ }
1235
2088
 
1236
2089
  // src/notes-editor.ts
1237
2090
  var EDITOR_ONLY = "";
@@ -1244,10 +2097,10 @@ var CLASS_LABEL = {
1244
2097
  };
1245
2098
  var label = (cls) => cls === EDITOR_ONLY ? "Note (editor-only)" : CLASS_LABEL[cls] ?? cls[0].toUpperCase() + cls.slice(1);
1246
2099
  var PLACEHOLDER = {
1247
- vo: "direction for the voice actor / director",
1248
- loc: "context for translators"
2100
+ vo: "Direction for the voice actor or director",
2101
+ loc: "Context for translators"
1249
2102
  };
1250
- var placeholder = (cls) => cls === EDITOR_ONLY ? "an internal note - never exported" : PLACEHOLDER[cls] ?? "intent / rationale - why this is here";
2103
+ var placeholder = (cls) => cls === EDITOR_ONLY ? "An internal note, never exported" : PLACEHOLDER[cls] ?? "Why this is here";
1251
2104
  function openNotesEditor(opts) {
1252
2105
  const { notes } = opts;
1253
2106
  const seed = /* @__PURE__ */ new Map();
@@ -1301,7 +2154,7 @@ ${line.text}` : line.text);
1301
2154
  { className: "shell-notes-class" },
1302
2155
  el("label", "shell-notes-caption", label(cls))
1303
2156
  );
1304
- const area = el("textarea", "shell-notes-text");
2157
+ const area = el("textarea", "field shell-notes-text");
1305
2158
  area.value = areas.get(cls)?.value ?? seed.get(cls) ?? "";
1306
2159
  area.rows = 4;
1307
2160
  area.placeholder = placeholder(cls);
@@ -1327,29 +2180,24 @@ ${line.text}` : line.text);
1327
2180
  }
1328
2181
  };
1329
2182
  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);
2183
+ const done = el("button", "btn primary shell-notes-btn", "Done");
2184
+ done.type = "button";
1340
2185
  const before = JSON.stringify(notes.own);
1341
2186
  let closed = false;
1342
- const close = () => {
2187
+ const commit = () => {
1343
2188
  if (closed) return;
1344
2189
  closed = true;
1345
2190
  const next = value();
1346
2191
  if (JSON.stringify(next) !== before) opts.save(next);
1347
- dialog.close();
1348
- dialog.remove();
1349
2192
  };
1350
- done.addEventListener("click", close);
1351
- dialog.addEventListener("close", close);
1352
- dialog.showModal();
2193
+ const frame = dialogFrame({ title: `Notes: ${opts.subject}`, className: "shell-notes", onClose: commit });
2194
+ frame.body.append(body);
2195
+ frame.actions.append(done);
2196
+ done.addEventListener("click", () => {
2197
+ commit();
2198
+ frame.close();
2199
+ });
2200
+ frame.open();
1353
2201
  areas.get(shown[0] ?? EDITOR_ONLY)?.focus();
1354
2202
  }
1355
2203
 
@@ -1421,7 +2269,7 @@ function openComments(opts) {
1421
2269
  el("span", { className: "shell-cmt-ts", text: stamp(message.ts) })
1422
2270
  );
1423
2271
  if (opts.deleteMessage && message.deleted !== true) {
1424
- const bin = el("button", { className: "shell-cmt-del", text: "\u2715" });
2272
+ const bin = el("button", { className: "shell-cmt-del" }, iconNode("close", 12));
1425
2273
  bin.dataset.tip = "Delete this comment";
1426
2274
  bin.setAttribute("aria-label", "Delete this comment");
1427
2275
  bin.addEventListener("click", () => opts.deleteMessage?.(thread.id, index));
@@ -1486,37 +2334,65 @@ var clock = (ms) => {
1486
2334
  var estimable = (done, total, elapsedMs) => done > 0 && total > 0 && elapsedMs > 1e3 && done / total > 0.1;
1487
2335
  function mountJobProgress(host, opts) {
1488
2336
  const units = opts.units ?? "runs";
2337
+ const pinned = opts.indeterminate === true;
2338
+ const cancellableByDefault = opts.cancellable ?? opts.onCancel !== void 0;
1489
2339
  const fill = el("i", { className: "job-fill" });
1490
2340
  const bar = el("div", { className: "job-bar" }, fill);
2341
+ bar.setAttribute("role", "progressbar");
2342
+ bar.setAttribute("aria-valuemin", "0");
2343
+ bar.setAttribute("aria-valuemax", "100");
1491
2344
  const what = el("span", { className: "job-label" });
1492
2345
  const count = el("span", { className: "job-count" });
1493
2346
  const time = el("span", { className: "job-time" });
1494
2347
  const cancel = el("button", { className: "job-cancel", text: "Cancel" });
2348
+ cancel.type = "button";
1495
2349
  const strip = el("div", { className: "job-strip" }, what, bar, count, time, cancel);
1496
2350
  strip.hidden = true;
1497
2351
  host.append(strip);
2352
+ let runPinned = pinned;
2353
+ let indeterminate = false;
2354
+ const setMode = (band) => {
2355
+ indeterminate = band;
2356
+ strip.classList.toggle("indeterminate", band);
2357
+ count.hidden = band;
2358
+ if (band) {
2359
+ fill.style.width = "";
2360
+ bar.removeAttribute("aria-valuenow");
2361
+ }
2362
+ };
1498
2363
  const view = {
1499
2364
  element: strip,
1500
2365
  get visible() {
1501
2366
  return !strip.hidden;
1502
2367
  },
1503
- begin(label2) {
2368
+ get indeterminate() {
2369
+ return indeterminate;
2370
+ },
2371
+ begin(label2, run) {
2372
+ runPinned = run?.indeterminate ?? pinned;
2373
+ const cancellable = run?.cancellable ?? cancellableByDefault;
1504
2374
  what.textContent = label2 ?? "Running\u2026";
1505
2375
  count.textContent = "";
1506
2376
  time.textContent = "";
1507
- fill.style.width = "0%";
2377
+ cancel.hidden = !cancellable;
1508
2378
  cancel.disabled = false;
1509
2379
  cancel.textContent = "Cancel";
2380
+ setMode(true);
1510
2381
  strip.hidden = false;
1511
2382
  },
1512
2383
  update(done, total, elapsedMs) {
1513
2384
  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)) {
2385
+ const band = runPinned || !(total > 0);
2386
+ setMode(band);
2387
+ if (!band) {
2388
+ const pct = Math.min(100, done / total * 100);
2389
+ fill.style.width = `${pct}%`;
2390
+ bar.setAttribute("aria-valuenow", String(Math.round(pct)));
2391
+ count.textContent = `${done} / ${total} ${units}`.trimEnd();
2392
+ }
2393
+ if (!band && estimable(done, total, elapsedMs)) {
1518
2394
  const remaining = elapsedMs / done * (total - done);
1519
- time.textContent = `${clock(elapsedMs)} elapsed \xB7 about ${clock(remaining)} left`;
2395
+ time.textContent = `${clock(elapsedMs)} elapsed, about ${clock(remaining)} left`;
1520
2396
  } else {
1521
2397
  time.textContent = clock(elapsedMs);
1522
2398
  }
@@ -1528,8 +2404,10 @@ function mountJobProgress(host, opts) {
1528
2404
  cancel.addEventListener("click", () => {
1529
2405
  cancel.disabled = true;
1530
2406
  cancel.textContent = "Stopping\u2026";
1531
- opts.onCancel();
2407
+ opts.onCancel?.();
1532
2408
  });
2409
+ cancel.hidden = !cancellableByDefault;
2410
+ setMode(true);
1533
2411
  return view;
1534
2412
  }
1535
2413
 
@@ -1655,11 +2533,14 @@ function staleBar(opts) {
1655
2533
  className: "stale-bar-msg",
1656
2534
  text: `${opts.subject} changed in the editor. ${opts.advice ?? "Restart to play the new version."}`
1657
2535
  }),
1658
- el("button", {
1659
- className: "stale-bar-go",
1660
- text: `${icon.restart} ${label2}`,
1661
- onClick: () => opts.onRestart()
1662
- })
2536
+ // The icon is a node beside the word, not a character inside it; the
2537
+ // button's flex gap sets the space (stale.css).
2538
+ el(
2539
+ "button",
2540
+ { className: "stale-bar-go", onClick: () => opts.onRestart() },
2541
+ iconNode("restart", 12),
2542
+ label2
2543
+ )
1663
2544
  );
1664
2545
  }
1665
2546
 
@@ -1701,14 +2582,14 @@ function dismissToast() {
1701
2582
  }
1702
2583
 
1703
2584
  // src/stepper.ts
2585
+ var STEP_ICON = 12;
1704
2586
  function renderStepperBar(host, opts) {
1705
2587
  const { items } = opts;
1706
2588
  const close = opts.onClose === void 0 ? null : el("button", {
1707
2589
  className: "stepbar-nav stepbar-close",
1708
- text: icon.close,
1709
2590
  tip: opts.closeTip ?? "Close",
1710
2591
  onClick: opts.onClose
1711
- });
2592
+ }, iconNode("close", STEP_ICON));
1712
2593
  const actions = (opts.actions ?? []).filter((a) => a !== null && a !== void 0);
1713
2594
  if (items.length === 0) {
1714
2595
  if (opts.empty === void 0) {
@@ -1732,17 +2613,15 @@ function renderStepperBar(host, opts) {
1732
2613
  const nav = items.length > 1 ? [
1733
2614
  el("button", {
1734
2615
  className: "stepbar-nav",
1735
- text: icon.back,
1736
2616
  tip: opts.tips?.prev ?? "Previous",
1737
2617
  onClick: () => step(-1)
1738
- }),
2618
+ }, iconNode("back", STEP_ICON)),
1739
2619
  el("span", { className: "stepbar-of", text: `${index + 1}/${items.length}` }),
1740
2620
  el("button", {
1741
2621
  className: "stepbar-nav",
1742
- text: icon.forward,
1743
2622
  tip: opts.tips?.next ?? "Next",
1744
2623
  onClick: () => step(1)
1745
- })
2624
+ }, iconNode("forward", STEP_ICON))
1746
2625
  ] : [];
1747
2626
  host.replaceChildren(
1748
2627
  el("span", { className: `stepbar-count ${opts.tone ?? "muted"}`, text: String(items.length) }),
@@ -1751,7 +2630,7 @@ function renderStepperBar(host, opts) {
1751
2630
  "button",
1752
2631
  { className: "stepbar-cur", tip: opts.tips?.go ?? "Go to this", onClick: () => opts.onGo(index) },
1753
2632
  item.kind === void 0 ? null : el("span", { className: `stepbar-cat${item.kindClass ? ` ${item.kindClass}` : ""}`, text: item.kind }),
1754
- item.where === void 0 ? null : el("span", { className: "stepbar-where", text: item.where }),
2633
+ item.where === void 0 ? null : typeof item.where === "string" ? el("span", { className: "stepbar-where", text: item.where }) : el("span", "stepbar-where", item.where),
1755
2634
  el("span", { className: "stepbar-msg", text: item.text })
1756
2635
  ),
1757
2636
  ...actions,
@@ -1759,19 +2638,383 @@ function renderStepperBar(host, opts) {
1759
2638
  );
1760
2639
  }
1761
2640
 
2641
+ // src/updater-view.ts
2642
+ var liveProgress = null;
2643
+ var mb = (n) => (n / (1024 * 1024)).toFixed(1);
2644
+ function feedUpdaterDownloadProgress(p) {
2645
+ if (!liveProgress) return;
2646
+ const pct = Math.max(0, Math.min(100, p.percent));
2647
+ liveProgress.bar.style.width = `${pct}%`;
2648
+ liveProgress.label.textContent = `${pct.toFixed(0)}%, ${mb(p.transferred)} of ${mb(p.total)} MB (${mb(p.bytesPerSecond)} MB/s)`;
2649
+ }
2650
+ function showUpdaterDialog(opts) {
2651
+ return new Promise((resolve) => {
2652
+ const defaultId = opts.defaultId ?? 0;
2653
+ const cancelId = opts.cancelId ?? opts.buttons.length - 1;
2654
+ let done = false;
2655
+ const finish = (idx) => {
2656
+ if (done) return;
2657
+ done = true;
2658
+ liveProgress = null;
2659
+ resolve(idx);
2660
+ frame.close();
2661
+ };
2662
+ const frame = dialogFrame({
2663
+ title: opts.message,
2664
+ className: "updater-dialog",
2665
+ // Esc is the cancel button, not a further answer: main is waiting on an index.
2666
+ onClose: () => finish(cancelId)
2667
+ });
2668
+ if (opts.detail) frame.body.append(el("p", "updater-detail", opts.detail));
2669
+ if (opts.progress) {
2670
+ const track = el("div", "updater-progress-track");
2671
+ const bar = el("div", "updater-progress-bar");
2672
+ track.append(bar);
2673
+ const label2 = el("p", "updater-progress-label", "Starting download\u2026");
2674
+ frame.body.append(track, label2);
2675
+ liveProgress = { bar, label: label2 };
2676
+ }
2677
+ if (opts.links?.length) {
2678
+ const row = el("p", "updater-links");
2679
+ for (const link of opts.links) {
2680
+ const a = el("a", void 0, link.label);
2681
+ a.href = link.url;
2682
+ a.addEventListener("click", (e) => {
2683
+ e.preventDefault();
2684
+ opts.openExternal?.(link.url);
2685
+ });
2686
+ row.append(a);
2687
+ }
2688
+ frame.body.append(row);
2689
+ }
2690
+ opts.buttons.forEach((label2, i) => {
2691
+ const b = el("button", i === defaultId ? "btn primary" : "btn", label2);
2692
+ b.type = "button";
2693
+ b.addEventListener("click", () => finish(i));
2694
+ frame.actions.append(b);
2695
+ if (i === defaultId) queueMicrotask(() => b.focus());
2696
+ });
2697
+ frame.open();
2698
+ });
2699
+ }
2700
+
2701
+ // src/welcome.ts
2702
+ function mountWelcome(host, opts) {
2703
+ const caption = (text) => el("h2", "welcome-caption", text);
2704
+ const group = (label2, ...body) => el("section", "welcome-group", caption(label2), ...body);
2705
+ const actions = el("div", "welcome-actions");
2706
+ for (const a of opts.actions) {
2707
+ const b = el("button", { className: `btn${a.primary ? " primary" : ""}`, text: a.label, onClick: a.onClick });
2708
+ b.type = "button";
2709
+ actions.append(b);
2710
+ }
2711
+ const groups = (opts.groups ?? []).map((g) => group(
2712
+ g.caption,
2713
+ g.note !== void 0 ? el("p", "welcome-note", g.note) : null,
2714
+ el("div", "welcome-list", ...g.items.map((item) => {
2715
+ const b = el(
2716
+ "button",
2717
+ { className: "welcome-item", onClick: item.onOpen },
2718
+ el("span", "welcome-item-name", item.name),
2719
+ item.hint !== void 0 ? el("span", "welcome-item-hint", item.hint) : null
2720
+ );
2721
+ b.type = "button";
2722
+ return b;
2723
+ }))
2724
+ ));
2725
+ const recentsHost = el("div", "welcome-recents-slot");
2726
+ const drawRecents = (recents) => {
2727
+ const shown = recents.slice(0, opts.maxRecents ?? 5);
2728
+ recentsHost.replaceChildren();
2729
+ if (shown.length === 0) return;
2730
+ recentsHost.append(group(
2731
+ opts.recentsCaption ?? "Recent",
2732
+ el("div", "welcome-recents", ...shown.map((r) => {
2733
+ const b = el(
2734
+ "button",
2735
+ { className: "welcome-recent", onClick: r.onOpen },
2736
+ el("span", "welcome-recent-name", r.name),
2737
+ el("span", "welcome-recent-path", r.path)
2738
+ );
2739
+ b.type = "button";
2740
+ return b;
2741
+ }))
2742
+ ));
2743
+ };
2744
+ drawRecents(opts.recents);
2745
+ const error = el("p", "welcome-error");
2746
+ const setError = (text) => {
2747
+ error.textContent = text ?? "";
2748
+ error.hidden = text === void 0 || text === "";
2749
+ };
2750
+ setError(opts.error);
2751
+ const root = el("div", "welcome", el(
2752
+ "div",
2753
+ "welcome-card",
2754
+ el("h1", "welcome-title", opts.title),
2755
+ el("p", "welcome-sub", opts.sub),
2756
+ actions,
2757
+ opts.tourLine ? el("div", "welcome-line", opts.tourLine) : null,
2758
+ ...groups,
2759
+ recentsHost,
2760
+ error
2761
+ ));
2762
+ host.replaceChildren(root);
2763
+ return { el: root, setRecents: drawRecents, setError };
2764
+ }
2765
+
2766
+ // src/link-status.ts
2767
+ function linkStatusClass(s) {
2768
+ if (s.state === "off" || s.state === "error") return "off";
2769
+ if (s.state === "listening") return "listening";
2770
+ return s.build === "stale" ? "stale" : "live";
2771
+ }
2772
+ function linkStatusTip(s) {
2773
+ switch (s.state) {
2774
+ case "off":
2775
+ return "Live link is off. Click to start listening.";
2776
+ case "error":
2777
+ return `Live link failed (${s.message}). Click to retry.`;
2778
+ case "listening":
2779
+ return "Live link is listening for a game. Click to stop.";
2780
+ case "connected": {
2781
+ const who = s.project !== void 0 ? ` to ${s.project}` : "";
2782
+ const build = s.build === "stale" ? " on a different build, so save or rebuild to re-sync" : s.build === "match" ? " and in sync" : "";
2783
+ const note = s.note ? ` ${s.note}` : "";
2784
+ return `Live link is connected${who}${build}.${note} Click to stop.`;
2785
+ }
2786
+ }
2787
+ }
2788
+ function linkAddress(s) {
2789
+ if (s.state !== "listening" && s.state !== "connected") return void 0;
2790
+ return s.address ?? `ws://127.0.0.1:${s.port}`;
2791
+ }
2792
+ function mountLinkStatus(host, opts) {
2793
+ const wrap = el("div", "linkstatus");
2794
+ wrap.hidden = true;
2795
+ const url = el("button", "linkstatus-url");
2796
+ url.type = "button";
2797
+ url.hidden = true;
2798
+ const toggle = el("button", "linkstatus-toggle off");
2799
+ toggle.type = "button";
2800
+ toggle.append(iconNode("connect"));
2801
+ if (opts.label !== void 0) wrap.append(el("span", "linkstatus-word", opts.label));
2802
+ wrap.append(...opts.lead ?? [], url, toggle);
2803
+ host.append(wrap);
2804
+ let status = { state: "off" };
2805
+ const render = () => {
2806
+ toggle.className = `linkstatus-toggle ${linkStatusClass(status)}`;
2807
+ const tip2 = linkStatusTip(status);
2808
+ toggle.dataset["tip"] = tip2;
2809
+ toggle.setAttribute("aria-label", tip2);
2810
+ const addr = linkAddress(status);
2811
+ if (addr !== void 0) {
2812
+ url.textContent = addr;
2813
+ url.dataset["tip"] = "Click to copy the live link address";
2814
+ url.hidden = false;
2815
+ } else url.hidden = true;
2816
+ };
2817
+ const apply = (s) => {
2818
+ status = s;
2819
+ render();
2820
+ };
2821
+ render();
2822
+ toggle.addEventListener("click", () => {
2823
+ const out = opts.onToggle(status);
2824
+ void Promise.resolve(out).then((next) => {
2825
+ if (next) apply(next);
2826
+ });
2827
+ });
2828
+ url.addEventListener("click", () => {
2829
+ const addr = url.textContent ?? "";
2830
+ if (!addr) return;
2831
+ void copyWithFeedback(url, addr);
2832
+ opts.onCopy?.(addr);
2833
+ });
2834
+ return {
2835
+ el: wrap,
2836
+ apply,
2837
+ status: () => status,
2838
+ setVisible(on) {
2839
+ wrap.hidden = !on;
2840
+ },
2841
+ toggle() {
2842
+ toggle.click();
2843
+ }
2844
+ };
2845
+ }
2846
+
2847
+ // src/property-list.ts
2848
+ var PROPERTY_TYPES = [
2849
+ ["number", "Number"],
2850
+ ["boolean", "True / False"],
2851
+ ["string", "Text"],
2852
+ ["enum", "List"],
2853
+ ["flags", "Flags"],
2854
+ ["quality", "Quality"]
2855
+ ];
2856
+ var option = (value, label2, selected) => {
2857
+ const o = el("option", void 0, label2);
2858
+ o.value = value;
2859
+ o.selected = selected;
2860
+ return o;
2861
+ };
2862
+ var NONE = "(none)";
2863
+ function defaultControl(p, onChange, opts = {}) {
2864
+ const typed = opts.typed === true;
2865
+ const write = (raw) => {
2866
+ if (raw === "") {
2867
+ if (typed) delete p.default;
2868
+ else p.default = "";
2869
+ } else if (typed && p.type === "boolean") p.default = raw === "true";
2870
+ else if (typed && p.type === "number") p.default = Number(raw);
2871
+ else p.default = raw;
2872
+ onChange?.();
2873
+ };
2874
+ const current3 = p.default === void 0 || p.default === null ? "" : String(p.default);
2875
+ if (p.type === "boolean") {
2876
+ const sel = el("select", "field set-default");
2877
+ sel.append(option("", NONE, current3 === ""), option("true", "True", current3 === "true"), option("false", "False", current3 === "false"));
2878
+ sel.addEventListener("change", () => write(sel.value));
2879
+ return sel;
2880
+ }
2881
+ if (p.type === "enum") {
2882
+ const sel = el("select", "field set-default");
2883
+ sel.append(option("", NONE, current3 === ""));
2884
+ for (const v of p.values ?? []) sel.append(option(v, v, current3 === v));
2885
+ sel.addEventListener("change", () => write(sel.value));
2886
+ return sel;
2887
+ }
2888
+ if (p.type === "quality") {
2889
+ const sel = el("select", "field set-default");
2890
+ sel.append(option("", "(first stage)", current3 === ""));
2891
+ for (const v of p.stages ?? []) sel.append(option(v, v, current3 === v));
2892
+ sel.addEventListener("change", () => write(sel.value));
2893
+ return sel;
2894
+ }
2895
+ if (p.type === "flags") {
2896
+ const s = el("span", "set-flagnote", "Starts empty");
2897
+ s.dataset["tip"] = "A flags property starts with none set.";
2898
+ return s;
2899
+ }
2900
+ const input = el("input", "field set-default");
2901
+ input.type = p.type === "number" ? "number" : "text";
2902
+ input.placeholder = "Default";
2903
+ input.value = current3;
2904
+ input.addEventListener("input", () => write(input.value));
2905
+ return input;
2906
+ }
2907
+ function mountPropertyList(host, decls, opts = {}) {
2908
+ const changed = () => opts.onChange?.();
2909
+ const guard = dupGuard();
2910
+ const types = opts.types ?? PROPERTY_TYPES;
2911
+ const list = el("div", "set-list");
2912
+ const keep = (parts) => (parts ?? []).filter((p) => p !== null);
2913
+ const row = (p, i) => {
2914
+ const name = el("input", "field set-name");
2915
+ name.type = "text";
2916
+ name.placeholder = "Property name";
2917
+ name.value = p.name;
2918
+ name.spellcheck = false;
2919
+ name.dataset["tip"] = PROPERTY_NAME_HINT;
2920
+ bindPropertyName(name, (v) => {
2921
+ p.name = v;
2922
+ changed();
2923
+ }, { hint: PROPERTY_NAME_HINT });
2924
+ guard.track(name);
2925
+ const type = el("select", "field set-type");
2926
+ for (const [v, label2] of types) type.append(option(v, label2, v === p.type));
2927
+ type.addEventListener("change", () => {
2928
+ p.type = type.value;
2929
+ if (opts.typed) delete p.default;
2930
+ else p.default = "";
2931
+ if (p.type === "enum" || p.type === "flags") p.values ??= [];
2932
+ else delete p.values;
2933
+ if (p.type === "quality") p.stages ??= [];
2934
+ else delete p.stages;
2935
+ render();
2936
+ changed();
2937
+ });
2938
+ let def = defaultControl(p, changed, opts);
2939
+ const refreshDefault = () => {
2940
+ const fresh = defaultControl(p, changed, opts);
2941
+ def.replaceWith(fresh);
2942
+ def = fresh;
2943
+ changed();
2944
+ };
2945
+ const up = iconBtn("up", "Move up", () => {
2946
+ if (moveItem(decls, i, -1)) {
2947
+ render();
2948
+ changed();
2949
+ }
2950
+ }, i === 0);
2951
+ const down = iconBtn("down", "Move down", () => {
2952
+ if (moveItem(decls, i, 1)) {
2953
+ render();
2954
+ changed();
2955
+ }
2956
+ }, i === decls.length - 1);
2957
+ const del = iconBtn("close", "Delete property", () => {
2958
+ decls.splice(i, 1);
2959
+ render();
2960
+ changed();
2961
+ }, false, true);
2962
+ const details = keep(opts.extraDetails?.(p, i));
2963
+ if (p.type === "enum" || p.type === "flags") details.push(labelled("Values", tagChips(p, refreshDefault)));
2964
+ if (p.type === "quality") details.push(labelled("Stages (in order)", stageChips(p, refreshDefault)));
2965
+ if (opts.purpose !== false) {
2966
+ const purpose = el("input", "field set-purpose");
2967
+ purpose.type = "text";
2968
+ purpose.placeholder = "What this property is for";
2969
+ purpose.value = p.purpose ?? "";
2970
+ purpose.addEventListener("input", () => {
2971
+ if (purpose.value.trim()) p.purpose = purpose.value;
2972
+ else delete p.purpose;
2973
+ changed();
2974
+ });
2975
+ details.push(labelled("Purpose", purpose));
2976
+ }
2977
+ return expandableRow({ line: [name, type, def, ...keep(opts.extraLine?.(p, i)), up, down, del], details, name: p.name });
2978
+ };
2979
+ const render = () => {
2980
+ guard.reset();
2981
+ list.replaceChildren();
2982
+ if (decls.length === 0) list.append(el("p", "empty", opts.emptyText ?? "No properties yet."));
2983
+ else decls.forEach((p, i) => list.append(row(p, i)));
2984
+ guard.check();
2985
+ };
2986
+ render();
2987
+ const add = el("button", "btn set-add", opts.addLabel ?? "+ Add property");
2988
+ add.type = "button";
2989
+ add.addEventListener("click", () => {
2990
+ decls.push(opts.newDecl ? opts.newDecl() : { name: "", type: types[0]?.[0] ?? "string", ...opts.typed ? {} : { default: "" } });
2991
+ render();
2992
+ changed();
2993
+ focusNewRow(list);
2994
+ });
2995
+ host.append(list, add);
2996
+ return {
2997
+ firstDuplicate: () => guard.firstDuplicate(),
2998
+ firstIllegalName: () => firstIllegalPropertyName(host),
2999
+ // The Save gate takes both faults; the field's own rollover says which.
3000
+ firstInvalid: () => guard.firstDuplicate() ?? firstIllegalPropertyName(host),
3001
+ render
3002
+ };
3003
+ }
3004
+
1762
3005
  // src/nav-history.ts
1763
3006
  function createNavHistory(opts) {
1764
3007
  const cap = opts.cap ?? 100;
1765
3008
  const usable = opts.usable ?? (() => true);
1766
3009
  let past = [];
1767
3010
  let ahead = [];
1768
- const step = (from, to, current2) => {
3011
+ const step = (from, to, current3) => {
1769
3012
  for (; ; ) {
1770
3013
  const p = from.pop();
1771
3014
  if (p === void 0) return void 0;
1772
3015
  if (!usable(p)) continue;
1773
- if (opts.same(p, current2)) continue;
1774
- to.push(current2);
3016
+ if (opts.same(p, current3)) continue;
3017
+ to.push(current3);
1775
3018
  return p;
1776
3019
  }
1777
3020
  };
@@ -1783,11 +3026,11 @@ function createNavHistory(opts) {
1783
3026
  else past.push(place2);
1784
3027
  if (past.length > cap) past = past.slice(past.length - cap);
1785
3028
  },
1786
- back(current2) {
1787
- return step(past, ahead, current2);
3029
+ back(current3) {
3030
+ return step(past, ahead, current3);
1788
3031
  },
1789
- forward(current2) {
1790
- return step(ahead, past, current2);
3032
+ forward(current3) {
3033
+ return step(ahead, past, current3);
1791
3034
  },
1792
3035
  canBack() {
1793
3036
  return past.length > 0;
@@ -1798,18 +3041,18 @@ function createNavHistory(opts) {
1798
3041
  };
1799
3042
  }
1800
3043
  function historyNav(onBack, onForward) {
1801
- const btn = (glyph, tip2, onClick) => {
3044
+ const btn = (name, tip2, onClick) => {
1802
3045
  const b = document.createElement("button");
1803
3046
  b.type = "button";
1804
3047
  b.className = "shell-histnav-btn";
1805
- b.textContent = glyph;
3048
+ b.append(iconNode(name));
1806
3049
  b.dataset["tip"] = tip2;
1807
3050
  b.setAttribute("aria-label", tip2);
1808
3051
  b.addEventListener("click", onClick);
1809
3052
  return b;
1810
3053
  };
1811
- const back = btn("\u2190", "Back", onBack);
1812
- const forward = btn("\u2192", "Forward", onForward);
3054
+ const back = btn("arrowLeft", "Back", onBack);
3055
+ const forward = btn("arrowRight", "Forward", onForward);
1813
3056
  const el2 = document.createElement("span");
1814
3057
  el2.className = "shell-histnav";
1815
3058
  el2.append(back, forward);
@@ -1823,67 +3066,117 @@ function historyNav(onBack, onForward) {
1823
3066
  }
1824
3067
 
1825
3068
  exports.EDIT_MENU = EDIT_MENU;
3069
+ exports.FILE_MENU = FILE_MENU;
1826
3070
  exports.GO_MENU = GO_MENU;
3071
+ exports.ICON_NAMES = ICON_NAMES;
3072
+ exports.ICON_SIZE = ICON_SIZE;
1827
3073
  exports.JOB_PROGRESS = JOB_PROGRESS;
1828
3074
  exports.PALETTE = PALETTE;
1829
3075
  exports.PALETTE_SIZE = PALETTE_SIZE;
1830
3076
  exports.PANE_MENU = PANE_MENU;
3077
+ exports.PLAY_MENU = PLAY_MENU;
1831
3078
  exports.PROPERTY_NAME_HINT = PROPERTY_NAME_HINT;
3079
+ exports.PROPERTY_TYPES = PROPERTY_TYPES;
3080
+ exports.PUBLISH_MENU = PUBLISH_MENU;
1832
3081
  exports.RESERVED_PROPERTY_NAMES = RESERVED_PROPERTY_NAMES;
3082
+ exports.REVIEW_MENU = REVIEW_MENU;
3083
+ exports.VIEW_MENU = VIEW_MENU;
1833
3084
  exports.askIdentity = askIdentity;
1834
3085
  exports.bindPropertyName = bindPropertyName;
1835
3086
  exports.bindPropertyRef = bindPropertyRef;
3087
+ exports.breadcrumb = breadcrumb;
1836
3088
  exports.closeAnchoredPanel = closeAnchoredPanel;
1837
3089
  exports.closeWithExit = closeWithExit;
1838
3090
  exports.colourFor = colourFor;
1839
3091
  exports.colourIndex = colourIndex;
1840
3092
  exports.confirmDialog = confirmDialog;
3093
+ exports.copyWithFeedback = copyWithFeedback;
3094
+ exports.createFloating = createFloating;
1841
3095
  exports.createNavHistory = createNavHistory;
1842
3096
  exports.createSaveController = createSaveController;
3097
+ exports.debounce = debounce;
3098
+ exports.defaultControl = defaultControl;
3099
+ exports.defaultProblemCopy = defaultProblemCopy;
3100
+ exports.describeProblem = describeProblem;
3101
+ exports.dialogFrame = dialogFrame;
1843
3102
  exports.dismissToast = dismissToast;
1844
3103
  exports.dupGuard = dupGuard;
1845
3104
  exports.el = el;
1846
3105
  exports.expandableRow = expandableRow;
3106
+ exports.feedUpdaterDownloadProgress = feedUpdaterDownloadProgress;
1847
3107
  exports.firstIllegalPropertyName = firstIllegalPropertyName;
1848
3108
  exports.focusNewRow = focusNewRow;
1849
3109
  exports.foldVc = foldVc;
3110
+ exports.followButton = followButton;
3111
+ exports.formatCount = formatCount;
1850
3112
  exports.gameIdify = gameIdify;
1851
3113
  exports.hideTip = hideTip;
3114
+ exports.hintBar = hintBar;
1852
3115
  exports.historyNav = historyNav;
1853
3116
  exports.icon = icon;
1854
3117
  exports.iconBtn = iconBtn;
3118
+ exports.iconHtml = iconHtml;
3119
+ exports.iconNameOfGlyph = iconNameOfGlyph;
3120
+ exports.iconNode = iconNode;
1855
3121
  exports.iconSvg = iconSvg;
1856
3122
  exports.initTooltips = initTooltips;
1857
3123
  exports.isCaseOnlyPropertyName = isCaseOnlyPropertyName;
3124
+ exports.isEditableTarget = isEditableTarget;
3125
+ exports.isIconName = isIconName;
3126
+ exports.isKeyCombo = isKeyCombo;
1858
3127
  exports.isValidGameId = isValidGameId;
1859
3128
  exports.isValidPropertyName = isValidPropertyName;
3129
+ exports.keyHint = keyHint;
3130
+ exports.keyLabel = keyLabel;
3131
+ exports.keyLegends = keyLegends;
3132
+ exports.keyPlatform = keyPlatform;
1860
3133
  exports.labelled = labelled;
3134
+ exports.linkAddress = linkAddress;
3135
+ exports.linkStatusClass = linkStatusClass;
3136
+ exports.linkStatusTip = linkStatusTip;
1861
3137
  exports.lockControls = lockControls;
1862
3138
  exports.lockNotice = lockNotice;
3139
+ exports.metaLine = metaLine;
1863
3140
  exports.mountJobProgress = mountJobProgress;
3141
+ exports.mountLinkStatus = mountLinkStatus;
1864
3142
  exports.mountPaneShell = mountPaneShell;
3143
+ exports.mountPropertyList = mountPropertyList;
1865
3144
  exports.mountSettingsDialog = mountSettingsDialog;
3145
+ exports.mountWelcome = mountWelcome;
1866
3146
  exports.moveItem = moveItem;
1867
3147
  exports.openAnchoredPanel = openAnchoredPanel;
1868
3148
  exports.openComments = openComments;
3149
+ exports.openContextMenu = openContextMenu;
1869
3150
  exports.openGameIdEditor = openGameIdEditor;
1870
3151
  exports.openNotesEditor = openNotesEditor;
3152
+ exports.openPopover = openPopover;
1871
3153
  exports.paintVcBadges = paintVcBadges;
1872
3154
  exports.pinButton = pinButton;
1873
3155
  exports.placeAnchored = placeAnchored;
3156
+ exports.plural = plural;
3157
+ exports.problemLine = problemLine;
3158
+ exports.problemName = problemName;
1874
3159
  exports.propertyNameProblem = propertyNameProblem;
1875
3160
  exports.propertyNameify = propertyNameify;
1876
3161
  exports.propertyRefProblem = propertyRefProblem;
3162
+ exports.relativeTime = relativeTime;
1877
3163
  exports.renderStepperBar = renderStepperBar;
1878
3164
  exports.revalidatePropertyRefs = revalidatePropertyRefs;
1879
3165
  exports.revealRow = revealRow;
3166
+ exports.revealRowWhenReady = revealRowWhenReady;
1880
3167
  exports.saveIndicator = saveIndicator;
3168
+ exports.setKeyPlatform = setKeyPlatform;
1881
3169
  exports.showAbout = showAbout;
3170
+ exports.showUpdaterDialog = showUpdaterDialog;
3171
+ exports.stageChips = stageChips;
1882
3172
  exports.staleBar = staleBar;
1883
3173
  exports.tagChips = tagChips;
1884
3174
  exports.tipAt = tipAt;
1885
3175
  exports.tipBold = tipBold;
3176
+ exports.tipWithKey = tipWithKey;
1886
3177
  exports.toast = toast;
3178
+ exports.toolWindowHead = toolWindowHead;
1887
3179
  exports.vcBadgeFor = vcBadgeFor;
3180
+ exports.wireReorder = wireReorder;
1888
3181
  //# sourceMappingURL=index.cjs.map
1889
3182
  //# sourceMappingURL=index.cjs.map