koneck 2.92.0 → 2.93.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/dist/web/ui-client.d.ts.map +1 -1
- package/dist/web/ui-client.js +284 -65
- package/dist/web/ui-client.js.map +1 -1
- package/dist/web/ui-css.d.ts.map +1 -1
- package/dist/web/ui-css.js +37 -0
- package/dist/web/ui-css.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui-client.d.ts","sourceRoot":"","sources":["../../src/web/ui-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,IAAI,MAAM,
|
|
1
|
+
{"version":3,"file":"ui-client.d.ts","sourceRoot":"","sources":["../../src/web/ui-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,IAAI,MAAM,CA6qMrC"}
|
package/dist/web/ui-client.js
CHANGED
|
@@ -4446,7 +4446,15 @@ $('tfontdown').onclick = () => { term.fontPx = Math.max(9, term.fontPx - 1); app
|
|
|
4446
4446
|
* is dispatched as a real mouse event at the same point, so what happens is what would happen to a
|
|
4447
4447
|
* person clicking there.
|
|
4448
4448
|
*/
|
|
4449
|
-
|
|
4449
|
+
/*
|
|
4450
|
+
* Nothing here: this held { shot, width } and was read by nothing at all.
|
|
4451
|
+
*
|
|
4452
|
+
* Left over from a version where the panel kept the picture itself. The name is now the panel's own
|
|
4453
|
+
* elements, a few lines down, and two declarations of it stopped the whole client script from
|
|
4454
|
+
* parsing — which takes every handler on the page with it, so the view menu, the composer and the
|
|
4455
|
+
* panel all stop responding at once. Worth knowing that is the symptom: not one broken control but
|
|
4456
|
+
* all of them.
|
|
4457
|
+
*/
|
|
4450
4458
|
|
|
4451
4459
|
/*
|
|
4452
4460
|
* Typing into the page.
|
|
@@ -4487,6 +4495,19 @@ function flushTyping() {
|
|
|
4487
4495
|
async function pageKey(ev) {
|
|
4488
4496
|
// Left alone: the browser's own shortcuts, and anything with a modifier that is not Shift.
|
|
4489
4497
|
if (ev.metaKey || ev.ctrlKey || ev.altKey) return;
|
|
4498
|
+
/*
|
|
4499
|
+
* Scrolling first, and handled here rather than sent to the page as a keypress.
|
|
4500
|
+
*
|
|
4501
|
+
* Pressing PageDown in the page would work too, but only when something scrollable has focus
|
|
4502
|
+
* there — click a text field first and PageDown moves the caret instead, which is the page
|
|
4503
|
+
* behaving correctly and the panel appearing not to. Scrolling the window is unambiguous.
|
|
4504
|
+
*/
|
|
4505
|
+
const scroll = SCROLL_KEYS[ev.key];
|
|
4506
|
+
if (scroll) {
|
|
4507
|
+
ev.preventDefault();
|
|
4508
|
+
scroll();
|
|
4509
|
+
return;
|
|
4510
|
+
}
|
|
4490
4511
|
const special = SPECIAL_KEYS[ev.key];
|
|
4491
4512
|
if (special) {
|
|
4492
4513
|
ev.preventDefault();
|
|
@@ -4516,92 +4537,215 @@ function pageCoords(img, ev) {
|
|
|
4516
4537
|
};
|
|
4517
4538
|
}
|
|
4518
4539
|
|
|
4519
|
-
|
|
4540
|
+
/**
|
|
4541
|
+
* The preview's own elements, built once and kept.
|
|
4542
|
+
*
|
|
4543
|
+
* It used to empty the panel and build a fresh <img> on every update — so every scroll, click and
|
|
4544
|
+
* refresh blanked the picture, decoded a new one, and put it back. That is the blinking: not a slow
|
|
4545
|
+
* page but a panel demolishing itself thirty times a minute. Reported as "no smooth scrolling, like
|
|
4546
|
+
* opening sections", which is exactly what a sequence of rebuilt stills looks like.
|
|
4547
|
+
*
|
|
4548
|
+
* Held here rather than found by id each time, because the handlers are wired once and must read
|
|
4549
|
+
* whatever is current rather than closing over one draw's data.
|
|
4550
|
+
*/
|
|
4551
|
+
const preview = { stage: null, img: null, cursor: null, hint: null, chips: null,
|
|
4552
|
+
controls: [], swapping: false, next: null };
|
|
4553
|
+
|
|
4554
|
+
/**
|
|
4555
|
+
* The pointer, where something just happened.
|
|
4556
|
+
*
|
|
4557
|
+
* Asked for as "see the mouse pointer like a human when operating" — and it is not decoration: a
|
|
4558
|
+
* click whose target you cannot see is a click you cannot check. It moves to the point, presses,
|
|
4559
|
+
* and fades, so watching the panel tells you where the agent went and whether it hit the thing it
|
|
4560
|
+
* meant to.
|
|
4561
|
+
*/
|
|
4562
|
+
function showPointer(pageX, pageY, kind) {
|
|
4563
|
+
const img = preview.img;
|
|
4564
|
+
const cursor = preview.cursor;
|
|
4565
|
+
if (!img || !cursor || !img.naturalWidth) return;
|
|
4566
|
+
const box = img.getBoundingClientRect();
|
|
4567
|
+
const stage = preview.stage.getBoundingClientRect();
|
|
4568
|
+
const scale = box.width / img.naturalWidth;
|
|
4569
|
+
cursor.style.left = Math.round(box.left - stage.left + pageX * scale) + 'px';
|
|
4570
|
+
cursor.style.top = Math.round(box.top - stage.top + pageY * scale) + 'px';
|
|
4571
|
+
cursor.hidden = false;
|
|
4572
|
+
// Restarted rather than added to: a second click during the first animation should look like a
|
|
4573
|
+
// second click, not like nothing.
|
|
4574
|
+
cursor.classList.remove('press', 'move');
|
|
4575
|
+
void cursor.offsetWidth;
|
|
4576
|
+
cursor.classList.add(kind === 'press' ? 'press' : 'move');
|
|
4577
|
+
}
|
|
4578
|
+
|
|
4579
|
+
/** Where a control is on the page, from the last read, so the agent's clicks can be pointed at. */
|
|
4580
|
+
function boxOfControl(want) {
|
|
4581
|
+
const found = (preview.controls || []).find((c) =>
|
|
4582
|
+
(typeof want.ref === 'number' && c.ref === want.ref)
|
|
4583
|
+
|| (want.selector && c.selector === want.selector)
|
|
4584
|
+
|| (want.label && String(c.label || '').toLowerCase() === String(want.label).toLowerCase()));
|
|
4585
|
+
if (!found || !found.box) return null;
|
|
4586
|
+
return { x: found.box.x + found.box.w / 2, y: found.box.y + found.box.h / 2 };
|
|
4587
|
+
}
|
|
4588
|
+
|
|
4589
|
+
/**
|
|
4590
|
+
* Swaps the picture without a blank frame in between.
|
|
4591
|
+
*
|
|
4592
|
+
* decode() waits for the new image to be ready before it is shown, so the old one stays up until
|
|
4593
|
+
* the new one can replace it in a single paint. Without it the src change clears the element and
|
|
4594
|
+
* the panel flashes white between every frame, which is the whole of the flicker.
|
|
4595
|
+
*
|
|
4596
|
+
* Coalesced, because pictures can arrive faster than they decode: while one is being prepared the
|
|
4597
|
+
* newest arrival is held, and anything older than it is dropped — showing a stale frame after a
|
|
4598
|
+
* fresh one is worse than skipping it.
|
|
4599
|
+
*/
|
|
4600
|
+
async function swapShot(src) {
|
|
4601
|
+
if (preview.swapping) { preview.next = src; return; }
|
|
4602
|
+
preview.swapping = true;
|
|
4603
|
+
try {
|
|
4604
|
+
for (let src2 = src; src2; src2 = preview.next, preview.next = null) {
|
|
4605
|
+
const loading = new Image();
|
|
4606
|
+
loading.src = src2;
|
|
4607
|
+
try { if (loading.decode) await loading.decode(); } catch (e) { /* show it regardless */ }
|
|
4608
|
+
if (preview.img) preview.img.src = src2;
|
|
4609
|
+
}
|
|
4610
|
+
} finally {
|
|
4611
|
+
preview.swapping = false;
|
|
4612
|
+
}
|
|
4613
|
+
}
|
|
4614
|
+
|
|
4615
|
+
/** The panel's furniture, wired once. */
|
|
4616
|
+
function previewShell() {
|
|
4617
|
+
if (preview.stage && preview.stage.parentNode) return;
|
|
4520
4618
|
const host = $('pvbody');
|
|
4521
4619
|
host.innerHTML = '';
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|| 'Nothing open yet.'));
|
|
4525
|
-
$('pvinfo').textContent = '';
|
|
4526
|
-
return;
|
|
4527
|
-
}
|
|
4528
|
-
$('pvurl').value = data.url || $('pvurl').value;
|
|
4529
|
-
$('pvinfo').textContent = data.title || '';
|
|
4530
|
-
// Shown as unusable rather than silently doing nothing, which is the difference between a
|
|
4531
|
-
// control that is off and one that is broken.
|
|
4532
|
-
$('pvback').disabled = !(data.nav && data.nav.back);
|
|
4533
|
-
$('pvfwd').disabled = !(data.nav && data.nav.forward);
|
|
4534
|
-
if (!data.shot) {
|
|
4535
|
-
host.appendChild(el('div', 'pvnone', 'The browser is open but produced no picture.'));
|
|
4536
|
-
return;
|
|
4537
|
-
}
|
|
4620
|
+
|
|
4621
|
+
const stage = el('div', 'pvstage');
|
|
4538
4622
|
const img = document.createElement('img');
|
|
4539
4623
|
img.className = 'pvshot';
|
|
4540
|
-
img.
|
|
4541
|
-
|
|
4624
|
+
img.alt = 'the page';
|
|
4625
|
+
/*
|
|
4626
|
+
* Focusable, so the keyboard has somewhere to go.
|
|
4627
|
+
*
|
|
4628
|
+
* An image cannot receive key events without this, which was the mechanical reason typing did
|
|
4629
|
+
* nothing: there was no element for the keys to arrive at.
|
|
4630
|
+
*/
|
|
4631
|
+
img.tabIndex = 0;
|
|
4632
|
+
const cursor = el('div', 'pvcursor');
|
|
4633
|
+
cursor.hidden = true;
|
|
4634
|
+
stage.appendChild(img);
|
|
4635
|
+
stage.appendChild(cursor);
|
|
4636
|
+
host.appendChild(stage);
|
|
4637
|
+
|
|
4638
|
+
const hint = el('div', 'pvhint');
|
|
4639
|
+
host.appendChild(hint);
|
|
4640
|
+
const chips = el('div', 'pvctl');
|
|
4641
|
+
host.appendChild(chips);
|
|
4642
|
+
|
|
4643
|
+
/*
|
|
4644
|
+
* Buttons for the same thing the keys do.
|
|
4645
|
+
*
|
|
4646
|
+
* They moved a flat 600 pixels, which is most of a page on a laptop and a third of one on a tall
|
|
4647
|
+
* window — so "scroll down" twice could skip a band of the page or barely move. A page is the
|
|
4648
|
+
* picture's height, whatever that is.
|
|
4649
|
+
*/
|
|
4650
|
+
const scroller = el('div', 'pvhint');
|
|
4651
|
+
for (const [label, go] of [['↑ page', () => scrollByPages(-1)], ['↓ page', () => scrollByPages(1)],
|
|
4652
|
+
['top', () => SCROLL_KEYS.Home()], ['end', () => SCROLL_KEYS.End()]]) {
|
|
4653
|
+
const b = el('button', 'pvbtn', label);
|
|
4654
|
+
b.style.margin = '0 4px';
|
|
4655
|
+
b.onclick = () => { if (preview.img) preview.img.focus(); go(); };
|
|
4656
|
+
scroller.appendChild(b);
|
|
4657
|
+
}
|
|
4658
|
+
scroller.appendChild(el('span', 'dim', ' \u2014 or the wheel, PageUp/PageDown, Home/End'));
|
|
4659
|
+
host.appendChild(scroller);
|
|
4660
|
+
|
|
4542
4661
|
/*
|
|
4543
4662
|
* The cursor answers the question a browser answers: is there something here to click?
|
|
4544
4663
|
*
|
|
4545
|
-
*
|
|
4546
|
-
*
|
|
4547
|
-
*
|
|
4548
|
-
* and was discarding.
|
|
4664
|
+
* A crosshair over the whole page is the language of a screenshot tool. The controls carry their
|
|
4665
|
+
* boxes, so hovering one shows a hand and its label and everywhere else shows an arrow — the same
|
|
4666
|
+
* two states a real page has, from information the panel already had.
|
|
4549
4667
|
*/
|
|
4550
|
-
const controls = (data.controls || []).filter((c) => c.box);
|
|
4551
4668
|
img.onmousemove = (ev) => {
|
|
4552
4669
|
const at = pageCoords(img, ev);
|
|
4553
|
-
const over = controls.find((c) =>
|
|
4670
|
+
const over = (preview.controls || []).filter((c) => c.box).find((c) =>
|
|
4554
4671
|
at.x >= c.box.x && at.x <= c.box.x + c.box.w &&
|
|
4555
4672
|
at.y >= c.box.y && at.y <= c.box.y + c.box.h);
|
|
4556
4673
|
img.style.cursor = over ? 'pointer' : 'default';
|
|
4557
4674
|
img.title = over ? (over.label || over.tag) : '';
|
|
4558
4675
|
};
|
|
4559
4676
|
img.onmouseleave = () => { img.style.cursor = 'default'; img.title = ''; };
|
|
4560
|
-
/*
|
|
4561
|
-
* Focusable, so the keyboard has somewhere to go.
|
|
4562
|
-
*
|
|
4563
|
-
* An image cannot receive key events without this, which is the mechanical reason typing did
|
|
4564
|
-
* nothing: there was no element for the keys to arrive at. Clicking the page focuses it, so
|
|
4565
|
-
* clicking a field and then typing works the way it reads.
|
|
4566
|
-
*/
|
|
4567
|
-
img.tabIndex = 0;
|
|
4568
4677
|
img.onkeydown = (ev) => { void pageKey(ev); };
|
|
4569
4678
|
img.onblur = () => { void flushTyping(); };
|
|
4570
4679
|
|
|
4571
|
-
// The picture is 1280 wide whatever it is displayed at, so a click
|
|
4572
|
-
//
|
|
4680
|
+
// The picture is 1280 wide whatever it is displayed at, so a click is scaled back into page
|
|
4681
|
+
// coordinates or it lands somewhere else entirely.
|
|
4573
4682
|
img.onclick = async (ev) => {
|
|
4574
4683
|
img.focus();
|
|
4575
4684
|
const at = pageCoords(img, ev);
|
|
4685
|
+
showPointer(at.x, at.y, 'press');
|
|
4576
4686
|
$('pvinfo').textContent = 'clicking ' + at.x + ',' + at.y + '…';
|
|
4577
4687
|
await browserDo({ action: 'click', x: at.x, y: at.y });
|
|
4578
4688
|
};
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4689
|
+
|
|
4690
|
+
preview.stage = stage;
|
|
4691
|
+
preview.img = img;
|
|
4692
|
+
preview.cursor = cursor;
|
|
4693
|
+
preview.hint = hint;
|
|
4694
|
+
preview.chips = chips;
|
|
4695
|
+
}
|
|
4696
|
+
|
|
4697
|
+
function drawPreview(data) {
|
|
4698
|
+
const host = $('pvbody');
|
|
4699
|
+
if (!data || data.running === false) {
|
|
4700
|
+
host.innerHTML = '';
|
|
4701
|
+
preview.stage = null;
|
|
4702
|
+
host.appendChild(el('div', 'pvnone', (data && data.note) || 'Nothing open yet.'));
|
|
4703
|
+
$('pvinfo').textContent = '';
|
|
4704
|
+
return;
|
|
4705
|
+
}
|
|
4706
|
+
$('pvurl').value = data.url || $('pvurl').value;
|
|
4707
|
+
$('pvinfo').textContent = data.title || '';
|
|
4708
|
+
// Shown as unusable rather than silently doing nothing, which is the difference between a
|
|
4709
|
+
// control that is off and one that is broken.
|
|
4710
|
+
$('pvback').disabled = !(data.nav && data.nav.back);
|
|
4711
|
+
$('pvfwd').disabled = !(data.nav && data.nav.forward);
|
|
4712
|
+
if (!data.shot) {
|
|
4713
|
+
host.innerHTML = '';
|
|
4714
|
+
preview.stage = null;
|
|
4715
|
+
host.appendChild(el('div', 'pvnone', 'The browser is open but produced no picture.'));
|
|
4716
|
+
return;
|
|
4717
|
+
}
|
|
4718
|
+
|
|
4719
|
+
previewShell();
|
|
4720
|
+
preview.controls = data.controls || [];
|
|
4721
|
+
void swapShot(data.shot);
|
|
4722
|
+
preview.hint.textContent = 'Click the page to click it for real, then type. '
|
|
4723
|
+
+ preview.controls.length + ' interactive elements.';
|
|
4724
|
+
|
|
4725
|
+
/*
|
|
4726
|
+
* The chips, rebuilt only when they have changed.
|
|
4727
|
+
*
|
|
4728
|
+
* They are the one part that genuinely differs between pages, and rebuilding them on every scroll
|
|
4729
|
+
* — when the controls are identical — was throwing away the element under somebody's pointer
|
|
4730
|
+
* mid-click.
|
|
4731
|
+
*/
|
|
4732
|
+
const want = preview.controls.slice(0, 18)
|
|
4733
|
+
.map((c) => c.ref + '. ' + (c.label || c.tag)).join('|');
|
|
4734
|
+
if (preview.chips.dataset.shape !== want) {
|
|
4735
|
+
preview.chips.dataset.shape = want;
|
|
4736
|
+
preview.chips.innerHTML = '';
|
|
4737
|
+
for (const c of preview.controls.slice(0, 18)) {
|
|
4589
4738
|
const b = el('button', 'pvchip', c.ref + '. ' + (c.label || c.tag));
|
|
4590
4739
|
b.type = 'button';
|
|
4591
4740
|
b.title = c.selector;
|
|
4592
|
-
b.onclick = () =>
|
|
4593
|
-
|
|
4741
|
+
b.onclick = () => {
|
|
4742
|
+
const at = boxOfControl({ ref: c.ref });
|
|
4743
|
+
if (at) showPointer(at.x, at.y, 'press');
|
|
4744
|
+
void browserDo({ action: 'click', ref: c.ref });
|
|
4745
|
+
};
|
|
4746
|
+
preview.chips.appendChild(b);
|
|
4594
4747
|
}
|
|
4595
|
-
host.appendChild(row);
|
|
4596
|
-
}
|
|
4597
|
-
const row = el('div', 'pvhint');
|
|
4598
|
-
for (const [label, by] of [['scroll up', -600], ['scroll down', 600]]) {
|
|
4599
|
-
const b = el('button', 'pvbtn', label);
|
|
4600
|
-
b.style.margin = '0 4px';
|
|
4601
|
-
b.onclick = () => void browserDo({ action: 'scroll', by: by });
|
|
4602
|
-
row.appendChild(b);
|
|
4603
4748
|
}
|
|
4604
|
-
host.appendChild(row);
|
|
4605
4749
|
}
|
|
4606
4750
|
|
|
4607
4751
|
/*
|
|
@@ -4622,16 +4766,69 @@ function drawPreview(data) {
|
|
|
4622
4766
|
$('pvbody').addEventListener('wheel', (ev) => {
|
|
4623
4767
|
if (!document.querySelector('#pvbody .pvshot')) return; // nothing open to scroll
|
|
4624
4768
|
ev.preventDefault();
|
|
4625
|
-
|
|
4626
|
-
if (scrolling.timer) return;
|
|
4627
|
-
scrolling.timer = setTimeout(() => {
|
|
4628
|
-
const by = Math.round(scrolling.by);
|
|
4629
|
-
scrolling.by = 0;
|
|
4630
|
-
scrolling.timer = null;
|
|
4631
|
-
if (by !== 0) void browserDo({ action: 'scroll', by: by });
|
|
4632
|
-
}, SCROLL_BATCH_MS);
|
|
4769
|
+
queueScroll(ev.deltaY);
|
|
4633
4770
|
}, { passive: false });
|
|
4634
4771
|
|
|
4772
|
+
/*
|
|
4773
|
+
* A click anywhere in the panel gives the page the keyboard.
|
|
4774
|
+
*
|
|
4775
|
+
* Only the picture is focusable, so Page Down after clicking the space beside it did nothing —
|
|
4776
|
+
* focus was on the document body and the panel's keys never fired. A browser viewport does not work
|
|
4777
|
+
* that way: clicking near the page is clicking the page. Buttons and fields are left alone, or
|
|
4778
|
+
* pressing one would take the focus straight back off it again.
|
|
4779
|
+
*/
|
|
4780
|
+
$('pvbody').addEventListener('mousedown', (ev) => {
|
|
4781
|
+
if (!preview.img) return;
|
|
4782
|
+
if (ev.target.closest('button, input, select, textarea, a')) return;
|
|
4783
|
+
preview.img.focus();
|
|
4784
|
+
});
|
|
4785
|
+
|
|
4786
|
+
/** Gathers a burst, sends it as one movement. */
|
|
4787
|
+
function queueScroll(by) {
|
|
4788
|
+
scrolling.by += by;
|
|
4789
|
+
if (scrolling.timer) return;
|
|
4790
|
+
scrolling.timer = setTimeout(flushScroll, SCROLL_BATCH_MS);
|
|
4791
|
+
}
|
|
4792
|
+
|
|
4793
|
+
function flushScroll() {
|
|
4794
|
+
const by = Math.round(scrolling.by);
|
|
4795
|
+
scrolling.by = 0;
|
|
4796
|
+
if (scrolling.timer) clearTimeout(scrolling.timer);
|
|
4797
|
+
scrolling.timer = null;
|
|
4798
|
+
if (by !== 0) void browserDo({ action: 'scroll', by: by });
|
|
4799
|
+
}
|
|
4800
|
+
|
|
4801
|
+
/*
|
|
4802
|
+
* Page Down, and the rest of the keys that move a page rather than a few lines.
|
|
4803
|
+
*
|
|
4804
|
+
* A wheel is for reading something already on screen. Getting to the bottom of a long form with one
|
|
4805
|
+
* is thirty gestures, each costing a screenshot, and the panel is not the place to do that. Page Up
|
|
4806
|
+
* and Page Down move a viewport at a time; Home and End go to the ends, expressed as a scroll far
|
|
4807
|
+
* larger than any page because scrollBy clamps and asking for a million pixels lands at the edge.
|
|
4808
|
+
*
|
|
4809
|
+
* A viewport is the picture's own height: the shot is the viewport and nothing beyond it, so its
|
|
4810
|
+
* natural height is exactly how far a page goes. Less a small overlap, so the line you stopped
|
|
4811
|
+
* reading on is still there after the jump — the reason every document reader does this.
|
|
4812
|
+
*
|
|
4813
|
+
* Sent immediately rather than batched. A held Page Down is a handful of events, not fifty, and
|
|
4814
|
+
* waiting on a deliberate keypress reads as the key not working.
|
|
4815
|
+
*/
|
|
4816
|
+
const PAGE_OVERLAP = 60;
|
|
4817
|
+
|
|
4818
|
+
function scrollByPages(pages) {
|
|
4819
|
+
const img = preview.img;
|
|
4820
|
+
const viewport = img && img.naturalHeight ? img.naturalHeight : 720;
|
|
4821
|
+
scrolling.by += pages * Math.max(120, viewport - PAGE_OVERLAP);
|
|
4822
|
+
flushScroll();
|
|
4823
|
+
}
|
|
4824
|
+
|
|
4825
|
+
const SCROLL_KEYS = {
|
|
4826
|
+
PageDown: () => scrollByPages(1),
|
|
4827
|
+
PageUp: () => scrollByPages(-1),
|
|
4828
|
+
Home: () => { scrolling.by -= 1e7; flushScroll(); },
|
|
4829
|
+
End: () => { scrolling.by += 1e7; flushScroll(); },
|
|
4830
|
+
};
|
|
4831
|
+
|
|
4635
4832
|
async function browserDo(body) {
|
|
4636
4833
|
$('pvinfo').textContent = 'working…';
|
|
4637
4834
|
let out;
|
|
@@ -5037,6 +5234,28 @@ function watchTool(e, done) {
|
|
|
5037
5234
|
if (e.name === 'browser') {
|
|
5038
5235
|
$('watch').hidden = false;
|
|
5039
5236
|
if (t.url) $('pvurl').value = t.url;
|
|
5237
|
+
/*
|
|
5238
|
+
* The pointer goes where the agent is acting, before the picture catches up.
|
|
5239
|
+
*
|
|
5240
|
+
* Watching a screenshot change is watching the aftermath; watching the pointer arrive and press
|
|
5241
|
+
* is watching the work. The coordinates come either straight from the call or from the box of
|
|
5242
|
+
* the control it named in the last read, which is how a click by ref or label can be pointed at
|
|
5243
|
+
* rather than merely reported.
|
|
5244
|
+
*/
|
|
5245
|
+
if (!done) {
|
|
5246
|
+
let asked = null;
|
|
5247
|
+
try { asked = e.args ? JSON.parse(e.args) : null; } catch (err) { asked = null; }
|
|
5248
|
+
if (asked && /^(click|type|fill)$/.test(String(asked.action ?? ''))) {
|
|
5249
|
+
const at = typeof asked.x === 'number' && typeof asked.y === 'number'
|
|
5250
|
+
? { x: asked.x, y: asked.y }
|
|
5251
|
+
: boxOfControl({ ref: asked.ref, label: asked.label, selector: asked.selector })
|
|
5252
|
+
// A fill names its fields rather than one target: point at the first it can place.
|
|
5253
|
+
?? (Array.isArray(asked.fields)
|
|
5254
|
+
? asked.fields.map((f) => boxOfControl(f)).find(Boolean) ?? null
|
|
5255
|
+
: null);
|
|
5256
|
+
if (at) showPointer(at.x, at.y, asked.action === 'click' ? 'press' : 'move');
|
|
5257
|
+
}
|
|
5258
|
+
}
|
|
5040
5259
|
watchHead(verb, t.url || 'the page');
|
|
5041
5260
|
trailPush({ label: t.url ? shortUrl(t.url) : 'page', verb, kind: 'page',
|
|
5042
5261
|
url: t.url || '', ok: done ? e.ok !== false : undefined });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui-client.js","sourceRoot":"","sources":["../../src/web/ui-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,YAAY;IAC1B,OAAO,MAAM,CAAC,GAAG,CAAA
|
|
1
|
+
{"version":3,"file":"ui-client.js","sourceRoot":"","sources":["../../src/web/ui-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,YAAY;IAC1B,OAAO,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2qMlB,CAAC;AACF,CAAC"}
|
package/dist/web/ui-css.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui-css.d.ts","sourceRoot":"","sources":["../../src/web/ui-css.ts"],"names":[],"mappings":"AA8CA,wBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"ui-css.d.ts","sourceRoot":"","sources":["../../src/web/ui-css.ts"],"names":[],"mappings":"AA8CA,wBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA61C3C"}
|
package/dist/web/ui-css.js
CHANGED
|
@@ -560,6 +560,43 @@ footer{flex:0 0 auto;border-top:1px solid var(--line);background:var(--panel);pa
|
|
|
560
560
|
.tobottom:hover{background:var(--panel-2)}
|
|
561
561
|
.tobottom[hidden]{display:none}
|
|
562
562
|
|
|
563
|
+
/*
|
|
564
|
+
* The page, and a pointer over it.
|
|
565
|
+
*
|
|
566
|
+
* The stage exists so the cursor can be positioned against the picture without being inside it —
|
|
567
|
+
* an <img> cannot hold a child, and absolutely positioning against the scrolling panel would put
|
|
568
|
+
* the pointer in the wrong place the moment anything moved.
|
|
569
|
+
*/
|
|
570
|
+
.pvstage{position:relative;flex:0 0 auto;line-height:0;max-width:100%}
|
|
571
|
+
/*
|
|
572
|
+
* A pointer you can see, where something just happened.
|
|
573
|
+
*
|
|
574
|
+
* Asked for as "see the mouse pointer like a human when operating", and it is not decoration: a
|
|
575
|
+
* click whose target you cannot see is a click you cannot check. Drawn rather than an image, so it
|
|
576
|
+
* costs nothing to load and takes the theme's own colour.
|
|
577
|
+
*/
|
|
578
|
+
.pvcursor{position:absolute;width:15px;height:22px;margin:-2px 0 0 -2px;pointer-events:none;
|
|
579
|
+
z-index:5;opacity:0;
|
|
580
|
+
/* An arrow, in the shape every desktop draws it. */
|
|
581
|
+
background:var(--ink);
|
|
582
|
+
-webkit-clip-path:polygon(0 0,0 72%,26% 55%,44% 96%,62% 88%,44% 48%,74% 44%);
|
|
583
|
+
clip-path:polygon(0 0,0 72%,26% 55%,44% 96%,62% 88%,44% 48%,74% 44%);
|
|
584
|
+
filter:drop-shadow(0 0 1px var(--bg)) drop-shadow(0 1px 2px rgba(0,0,0,.5))}
|
|
585
|
+
.pvcursor[hidden]{display:none}
|
|
586
|
+
/* Arriving somewhere: visible, and settling. */
|
|
587
|
+
.pvcursor.move{animation:pvmove .5s ease-out forwards}
|
|
588
|
+
@keyframes pvmove{0%{opacity:.55;transform:scale(1.25)}100%{opacity:.9;transform:scale(1)}}
|
|
589
|
+
/*
|
|
590
|
+
* Pressing: a ring leaves the point, the way a tap looks on a touchscreen.
|
|
591
|
+
*
|
|
592
|
+
* The ring rather than moving the arrow, because an arrow that jumps on click reads as a glitch
|
|
593
|
+
* while a ring reads as contact.
|
|
594
|
+
*/
|
|
595
|
+
.pvcursor.press{animation:pvmove .5s ease-out forwards}
|
|
596
|
+
.pvcursor.press::after{content:'';position:absolute;left:-9px;top:-9px;width:26px;height:26px;
|
|
597
|
+
border-radius:50%;border:2px solid var(--cyan);animation:pvring .55s ease-out forwards}
|
|
598
|
+
@keyframes pvring{0%{transform:scale(.2);opacity:.95}100%{transform:scale(1.6);opacity:0}}
|
|
599
|
+
|
|
563
600
|
/* A heading inside a settings section: quieter than a section, louder than a row. */
|
|
564
601
|
.sgroup{margin:18px 0 2px;font-size:calc(var(--ui) * 0.7857);letter-spacing:.09em;text-transform:uppercase;
|
|
565
602
|
color:var(--muted);font-weight:600}
|
package/dist/web/ui-css.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ui-css.js","sourceRoot":"","sources":["../../src/web/ui-css.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH;;;;;;;;;;;;GAYG;AACH,MAAM,IAAI,GAAG;;;;;;;;;;;;;;;;;;CAkBZ,CAAC;AAEF,MAAM,UAAU,GAAG,CAAC,OAAe;IACjC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;gBAyBO,OAAO;;;oCAGa,IAAI;;2BAEb,IAAI
|
|
1
|
+
{"version":3,"file":"ui-css.js","sourceRoot":"","sources":["../../src/web/ui-css.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH;;;;;;;;;;;;GAYG;AACH,MAAM,IAAI,GAAG;;;;;;;;;;;;;;;;;;CAkBZ,CAAC;AAEF,MAAM,UAAU,GAAG,CAAC,OAAe;IACjC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;gBAyBO,OAAO;;;oCAGa,IAAI;;2BAEb,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6zC9B,CAAC;AACF,CAAC"}
|