@wildwinter/app-shell 0.1.0 → 0.14.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.
- package/README.md +35 -0
- package/dist/about.css +76 -0
- package/dist/about.css.map +1 -0
- package/dist/anchored.css +50 -0
- package/dist/anchored.css.map +1 -0
- package/dist/app-store.cjs +74 -0
- package/dist/app-store.cjs.map +1 -0
- package/dist/app-store.d.cts +75 -0
- package/dist/app-store.d.ts +75 -0
- package/dist/app-store.js +72 -0
- package/dist/app-store.js.map +1 -0
- package/dist/chunk-MDGOIT3O.js +58 -0
- package/dist/chunk-MDGOIT3O.js.map +1 -0
- package/dist/chunk-R6RO5TXB.js +16 -0
- package/dist/chunk-R6RO5TXB.js.map +1 -0
- package/dist/comments.css +109 -0
- package/dist/comments.css.map +1 -0
- package/dist/confirm.css +62 -0
- package/dist/confirm.css.map +1 -0
- package/dist/identity.css +62 -0
- package/dist/identity.css.map +1 -0
- package/dist/index.cjs +1287 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +488 -1
- package/dist/index.d.ts +488 -1
- package/dist/index.js +1241 -1
- package/dist/index.js.map +1 -1
- package/dist/job.cjs +61 -0
- package/dist/job.cjs.map +1 -0
- package/dist/job.css +70 -0
- package/dist/job.css.map +1 -0
- package/dist/job.d.cts +56 -0
- package/dist/job.d.ts +56 -0
- package/dist/job.js +3 -0
- package/dist/job.js.map +1 -0
- package/dist/menu.cjs +19 -0
- package/dist/menu.cjs.map +1 -0
- package/dist/menu.d.cts +32 -0
- package/dist/menu.d.ts +32 -0
- package/dist/menu.js +3 -0
- package/dist/menu.js.map +1 -0
- package/dist/notes-editor.css +107 -0
- package/dist/notes-editor.css.map +1 -0
- package/dist/pane-shell.css +114 -0
- package/dist/pane-shell.css.map +1 -0
- package/dist/session.cjs +52 -0
- package/dist/session.cjs.map +1 -0
- package/dist/session.d.cts +79 -0
- package/dist/session.d.ts +79 -0
- package/dist/session.js +50 -0
- package/dist/session.js.map +1 -0
- package/dist/settings.css +266 -0
- package/dist/settings.css.map +1 -0
- package/dist/tool-window.cjs +79 -0
- package/dist/tool-window.cjs.map +1 -0
- package/dist/tool-window.css +60 -0
- package/dist/tool-window.css.map +1 -0
- package/dist/tool-window.d.cts +63 -0
- package/dist/tool-window.d.ts +63 -0
- package/dist/tool-window.js +73 -0
- package/dist/tool-window.js.map +1 -0
- package/dist/tooltip.css +22 -0
- package/dist/tooltip.css.map +1 -0
- package/dist/updater.cjs +315 -0
- package/dist/updater.cjs.map +1 -0
- package/dist/updater.d.cts +53 -0
- package/dist/updater.d.ts +53 -0
- package/dist/updater.js +306 -0
- package/dist/updater.js.map +1 -0
- package/dist/vc-status.cjs +77 -0
- package/dist/vc-status.cjs.map +1 -0
- package/dist/vc-status.d.cts +40 -0
- package/dist/vc-status.d.ts +40 -0
- package/dist/vc-status.js +71 -0
- package/dist/vc-status.js.map +1 -0
- package/dist/vc.css +41 -0
- package/dist/vc.css.map +1 -0
- package/package.json +101 -2
- package/dist/tokens.d.cts +0 -2
- package/dist/tokens.d.ts +0 -2
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
export { JOB_PROGRESS } from './chunk-MDGOIT3O.js';
|
|
2
|
+
export { EDIT_MENU, PANE_MENU } from './chunk-R6RO5TXB.js';
|
|
3
|
+
|
|
1
4
|
// src/dom.ts
|
|
2
5
|
function el(tag, classNameOrProps, ...children) {
|
|
3
6
|
const node = document.createElement(tag);
|
|
@@ -5,6 +8,10 @@ function el(tag, classNameOrProps, ...children) {
|
|
|
5
8
|
if (props.className) node.className = props.className;
|
|
6
9
|
if (props.text !== void 0 && props.text !== null) node.textContent = props.text;
|
|
7
10
|
if (props.title !== void 0) node.title = props.title;
|
|
11
|
+
if (props.tip !== void 0) {
|
|
12
|
+
node.dataset["tip"] = props.tip;
|
|
13
|
+
node.setAttribute("aria-label", props.tip);
|
|
14
|
+
}
|
|
8
15
|
if (props.onClick) node.addEventListener("click", props.onClick);
|
|
9
16
|
for (const child of children) {
|
|
10
17
|
if (child === null || child === void 0) continue;
|
|
@@ -12,6 +19,67 @@ function el(tag, classNameOrProps, ...children) {
|
|
|
12
19
|
}
|
|
13
20
|
return node;
|
|
14
21
|
}
|
|
22
|
+
function iconBtn(glyph, title, onClick, disabled = false, danger = false) {
|
|
23
|
+
const b = el("button", `shell-icon${danger ? " danger" : ""}`);
|
|
24
|
+
b.type = "button";
|
|
25
|
+
b.textContent = glyph;
|
|
26
|
+
b.dataset["tip"] = title;
|
|
27
|
+
b.setAttribute("aria-label", title);
|
|
28
|
+
b.disabled = disabled;
|
|
29
|
+
b.addEventListener("click", onClick);
|
|
30
|
+
return b;
|
|
31
|
+
}
|
|
32
|
+
function labelled(label2, control) {
|
|
33
|
+
const w = el("label", "shell-labelled");
|
|
34
|
+
w.append(el("span", "shell-fieldcap", label2), control);
|
|
35
|
+
return w;
|
|
36
|
+
}
|
|
37
|
+
function moveItem(arr, i, delta) {
|
|
38
|
+
const j = i + delta;
|
|
39
|
+
if (j < 0 || j >= arr.length) return false;
|
|
40
|
+
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
function tagChips(holder, onChange) {
|
|
44
|
+
const wrap = el("div", "shell-tags");
|
|
45
|
+
const input = el("input", "shell-tag-input");
|
|
46
|
+
input.type = "text";
|
|
47
|
+
input.placeholder = "add value";
|
|
48
|
+
input.spellcheck = false;
|
|
49
|
+
const makeChip = (v) => {
|
|
50
|
+
const chip = el("span", "shell-tag", v);
|
|
51
|
+
const x = el("button", "shell-tag-x", "\u2715");
|
|
52
|
+
x.type = "button";
|
|
53
|
+
x.title = `remove ${v}`;
|
|
54
|
+
x.setAttribute("aria-label", `remove ${v}`);
|
|
55
|
+
x.addEventListener("click", () => {
|
|
56
|
+
holder.values = (holder.values ?? []).filter((o) => o !== v);
|
|
57
|
+
chip.remove();
|
|
58
|
+
onChange?.();
|
|
59
|
+
});
|
|
60
|
+
chip.append(x);
|
|
61
|
+
return chip;
|
|
62
|
+
};
|
|
63
|
+
const commit = () => {
|
|
64
|
+
const v = input.value.trim();
|
|
65
|
+
if (v && !(holder.values ?? []).includes(v)) {
|
|
66
|
+
(holder.values ??= []).push(v);
|
|
67
|
+
wrap.insertBefore(makeChip(v), input);
|
|
68
|
+
onChange?.();
|
|
69
|
+
}
|
|
70
|
+
input.value = "";
|
|
71
|
+
};
|
|
72
|
+
input.addEventListener("keydown", (e) => {
|
|
73
|
+
if (e.key === "Enter" || e.key === ",") {
|
|
74
|
+
e.preventDefault();
|
|
75
|
+
commit();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
input.addEventListener("blur", commit);
|
|
79
|
+
for (const v of holder.values ?? []) wrap.append(makeChip(v));
|
|
80
|
+
wrap.append(input);
|
|
81
|
+
return wrap;
|
|
82
|
+
}
|
|
15
83
|
|
|
16
84
|
// src/colour.ts
|
|
17
85
|
var PALETTE_SIZE = 12;
|
|
@@ -33,6 +101,1178 @@ function colourFor(name) {
|
|
|
33
101
|
return PALETTE[colourIndex(name)];
|
|
34
102
|
}
|
|
35
103
|
|
|
36
|
-
|
|
104
|
+
// src/icons.ts
|
|
105
|
+
var icon = {
|
|
106
|
+
// --- dismissing and revealing ---------------------------------------------
|
|
107
|
+
/** Close a panel, a dialog, a popover. Patterpad's, twelve times over. */
|
|
108
|
+
close: "\u2715",
|
|
109
|
+
/** The overflow menu on a document or a row: "more about this". */
|
|
110
|
+
more: "\u22EF",
|
|
111
|
+
/** Collapsed disclosure, pointing at what would open. */
|
|
112
|
+
collapsed: "\u25B8",
|
|
113
|
+
/** Expanded disclosure. */
|
|
114
|
+
expanded: "\u25BE",
|
|
115
|
+
/** A dropdown's affordance, where it is not a real <select>. */
|
|
116
|
+
dropdown: "\u25BE",
|
|
117
|
+
// --- moving through things ------------------------------------------------
|
|
118
|
+
/** Back / previous, in a breadcrumb or a stepper. */
|
|
119
|
+
back: "\u2039",
|
|
120
|
+
/** Forward / next. */
|
|
121
|
+
forward: "\u203A",
|
|
122
|
+
/** Up one, in a list. */
|
|
123
|
+
up: "\u2191",
|
|
124
|
+
/** Down one. */
|
|
125
|
+
down: "\u2193",
|
|
126
|
+
// --- state ----------------------------------------------------------------
|
|
127
|
+
/** Done, applied, agreed. */
|
|
128
|
+
tick: "\u2713",
|
|
129
|
+
/** Somebody else holds this: the version-control lock. */
|
|
130
|
+
locked: "\u2298",
|
|
131
|
+
/** Read-only on disk with no other holder: the resting state of everything
|
|
132
|
+
* in a lock-based working copy that nobody has checked out yet. */
|
|
133
|
+
readOnly: "\u25CB",
|
|
134
|
+
/** Something needs attention but is not fatal. */
|
|
135
|
+
warning: "\u203C",
|
|
136
|
+
/** A live thing: the running position, an active marker. */
|
|
137
|
+
dot: "\u25CF",
|
|
138
|
+
/** Play, in the sense of "run this". */
|
|
139
|
+
play: "\u25B6",
|
|
140
|
+
// --- handling -------------------------------------------------------------
|
|
141
|
+
/** The drag grip on a reorderable row. */
|
|
142
|
+
grip: "\u283F",
|
|
143
|
+
/** Add one of whatever this list holds. */
|
|
144
|
+
add: "+",
|
|
145
|
+
// --- annotation -----------------------------------------------------------
|
|
146
|
+
/** Documentation notes: the pencil. Both apps had chosen it independently,
|
|
147
|
+
* which is the happiest way for a vocabulary entry to arrive. */
|
|
148
|
+
note: "\u270E",
|
|
149
|
+
/** A comment thread. Speech, not a pencil: documentation states the reason,
|
|
150
|
+
* comments are the conversation, and the two must never look alike. */
|
|
151
|
+
comment: "\u275D"
|
|
152
|
+
};
|
|
153
|
+
var iconSvg = {
|
|
154
|
+
/** A document with lines: notes exist on this thing. */
|
|
155
|
+
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>',
|
|
156
|
+
/** The same document, empty: nothing written here yet. */
|
|
157
|
+
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>'
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// src/pane-shell.ts
|
|
161
|
+
var DEFAULT_MAX = (w) => Math.min(640, w - 420);
|
|
162
|
+
var openVar = { nav: "--nav-open-w", inspector: "--insp-open-w" };
|
|
163
|
+
var collapseClass = { nav: "no-nav", inspector: "no-inspector" };
|
|
164
|
+
function mountPaneShell(host, opts) {
|
|
165
|
+
const cfg = { nav: opts.nav, inspector: opts.inspector };
|
|
166
|
+
const open = { nav: opts.initial?.open?.nav ?? true, inspector: opts.initial?.open?.inspector ?? true };
|
|
167
|
+
const width = { ...opts.initial?.width ?? {} };
|
|
168
|
+
const el2 = (tag, cls) => {
|
|
169
|
+
const n = document.createElement(tag);
|
|
170
|
+
if (cls) n.className = cls;
|
|
171
|
+
return n;
|
|
172
|
+
};
|
|
173
|
+
const navToggle = el2("button", "pane-toggle");
|
|
174
|
+
navToggle.type = "button";
|
|
175
|
+
const inspToggle = el2("button", "pane-toggle");
|
|
176
|
+
inspToggle.type = "button";
|
|
177
|
+
const topbarLead = el2("div", "topbar-lead");
|
|
178
|
+
const topbarTrail = el2("div", "topbar-trail");
|
|
179
|
+
const spacer = el2("span", "topbar-spacer");
|
|
180
|
+
const topbar = el2("header", "topbar");
|
|
181
|
+
topbar.append(navToggle, topbarLead, spacer, topbarTrail, inspToggle);
|
|
182
|
+
const panes = el2("main", "panes");
|
|
183
|
+
const navPane = el2("nav", "pane pane-nav");
|
|
184
|
+
const navInner = el2("div", "pane-inner");
|
|
185
|
+
navPane.append(navInner);
|
|
186
|
+
const centre = el2("section", "pane-centre");
|
|
187
|
+
const inspPane = el2("aside", "pane pane-inspector");
|
|
188
|
+
const inspInner = el2("div", "pane-inner");
|
|
189
|
+
inspPane.append(inspInner);
|
|
190
|
+
const navResizer = el2("div", "pane-resizer pane-resizer-nav");
|
|
191
|
+
const inspResizer = el2("div", "pane-resizer pane-resizer-insp");
|
|
192
|
+
panes.append(navPane, centre, inspPane, navResizer, inspResizer);
|
|
193
|
+
const root = el2("div", "pane-shell");
|
|
194
|
+
root.append(topbar, panes);
|
|
195
|
+
host.append(root);
|
|
196
|
+
function applyToggleGlyph(side, button) {
|
|
197
|
+
const isOpen = open[side];
|
|
198
|
+
button.textContent = side === "nav" ? isOpen ? "\u2039" : "\u203A" : isOpen ? "\u203A" : "\u2039";
|
|
199
|
+
const noun = cfg[side].label ?? side;
|
|
200
|
+
const hint = cfg[side].shortcutHint ? ` (${cfg[side].shortcutHint})` : "";
|
|
201
|
+
button.title = `${isOpen ? "Hide" : "Show"} ${noun}${hint}`;
|
|
202
|
+
button.setAttribute("aria-label", button.title);
|
|
203
|
+
button.setAttribute("aria-pressed", String(isOpen));
|
|
204
|
+
}
|
|
205
|
+
function apply() {
|
|
206
|
+
panes.classList.toggle(collapseClass.nav, !open.nav);
|
|
207
|
+
panes.classList.toggle(collapseClass.inspector, !open.inspector);
|
|
208
|
+
panes.style.setProperty(openVar.nav, width.nav !== void 0 ? `${width.nav}px` : cfg.nav.defaultWidth);
|
|
209
|
+
panes.style.setProperty(openVar.inspector, width.inspector !== void 0 ? `${width.inspector}px` : cfg.inspector.defaultWidth);
|
|
210
|
+
applyToggleGlyph("nav", navToggle);
|
|
211
|
+
applyToggleGlyph("inspector", inspToggle);
|
|
212
|
+
navResizer.style.display = cfg.nav.resizable === false ? "none" : "";
|
|
213
|
+
inspResizer.style.display = cfg.inspector.resizable === false ? "none" : "";
|
|
214
|
+
}
|
|
215
|
+
const snapshot = () => ({ open: { ...open }, width: { ...width } });
|
|
216
|
+
const persist = () => opts.onChange?.(snapshot());
|
|
217
|
+
function setPaneOpen(side, next) {
|
|
218
|
+
if (open[side] === next) return;
|
|
219
|
+
open[side] = next;
|
|
220
|
+
apply();
|
|
221
|
+
persist();
|
|
222
|
+
}
|
|
223
|
+
function togglePane(side) {
|
|
224
|
+
setPaneOpen(side, !open[side]);
|
|
225
|
+
}
|
|
226
|
+
function resetWidths() {
|
|
227
|
+
delete width.nav;
|
|
228
|
+
delete width.inspector;
|
|
229
|
+
apply();
|
|
230
|
+
persist();
|
|
231
|
+
}
|
|
232
|
+
navToggle.addEventListener("click", () => togglePane("nav"));
|
|
233
|
+
inspToggle.addEventListener("click", () => togglePane("inspector"));
|
|
234
|
+
function beginResize(side, startEvent) {
|
|
235
|
+
if (cfg[side].resizable === false || !open[side]) return;
|
|
236
|
+
startEvent.preventDefault();
|
|
237
|
+
const pane = side === "nav" ? navPane : inspPane;
|
|
238
|
+
const min = cfg[side].minWidth ?? 160;
|
|
239
|
+
const max = cfg[side].maxWidth ?? DEFAULT_MAX;
|
|
240
|
+
const startX = startEvent.clientX;
|
|
241
|
+
const startW = pane.getBoundingClientRect().width;
|
|
242
|
+
document.body.classList.add("pane-resizing");
|
|
243
|
+
let latest = startW;
|
|
244
|
+
const onMove = (e) => {
|
|
245
|
+
const delta = side === "nav" ? e.clientX - startX : startX - e.clientX;
|
|
246
|
+
latest = Math.max(min, Math.min(max(window.innerWidth), startW + delta));
|
|
247
|
+
panes.style.setProperty(openVar[side], `${latest}px`);
|
|
248
|
+
};
|
|
249
|
+
const onUp = () => {
|
|
250
|
+
window.removeEventListener("pointermove", onMove);
|
|
251
|
+
window.removeEventListener("pointerup", onUp);
|
|
252
|
+
document.body.classList.remove("pane-resizing");
|
|
253
|
+
width[side] = latest;
|
|
254
|
+
persist();
|
|
255
|
+
};
|
|
256
|
+
window.addEventListener("pointermove", onMove);
|
|
257
|
+
window.addEventListener("pointerup", onUp);
|
|
258
|
+
}
|
|
259
|
+
navResizer.addEventListener("pointerdown", (e) => beginResize("nav", e));
|
|
260
|
+
inspResizer.addEventListener("pointerdown", (e) => beginResize("inspector", e));
|
|
261
|
+
navResizer.addEventListener("dblclick", () => {
|
|
262
|
+
delete width.nav;
|
|
263
|
+
apply();
|
|
264
|
+
persist();
|
|
265
|
+
});
|
|
266
|
+
inspResizer.addEventListener("dblclick", () => {
|
|
267
|
+
delete width.inspector;
|
|
268
|
+
apply();
|
|
269
|
+
persist();
|
|
270
|
+
});
|
|
271
|
+
apply();
|
|
272
|
+
return {
|
|
273
|
+
root,
|
|
274
|
+
topbar,
|
|
275
|
+
topbarLead,
|
|
276
|
+
topbarTrail,
|
|
277
|
+
nav: navInner,
|
|
278
|
+
centre,
|
|
279
|
+
inspector: inspInner,
|
|
280
|
+
togglePane,
|
|
281
|
+
setPaneOpen,
|
|
282
|
+
isOpen: (s) => open[s],
|
|
283
|
+
resetWidths,
|
|
284
|
+
state: snapshot,
|
|
285
|
+
destroy: () => {
|
|
286
|
+
root.remove();
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/settings.ts
|
|
292
|
+
function expandableRow(opts) {
|
|
293
|
+
const row = el("div", "set-row");
|
|
294
|
+
const line = el("div", "set-rowline");
|
|
295
|
+
if (opts.details && opts.details.length) {
|
|
296
|
+
const details = el("div", "set-details");
|
|
297
|
+
details.hidden = true;
|
|
298
|
+
for (const d of opts.details) details.append(d);
|
|
299
|
+
const toggle = el("button", "set-expand");
|
|
300
|
+
toggle.type = "button";
|
|
301
|
+
toggle.title = "More";
|
|
302
|
+
toggle.setAttribute("aria-label", "More");
|
|
303
|
+
toggle.setAttribute("aria-expanded", "false");
|
|
304
|
+
toggle.addEventListener("click", () => {
|
|
305
|
+
const open = details.hidden;
|
|
306
|
+
details.hidden = !open;
|
|
307
|
+
toggle.setAttribute("aria-expanded", String(open));
|
|
308
|
+
toggle.classList.toggle("open", open);
|
|
309
|
+
});
|
|
310
|
+
line.append(toggle);
|
|
311
|
+
opts.line.forEach((c) => line.append(c));
|
|
312
|
+
row.append(line, details);
|
|
313
|
+
} else {
|
|
314
|
+
opts.line.forEach((c) => line.append(c));
|
|
315
|
+
row.append(line);
|
|
316
|
+
}
|
|
317
|
+
return row;
|
|
318
|
+
}
|
|
319
|
+
function focusNewRow(listEl) {
|
|
320
|
+
const last = listEl?.lastElementChild;
|
|
321
|
+
if (!(last instanceof HTMLElement)) return;
|
|
322
|
+
last.scrollIntoView({ block: "nearest" });
|
|
323
|
+
const name = last.querySelector("input.set-name") ?? last.querySelector("input");
|
|
324
|
+
name?.focus();
|
|
325
|
+
}
|
|
326
|
+
function dupGuard() {
|
|
327
|
+
const items = [];
|
|
328
|
+
const check = () => {
|
|
329
|
+
const counts = /* @__PURE__ */ new Map();
|
|
330
|
+
for (const it of items) {
|
|
331
|
+
const k = it.key().trim().toLowerCase();
|
|
332
|
+
if (k) counts.set(k, (counts.get(k) ?? 0) + 1);
|
|
333
|
+
}
|
|
334
|
+
let any = false;
|
|
335
|
+
for (const it of items) {
|
|
336
|
+
const k = it.key().trim().toLowerCase();
|
|
337
|
+
const dup = !!k && (counts.get(k) ?? 0) > 1;
|
|
338
|
+
it.input.classList.toggle("invalid", dup);
|
|
339
|
+
if (dup) {
|
|
340
|
+
it.input.title = "Duplicate name. Names must be unique.";
|
|
341
|
+
any = true;
|
|
342
|
+
} else if (it.input.title === "Duplicate name. Names must be unique.") it.input.removeAttribute("title");
|
|
343
|
+
}
|
|
344
|
+
return any;
|
|
345
|
+
};
|
|
346
|
+
return {
|
|
347
|
+
reset() {
|
|
348
|
+
items.length = 0;
|
|
349
|
+
},
|
|
350
|
+
track(input, key = () => input.value, also) {
|
|
351
|
+
items.push({ input, key });
|
|
352
|
+
input.addEventListener("input", check);
|
|
353
|
+
for (const a of also ?? []) a.addEventListener("input", check);
|
|
354
|
+
},
|
|
355
|
+
check,
|
|
356
|
+
firstDuplicate() {
|
|
357
|
+
check();
|
|
358
|
+
return items.find((it) => it.input.classList.contains("invalid"))?.input ?? null;
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function mountSettingsDialog(opts) {
|
|
363
|
+
const dialog = el("dialog", "settings-dialog");
|
|
364
|
+
const form = el("form", "settings-form");
|
|
365
|
+
form.method = "dialog";
|
|
366
|
+
const tabs = el("nav", "settings-tabs");
|
|
367
|
+
const panels = el("div", "settings-panels");
|
|
368
|
+
const error = el("p", "settings-error");
|
|
369
|
+
error.hidden = true;
|
|
370
|
+
const cancel = el("button", "settings-cancel", "Cancel");
|
|
371
|
+
cancel.type = "button";
|
|
372
|
+
cancel.value = "cancel";
|
|
373
|
+
const save = el("button", "settings-save primary", "Save");
|
|
374
|
+
save.value = "save";
|
|
375
|
+
const panelFor = /* @__PURE__ */ new Map();
|
|
376
|
+
const tabFor = /* @__PURE__ */ new Map();
|
|
377
|
+
const handles = /* @__PURE__ */ new Map();
|
|
378
|
+
let active2 = opts.sections[0]?.id ?? "";
|
|
379
|
+
let lastGroup;
|
|
380
|
+
for (const s of opts.sections) {
|
|
381
|
+
if (s.group && s.group !== lastGroup) {
|
|
382
|
+
tabs.append(el("div", "settings-group", s.group));
|
|
383
|
+
lastGroup = s.group;
|
|
384
|
+
}
|
|
385
|
+
const tab = el("button", "settings-tab", s.label);
|
|
386
|
+
tab.type = "button";
|
|
387
|
+
tab.dataset["tab"] = s.id;
|
|
388
|
+
tab.addEventListener("click", () => showTab(s.id));
|
|
389
|
+
tabs.append(tab);
|
|
390
|
+
tabFor.set(s.id, tab);
|
|
391
|
+
const panel = el("section", "settings-panel");
|
|
392
|
+
panel.dataset["panel"] = s.id;
|
|
393
|
+
panel.hidden = true;
|
|
394
|
+
panels.append(panel);
|
|
395
|
+
panelFor.set(s.id, panel);
|
|
396
|
+
}
|
|
397
|
+
function showTab(id) {
|
|
398
|
+
active2 = id;
|
|
399
|
+
for (const [sid, t] of tabFor) t.classList.toggle("active", sid === id);
|
|
400
|
+
for (const [sid, p] of panelFor) p.hidden = sid !== id;
|
|
401
|
+
}
|
|
402
|
+
form.append(
|
|
403
|
+
el("h2", "settings-title", opts.title),
|
|
404
|
+
el("div", "settings-body", tabs, panels),
|
|
405
|
+
el("div", "settings-actions", error, cancel, save)
|
|
406
|
+
);
|
|
407
|
+
dialog.append(form);
|
|
408
|
+
document.body.append(dialog);
|
|
409
|
+
form.addEventListener("keydown", (e) => {
|
|
410
|
+
if (e.key === "Enter" && e.target?.tagName === "INPUT") e.preventDefault();
|
|
411
|
+
});
|
|
412
|
+
cancel.addEventListener("click", () => dialog.close("cancel"));
|
|
413
|
+
save.addEventListener("click", (e) => {
|
|
414
|
+
e.preventDefault();
|
|
415
|
+
error.hidden = true;
|
|
416
|
+
for (const s of opts.sections) {
|
|
417
|
+
const bad = handles.get(s.id)?.firstInvalid?.();
|
|
418
|
+
if (bad) {
|
|
419
|
+
showTab(s.id);
|
|
420
|
+
bad.focus();
|
|
421
|
+
error.textContent = "Fix the highlighted fields first.";
|
|
422
|
+
error.hidden = false;
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
void (async () => {
|
|
427
|
+
await opts.onSave();
|
|
428
|
+
dialog.close("save");
|
|
429
|
+
})();
|
|
430
|
+
});
|
|
431
|
+
return {
|
|
432
|
+
open(tabId) {
|
|
433
|
+
handles.clear();
|
|
434
|
+
for (const s of opts.sections) {
|
|
435
|
+
const panel = panelFor.get(s.id);
|
|
436
|
+
panel.replaceChildren();
|
|
437
|
+
handles.set(s.id, s.mount(panel));
|
|
438
|
+
}
|
|
439
|
+
error.hidden = true;
|
|
440
|
+
showTab(tabId ?? active2 ?? opts.sections[0]?.id ?? "");
|
|
441
|
+
dialog.showModal();
|
|
442
|
+
},
|
|
443
|
+
destroy() {
|
|
444
|
+
dialog.remove();
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/tool-window-web.ts
|
|
450
|
+
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>`;
|
|
451
|
+
function pinButton(opts) {
|
|
452
|
+
const b = el("button", "swin-pin");
|
|
453
|
+
b.type = "button";
|
|
454
|
+
b.innerHTML = PIN_ICON;
|
|
455
|
+
let pinned = opts.pinned;
|
|
456
|
+
const reflect = () => {
|
|
457
|
+
b.classList.toggle("on", pinned);
|
|
458
|
+
b.setAttribute("aria-pressed", String(pinned));
|
|
459
|
+
b.title = pinned ? "Pinned on top: click to unpin" : "Click to keep on top";
|
|
460
|
+
};
|
|
461
|
+
reflect();
|
|
462
|
+
b.addEventListener("click", () => {
|
|
463
|
+
pinned = !pinned;
|
|
464
|
+
reflect();
|
|
465
|
+
opts.onToggle(pinned);
|
|
466
|
+
});
|
|
467
|
+
return b;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// src/confirm.ts
|
|
471
|
+
function confirmDialog(opts) {
|
|
472
|
+
return new Promise((resolve) => {
|
|
473
|
+
const cancel = el("button", "confirm-btn cancel", "Cancel");
|
|
474
|
+
const danger = el("button", "confirm-btn danger", opts.confirmLabel);
|
|
475
|
+
cancel.type = "button";
|
|
476
|
+
danger.type = "button";
|
|
477
|
+
const dlg = el(
|
|
478
|
+
"dialog",
|
|
479
|
+
"confirm-dialog",
|
|
480
|
+
el("div", "confirm-title", opts.title),
|
|
481
|
+
el("div", "confirm-body", opts.body),
|
|
482
|
+
el("div", "confirm-actions", cancel, danger)
|
|
483
|
+
);
|
|
484
|
+
dlg.setAttribute("aria-label", opts.title);
|
|
485
|
+
document.body.append(dlg);
|
|
486
|
+
let done = false;
|
|
487
|
+
const finish = (ok) => {
|
|
488
|
+
if (done) return;
|
|
489
|
+
done = true;
|
|
490
|
+
resolve(ok);
|
|
491
|
+
dlg.classList.add("closing");
|
|
492
|
+
const dur = parseFloat(getComputedStyle(dlg).animationDuration) || 0;
|
|
493
|
+
const teardown = () => {
|
|
494
|
+
dlg.close();
|
|
495
|
+
dlg.remove();
|
|
496
|
+
};
|
|
497
|
+
if (dur === 0) {
|
|
498
|
+
teardown();
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const timer2 = setTimeout(teardown, dur * 1e3 + 120);
|
|
502
|
+
dlg.addEventListener("animationend", (e) => {
|
|
503
|
+
if (e.target !== dlg) return;
|
|
504
|
+
clearTimeout(timer2);
|
|
505
|
+
teardown();
|
|
506
|
+
});
|
|
507
|
+
};
|
|
508
|
+
cancel.addEventListener("click", () => finish(false));
|
|
509
|
+
danger.addEventListener("click", () => finish(true));
|
|
510
|
+
dlg.addEventListener("cancel", (e) => {
|
|
511
|
+
e.preventDefault();
|
|
512
|
+
finish(false);
|
|
513
|
+
});
|
|
514
|
+
dlg.addEventListener("mousedown", (e) => {
|
|
515
|
+
if (e.target === dlg) finish(false);
|
|
516
|
+
});
|
|
517
|
+
dlg.showModal();
|
|
518
|
+
cancel.focus();
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// src/about.ts
|
|
523
|
+
function showAbout(opts) {
|
|
524
|
+
return new Promise((resolve) => {
|
|
525
|
+
const dialog = el("dialog", "shell-about");
|
|
526
|
+
if (opts.wordmark !== void 0) {
|
|
527
|
+
const mark = el("div", "shell-about-mark");
|
|
528
|
+
mark.innerHTML = opts.wordmark;
|
|
529
|
+
dialog.append(mark);
|
|
530
|
+
}
|
|
531
|
+
dialog.append(
|
|
532
|
+
el("div", "shell-about-name", opts.appName),
|
|
533
|
+
el("div", "shell-about-version", `Version ${opts.version}`)
|
|
534
|
+
);
|
|
535
|
+
if (opts.blurb !== void 0) dialog.append(el("p", "shell-about-blurb", opts.blurb));
|
|
536
|
+
if (opts.credits !== void 0) dialog.append(el("p", "shell-about-credits", opts.credits));
|
|
537
|
+
if (opts.links?.length) {
|
|
538
|
+
const row = el("div", "shell-about-links");
|
|
539
|
+
for (const link of opts.links) {
|
|
540
|
+
if (!opts.onOpenLink) {
|
|
541
|
+
row.append(el("span", "shell-about-link", link.label));
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
const a = el("button", "shell-about-link", link.label);
|
|
545
|
+
a.type = "button";
|
|
546
|
+
a.addEventListener("click", () => opts.onOpenLink?.(link.url));
|
|
547
|
+
row.append(a);
|
|
548
|
+
}
|
|
549
|
+
dialog.append(row);
|
|
550
|
+
}
|
|
551
|
+
const close = el("button", "shell-about-close", "Close");
|
|
552
|
+
close.type = "button";
|
|
553
|
+
dialog.append(el("div", "shell-about-actions", close));
|
|
554
|
+
dialog.setAttribute("aria-label", `About ${opts.appName}`);
|
|
555
|
+
document.body.append(dialog);
|
|
556
|
+
let done = false;
|
|
557
|
+
const finish = () => {
|
|
558
|
+
if (done) return;
|
|
559
|
+
done = true;
|
|
560
|
+
dialog.close();
|
|
561
|
+
dialog.remove();
|
|
562
|
+
resolve();
|
|
563
|
+
};
|
|
564
|
+
close.addEventListener("click", finish);
|
|
565
|
+
dialog.addEventListener("close", finish);
|
|
566
|
+
dialog.showModal();
|
|
567
|
+
close.focus();
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// src/anchored.ts
|
|
572
|
+
var current = null;
|
|
573
|
+
function openAnchoredPanel(opts) {
|
|
574
|
+
if (current) {
|
|
575
|
+
const prev = current;
|
|
576
|
+
current = null;
|
|
577
|
+
prev.close();
|
|
578
|
+
if (prev.anchor === opts.anchor) return null;
|
|
579
|
+
}
|
|
580
|
+
const panel = el("div", `shell-anchored ${opts.className ?? ""}`.trim());
|
|
581
|
+
const closeBtn = el("button", "shell-anchored-close", "\u2715");
|
|
582
|
+
closeBtn.type = "button";
|
|
583
|
+
closeBtn.setAttribute("aria-label", "Close");
|
|
584
|
+
const body = el("div", "shell-anchored-body");
|
|
585
|
+
panel.append(
|
|
586
|
+
el("div", "shell-anchored-head", el("span", "shell-anchored-title", opts.title), closeBtn),
|
|
587
|
+
body
|
|
588
|
+
);
|
|
589
|
+
document.body.append(panel);
|
|
590
|
+
placeAnchored(panel, opts.anchor, opts.width, opts.prefer ?? "below", opts.keepClear ?? []);
|
|
591
|
+
let closed = false;
|
|
592
|
+
const close = () => {
|
|
593
|
+
if (closed) return;
|
|
594
|
+
closed = true;
|
|
595
|
+
if (current?.panel === panel) current = null;
|
|
596
|
+
document.removeEventListener("pointerdown", onDown, true);
|
|
597
|
+
document.removeEventListener("keydown", onKey, true);
|
|
598
|
+
panel.remove();
|
|
599
|
+
opts.onClose?.();
|
|
600
|
+
};
|
|
601
|
+
closeBtn.addEventListener("click", close);
|
|
602
|
+
const onDown = (e) => {
|
|
603
|
+
const target = e.target;
|
|
604
|
+
if (panel.contains(target) || opts.anchor.contains(target)) return;
|
|
605
|
+
if (opts.ignoreDown !== void 0 && target instanceof Element && target.closest(opts.ignoreDown)) return;
|
|
606
|
+
close();
|
|
607
|
+
};
|
|
608
|
+
const onKey = (e) => {
|
|
609
|
+
if (e.key !== "Escape") return;
|
|
610
|
+
if (opts.deferEscape !== void 0 && document.querySelector(opts.deferEscape)) return;
|
|
611
|
+
close();
|
|
612
|
+
};
|
|
613
|
+
setTimeout(() => {
|
|
614
|
+
document.addEventListener("pointerdown", onDown, true);
|
|
615
|
+
document.addEventListener("keydown", onKey, true);
|
|
616
|
+
}, 0);
|
|
617
|
+
current = { anchor: opts.anchor, panel, close };
|
|
618
|
+
return { panel, body, close };
|
|
619
|
+
}
|
|
620
|
+
function closeAnchoredPanel() {
|
|
621
|
+
const open = current;
|
|
622
|
+
current = null;
|
|
623
|
+
open?.close();
|
|
624
|
+
}
|
|
625
|
+
function placeAnchored(panel, anchor, width, prefer, keepClear) {
|
|
626
|
+
const a = anchor.getBoundingClientRect();
|
|
627
|
+
const GAP2 = 10;
|
|
628
|
+
const CAP = window.innerHeight * 0.7;
|
|
629
|
+
const MIN_H = 140;
|
|
630
|
+
panel.style.width = `${width}px`;
|
|
631
|
+
const leftOf = a.left - width - GAP2;
|
|
632
|
+
const alignedRight = Math.min(a.right - width, window.innerWidth - width - 8);
|
|
633
|
+
panel.style.left = `${Math.round(prefer === "left" && leftOf >= 8 ? leftOf : Math.max(8, prefer === "left" ? a.left : alignedRight))}px`;
|
|
634
|
+
let bottomLimit = window.innerHeight - 8;
|
|
635
|
+
for (const selector of keepClear) {
|
|
636
|
+
const bar = document.querySelector(selector);
|
|
637
|
+
if (!bar) continue;
|
|
638
|
+
const r = bar.getBoundingClientRect();
|
|
639
|
+
if (r.height > 0) bottomLimit = Math.min(bottomLimit, r.top - 6);
|
|
640
|
+
}
|
|
641
|
+
const roomBelow = bottomLimit - (a.bottom + GAP2);
|
|
642
|
+
const roomAbove = a.top - GAP2 - 8;
|
|
643
|
+
if (roomAbove > roomBelow) {
|
|
644
|
+
panel.style.top = "auto";
|
|
645
|
+
panel.style.bottom = `${Math.round(window.innerHeight - (a.top - GAP2))}px`;
|
|
646
|
+
panel.style.maxHeight = `${Math.round(Math.max(MIN_H, Math.min(CAP, roomAbove)))}px`;
|
|
647
|
+
} else {
|
|
648
|
+
panel.style.bottom = "auto";
|
|
649
|
+
panel.style.top = `${Math.round(Math.max(8, a.bottom + GAP2))}px`;
|
|
650
|
+
panel.style.maxHeight = `${Math.round(Math.max(MIN_H, Math.min(CAP, roomBelow)))}px`;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// src/vc-view.ts
|
|
655
|
+
function foldVc(shards, keys) {
|
|
656
|
+
if (!keys) return void 0;
|
|
657
|
+
let out;
|
|
658
|
+
for (const key of keys.split(" ")) {
|
|
659
|
+
const s = shards.get(key);
|
|
660
|
+
if (!s) continue;
|
|
661
|
+
out ??= { key, writable: true };
|
|
662
|
+
if (!s.writable) out.writable = false;
|
|
663
|
+
if (s.lockedBy?.length) out.lockedBy = [.../* @__PURE__ */ new Set([...out.lockedBy ?? [], ...s.lockedBy])];
|
|
664
|
+
if (s.outOfDate) out.outOfDate = true;
|
|
665
|
+
}
|
|
666
|
+
return out;
|
|
667
|
+
}
|
|
668
|
+
function vcBadgeFor(s) {
|
|
669
|
+
if (s?.lockedBy?.length) return { glyph: icon.locked, cls: "vc-locked", title: `Locked by ${s.lockedBy.join(", ")}` };
|
|
670
|
+
if (s?.outOfDate) return { glyph: icon.down, cls: "vc-stale", title: "Out of date - a newer version is on the server" };
|
|
671
|
+
if (s && !s.writable) return { glyph: icon.readOnly, cls: "vc-frozen", title: "Read-only on disk - saving checks it out" };
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
function paintVcBadges(root, shards) {
|
|
675
|
+
root.querySelectorAll("[data-vc]").forEach((host) => {
|
|
676
|
+
host.querySelector(":scope > .vc-badge")?.remove();
|
|
677
|
+
const badge = vcBadgeFor(foldVc(shards, host.dataset["vc"]));
|
|
678
|
+
if (!badge) return;
|
|
679
|
+
const span = el("span", { className: `vc-badge ${badge.cls}`, text: badge.glyph, tip: badge.title });
|
|
680
|
+
host.append(span);
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
function lockControls(host, off, staysLive) {
|
|
684
|
+
host.classList.toggle("vc-readonly", off);
|
|
685
|
+
if (off) {
|
|
686
|
+
host.querySelectorAll("input, textarea, select, button").forEach((c) => {
|
|
687
|
+
if (c.disabled || staysLive !== "" && c.matches(staysLive)) return;
|
|
688
|
+
c.disabled = true;
|
|
689
|
+
c.classList.add("vc-off");
|
|
690
|
+
});
|
|
691
|
+
} else {
|
|
692
|
+
host.querySelectorAll(".vc-off").forEach((c) => {
|
|
693
|
+
c.disabled = false;
|
|
694
|
+
c.classList.remove("vc-off");
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function lockNotice(holders) {
|
|
699
|
+
return el(
|
|
700
|
+
"div",
|
|
701
|
+
{ className: "vc-lock" },
|
|
702
|
+
el("span", { className: "vc-lock-glyph", text: icon.locked }),
|
|
703
|
+
el("span", { text: `Locked by ${holders.join(", ")} - read-only until they release it.` })
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// src/identity.ts
|
|
708
|
+
function askIdentity(opts = {}) {
|
|
709
|
+
return new Promise((resolve) => {
|
|
710
|
+
const seed = opts.current ?? opts.suggested;
|
|
711
|
+
const name = el("input", "shell-ident-input");
|
|
712
|
+
name.placeholder = "Your name";
|
|
713
|
+
name.value = seed?.name ?? "";
|
|
714
|
+
const email = el("input", "shell-ident-input");
|
|
715
|
+
email.placeholder = "Email (optional)";
|
|
716
|
+
email.type = "email";
|
|
717
|
+
email.value = seed?.email ?? "";
|
|
718
|
+
const skip = el("button", "shell-ident-btn", "Skip");
|
|
719
|
+
const save = el("button", "shell-ident-btn primary", "Save");
|
|
720
|
+
skip.type = "button";
|
|
721
|
+
save.type = "button";
|
|
722
|
+
const dialog = el(
|
|
723
|
+
"dialog",
|
|
724
|
+
"shell-ident",
|
|
725
|
+
el("div", "shell-ident-title", "Who is working here?"),
|
|
726
|
+
el(
|
|
727
|
+
"p",
|
|
728
|
+
"shell-ident-hint",
|
|
729
|
+
"Stamped on the comments you write. Kept in this app, never in the project, and changeable later."
|
|
730
|
+
),
|
|
731
|
+
el("div", "shell-ident-fields", name, email),
|
|
732
|
+
el("div", "shell-ident-actions", skip, save)
|
|
733
|
+
);
|
|
734
|
+
document.body.append(dialog);
|
|
735
|
+
let done = false;
|
|
736
|
+
const finish = (keep) => {
|
|
737
|
+
if (done) return;
|
|
738
|
+
done = true;
|
|
739
|
+
const typed = name.value.trim();
|
|
740
|
+
const value = email.value.trim();
|
|
741
|
+
const answer = keep && typed !== "" ? value === "" ? { name: typed } : { name: typed, email: value } : void 0;
|
|
742
|
+
dialog.close();
|
|
743
|
+
dialog.remove();
|
|
744
|
+
resolve(answer);
|
|
745
|
+
};
|
|
746
|
+
skip.addEventListener("click", () => finish(false));
|
|
747
|
+
save.addEventListener("click", () => finish(true));
|
|
748
|
+
name.addEventListener("keydown", (e) => {
|
|
749
|
+
if (e.key === "Enter") finish(true);
|
|
750
|
+
});
|
|
751
|
+
email.addEventListener("keydown", (e) => {
|
|
752
|
+
if (e.key === "Enter") finish(true);
|
|
753
|
+
});
|
|
754
|
+
dialog.addEventListener("close", () => finish(false));
|
|
755
|
+
dialog.showModal();
|
|
756
|
+
name.focus();
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// src/notes-editor.ts
|
|
761
|
+
var EDITOR_ONLY = "";
|
|
762
|
+
var UNTYPED_KEY = "__untyped";
|
|
763
|
+
var CLASS_LABEL = {
|
|
764
|
+
everyone: "Everyone",
|
|
765
|
+
writing: "Writing",
|
|
766
|
+
vo: "Voice (VO)",
|
|
767
|
+
loc: "Localisers"
|
|
768
|
+
};
|
|
769
|
+
var label = (cls) => cls === EDITOR_ONLY ? "Note (editor-only)" : CLASS_LABEL[cls] ?? cls[0].toUpperCase() + cls.slice(1);
|
|
770
|
+
var PLACEHOLDER = {
|
|
771
|
+
vo: "direction for the voice actor / director",
|
|
772
|
+
loc: "context for translators"
|
|
773
|
+
};
|
|
774
|
+
var placeholder = (cls) => cls === EDITOR_ONLY ? "an internal note - never exported" : PLACEHOLDER[cls] ?? "intent / rationale - why this is here";
|
|
775
|
+
function openNotesEditor(opts) {
|
|
776
|
+
const { notes } = opts;
|
|
777
|
+
const seed = /* @__PURE__ */ new Map();
|
|
778
|
+
for (const line of notes.own) {
|
|
779
|
+
const key = line.type ?? EDITOR_ONLY;
|
|
780
|
+
seed.set(key, seed.has(key) ? `${seed.get(key)}
|
|
781
|
+
${line.text}` : line.text);
|
|
782
|
+
}
|
|
783
|
+
const shown = [...seed.keys()];
|
|
784
|
+
if (shown.length === 0) shown.push(notes.classes[0] ?? EDITOR_ONLY);
|
|
785
|
+
const areas = /* @__PURE__ */ new Map();
|
|
786
|
+
const value = () => {
|
|
787
|
+
const out = [];
|
|
788
|
+
for (const cls of shown) {
|
|
789
|
+
const area = areas.get(cls);
|
|
790
|
+
if (!area) continue;
|
|
791
|
+
for (const raw of area.value.split("\n")) {
|
|
792
|
+
const text = raw.trim();
|
|
793
|
+
if (text !== "") out.push(cls === EDITOR_ONLY ? { text } : { type: cls, text });
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return out;
|
|
797
|
+
};
|
|
798
|
+
const body = el("div", "shell-notes-body");
|
|
799
|
+
const render = () => {
|
|
800
|
+
body.replaceChildren();
|
|
801
|
+
if (notes.inherited.length > 0) {
|
|
802
|
+
const from = el(
|
|
803
|
+
"div",
|
|
804
|
+
{ className: "shell-notes-inherited" },
|
|
805
|
+
el("div", "shell-notes-caption", "Inherited")
|
|
806
|
+
);
|
|
807
|
+
for (const level of notes.inherited) {
|
|
808
|
+
from.append(el(
|
|
809
|
+
"div",
|
|
810
|
+
{ className: "shell-notes-level" },
|
|
811
|
+
el("span", "shell-notes-level-label", level.label),
|
|
812
|
+
...level.lines.map((line) => el(
|
|
813
|
+
"div",
|
|
814
|
+
{ className: "shell-notes-line" },
|
|
815
|
+
...line.type !== void 0 && line.type !== "everyone" ? [el("span", "shell-notes-cls", label(line.type))] : [],
|
|
816
|
+
el("span", "", line.text)
|
|
817
|
+
))
|
|
818
|
+
));
|
|
819
|
+
}
|
|
820
|
+
body.append(from);
|
|
821
|
+
}
|
|
822
|
+
for (const cls of shown) {
|
|
823
|
+
const block = el(
|
|
824
|
+
"div",
|
|
825
|
+
{ className: "shell-notes-class" },
|
|
826
|
+
el("label", "shell-notes-caption", label(cls))
|
|
827
|
+
);
|
|
828
|
+
const area = el("textarea", "shell-notes-text");
|
|
829
|
+
area.value = areas.get(cls)?.value ?? seed.get(cls) ?? "";
|
|
830
|
+
area.rows = 4;
|
|
831
|
+
area.placeholder = placeholder(cls);
|
|
832
|
+
area.spellcheck = true;
|
|
833
|
+
areas.set(cls, area);
|
|
834
|
+
block.append(area);
|
|
835
|
+
body.append(block);
|
|
836
|
+
}
|
|
837
|
+
body.append(el("p", "shell-notes-hint", "One note per line."));
|
|
838
|
+
const remaining = [...notes.classes, EDITOR_ONLY].filter((c) => !shown.includes(c));
|
|
839
|
+
if (remaining.length > 0) {
|
|
840
|
+
const add = el("select", "shell-notes-add");
|
|
841
|
+
add.append(new Option("Add a note for\u2026", ""));
|
|
842
|
+
for (const cls of remaining) add.append(new Option(label(cls), cls === EDITOR_ONLY ? UNTYPED_KEY : cls));
|
|
843
|
+
add.addEventListener("change", () => {
|
|
844
|
+
if (add.value === "") return;
|
|
845
|
+
const cls = add.value === UNTYPED_KEY ? EDITOR_ONLY : add.value;
|
|
846
|
+
shown.push(cls);
|
|
847
|
+
render();
|
|
848
|
+
areas.get(cls)?.focus();
|
|
849
|
+
});
|
|
850
|
+
body.append(add);
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
render();
|
|
854
|
+
const done = el("button", "shell-notes-btn primary", "Done");
|
|
855
|
+
const dialog = el(
|
|
856
|
+
"dialog",
|
|
857
|
+
{ className: "shell-notes" },
|
|
858
|
+
el("div", { className: "shell-notes-title", text: `Notes: ${opts.subject}` }),
|
|
859
|
+
body,
|
|
860
|
+
el("div", { className: "shell-notes-actions" }, done)
|
|
861
|
+
);
|
|
862
|
+
dialog.setAttribute("aria-label", `Notes: ${opts.subject}`);
|
|
863
|
+
document.body.append(dialog);
|
|
864
|
+
const before = JSON.stringify(notes.own);
|
|
865
|
+
let closed = false;
|
|
866
|
+
const close = () => {
|
|
867
|
+
if (closed) return;
|
|
868
|
+
closed = true;
|
|
869
|
+
const next = value();
|
|
870
|
+
if (JSON.stringify(next) !== before) opts.save(next);
|
|
871
|
+
dialog.close();
|
|
872
|
+
dialog.remove();
|
|
873
|
+
};
|
|
874
|
+
done.addEventListener("click", close);
|
|
875
|
+
dialog.addEventListener("close", close);
|
|
876
|
+
dialog.showModal();
|
|
877
|
+
areas.get(shown[0] ?? EDITOR_ONLY)?.focus();
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// src/comments.ts
|
|
881
|
+
var stamp = (ts) => {
|
|
882
|
+
const when = new Date(ts);
|
|
883
|
+
if (Number.isNaN(when.getTime())) return ts;
|
|
884
|
+
return when.toLocaleString(void 0, { dateStyle: "medium", timeStyle: "short" });
|
|
885
|
+
};
|
|
886
|
+
var gist = (thread) => {
|
|
887
|
+
const first = thread.messages[0]?.body ?? "";
|
|
888
|
+
return first.length > 60 ? `${first.slice(0, 57)}\u2026` : first;
|
|
889
|
+
};
|
|
890
|
+
function openComments(opts) {
|
|
891
|
+
const visible = opts.threads.filter((t) => opts.showResolved || t.resolved !== true);
|
|
892
|
+
const panel = openAnchoredPanel({
|
|
893
|
+
anchor: opts.anchor,
|
|
894
|
+
className: "comments",
|
|
895
|
+
title: "Comments",
|
|
896
|
+
width: 320,
|
|
897
|
+
// Below its button (the topline), unlike Patterpad's inspector rows which
|
|
898
|
+
// want it to the left; and never over the problems bar, which an author may
|
|
899
|
+
// be stepping through while reading the thread.
|
|
900
|
+
prefer: "below",
|
|
901
|
+
keepClear: [".problembar"]
|
|
902
|
+
});
|
|
903
|
+
if (!panel) return;
|
|
904
|
+
const { body, close } = panel;
|
|
905
|
+
let open = visible.length === 1 ? visible[0] : void 0;
|
|
906
|
+
let composing = visible.length === 0;
|
|
907
|
+
const render = () => {
|
|
908
|
+
body.replaceChildren();
|
|
909
|
+
if (!composing && open === void 0) {
|
|
910
|
+
for (const thread2 of visible) {
|
|
911
|
+
const row = el(
|
|
912
|
+
"button",
|
|
913
|
+
{ className: `shell-cmt-row${thread2.resolved === true ? " resolved" : ""}` },
|
|
914
|
+
el("span", { className: "shell-cmt-row-author", text: thread2.messages[0]?.author ?? "Someone" }),
|
|
915
|
+
el("span", { className: "shell-cmt-row-gist", text: gist(thread2) }),
|
|
916
|
+
...thread2.messages.length > 1 ? [el("span", { className: "shell-cmt-row-count", text: `${thread2.messages.length}` })] : []
|
|
917
|
+
);
|
|
918
|
+
row.addEventListener("click", () => {
|
|
919
|
+
open = thread2;
|
|
920
|
+
render();
|
|
921
|
+
});
|
|
922
|
+
body.append(row);
|
|
923
|
+
}
|
|
924
|
+
const fresh = el("button", { className: "shell-cmt-new", text: "New comment" });
|
|
925
|
+
fresh.addEventListener("click", () => {
|
|
926
|
+
open = void 0;
|
|
927
|
+
composing = true;
|
|
928
|
+
render();
|
|
929
|
+
});
|
|
930
|
+
body.append(fresh);
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
const thread = open;
|
|
934
|
+
if (thread) {
|
|
935
|
+
const list = el("div", { className: `shell-cmt-list${thread.resolved === true ? " resolved" : ""}` });
|
|
936
|
+
for (const message of thread.messages) {
|
|
937
|
+
list.append(el(
|
|
938
|
+
"div",
|
|
939
|
+
{ className: "shell-cmt-msg" },
|
|
940
|
+
el(
|
|
941
|
+
"div",
|
|
942
|
+
{ className: "shell-cmt-msg-head" },
|
|
943
|
+
el("span", { className: "shell-cmt-author", text: message.author === "" ? "Someone" : message.author }),
|
|
944
|
+
el("span", { className: "shell-cmt-ts", text: stamp(message.ts) })
|
|
945
|
+
),
|
|
946
|
+
el("div", { className: "shell-cmt-body", text: message.body })
|
|
947
|
+
));
|
|
948
|
+
}
|
|
949
|
+
body.append(list);
|
|
950
|
+
}
|
|
951
|
+
const input = el("textarea", { className: "shell-cmt-input" });
|
|
952
|
+
input.rows = 2;
|
|
953
|
+
input.placeholder = thread ? "Reply\u2026" : "Comment\u2026";
|
|
954
|
+
input.addEventListener("keydown", (e) => {
|
|
955
|
+
e.stopPropagation();
|
|
956
|
+
if (e.key !== "Enter" || e.shiftKey) return;
|
|
957
|
+
e.preventDefault();
|
|
958
|
+
submit(true);
|
|
959
|
+
});
|
|
960
|
+
body.append(input);
|
|
961
|
+
const submit = (andClose) => {
|
|
962
|
+
const text = input.value.trim();
|
|
963
|
+
if (text === "") return;
|
|
964
|
+
const id = thread?.id ?? opts.newThreadId();
|
|
965
|
+
opts.post(id, text);
|
|
966
|
+
if (andClose) close();
|
|
967
|
+
else {
|
|
968
|
+
input.value = "";
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
const actions = el("div", { className: "shell-cmt-actions" });
|
|
972
|
+
const post = el("button", { className: "shell-cmt-btn primary", text: thread ? "Reply" : "Comment" });
|
|
973
|
+
post.addEventListener("click", () => submit(false));
|
|
974
|
+
actions.append(post);
|
|
975
|
+
if (thread) {
|
|
976
|
+
const resolved = thread.resolved === true;
|
|
977
|
+
const toggle = el("button", { className: "shell-cmt-btn", text: resolved ? "Reopen" : "Mark complete" });
|
|
978
|
+
toggle.addEventListener("click", () => {
|
|
979
|
+
opts.setResolved(thread.id, !resolved);
|
|
980
|
+
if (!resolved) close();
|
|
981
|
+
else render();
|
|
982
|
+
});
|
|
983
|
+
actions.append(toggle);
|
|
984
|
+
}
|
|
985
|
+
body.append(actions);
|
|
986
|
+
if (!thread) input.focus();
|
|
987
|
+
};
|
|
988
|
+
render();
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// src/job-view.ts
|
|
992
|
+
var clock = (ms) => {
|
|
993
|
+
const total = Math.round(ms / 1e3);
|
|
994
|
+
const m = Math.floor(total / 60);
|
|
995
|
+
const s = total % 60;
|
|
996
|
+
return m > 0 ? `${m}m ${String(s).padStart(2, "0")}s` : `${s}s`;
|
|
997
|
+
};
|
|
998
|
+
var estimable = (done, total, elapsedMs) => done > 0 && total > 0 && elapsedMs > 1e3 && done / total > 0.1;
|
|
999
|
+
function mountJobProgress(host, opts) {
|
|
1000
|
+
const units = opts.units ?? "runs";
|
|
1001
|
+
const fill = el("i", { className: "job-fill" });
|
|
1002
|
+
const bar = el("div", { className: "job-bar" }, fill);
|
|
1003
|
+
const what = el("span", { className: "job-label" });
|
|
1004
|
+
const count = el("span", { className: "job-count" });
|
|
1005
|
+
const time = el("span", { className: "job-time" });
|
|
1006
|
+
const cancel = el("button", { className: "job-cancel", text: "Cancel" });
|
|
1007
|
+
const strip = el("div", { className: "job-strip" }, what, bar, count, time, cancel);
|
|
1008
|
+
strip.hidden = true;
|
|
1009
|
+
host.append(strip);
|
|
1010
|
+
const view = {
|
|
1011
|
+
element: strip,
|
|
1012
|
+
get visible() {
|
|
1013
|
+
return !strip.hidden;
|
|
1014
|
+
},
|
|
1015
|
+
begin(label2) {
|
|
1016
|
+
what.textContent = label2 ?? "Running\u2026";
|
|
1017
|
+
count.textContent = "";
|
|
1018
|
+
time.textContent = "";
|
|
1019
|
+
fill.style.width = "0%";
|
|
1020
|
+
cancel.disabled = false;
|
|
1021
|
+
cancel.textContent = "Cancel";
|
|
1022
|
+
strip.hidden = false;
|
|
1023
|
+
},
|
|
1024
|
+
update(done, total, elapsedMs) {
|
|
1025
|
+
if (strip.hidden) strip.hidden = false;
|
|
1026
|
+
const pct = total > 0 ? Math.min(100, done / total * 100) : 0;
|
|
1027
|
+
fill.style.width = `${pct}%`;
|
|
1028
|
+
count.textContent = total > 0 ? `${done} / ${total} ${units}` : `${done} ${units}`;
|
|
1029
|
+
if (estimable(done, total, elapsedMs)) {
|
|
1030
|
+
const remaining = elapsedMs / done * (total - done);
|
|
1031
|
+
time.textContent = `${clock(elapsedMs)} elapsed \xB7 about ${clock(remaining)} left`;
|
|
1032
|
+
} else {
|
|
1033
|
+
time.textContent = clock(elapsedMs);
|
|
1034
|
+
}
|
|
1035
|
+
},
|
|
1036
|
+
end() {
|
|
1037
|
+
strip.hidden = true;
|
|
1038
|
+
}
|
|
1039
|
+
};
|
|
1040
|
+
cancel.addEventListener("click", () => {
|
|
1041
|
+
cancel.disabled = true;
|
|
1042
|
+
cancel.textContent = "Stopping\u2026";
|
|
1043
|
+
opts.onCancel();
|
|
1044
|
+
});
|
|
1045
|
+
return view;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// src/tooltip.ts
|
|
1049
|
+
var B_OPEN = "";
|
|
1050
|
+
var B_CLOSE = "";
|
|
1051
|
+
function tipBold(s) {
|
|
1052
|
+
return `${B_OPEN}${s}${B_CLOSE}`;
|
|
1053
|
+
}
|
|
1054
|
+
function renderTip(host, text) {
|
|
1055
|
+
host.textContent = "";
|
|
1056
|
+
const re = /\u0002([^\u0003]*)\u0003/g;
|
|
1057
|
+
let last = 0;
|
|
1058
|
+
let m;
|
|
1059
|
+
while ((m = re.exec(text)) !== null) {
|
|
1060
|
+
if (m.index > last) host.append(text.slice(last, m.index));
|
|
1061
|
+
const strong = document.createElement("strong");
|
|
1062
|
+
strong.textContent = m[1] ?? "";
|
|
1063
|
+
host.append(strong);
|
|
1064
|
+
last = m.index + m[0].length;
|
|
1065
|
+
}
|
|
1066
|
+
if (last < text.length) host.append(text.slice(last));
|
|
1067
|
+
}
|
|
1068
|
+
var inited = false;
|
|
1069
|
+
var suppressed;
|
|
1070
|
+
var SHOW_DELAY = 350;
|
|
1071
|
+
var EDGE = 6;
|
|
1072
|
+
var GAP = 7;
|
|
1073
|
+
var tip = null;
|
|
1074
|
+
var timer = 0;
|
|
1075
|
+
var active = null;
|
|
1076
|
+
var activeKey = null;
|
|
1077
|
+
function ensureEl() {
|
|
1078
|
+
if (tip) return tip;
|
|
1079
|
+
const el2 = document.createElement("div");
|
|
1080
|
+
el2.className = "tooltip";
|
|
1081
|
+
el2.setAttribute("role", "tooltip");
|
|
1082
|
+
el2.hidden = true;
|
|
1083
|
+
document.body.appendChild(el2);
|
|
1084
|
+
tip = el2;
|
|
1085
|
+
return el2;
|
|
1086
|
+
}
|
|
1087
|
+
var rectOf = (anchor) => anchor instanceof HTMLElement ? anchor.getBoundingClientRect() : anchor;
|
|
1088
|
+
function place(anchor) {
|
|
1089
|
+
const t = ensureEl();
|
|
1090
|
+
const r = rectOf(anchor);
|
|
1091
|
+
const tr = t.getBoundingClientRect();
|
|
1092
|
+
let top = r.top - tr.height - GAP;
|
|
1093
|
+
if (top < EDGE) top = r.top + r.height + GAP;
|
|
1094
|
+
let left = r.left + r.width / 2 - tr.width / 2;
|
|
1095
|
+
left = Math.max(EDGE, Math.min(left, window.innerWidth - tr.width - EDGE));
|
|
1096
|
+
t.style.left = `${Math.round(left)}px`;
|
|
1097
|
+
t.style.top = `${Math.round(top)}px`;
|
|
1098
|
+
}
|
|
1099
|
+
function show(anchor) {
|
|
1100
|
+
const text = anchor.dataset["tip"];
|
|
1101
|
+
if (!text) return;
|
|
1102
|
+
paint(anchor, text);
|
|
1103
|
+
}
|
|
1104
|
+
function paint(anchor, text) {
|
|
1105
|
+
if (suppressed?.()) return;
|
|
1106
|
+
const t = ensureEl();
|
|
1107
|
+
const host = (anchor instanceof HTMLElement ? anchor.closest("dialog[open]") : null) ?? document.body;
|
|
1108
|
+
if (t.parentElement !== host) host.appendChild(t);
|
|
1109
|
+
active = anchor instanceof HTMLElement ? anchor : null;
|
|
1110
|
+
renderTip(t, text);
|
|
1111
|
+
t.hidden = false;
|
|
1112
|
+
t.classList.remove("show");
|
|
1113
|
+
void t.offsetWidth;
|
|
1114
|
+
place(anchor);
|
|
1115
|
+
t.classList.add("show");
|
|
1116
|
+
}
|
|
1117
|
+
function hide() {
|
|
1118
|
+
if (timer) {
|
|
1119
|
+
window.clearTimeout(timer);
|
|
1120
|
+
timer = 0;
|
|
1121
|
+
}
|
|
1122
|
+
active = null;
|
|
1123
|
+
activeKey = null;
|
|
1124
|
+
if (tip) {
|
|
1125
|
+
tip.hidden = true;
|
|
1126
|
+
tip.classList.remove("show");
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
function schedule(anchor) {
|
|
1130
|
+
if (anchor === active) return;
|
|
1131
|
+
if (timer) window.clearTimeout(timer);
|
|
1132
|
+
timer = window.setTimeout(() => show(anchor), SHOW_DELAY);
|
|
1133
|
+
}
|
|
1134
|
+
function tipAt(key, rect, text) {
|
|
1135
|
+
if (key === activeKey) {
|
|
1136
|
+
if (tip && !tip.hidden) {
|
|
1137
|
+
renderTip(tip, text);
|
|
1138
|
+
place(rect);
|
|
1139
|
+
}
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
hide();
|
|
1143
|
+
activeKey = key;
|
|
1144
|
+
timer = window.setTimeout(() => {
|
|
1145
|
+
timer = 0;
|
|
1146
|
+
paint(rect, text);
|
|
1147
|
+
}, SHOW_DELAY);
|
|
1148
|
+
}
|
|
1149
|
+
function hideTip() {
|
|
1150
|
+
hide();
|
|
1151
|
+
}
|
|
1152
|
+
var tipAncestor = (n) => n instanceof Element ? n.closest("[data-tip]") : null;
|
|
1153
|
+
function initTooltips(opts = {}) {
|
|
1154
|
+
if (inited) return;
|
|
1155
|
+
inited = true;
|
|
1156
|
+
suppressed = opts.suppressed;
|
|
1157
|
+
document.addEventListener("pointerover", (e) => {
|
|
1158
|
+
const el2 = tipAncestor(e.target);
|
|
1159
|
+
if (el2) schedule(el2);
|
|
1160
|
+
});
|
|
1161
|
+
document.addEventListener("pointerout", (e) => {
|
|
1162
|
+
const el2 = tipAncestor(e.target);
|
|
1163
|
+
if (!el2) return;
|
|
1164
|
+
const to = e.relatedTarget;
|
|
1165
|
+
if (to instanceof Node && el2.contains(to)) return;
|
|
1166
|
+
hide();
|
|
1167
|
+
});
|
|
1168
|
+
document.addEventListener("focusin", (e) => {
|
|
1169
|
+
const el2 = tipAncestor(e.target);
|
|
1170
|
+
if (el2) schedule(el2);
|
|
1171
|
+
});
|
|
1172
|
+
document.addEventListener("focusout", () => hide());
|
|
1173
|
+
document.addEventListener("pointerdown", () => hide(), true);
|
|
1174
|
+
document.addEventListener("scroll", () => {
|
|
1175
|
+
if (active) hide();
|
|
1176
|
+
}, true);
|
|
1177
|
+
document.addEventListener("keydown", (e) => {
|
|
1178
|
+
if (e.key === "Escape") hide();
|
|
1179
|
+
});
|
|
1180
|
+
window.addEventListener("blur", () => hide());
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
// src/save.ts
|
|
1184
|
+
function createSaveController(opts) {
|
|
1185
|
+
const delayMs = opts.delayMs ?? 700;
|
|
1186
|
+
const maxWaitMs = opts.maxWaitMs ?? 5e3;
|
|
1187
|
+
let status = "saved";
|
|
1188
|
+
let auto = true;
|
|
1189
|
+
let dirty = false;
|
|
1190
|
+
let dirtySince = 0;
|
|
1191
|
+
let timer2;
|
|
1192
|
+
let inFlight;
|
|
1193
|
+
const setStatus = (next) => {
|
|
1194
|
+
if (next === status) return;
|
|
1195
|
+
status = next;
|
|
1196
|
+
opts.onStatus?.(status);
|
|
1197
|
+
};
|
|
1198
|
+
const clearTimer = () => {
|
|
1199
|
+
if (timer2 === void 0) return;
|
|
1200
|
+
clearTimeout(timer2);
|
|
1201
|
+
timer2 = void 0;
|
|
1202
|
+
};
|
|
1203
|
+
const arm = () => {
|
|
1204
|
+
clearTimer();
|
|
1205
|
+
if (!auto) return;
|
|
1206
|
+
const deadline = dirtySince + maxWaitMs;
|
|
1207
|
+
const wait = Math.max(0, Math.min(delayMs, deadline - Date.now()));
|
|
1208
|
+
timer2 = setTimeout(() => {
|
|
1209
|
+
timer2 = void 0;
|
|
1210
|
+
void run();
|
|
1211
|
+
}, wait);
|
|
1212
|
+
};
|
|
1213
|
+
const run = async () => {
|
|
1214
|
+
if (inFlight) return inFlight;
|
|
1215
|
+
if (!dirty) {
|
|
1216
|
+
setStatus("saved");
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
clearTimer();
|
|
1220
|
+
dirty = false;
|
|
1221
|
+
setStatus("saving");
|
|
1222
|
+
inFlight = (async () => {
|
|
1223
|
+
let ok = false;
|
|
1224
|
+
try {
|
|
1225
|
+
ok = await opts.write();
|
|
1226
|
+
} catch {
|
|
1227
|
+
ok = false;
|
|
1228
|
+
}
|
|
1229
|
+
if (!ok) {
|
|
1230
|
+
if (!dirty) {
|
|
1231
|
+
dirty = true;
|
|
1232
|
+
dirtySince = dirtySince || Date.now();
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
setStatus(dirty ? "unsaved" : "saved");
|
|
1236
|
+
if (dirty) arm();
|
|
1237
|
+
})();
|
|
1238
|
+
try {
|
|
1239
|
+
await inFlight;
|
|
1240
|
+
} finally {
|
|
1241
|
+
inFlight = void 0;
|
|
1242
|
+
}
|
|
1243
|
+
};
|
|
1244
|
+
return {
|
|
1245
|
+
get status() {
|
|
1246
|
+
return status;
|
|
1247
|
+
},
|
|
1248
|
+
get pending() {
|
|
1249
|
+
return dirty || inFlight !== void 0;
|
|
1250
|
+
},
|
|
1251
|
+
touch() {
|
|
1252
|
+
if (!dirty) dirtySince = Date.now();
|
|
1253
|
+
dirty = true;
|
|
1254
|
+
if (!inFlight) setStatus("unsaved");
|
|
1255
|
+
arm();
|
|
1256
|
+
},
|
|
1257
|
+
async flush() {
|
|
1258
|
+
if (inFlight) await inFlight;
|
|
1259
|
+
if (dirty) await run();
|
|
1260
|
+
else setStatus("saved");
|
|
1261
|
+
},
|
|
1262
|
+
cancel() {
|
|
1263
|
+
clearTimer();
|
|
1264
|
+
dirty = false;
|
|
1265
|
+
setStatus(inFlight ? "saving" : "saved");
|
|
1266
|
+
},
|
|
1267
|
+
setAuto(on) {
|
|
1268
|
+
auto = on;
|
|
1269
|
+
if (on) {
|
|
1270
|
+
if (dirty) arm();
|
|
1271
|
+
} else clearTimer();
|
|
1272
|
+
}
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
export { PALETTE, PALETTE_SIZE, askIdentity, closeAnchoredPanel, colourFor, colourIndex, confirmDialog, createSaveController, dupGuard, el, expandableRow, focusNewRow, foldVc, hideTip, icon, iconBtn, iconSvg, initTooltips, labelled, lockControls, lockNotice, mountJobProgress, mountPaneShell, mountSettingsDialog, moveItem, openAnchoredPanel, openComments, openNotesEditor, paintVcBadges, pinButton, placeAnchored, showAbout, tagChips, tipAt, tipBold, vcBadgeFor };
|
|
37
1277
|
//# sourceMappingURL=index.js.map
|
|
38
1278
|
//# sourceMappingURL=index.js.map
|