@wildwinter/app-shell 0.4.1 → 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 +926 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +371 -3
- package/dist/index.d.ts +371 -3
- package/dist/index.js +896 -8
- 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 +7 -0
- package/dist/menu.cjs.map +1 -1
- package/dist/menu.d.cts +20 -1
- package/dist/menu.d.ts +20 -1
- package/dist/menu.js +1 -1
- package/dist/notes-editor.css +107 -0
- package/dist/notes-editor.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/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 +90 -1
- package/dist/chunk-KQT3XYRS.js +0 -10
- package/dist/chunk-KQT3XYRS.js.map +0 -1
- package/dist/pane-shell.d.cts +0 -2
- package/dist/pane-shell.d.ts +0 -2
- package/dist/settings.d.cts +0 -2
- package/dist/settings.d.ts +0 -2
- package/dist/tokens.d.cts +0 -2
- package/dist/tokens.d.ts +0 -2
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { JOB_PROGRESS } from './chunk-MDGOIT3O.js';
|
|
2
|
+
export { EDIT_MENU, PANE_MENU } from './chunk-R6RO5TXB.js';
|
|
2
3
|
|
|
3
4
|
// src/dom.ts
|
|
4
5
|
function el(tag, classNameOrProps, ...children) {
|
|
@@ -7,6 +8,10 @@ function el(tag, classNameOrProps, ...children) {
|
|
|
7
8
|
if (props.className) node.className = props.className;
|
|
8
9
|
if (props.text !== void 0 && props.text !== null) node.textContent = props.text;
|
|
9
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
|
+
}
|
|
10
15
|
if (props.onClick) node.addEventListener("click", props.onClick);
|
|
11
16
|
for (const child of children) {
|
|
12
17
|
if (child === null || child === void 0) continue;
|
|
@@ -18,15 +23,15 @@ function iconBtn(glyph, title, onClick, disabled = false, danger = false) {
|
|
|
18
23
|
const b = el("button", `shell-icon${danger ? " danger" : ""}`);
|
|
19
24
|
b.type = "button";
|
|
20
25
|
b.textContent = glyph;
|
|
21
|
-
b.
|
|
26
|
+
b.dataset["tip"] = title;
|
|
22
27
|
b.setAttribute("aria-label", title);
|
|
23
28
|
b.disabled = disabled;
|
|
24
29
|
b.addEventListener("click", onClick);
|
|
25
30
|
return b;
|
|
26
31
|
}
|
|
27
|
-
function labelled(
|
|
32
|
+
function labelled(label2, control) {
|
|
28
33
|
const w = el("label", "shell-labelled");
|
|
29
|
-
w.append(el("span", "shell-fieldcap",
|
|
34
|
+
w.append(el("span", "shell-fieldcap", label2), control);
|
|
30
35
|
return w;
|
|
31
36
|
}
|
|
32
37
|
function moveItem(arr, i, delta) {
|
|
@@ -96,6 +101,62 @@ function colourFor(name) {
|
|
|
96
101
|
return PALETTE[colourIndex(name)];
|
|
97
102
|
}
|
|
98
103
|
|
|
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
|
+
|
|
99
160
|
// src/pane-shell.ts
|
|
100
161
|
var DEFAULT_MAX = (w) => Math.min(640, w - 420);
|
|
101
162
|
var openVar = { nav: "--nav-open-w", inspector: "--insp-open-w" };
|
|
@@ -314,7 +375,7 @@ function mountSettingsDialog(opts) {
|
|
|
314
375
|
const panelFor = /* @__PURE__ */ new Map();
|
|
315
376
|
const tabFor = /* @__PURE__ */ new Map();
|
|
316
377
|
const handles = /* @__PURE__ */ new Map();
|
|
317
|
-
let
|
|
378
|
+
let active2 = opts.sections[0]?.id ?? "";
|
|
318
379
|
let lastGroup;
|
|
319
380
|
for (const s of opts.sections) {
|
|
320
381
|
if (s.group && s.group !== lastGroup) {
|
|
@@ -334,7 +395,7 @@ function mountSettingsDialog(opts) {
|
|
|
334
395
|
panelFor.set(s.id, panel);
|
|
335
396
|
}
|
|
336
397
|
function showTab(id) {
|
|
337
|
-
|
|
398
|
+
active2 = id;
|
|
338
399
|
for (const [sid, t] of tabFor) t.classList.toggle("active", sid === id);
|
|
339
400
|
for (const [sid, p] of panelFor) p.hidden = sid !== id;
|
|
340
401
|
}
|
|
@@ -376,7 +437,7 @@ function mountSettingsDialog(opts) {
|
|
|
376
437
|
handles.set(s.id, s.mount(panel));
|
|
377
438
|
}
|
|
378
439
|
error.hidden = true;
|
|
379
|
-
showTab(tabId ??
|
|
440
|
+
showTab(tabId ?? active2 ?? opts.sections[0]?.id ?? "");
|
|
380
441
|
dialog.showModal();
|
|
381
442
|
},
|
|
382
443
|
destroy() {
|
|
@@ -385,6 +446,833 @@ function mountSettingsDialog(opts) {
|
|
|
385
446
|
};
|
|
386
447
|
}
|
|
387
448
|
|
|
388
|
-
|
|
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 };
|
|
389
1277
|
//# sourceMappingURL=index.js.map
|
|
390
1278
|
//# sourceMappingURL=index.js.map
|