@goflowstate/widget-sdk 0.3.1 → 0.3.2
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 +15 -0
- package/dist/canvas-widget-sdk.js +409 -11
- package/dist/canvas-widget-sdk.js.map +3 -3
- package/dist/feedback.d.ts +73 -0
- package/dist/feedback.d.ts.map +1 -1
- package/dist/index.mjs +409 -11
- package/dist/index.mjs.map +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -174,6 +174,21 @@ and shape of its main button, and its corner radius. To pin a role, set a custom
|
|
|
174
174
|
}
|
|
175
175
|
```
|
|
176
176
|
|
|
177
|
+
The button floats over the flow and belongs to the person using it. They can drag it anywhere,
|
|
178
|
+
and it settles on the nearer side. A flick at an edge, or a drag into one, docks it there as a
|
|
179
|
+
slim tab with the bug still showing; a tap on the tab opens the sheet, and pulling the tab
|
|
180
|
+
inward floats it again. The sheet also has a "Tuck the button to the side" link. Where a person
|
|
181
|
+
leaves the button is remembered.
|
|
182
|
+
|
|
183
|
+
Until a person moves it, the button keeps clear of the page's own controls. It starts in the bottom corner and, whenever
|
|
184
|
+
the page changes, resizes or stops scrolling, looks up the right edge for the lowest spot that is empty. It sits over a line of text only
|
|
185
|
+
when empty space is far above it, and on a control only when the whole edge is controls. Pinning
|
|
186
|
+
`--fs-feedback-bottom` turns that off and holds the icon where the flow puts it.
|
|
187
|
+
|
|
188
|
+
To take the icon off one screen of a flow (a film, a full-screen picture), set
|
|
189
|
+
`--fs-feedback-display: none` from the flow's stylesheet while that screen is up, for example
|
|
190
|
+
`:root:has(.film) { --fs-feedback-display: none; }`.
|
|
191
|
+
|
|
177
192
|
Name each screen so a report says where it came from: `document.body.dataset.pageKey = 'question-2'`.
|
|
178
193
|
A page that must not show the icon (a projected wall) sets `<html data-fs-feedback="off">`.
|
|
179
194
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @goflowstate/widget-sdk v0.3.
|
|
1
|
+
// @goflowstate/widget-sdk v0.3.2
|
|
2
2
|
// https://github.com/goflowstate/goflowstate-sdk
|
|
3
3
|
|
|
4
4
|
"use strict";
|
|
@@ -265,21 +265,168 @@ var CanvasWidgetSDK = (() => {
|
|
|
265
265
|
}
|
|
266
266
|
);
|
|
267
267
|
});
|
|
268
|
+
var PLACEMENT_BASE = 16;
|
|
269
|
+
var PLACEMENT_STEP = 8;
|
|
270
|
+
var PLACEMENT_PAD = 8;
|
|
271
|
+
var PLACEMENT_MAX_SHARE = 0.6;
|
|
272
|
+
var PLACEMENT_READ_COST = 240;
|
|
273
|
+
var EMPTY = 0;
|
|
274
|
+
var READ = 1;
|
|
275
|
+
var TAP = 2;
|
|
276
|
+
var SAMPLES = 5;
|
|
277
|
+
function choosePlacement(box, viewportHeight, at) {
|
|
278
|
+
const max = Math.max(PLACEMENT_BASE, viewportHeight * PLACEMENT_MAX_SHARE);
|
|
279
|
+
const span = box.size + PLACEMENT_PAD * 2;
|
|
280
|
+
const offsets = Array.from({ length: SAMPLES }, (_, i) => span * i / (SAMPLES - 1));
|
|
281
|
+
const x0 = box.left - PLACEMENT_PAD;
|
|
282
|
+
let best = PLACEMENT_BASE;
|
|
283
|
+
let bestCost = Infinity;
|
|
284
|
+
for (let bottom = PLACEMENT_BASE; bottom <= max; bottom += PLACEMENT_STEP) {
|
|
285
|
+
if (bottom >= bestCost) break;
|
|
286
|
+
const y0 = box.top - (bottom - box.bottom) - PLACEMENT_PAD;
|
|
287
|
+
let worst = EMPTY;
|
|
288
|
+
for (const dy of offsets) {
|
|
289
|
+
for (const dx of offsets) {
|
|
290
|
+
const found = at(x0 + dx, y0 + dy);
|
|
291
|
+
if (found > worst) worst = found;
|
|
292
|
+
if (worst === TAP) break;
|
|
293
|
+
}
|
|
294
|
+
if (worst === TAP) break;
|
|
295
|
+
}
|
|
296
|
+
if (worst === TAP) continue;
|
|
297
|
+
const cost = bottom + (worst === READ ? PLACEMENT_READ_COST : 0);
|
|
298
|
+
if (cost < bestCost) {
|
|
299
|
+
bestCost = cost;
|
|
300
|
+
best = bottom;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return best;
|
|
304
|
+
}
|
|
305
|
+
var TAPPABLE = 'a[href],button,input,select,textarea,summary,label,[role="button"],[role="link"],[role="tab"],[role="switch"],[role="checkbox"],[role="radio"],[role="slider"],[role="option"],[tabindex]:not([tabindex="-1"]),[contenteditable=""],[contenteditable="true"],[onclick]';
|
|
306
|
+
var PICTURE = "img,svg,canvas,video,picture";
|
|
307
|
+
var SURFACE_SHARE = 0.8;
|
|
308
|
+
function occupancyReader(host, doc = document) {
|
|
309
|
+
if (typeof doc.elementsFromPoint !== "function") return () => EMPTY;
|
|
310
|
+
const view = doc.defaultView;
|
|
311
|
+
const width = view?.innerWidth ?? 1;
|
|
312
|
+
const height = view?.innerHeight ?? 1;
|
|
313
|
+
const seen = /* @__PURE__ */ new Map();
|
|
314
|
+
const read = (el) => {
|
|
315
|
+
const known = seen.get(el);
|
|
316
|
+
if (known) return known;
|
|
317
|
+
const style = view ? view.getComputedStyle(el) : null;
|
|
318
|
+
let tap = el.matches(TAPPABLE) || style?.cursor === "pointer";
|
|
319
|
+
if (tap) {
|
|
320
|
+
const r = el.getBoundingClientRect();
|
|
321
|
+
if (r.width * r.height >= width * height * SURFACE_SHARE) tap = false;
|
|
322
|
+
}
|
|
323
|
+
const text = [];
|
|
324
|
+
if (typeof doc.createRange === "function") {
|
|
325
|
+
for (const node of Array.from(el.childNodes)) {
|
|
326
|
+
if (node.nodeType !== 3 || !node.nodeValue || !node.nodeValue.trim()) continue;
|
|
327
|
+
const range = doc.createRange();
|
|
328
|
+
range.selectNodeContents(node);
|
|
329
|
+
if (typeof range.getClientRects === "function")
|
|
330
|
+
text.push(...Array.from(range.getClientRects()));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const ground = parseColor(style?.backgroundColor);
|
|
334
|
+
const image = style?.backgroundImage ?? "";
|
|
335
|
+
const reading = {
|
|
336
|
+
tap,
|
|
337
|
+
picture: el.matches(PICTURE) || el.closest("svg") !== null,
|
|
338
|
+
text,
|
|
339
|
+
opaque: ground !== null && ground[3] >= 0.5 || image !== "" && image !== "none"
|
|
340
|
+
};
|
|
341
|
+
seen.set(el, reading);
|
|
342
|
+
return reading;
|
|
343
|
+
};
|
|
344
|
+
return (x, y) => {
|
|
345
|
+
const cx = Math.min(Math.max(x, 0), width - 1);
|
|
346
|
+
const cy = Math.min(Math.max(y, 0), height - 1);
|
|
347
|
+
const stack = doc.elementsFromPoint(cx, cy).filter((el) => el !== host && el !== doc.documentElement && el !== doc.body);
|
|
348
|
+
for (let el = stack[0] ?? null; el && el !== doc.body; el = el.parentElement) {
|
|
349
|
+
if (read(el).tap) return TAP;
|
|
350
|
+
}
|
|
351
|
+
for (const el of stack) {
|
|
352
|
+
const r = read(el);
|
|
353
|
+
if (r.picture || r.text.some((t) => cx >= t.left && cx <= t.right && cy >= t.top && cy <= t.bottom))
|
|
354
|
+
return READ;
|
|
355
|
+
if (r.opaque) break;
|
|
356
|
+
}
|
|
357
|
+
return EMPTY;
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
var BUTTON_SIZE = 44;
|
|
361
|
+
var TAB_WIDTH = 26;
|
|
362
|
+
var TAB_HEIGHT = 54;
|
|
363
|
+
var EDGE_MARGIN = 12;
|
|
364
|
+
var DOCK_PAST = 14;
|
|
365
|
+
var SPOT_KEY = "fs-feedback-spot";
|
|
366
|
+
var projectMomentum = (v, deceleration = 0.992) => v / 1e3 * deceleration / (1 - deceleration);
|
|
367
|
+
var rubberband = (overshoot, dimension, constant = 0.55) => overshoot * dimension * constant / (dimension + constant * Math.abs(overshoot));
|
|
368
|
+
var clampY = (y, height, view) => Math.min(
|
|
369
|
+
Math.max(y, view.top + EDGE_MARGIN),
|
|
370
|
+
Math.max(view.top + EDGE_MARGIN, view.height - view.bottom - EDGE_MARGIN - height)
|
|
371
|
+
);
|
|
372
|
+
function restingSpot(state, left, y, view) {
|
|
373
|
+
if (state === "float") {
|
|
374
|
+
return {
|
|
375
|
+
state,
|
|
376
|
+
x: left ? EDGE_MARGIN : view.width - BUTTON_SIZE - EDGE_MARGIN,
|
|
377
|
+
y: clampY(y, BUTTON_SIZE, view)
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
return { state, x: left ? 0 : view.width - TAB_WIDTH, y: clampY(y, TAB_HEIGHT, view) };
|
|
381
|
+
}
|
|
382
|
+
function settleRelease(from, velocity, view) {
|
|
383
|
+
const py = from.y + projectMomentum(velocity.y);
|
|
384
|
+
if (from.state !== "float") return restingSpot(from.state, from.state === "dock-left", py, view);
|
|
385
|
+
const px = from.x + projectMomentum(velocity.x);
|
|
386
|
+
const maxX = view.width - BUTTON_SIZE;
|
|
387
|
+
const tabY = py + (BUTTON_SIZE - TAB_HEIGHT) / 2;
|
|
388
|
+
if (px < -DOCK_PAST || from.x < 3) return restingSpot("dock-left", true, tabY, view);
|
|
389
|
+
if (px > maxX + DOCK_PAST || from.x > maxX - 3)
|
|
390
|
+
return restingSpot("dock-right", false, tabY, view);
|
|
391
|
+
return restingSpot("float", px + BUTTON_SIZE / 2 < view.width / 2, py, view);
|
|
392
|
+
}
|
|
268
393
|
var BUG_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="7" width="8" height="12" rx="4"/><path d="M9.5 7a2.5 2.5 0 0 1 5 0M12 11v8M4 13h4M16 13h4M5 7l3.2 2.4M19 7l-3.2 2.4M5 20l3.2-2.6M19 20l-3.2-2.6"/></svg>';
|
|
269
394
|
var STYLE = `
|
|
270
395
|
:host { all: initial; }
|
|
271
396
|
* { box-sizing: border-box; }
|
|
272
397
|
.bug {
|
|
273
|
-
position: fixed; z-index: 2147483000;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
width: 44px; height: 44px; border-radius: 50%; padding: 0;
|
|
277
|
-
display: grid; place-items: center; cursor: pointer;
|
|
398
|
+
position: fixed; z-index: 2147483000; left: 0; top: 0;
|
|
399
|
+
width: 44px; height: 44px; border-radius: 22px; padding: 0; margin: 0;
|
|
400
|
+
display: var(--fs-feedback-display, grid); place-items: center; cursor: grab;
|
|
278
401
|
background: var(--surface); color: var(--ink); border: 1px solid var(--line);
|
|
279
|
-
box-shadow: 0
|
|
280
|
-
-webkit-tap-highlight-color: transparent;
|
|
402
|
+
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.2), 0 2px 5px rgba(0, 0, 0, 0.12);
|
|
403
|
+
-webkit-tap-highlight-color: transparent; touch-action: none; user-select: none; -webkit-user-select: none;
|
|
404
|
+
will-change: transform;
|
|
405
|
+
}
|
|
406
|
+
@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
|
|
407
|
+
.bug {
|
|
408
|
+
background: color-mix(in srgb, var(--surface) 74%, transparent);
|
|
409
|
+
-webkit-backdrop-filter: blur(14px) saturate(160%); backdrop-filter: blur(14px) saturate(160%);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
.bug svg { width: 20px; height: 20px; pointer-events: none; }
|
|
413
|
+
.bug.down svg { transform: scale(0.9); }
|
|
414
|
+
.bug.dragging { cursor: grabbing; box-shadow: 0 18px 36px rgba(0, 0, 0, 0.26), 0 3px 8px rgba(0, 0, 0, 0.14); }
|
|
415
|
+
.bug[data-state^="dock"] { width: 26px; height: 54px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.14); opacity: 0.92; }
|
|
416
|
+
.bug[data-state="dock-right"] { border-radius: 16px 0 0 16px; border-right-width: 0; }
|
|
417
|
+
.bug[data-state="dock-left"] { border-radius: 0 16px 16px 0; border-left-width: 0; }
|
|
418
|
+
.bug[data-state^="dock"] svg { width: 16px; height: 16px; }
|
|
419
|
+
.probe {
|
|
420
|
+
position: fixed; visibility: hidden; pointer-events: none; width: 0; height: 0;
|
|
421
|
+
padding: env(safe-area-inset-top, 0px) 0 env(safe-area-inset-bottom, 0px) 0;
|
|
422
|
+
}
|
|
423
|
+
.tuck { align-self: center; font-size: 13px; color: var(--ink-dim); }
|
|
424
|
+
@media (prefers-reduced-transparency: reduce) {
|
|
425
|
+
.bug { background: var(--surface); -webkit-backdrop-filter: none; backdrop-filter: none; }
|
|
426
|
+
}
|
|
427
|
+
@media (prefers-contrast: more) {
|
|
428
|
+
.bug { background: var(--surface); border-color: var(--ink); -webkit-backdrop-filter: none; backdrop-filter: none; }
|
|
281
429
|
}
|
|
282
|
-
.bug svg { width: 20px; height: 20px; }
|
|
283
430
|
.bug:focus-visible, .send:focus-visible, .link:focus-visible, textarea:focus-visible {
|
|
284
431
|
outline: 2px solid var(--ink); outline-offset: 2px;
|
|
285
432
|
}
|
|
@@ -311,12 +458,18 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
311
458
|
.status { margin: 0; font-size: 13px; color: var(--ink-dim); min-height: 1.4em; }
|
|
312
459
|
[hidden] { display: none !important; }
|
|
313
460
|
@media (prefers-reduced-motion: no-preference) {
|
|
461
|
+
.bug {
|
|
462
|
+
transition: width 0.22s cubic-bezier(0.2, 0.8, 0.2, 1), height 0.22s cubic-bezier(0.2, 0.8, 0.2, 1),
|
|
463
|
+
border-radius 0.22s cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 0.22s ease;
|
|
464
|
+
}
|
|
465
|
+
.bug svg { transition: transform 0.1s ease-out, width 0.22s ease, height 0.22s ease; }
|
|
314
466
|
.sheet { animation: rise 0.22s ease-out; }
|
|
315
467
|
@keyframes rise { from { transform: translateY(24px); opacity: 0; } to { transform: none; opacity: 1; } }
|
|
316
468
|
}
|
|
317
469
|
`;
|
|
318
470
|
var mounted = null;
|
|
319
471
|
var feedbackOptedOut = (doc = document) => doc.documentElement.getAttribute("data-fs-feedback") === "off";
|
|
472
|
+
var SCROLL_OPTIONS = { capture: true, passive: true };
|
|
320
473
|
function mountFlowFeedback(options) {
|
|
321
474
|
if (typeof document === "undefined" || !document.body) return null;
|
|
322
475
|
if (mounted) return mounted;
|
|
@@ -325,7 +478,8 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
325
478
|
host.setAttribute(FEEDBACK_HOST_ATTR, "");
|
|
326
479
|
const root = host.attachShadow({ mode: "open" });
|
|
327
480
|
root.innerHTML = `<style>${STYLE}</style>
|
|
328
|
-
<button type="button" class="bug" aria-label="Send feedback on this page">${BUG_ICON}</button>
|
|
481
|
+
<button type="button" class="bug" data-state="float" aria-label="Send feedback on this page">${BUG_ICON}</button>
|
|
482
|
+
<div class="probe" aria-hidden="true"></div>
|
|
329
483
|
<div class="scrim" hidden></div>
|
|
330
484
|
<div class="sheet" role="dialog" aria-modal="true" aria-labelledby="fsf-title" hidden>
|
|
331
485
|
<div class="grab" aria-hidden="true"></div>
|
|
@@ -334,6 +488,7 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
334
488
|
<textarea aria-labelledby="fsf-title" placeholder="What should change?" maxlength="2000"></textarea>
|
|
335
489
|
<p class="status" role="status" aria-live="polite"></p>
|
|
336
490
|
<button type="button" class="send" disabled>Send feedback</button>
|
|
491
|
+
<button type="button" class="link tuck">Tuck the button to the side</button>
|
|
337
492
|
</div>`;
|
|
338
493
|
const q = (selector) => root.querySelector(selector);
|
|
339
494
|
const bug = q(".bug");
|
|
@@ -345,10 +500,199 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
345
500
|
const text = q("textarea");
|
|
346
501
|
const status = q(".status");
|
|
347
502
|
const send = q(".send");
|
|
503
|
+
const tuck = q(".tuck");
|
|
504
|
+
const probe = q(".probe");
|
|
348
505
|
let theme = readFlowTheme();
|
|
349
506
|
let screenshot = null;
|
|
350
507
|
let busy = false;
|
|
351
508
|
let closeTimer = null;
|
|
509
|
+
let placeTimer = null;
|
|
510
|
+
let autoBottom = PLACEMENT_BASE;
|
|
511
|
+
let spot = { state: "float", x: 0, y: 0 };
|
|
512
|
+
let placedByPerson = false;
|
|
513
|
+
let velocity = { x: 0, y: 0 };
|
|
514
|
+
let frame = 0;
|
|
515
|
+
const px = (name) => {
|
|
516
|
+
const raw = getComputedStyle(host).getPropertyValue(name).trim();
|
|
517
|
+
const n = raw ? parseFloat(raw) : NaN;
|
|
518
|
+
return Number.isFinite(n) ? n : null;
|
|
519
|
+
};
|
|
520
|
+
const viewport = () => {
|
|
521
|
+
const style = getComputedStyle(probe);
|
|
522
|
+
return {
|
|
523
|
+
width: window.innerWidth,
|
|
524
|
+
height: window.innerHeight,
|
|
525
|
+
top: parseFloat(style.paddingTop) || 0,
|
|
526
|
+
bottom: parseFloat(style.paddingBottom) || 0
|
|
527
|
+
};
|
|
528
|
+
};
|
|
529
|
+
const draw = () => {
|
|
530
|
+
bug.style.transform = `translate3d(${spot.x}px, ${spot.y}px, 0)`;
|
|
531
|
+
};
|
|
532
|
+
const setState = (next) => {
|
|
533
|
+
spot.state = next;
|
|
534
|
+
bug.dataset.state = next;
|
|
535
|
+
};
|
|
536
|
+
const remember = (spot2) => {
|
|
537
|
+
if (!placedByPerson) return;
|
|
538
|
+
try {
|
|
539
|
+
const view = viewport();
|
|
540
|
+
const width = spot2.state === "float" ? BUTTON_SIZE : TAB_WIDTH;
|
|
541
|
+
localStorage.setItem(
|
|
542
|
+
SPOT_KEY,
|
|
543
|
+
JSON.stringify({
|
|
544
|
+
state: spot2.state,
|
|
545
|
+
left: spot2.x + width / 2 < view.width / 2,
|
|
546
|
+
y: view.height > 0 ? spot2.y / view.height : 0
|
|
547
|
+
})
|
|
548
|
+
);
|
|
549
|
+
} catch {
|
|
550
|
+
}
|
|
551
|
+
};
|
|
552
|
+
const still = () => typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
553
|
+
const springTo = (to, flicked) => {
|
|
554
|
+
cancelAnimationFrame(frame);
|
|
555
|
+
frame = 0;
|
|
556
|
+
setState(to.state);
|
|
557
|
+
remember(to);
|
|
558
|
+
if (still() || typeof requestAnimationFrame !== "function") {
|
|
559
|
+
spot = { ...to };
|
|
560
|
+
velocity = { x: 0, y: 0 };
|
|
561
|
+
draw();
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const omega = 2 * Math.PI / 0.4;
|
|
565
|
+
const k = omega * omega;
|
|
566
|
+
const c = 2 * (flicked ? 0.8 : 1) * omega;
|
|
567
|
+
let last = performance.now();
|
|
568
|
+
const tick = (now) => {
|
|
569
|
+
const dt = Math.min(0.032, (now - last) / 1e3);
|
|
570
|
+
last = now;
|
|
571
|
+
velocity.x += (-k * (spot.x - to.x) - c * velocity.x) * dt;
|
|
572
|
+
velocity.y += (-k * (spot.y - to.y) - c * velocity.y) * dt;
|
|
573
|
+
spot.x += velocity.x * dt;
|
|
574
|
+
spot.y += velocity.y * dt;
|
|
575
|
+
const close2 = Math.abs(spot.x - to.x) < 0.3 && Math.abs(spot.y - to.y) < 0.3 && Math.hypot(velocity.x, velocity.y) < 6;
|
|
576
|
+
if (close2) {
|
|
577
|
+
spot = { ...to };
|
|
578
|
+
velocity = { x: 0, y: 0 };
|
|
579
|
+
frame = 0;
|
|
580
|
+
draw();
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
draw();
|
|
584
|
+
frame = requestAnimationFrame(tick);
|
|
585
|
+
};
|
|
586
|
+
frame = requestAnimationFrame(tick);
|
|
587
|
+
};
|
|
588
|
+
const place = () => {
|
|
589
|
+
placeTimer = null;
|
|
590
|
+
if (!sheet.hidden || bug.hidden || drag || frame) return;
|
|
591
|
+
const view = viewport();
|
|
592
|
+
if (placedByPerson || spot.state !== "float") {
|
|
593
|
+
const width = spot.state === "float" ? BUTTON_SIZE : TAB_WIDTH;
|
|
594
|
+
spot = restingSpot(spot.state, spot.x + width / 2 < view.width / 2, spot.y, view);
|
|
595
|
+
draw();
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
const right = px("--fs-feedback-right") ?? 14;
|
|
599
|
+
const left = view.width - right - BUTTON_SIZE;
|
|
600
|
+
const pinned = px("--fs-feedback-bottom");
|
|
601
|
+
if (pinned === null && typeof document.elementsFromPoint === "function") {
|
|
602
|
+
autoBottom = choosePlacement(
|
|
603
|
+
{
|
|
604
|
+
left,
|
|
605
|
+
top: view.height - view.bottom - autoBottom - BUTTON_SIZE,
|
|
606
|
+
size: BUTTON_SIZE,
|
|
607
|
+
bottom: autoBottom
|
|
608
|
+
},
|
|
609
|
+
view.height,
|
|
610
|
+
occupancyReader(host)
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
const bottom = pinned ?? autoBottom;
|
|
614
|
+
spot = { state: "float", x: left, y: view.height - view.bottom - bottom - BUTTON_SIZE };
|
|
615
|
+
draw();
|
|
616
|
+
};
|
|
617
|
+
const schedulePlace = () => {
|
|
618
|
+
if (placeTimer) clearTimeout(placeTimer);
|
|
619
|
+
placeTimer = setTimeout(place, 150);
|
|
620
|
+
};
|
|
621
|
+
let drag = null;
|
|
622
|
+
let suppressClick = false;
|
|
623
|
+
const onDown = (event) => {
|
|
624
|
+
if (!sheet.hidden || busy) return;
|
|
625
|
+
bug.setPointerCapture?.(event.pointerId);
|
|
626
|
+
cancelAnimationFrame(frame);
|
|
627
|
+
frame = 0;
|
|
628
|
+
velocity = { x: 0, y: 0 };
|
|
629
|
+
drag = {
|
|
630
|
+
id: event.pointerId,
|
|
631
|
+
ox: event.clientX - spot.x,
|
|
632
|
+
oy: event.clientY - spot.y,
|
|
633
|
+
sx: event.clientX,
|
|
634
|
+
sy: event.clientY,
|
|
635
|
+
moved: false,
|
|
636
|
+
history: [{ t: event.timeStamp, x: event.clientX, y: event.clientY }]
|
|
637
|
+
};
|
|
638
|
+
bug.classList.add("down");
|
|
639
|
+
};
|
|
640
|
+
const onMove = (event) => {
|
|
641
|
+
if (!drag || event.pointerId !== drag.id) return;
|
|
642
|
+
const dx = event.clientX - drag.sx;
|
|
643
|
+
const dy = event.clientY - drag.sy;
|
|
644
|
+
if (!drag.moved && Math.hypot(dx, dy) < 8) return;
|
|
645
|
+
if (!drag.moved) {
|
|
646
|
+
drag.moved = true;
|
|
647
|
+
bug.classList.add("dragging");
|
|
648
|
+
}
|
|
649
|
+
drag.history.push({ t: event.timeStamp, x: event.clientX, y: event.clientY });
|
|
650
|
+
if (drag.history.length > 6) drag.history.shift();
|
|
651
|
+
const view = viewport();
|
|
652
|
+
if (spot.state !== "float") {
|
|
653
|
+
const inward = spot.state === "dock-left" ? dx : -dx;
|
|
654
|
+
if (inward <= 18) {
|
|
655
|
+
spot = restingSpot(spot.state, spot.state === "dock-left", event.clientY - drag.oy, view);
|
|
656
|
+
draw();
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
setState("float");
|
|
660
|
+
drag.ox = BUTTON_SIZE / 2;
|
|
661
|
+
drag.oy = BUTTON_SIZE / 2;
|
|
662
|
+
}
|
|
663
|
+
let x = event.clientX - drag.ox;
|
|
664
|
+
let y = event.clientY - drag.oy;
|
|
665
|
+
const maxX = view.width - BUTTON_SIZE;
|
|
666
|
+
const maxY = view.height - BUTTON_SIZE;
|
|
667
|
+
if (x < 0) x = rubberband(x, 60);
|
|
668
|
+
else if (x > maxX) x = maxX + rubberband(x - maxX, 60);
|
|
669
|
+
if (y < 0) y = rubberband(y, 80);
|
|
670
|
+
else if (y > maxY) y = maxY + rubberband(y - maxY, 80);
|
|
671
|
+
spot.x = x;
|
|
672
|
+
spot.y = y;
|
|
673
|
+
draw();
|
|
674
|
+
};
|
|
675
|
+
const onUp = (event) => {
|
|
676
|
+
if (!drag || event.pointerId !== drag.id) return;
|
|
677
|
+
const ended = drag;
|
|
678
|
+
drag = null;
|
|
679
|
+
bug.classList.remove("down", "dragging");
|
|
680
|
+
if (!ended.moved) return;
|
|
681
|
+
suppressClick = true;
|
|
682
|
+
setTimeout(() => {
|
|
683
|
+
suppressClick = false;
|
|
684
|
+
}, 0);
|
|
685
|
+
const first = ended.history[0];
|
|
686
|
+
const last = ended.history[ended.history.length - 1];
|
|
687
|
+
const dt = Math.max(1, last.t - first.t);
|
|
688
|
+
const paused = event.timeStamp - last.t > 90;
|
|
689
|
+
velocity = paused ? { x: 0, y: 0 } : { x: (last.x - first.x) / dt * 1e3, y: (last.y - first.y) / dt * 1e3 };
|
|
690
|
+
placedByPerson = true;
|
|
691
|
+
springTo(settleRelease(spot, velocity, viewport()), Math.hypot(velocity.x, velocity.y) > 350);
|
|
692
|
+
};
|
|
693
|
+
const observer = typeof MutationObserver === "function" ? new MutationObserver((records) => {
|
|
694
|
+
if (records.some((r) => r.target !== host)) schedulePlace();
|
|
695
|
+
}) : null;
|
|
352
696
|
const paint = () => {
|
|
353
697
|
theme = readFlowTheme();
|
|
354
698
|
const vars = {
|
|
@@ -388,6 +732,7 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
388
732
|
status.textContent = "";
|
|
389
733
|
busy = false;
|
|
390
734
|
sync();
|
|
735
|
+
schedulePlace();
|
|
391
736
|
};
|
|
392
737
|
const open = async () => {
|
|
393
738
|
if (!sheet.hidden || busy) return;
|
|
@@ -398,6 +743,7 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
398
743
|
const url = await withTimeout(Promise.resolve().then(capture), CAPTURE_TIMEOUT_MS);
|
|
399
744
|
busy = false;
|
|
400
745
|
setShot(url);
|
|
746
|
+
tuck.hidden = spot.state !== "float";
|
|
401
747
|
scrim.hidden = false;
|
|
402
748
|
sheet.hidden = false;
|
|
403
749
|
sync();
|
|
@@ -428,7 +774,28 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
428
774
|
sync();
|
|
429
775
|
}
|
|
430
776
|
};
|
|
431
|
-
bug.addEventListener("click", () =>
|
|
777
|
+
bug.addEventListener("click", () => {
|
|
778
|
+
if (!suppressClick) void open();
|
|
779
|
+
});
|
|
780
|
+
bug.addEventListener("pointerdown", onDown);
|
|
781
|
+
bug.addEventListener("pointermove", onMove);
|
|
782
|
+
bug.addEventListener("pointerup", onUp);
|
|
783
|
+
bug.addEventListener("pointercancel", onUp);
|
|
784
|
+
tuck.addEventListener("click", () => {
|
|
785
|
+
const view = viewport();
|
|
786
|
+
const left = spot.x + BUTTON_SIZE / 2 < view.width / 2;
|
|
787
|
+
close();
|
|
788
|
+
placedByPerson = true;
|
|
789
|
+
springTo(
|
|
790
|
+
restingSpot(
|
|
791
|
+
left ? "dock-left" : "dock-right",
|
|
792
|
+
left,
|
|
793
|
+
spot.y + (BUTTON_SIZE - TAB_HEIGHT) / 2,
|
|
794
|
+
view
|
|
795
|
+
),
|
|
796
|
+
false
|
|
797
|
+
);
|
|
798
|
+
});
|
|
432
799
|
scrim.addEventListener("click", close);
|
|
433
800
|
remove.addEventListener("click", () => setShot(null));
|
|
434
801
|
text.addEventListener("input", sync);
|
|
@@ -442,6 +809,29 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
442
809
|
});
|
|
443
810
|
document.body.appendChild(host);
|
|
444
811
|
paint();
|
|
812
|
+
try {
|
|
813
|
+
const saved = JSON.parse(localStorage.getItem(SPOT_KEY) ?? "null");
|
|
814
|
+
if (saved && typeof saved.y === "number" && (saved.state === "float" || saved.state === "dock-left" || saved.state === "dock-right")) {
|
|
815
|
+
const view = viewport();
|
|
816
|
+
placedByPerson = true;
|
|
817
|
+
setState(saved.state);
|
|
818
|
+
spot = restingSpot(saved.state, saved.left === true, saved.y * view.height, view);
|
|
819
|
+
}
|
|
820
|
+
} catch {
|
|
821
|
+
}
|
|
822
|
+
place();
|
|
823
|
+
schedulePlace();
|
|
824
|
+
observer?.observe(document.body, {
|
|
825
|
+
childList: true,
|
|
826
|
+
subtree: true,
|
|
827
|
+
attributes: true,
|
|
828
|
+
attributeFilter: ["class", "style", "hidden"]
|
|
829
|
+
});
|
|
830
|
+
window.addEventListener("resize", schedulePlace);
|
|
831
|
+
window.addEventListener("load", schedulePlace);
|
|
832
|
+
window.addEventListener("scroll", schedulePlace, SCROLL_OPTIONS);
|
|
833
|
+
document.addEventListener("transitionend", schedulePlace, SCROLL_OPTIONS);
|
|
834
|
+
document.addEventListener("animationend", schedulePlace, SCROLL_OPTIONS);
|
|
445
835
|
options.onReady?.();
|
|
446
836
|
mounted = {
|
|
447
837
|
host,
|
|
@@ -452,6 +842,14 @@ textarea::placeholder { color: var(--ink-dim); }
|
|
|
452
842
|
close,
|
|
453
843
|
unmount: () => {
|
|
454
844
|
if (closeTimer) clearTimeout(closeTimer);
|
|
845
|
+
if (placeTimer) clearTimeout(placeTimer);
|
|
846
|
+
cancelAnimationFrame(frame);
|
|
847
|
+
observer?.disconnect();
|
|
848
|
+
window.removeEventListener("resize", schedulePlace);
|
|
849
|
+
window.removeEventListener("load", schedulePlace);
|
|
850
|
+
window.removeEventListener("scroll", schedulePlace, SCROLL_OPTIONS);
|
|
851
|
+
document.removeEventListener("transitionend", schedulePlace, SCROLL_OPTIONS);
|
|
852
|
+
document.removeEventListener("animationend", schedulePlace, SCROLL_OPTIONS);
|
|
455
853
|
host.remove();
|
|
456
854
|
mounted = null;
|
|
457
855
|
}
|