@autono/pinbox-toolbar 0.11.0 → 0.13.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/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as HubError, c as createStore, i as HubTransport, l as deriveUiStatus, n as defineToolbarElement, o as appendThreadMessage, r as PinboxToolbarElement, s as applyHubEvent, t as Pinbox, u as upsertPin } from "./src-ChTPrRo5.js";
1
+ import { a as HubError, c as createStore, i as HubTransport, l as deriveUiStatus, n as defineToolbarElement, o as appendThreadMessage, r as PinboxToolbarElement, s as applyHubEvent, t as Pinbox, u as upsertPin } from "./src-C3zvwQrr.js";
2
2
  export { HubError, HubTransport, Pinbox, PinboxToolbarElement, appendThreadMessage, applyHubEvent, createStore, defineToolbarElement, deriveUiStatus, upsertPin };
package/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as defineToolbarElement, r as PinboxToolbarElement } from "./src-ChTPrRo5.js";
1
+ import { n as defineToolbarElement, r as PinboxToolbarElement } from "./src-C3zvwQrr.js";
2
2
  import { createElement, useEffect, useRef } from "react";
3
3
  //#region src/react.ts
4
4
  /**
@@ -367,6 +367,8 @@ const LAYER_HIDE = 140;
367
367
  const BAR_RADIUS = 4;
368
368
  /** The carrier icon rides the surface only while it is puck-like (< this width). */
369
369
  const CARRIER_MAX_W = 260;
370
+ /** Fan close transition length before display:none. */
371
+ const FAN_HIDE = 300;
370
372
  function createMinimize(host) {
371
373
  const { win, bar, ui } = host;
372
374
  const main = mkSpring({
@@ -386,6 +388,8 @@ function createMinimize(host) {
386
388
  let holdTimer = 0;
387
389
  let fadeTimer = 0;
388
390
  let hideTimer = 0;
391
+ let fanOpen = false;
392
+ let fanTimer = 0;
389
393
  function loadDock() {
390
394
  try {
391
395
  const raw = host.storage?.getItem(`${host.storagePrefix}:dock`);
@@ -533,6 +537,7 @@ function createMinimize(host) {
533
537
  }
534
538
  function restore(keyboard = false) {
535
539
  if (mode !== "puck" || puckPos === null) return;
540
+ closeFan();
536
541
  pdown = null;
537
542
  keyboardToggle = keyboard;
538
543
  dock = { ...puckPos };
@@ -567,6 +572,54 @@ function createMinimize(host) {
567
572
  ensureLoop();
568
573
  }, HOLD_RESTORE);
569
574
  }
575
+ /** Fan out of the puck: direction away from the nearer vertical edge,
576
+ * labels sliding toward screen center. */
577
+ function openFan() {
578
+ if (mode !== "puck" || puckPos === null) return;
579
+ win.clearTimeout(fanTimer);
580
+ ui.fan.hidden = false;
581
+ const upward = puckPos.y + PUCK / 2 > win.innerHeight / 2;
582
+ ui.fan.classList.toggle("up", upward);
583
+ ui.fan.classList.toggle("down", !upward);
584
+ ui.fan.classList.toggle("labels-right", puckPos.x + PUCK / 2 < win.innerWidth / 2);
585
+ ui.fan.classList.toggle("labels-left", puckPos.x + PUCK / 2 >= win.innerWidth / 2);
586
+ ui.fan.style.left = `${puckPos.x + PUCK / 2 - 20}px`;
587
+ ui.fan.offsetHeight;
588
+ const h = ui.fan.offsetHeight;
589
+ ui.fan.style.top = upward ? `${puckPos.y - h - 10}px` : `${puckPos.y + PUCK + 10}px`;
590
+ ui.fan.classList.add("on");
591
+ ui.puck.setAttribute("aria-expanded", "true");
592
+ fanOpen = true;
593
+ }
594
+ function closeFan() {
595
+ if (!fanOpen) return false;
596
+ ui.fan.classList.remove("on");
597
+ ui.puck.setAttribute("aria-expanded", "false");
598
+ fanOpen = false;
599
+ win.clearTimeout(fanTimer);
600
+ fanTimer = win.setTimeout(() => {
601
+ ui.fan.hidden = true;
602
+ }, FAN_HIDE);
603
+ return true;
604
+ }
605
+ function onFanClick(e) {
606
+ const act = (e.target.closest?.("[data-act]"))?.getAttribute("data-act");
607
+ if (act == null) return;
608
+ if (act === "expand") {
609
+ closeFan();
610
+ restore(e.detail === 0);
611
+ return;
612
+ }
613
+ host.onFanAction(act);
614
+ }
615
+ /** Click-away, at the document level: shadow events retarget, so membership
616
+ * is checked via composedPath, not target. */
617
+ function onDocPointerDown(e) {
618
+ if (!fanOpen) return;
619
+ const path = e.composedPath();
620
+ if (path.includes(ui.fan) || path.includes(ui.puck)) return;
621
+ closeFan();
622
+ }
570
623
  function onPointerDown(e) {
571
624
  if (mode !== "puck" || e.button !== 0) return;
572
625
  try {
@@ -596,6 +649,7 @@ function createMinimize(host) {
596
649
  }
597
650
  if (Math.hypot(dx, dy) < DRAG_START) return;
598
651
  pdown.moved = true;
652
+ closeFan();
599
653
  mode = "drag";
600
654
  if (!host.reduced) {
601
655
  ui.puck.classList.add("pb-ghost");
@@ -651,7 +705,7 @@ function createMinimize(host) {
651
705
  hideMorph();
652
706
  mode = "puck";
653
707
  }
654
- restore(false);
708
+ if (!closeFan()) openFan();
655
709
  return;
656
710
  }
657
711
  if (host.reduced) {
@@ -679,9 +733,11 @@ function createMinimize(host) {
679
733
  }
680
734
  /** Keyboard/AT activation is a synthesized click (detail 0) with no pointer events. */
681
735
  function onClick(e) {
682
- if (e.detail === 0) restore(true);
736
+ if (e.detail !== 0) return;
737
+ if (!closeFan()) openFan();
683
738
  }
684
739
  function onResize() {
740
+ closeFan();
685
741
  if (mode === "puck" && puckPos !== null) {
686
742
  const p = clampPos(puckPos);
687
743
  placePuck(p.x, p.y);
@@ -693,12 +749,15 @@ function createMinimize(host) {
693
749
  ui.puck.addEventListener("pointerup", onPointerUp);
694
750
  ui.puck.addEventListener("pointercancel", onPointerUp);
695
751
  ui.puck.addEventListener("click", onClick);
752
+ ui.fan.addEventListener("click", onFanClick);
753
+ win.document.addEventListener("pointerdown", onDocPointerDown);
696
754
  win.addEventListener("resize", onResize);
697
755
  return {
698
756
  minimized: () => mode !== "bar",
699
757
  mode: () => mode,
700
758
  minimize,
701
759
  restore,
760
+ closeFan,
702
761
  applyInitial() {
703
762
  if (!loadMinimized()) return;
704
763
  const apply = () => {
@@ -719,12 +778,18 @@ function createMinimize(host) {
719
778
  ui.puck.removeEventListener("pointerup", onPointerUp);
720
779
  ui.puck.removeEventListener("pointercancel", onPointerUp);
721
780
  ui.puck.removeEventListener("click", onClick);
781
+ ui.fan.removeEventListener("click", onFanClick);
782
+ win.document.removeEventListener("pointerdown", onDocPointerDown);
722
783
  win.removeEventListener("resize", onResize);
723
784
  if (raf !== 0) win.cancelAnimationFrame(raf);
724
785
  raf = 0;
725
786
  win.clearTimeout(holdTimer);
726
787
  win.clearTimeout(fadeTimer);
727
788
  win.clearTimeout(hideTimer);
789
+ win.clearTimeout(fanTimer);
790
+ ui.fan.hidden = true;
791
+ ui.fan.classList.remove("on");
792
+ fanOpen = false;
728
793
  }
729
794
  };
730
795
  }
@@ -772,6 +837,39 @@ async function firstFrame(win, stream) {
772
837
  });
773
838
  return video;
774
839
  }
840
+ /**
841
+ * The one live capture stream, reused across pins. getDisplayMedia MUST
842
+ * prompt on every call (spec — no persistent grant exists), so the only way
843
+ * to stop asking per pin is to never re-call it: prompt once, keep the track,
844
+ * and read a fresh frame from the still-playing video for every capture.
845
+ * The browser's "sharing this tab" indicator stays on while the track lives —
846
+ * honest, and the user ending it from there simply re-prompts on the next pin.
847
+ */
848
+ let liveCapture = null;
849
+ async function captureVideo(win, media) {
850
+ const track = liveCapture?.stream.getVideoTracks()[0];
851
+ if (liveCapture && track?.readyState === "live") return liveCapture.video;
852
+ releaseCapture();
853
+ const stream = await media.getDisplayMedia({
854
+ video: true,
855
+ audio: false,
856
+ preferCurrentTab: true
857
+ });
858
+ const video = await firstFrame(win, stream);
859
+ stream.getVideoTracks()[0]?.addEventListener("ended", releaseCapture, { once: true });
860
+ liveCapture = {
861
+ stream,
862
+ video
863
+ };
864
+ return video;
865
+ }
866
+ /** Stop the cached stream (toolbar disconnect; also the track-ended handler). */
867
+ function releaseCapture() {
868
+ if (liveCapture === null) return;
869
+ for (const t of liveCapture.stream.getTracks()) t.stop();
870
+ liveCapture.video.srcObject = null;
871
+ liveCapture = null;
872
+ }
775
873
  /** webp-encode a bitmap; also emit the ≤32px placeholder data URL. */
776
874
  async function encode(bmp) {
777
875
  const canvas = new OffscreenCanvas(bmp.width, bmp.height);
@@ -811,21 +909,14 @@ async function captureElement(el) {
811
909
  const crop = visibleCropRect(el);
812
910
  if (source === null || crop === null) return null;
813
911
  const { win, media } = source;
814
- let stream = null;
815
912
  try {
816
- stream = await media.getDisplayMedia({
817
- video: true,
818
- audio: false,
819
- preferCurrentTab: true
820
- });
821
- const video = await firstFrame(win, stream);
913
+ const video = await captureVideo(win, media);
822
914
  const sx = video.videoWidth / win.innerWidth;
823
915
  const sy = video.videoHeight / win.innerHeight;
824
916
  return await encode(await createImageBitmap(video, Math.round(crop.x * sx), Math.round(crop.y * sy), Math.max(1, Math.round(crop.width * sx)), Math.max(1, Math.round(crop.height * sy))));
825
917
  } catch {
918
+ releaseCapture();
826
919
  return null;
827
- } finally {
828
- if (stream) for (const track of stream.getTracks()) track.stop();
829
920
  }
830
921
  }
831
922
  /**
@@ -2077,12 +2168,25 @@ function renderPins(layer, state) {
2077
2168
  //#region src/ui/puck.ts
2078
2169
  /** The bar's ident mark, sized up for the 48px puck face. */
2079
2170
  const PUCK_ICON = "<svg width=\"17\" height=\"17\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><rect x=\"2.5\" y=\"1.5\" width=\"11\" height=\"7\" rx=\"1\"/><path d=\"M8 8.5v6\"/><circle cx=\"8\" cy=\"14.6\" r=\".9\" fill=\"currentColor\" stroke=\"none\"/></svg>";
2171
+ const FAN_PIN = "<svg width=\"15\" height=\"15\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><rect x=\"3\" y=\"1.5\" width=\"10\" height=\"6.5\" rx=\"1\"/><path d=\"M8 8v6.5\"/></svg>";
2172
+ const FAN_INBOX = "<svg width=\"15\" height=\"15\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><path d=\"M1.8 8.5h3.4l1 2h3.6l1-2h3.4\"/><path d=\"M2.6 3.2h10.8l1.2 5.3v4a1 1 0 01-1 1H2.4a1 1 0 01-1-1v-4z\"/></svg>";
2173
+ const FAN_THEME = "<svg width=\"15\" height=\"15\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><path d=\"M8 1.6a6.4 6.4 0 100 12.8A5 5 0 018 1.6z\"/></svg>";
2174
+ const FAN_EXPAND = "<svg width=\"15\" height=\"15\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><path d=\"M9.5 6.5v-4h4\"/><path d=\"M6.5 9.5v4h-4\"/></svg>";
2175
+ function fanItem(act, index, icon, label, key) {
2176
+ return `<button type="button" class="pb-fan-item" data-act="${act}" style="--i:${index}" aria-label="${label}">${icon}${act === "inbox" ? "<span class=\"badge\" data-ref=\"count\" hidden>0</span>" : ""}<span class="fl">${label.toUpperCase()}<i>${key}</i></span></button>`;
2177
+ }
2080
2178
  function createMinimizeUi(doc) {
2081
2179
  const puck = doc.createElement("button");
2082
2180
  puck.type = "button";
2083
2181
  puck.className = "pb-puck pb-ghost";
2084
- puck.setAttribute("aria-label", "Restore Pinbox toolbar");
2182
+ puck.setAttribute("aria-label", "Pinbox menu");
2183
+ puck.setAttribute("aria-haspopup", "menu");
2085
2184
  puck.innerHTML = `<span class="in">${PUCK_ICON}</span><span class="badge" data-ref="count" hidden>0</span><span class="cdot"></span>`;
2185
+ const fan = doc.createElement("div");
2186
+ fan.className = "pb-fan";
2187
+ fan.hidden = true;
2188
+ fan.setAttribute("role", "menu");
2189
+ fan.innerHTML = fanItem("pin", 0, FAN_PIN, "Drop a pin", "P") + fanItem("inbox", 1, FAN_INBOX, "Inbox", "I") + fanItem("theme", 2, FAN_THEME, "Theme", "D") + fanItem("expand", 3, FAN_EXPAND, "Expand", "M");
2086
2190
  const morphWrap = doc.createElement("div");
2087
2191
  morphWrap.className = "pb-morph-wrap";
2088
2192
  morphWrap.hidden = true;
@@ -2093,9 +2197,14 @@ function createMinimizeUi(doc) {
2093
2197
  carrier.innerHTML = `${PUCK_ICON}<span class="badge" data-ref="count" hidden>0</span>`;
2094
2198
  morphWrap.appendChild(surface);
2095
2199
  morphWrap.appendChild(carrier);
2096
- const badges = [puck.querySelector("[data-ref=\"count\"]"), carrier.querySelector("[data-ref=\"count\"]")];
2200
+ const badges = [
2201
+ puck.querySelector("[data-ref=\"count\"]"),
2202
+ carrier.querySelector("[data-ref=\"count\"]"),
2203
+ fan.querySelector("[data-ref=\"count\"]")
2204
+ ];
2097
2205
  return {
2098
2206
  puck,
2207
+ fan,
2099
2208
  morphWrap,
2100
2209
  surface,
2101
2210
  carrier,
@@ -2107,6 +2216,7 @@ function createMinimizeUi(doc) {
2107
2216
  }
2108
2217
  const degraded = state.connection === "offline" || state.connection === "incompatible";
2109
2218
  puck.classList.toggle("degraded", degraded);
2219
+ puck.classList.toggle("armed", state.mode === "placing");
2110
2220
  }
2111
2221
  };
2112
2222
  }
@@ -2393,10 +2503,28 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
2393
2503
  .pb-puck:active { cursor: grabbing; }
2394
2504
  .pb-puck .in { display: flex; transition: transform 160ms var(--pb-ease); }
2395
2505
  .pb-puck:hover .in { transform: scale(1.12); }
2396
- .pb-puck .badge, .pb-carrier .badge { position: absolute; top: -5px; right: -5px; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px; background: var(--pb-amber); color: var(--pb-amber-ink); font-family: var(--pb-font-mono); font-size: 9px; font-weight: 500; display: flex; align-items: center; justify-content: center; }
2506
+ .pb-puck .badge, .pb-carrier .badge, .pb-fan-item .badge { position: absolute; top: -5px; right: -5px; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px; background: var(--pb-amber); color: var(--pb-amber-ink); font-family: var(--pb-font-mono); font-size: 9px; font-weight: 500; display: flex; align-items: center; justify-content: center; }
2397
2507
  /* The bar says "· OFFLINE" in words; the puck's amber dot is the same signal. */
2398
2508
  .pb-puck .cdot { position: absolute; bottom: -1px; right: -1px; width: 9px; height: 9px; border-radius: 999px; background: var(--pb-amber); border: 2px solid var(--pb-canvas); display: none; }
2399
2509
  .pb-puck.degraded .cdot { display: block; }
2510
+ /* Placing armed from the fan: the bar's armed-ring is hidden with the bar. */
2511
+ .pb-puck.armed { border-color: var(--pb-amber); box-shadow: 0 0 0 4px var(--pb-amber-soft), var(--pb-shadow); }
2512
+
2513
+ /* puck fan menu (ui/puck.ts, minimize.ts) — tap the puck and a vertical
2514
+ quick-menu fans out of it; EXPAND is how the bar comes back. Items stagger
2515
+ from the puck; hovering slides a label + key chip toward screen center. */
2516
+ .pb-fan { position: fixed; z-index: 96; display: flex; flex-direction: column; gap: 6px; align-items: center; }
2517
+ .pb-fan-item { position: relative; width: 40px; height: 40px; border-radius: 999px; background: var(--pb-bar); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border: 1px solid var(--pb-line-2); box-shadow: var(--pb-shadow); color: var(--pb-fg2); display: flex; align-items: center; justify-content: center; opacity: 0; transform: var(--fan-from) scale(0.5); transition: transform 300ms var(--pb-ease), opacity 180ms linear, color 140ms linear, border-color 140ms linear; transition-delay: calc(var(--i) * 35ms); }
2518
+ .pb-fan.up { --fan-from: translateY(16px); }
2519
+ .pb-fan.down { --fan-from: translateY(-16px); }
2520
+ .pb-fan.on .pb-fan-item { opacity: 1; transform: none; }
2521
+ .pb-fan-item:hover { color: var(--pb-amber); border-color: var(--pb-amber); }
2522
+ .pb-fan-item .badge { pointer-events: none; }
2523
+ .pb-fan-item .fl { position: absolute; top: 50%; display: flex; align-items: center; gap: 6px; padding: 4px 8px; background: var(--pb-elev); border: 1px solid var(--pb-line-2); border-radius: 2px; box-shadow: var(--pb-shadow); font-family: var(--pb-font-mono); font-size: 9px; letter-spacing: .14em; color: var(--pb-fg1); white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 140ms linear, transform 200ms var(--pb-ease); }
2524
+ .pb-fan.labels-right .fl { left: calc(100% + 10px); transform: translateY(-50%) translateX(-6px); }
2525
+ .pb-fan.labels-left .fl { right: calc(100% + 10px); transform: translateY(-50%) translateX(6px); }
2526
+ .pb-fan-item:hover .fl { opacity: 1; transform: translateY(-50%) translateX(0); }
2527
+ .pb-fan-item .fl i { font-style: normal; padding: 1px 5px; border: 1px solid var(--pb-line-2); border-radius: 2px; background: var(--pb-sunken); color: var(--pb-fg3); }
2400
2528
 
2401
2529
  /* shortcuts modal (ui/shortcuts.ts) — prototype lines 226–232 */
2402
2530
  .pb-modal { position: fixed; inset: 0; z-index: 120; display: flex; align-items: center; justify-content: center; background: var(--pb-scrim); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); animation: pb-fade 200ms ease-out both; }
@@ -2543,6 +2671,7 @@ var PinboxToolbarElement = class extends BaseElement {
2543
2671
  this.#aim = null;
2544
2672
  this.#min?.destroy();
2545
2673
  this.#min = null;
2674
+ releaseCapture();
2546
2675
  this.#unsubscribe?.();
2547
2676
  this.#unsubscribe = null;
2548
2677
  this.#pageStyle?.remove();
@@ -2603,6 +2732,7 @@ var PinboxToolbarElement = class extends BaseElement {
2603
2732
  this.#minUi = createMinimizeUi(document);
2604
2733
  shadow.appendChild(this.#minUi.morphWrap);
2605
2734
  shadow.appendChild(this.#minUi.puck);
2735
+ shadow.appendChild(this.#minUi.fan);
2606
2736
  this.#drawer = createDrawer(document, {
2607
2737
  onActivate: (pinId) => this.#activateFromInbox(pinId),
2608
2738
  onClose: () => this.store.update({ inboxOpen: false })
@@ -2627,7 +2757,12 @@ var PinboxToolbarElement = class extends BaseElement {
2627
2757
  storagePrefix: `pinbox:${this.config?.endpoint ?? ""}`,
2628
2758
  reduced: window.matchMedia("(prefers-reduced-motion: reduce)").matches,
2629
2759
  initialMinimized: this.config?.minimized === true,
2630
- onSettled: (minimized, keyboard) => this.#onMinimizeSettled(minimized, keyboard)
2760
+ onSettled: (minimized, keyboard) => this.#onMinimizeSettled(minimized, keyboard),
2761
+ onFanAction: (action) => {
2762
+ if (action === "pin") this.#togglePlacing();
2763
+ else if (action === "inbox") this.#toggleInbox();
2764
+ else this.#toggleTheme();
2765
+ }
2631
2766
  });
2632
2767
  this.#min.applyInitial();
2633
2768
  }
@@ -2654,11 +2789,6 @@ var PinboxToolbarElement = class extends BaseElement {
2654
2789
  restore(keyboard = false) {
2655
2790
  this.#min?.restore(keyboard);
2656
2791
  }
2657
- /** Bar-surface shortcuts pressed while minimized restore the bar, then run. */
2658
- #surfaced(run) {
2659
- if (this.#min?.minimized() === true) this.restore(false);
2660
- run();
2661
- }
2662
2792
  /**
2663
2793
  * Task 8 wiring: WS events mutate the store, connection state renders in the
2664
2794
  * bar, card actions hit the hub. The mirror seeds pins so an offline reload
@@ -2884,9 +3014,12 @@ var PinboxToolbarElement = class extends BaseElement {
2884
3014
  };
2885
3015
  /** Prototype keyboard map (v2-command-bar.html lines 701–712) + M (v3 minimize). */
2886
3016
  #shortcuts = {
2887
- escape: () => this.#dismiss(),
2888
- p: () => this.#surfaced(() => this.#togglePlacing()),
2889
- i: () => this.#surfaced(() => this.#toggleInbox()),
3017
+ escape: () => {
3018
+ if (this.#min?.closeFan() === true) return;
3019
+ this.#dismiss();
3020
+ },
3021
+ p: () => this.#togglePlacing(),
3022
+ i: () => this.#toggleInbox(),
2890
3023
  d: () => this.#toggleTheme(),
2891
3024
  r: () => this.#resolveActive(),
2892
3025
  c: () => this.#copyOpenPins(),
package/dist/svelte.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as HubError, c as createStore, i as HubTransport, l as deriveUiStatus, n as defineToolbarElement, o as appendThreadMessage, r as PinboxToolbarElement, s as applyHubEvent, t as Pinbox, u as upsertPin } from "./src-ChTPrRo5.js";
1
+ import { a as HubError, c as createStore, i as HubTransport, l as deriveUiStatus, n as defineToolbarElement, o as appendThreadMessage, r as PinboxToolbarElement, s as applyHubEvent, t as Pinbox, u as upsertPin } from "./src-C3zvwQrr.js";
2
2
  //#region src/svelte.ts
3
3
  /**
4
4
  * `<div use:pinbox={{ endpoint }} />` — creates + configures the element BEFORE insertion
@@ -369,6 +369,8 @@ var Pinbox = (function(exports) {
369
369
  const BAR_RADIUS = 4;
370
370
  /** The carrier icon rides the surface only while it is puck-like (< this width). */
371
371
  const CARRIER_MAX_W = 260;
372
+ /** Fan close transition length before display:none. */
373
+ const FAN_HIDE = 300;
372
374
  function createMinimize(host) {
373
375
  const { win, bar, ui } = host;
374
376
  const main = mkSpring({
@@ -388,6 +390,8 @@ var Pinbox = (function(exports) {
388
390
  let holdTimer = 0;
389
391
  let fadeTimer = 0;
390
392
  let hideTimer = 0;
393
+ let fanOpen = false;
394
+ let fanTimer = 0;
391
395
  function loadDock() {
392
396
  try {
393
397
  const raw = host.storage?.getItem(`${host.storagePrefix}:dock`);
@@ -535,6 +539,7 @@ var Pinbox = (function(exports) {
535
539
  }
536
540
  function restore(keyboard = false) {
537
541
  if (mode !== "puck" || puckPos === null) return;
542
+ closeFan();
538
543
  pdown = null;
539
544
  keyboardToggle = keyboard;
540
545
  dock = { ...puckPos };
@@ -569,6 +574,54 @@ var Pinbox = (function(exports) {
569
574
  ensureLoop();
570
575
  }, HOLD_RESTORE);
571
576
  }
577
+ /** Fan out of the puck: direction away from the nearer vertical edge,
578
+ * labels sliding toward screen center. */
579
+ function openFan() {
580
+ if (mode !== "puck" || puckPos === null) return;
581
+ win.clearTimeout(fanTimer);
582
+ ui.fan.hidden = false;
583
+ const upward = puckPos.y + PUCK / 2 > win.innerHeight / 2;
584
+ ui.fan.classList.toggle("up", upward);
585
+ ui.fan.classList.toggle("down", !upward);
586
+ ui.fan.classList.toggle("labels-right", puckPos.x + PUCK / 2 < win.innerWidth / 2);
587
+ ui.fan.classList.toggle("labels-left", puckPos.x + PUCK / 2 >= win.innerWidth / 2);
588
+ ui.fan.style.left = `${puckPos.x + PUCK / 2 - 20}px`;
589
+ ui.fan.offsetHeight;
590
+ const h = ui.fan.offsetHeight;
591
+ ui.fan.style.top = upward ? `${puckPos.y - h - 10}px` : `${puckPos.y + PUCK + 10}px`;
592
+ ui.fan.classList.add("on");
593
+ ui.puck.setAttribute("aria-expanded", "true");
594
+ fanOpen = true;
595
+ }
596
+ function closeFan() {
597
+ if (!fanOpen) return false;
598
+ ui.fan.classList.remove("on");
599
+ ui.puck.setAttribute("aria-expanded", "false");
600
+ fanOpen = false;
601
+ win.clearTimeout(fanTimer);
602
+ fanTimer = win.setTimeout(() => {
603
+ ui.fan.hidden = true;
604
+ }, FAN_HIDE);
605
+ return true;
606
+ }
607
+ function onFanClick(e) {
608
+ const act = (e.target.closest?.("[data-act]"))?.getAttribute("data-act");
609
+ if (act == null) return;
610
+ if (act === "expand") {
611
+ closeFan();
612
+ restore(e.detail === 0);
613
+ return;
614
+ }
615
+ host.onFanAction(act);
616
+ }
617
+ /** Click-away, at the document level: shadow events retarget, so membership
618
+ * is checked via composedPath, not target. */
619
+ function onDocPointerDown(e) {
620
+ if (!fanOpen) return;
621
+ const path = e.composedPath();
622
+ if (path.includes(ui.fan) || path.includes(ui.puck)) return;
623
+ closeFan();
624
+ }
572
625
  function onPointerDown(e) {
573
626
  if (mode !== "puck" || e.button !== 0) return;
574
627
  try {
@@ -598,6 +651,7 @@ var Pinbox = (function(exports) {
598
651
  }
599
652
  if (Math.hypot(dx, dy) < DRAG_START) return;
600
653
  pdown.moved = true;
654
+ closeFan();
601
655
  mode = "drag";
602
656
  if (!host.reduced) {
603
657
  ui.puck.classList.add("pb-ghost");
@@ -653,7 +707,7 @@ var Pinbox = (function(exports) {
653
707
  hideMorph();
654
708
  mode = "puck";
655
709
  }
656
- restore(false);
710
+ if (!closeFan()) openFan();
657
711
  return;
658
712
  }
659
713
  if (host.reduced) {
@@ -681,9 +735,11 @@ var Pinbox = (function(exports) {
681
735
  }
682
736
  /** Keyboard/AT activation is a synthesized click (detail 0) with no pointer events. */
683
737
  function onClick(e) {
684
- if (e.detail === 0) restore(true);
738
+ if (e.detail !== 0) return;
739
+ if (!closeFan()) openFan();
685
740
  }
686
741
  function onResize() {
742
+ closeFan();
687
743
  if (mode === "puck" && puckPos !== null) {
688
744
  const p = clampPos(puckPos);
689
745
  placePuck(p.x, p.y);
@@ -695,12 +751,15 @@ var Pinbox = (function(exports) {
695
751
  ui.puck.addEventListener("pointerup", onPointerUp);
696
752
  ui.puck.addEventListener("pointercancel", onPointerUp);
697
753
  ui.puck.addEventListener("click", onClick);
754
+ ui.fan.addEventListener("click", onFanClick);
755
+ win.document.addEventListener("pointerdown", onDocPointerDown);
698
756
  win.addEventListener("resize", onResize);
699
757
  return {
700
758
  minimized: () => mode !== "bar",
701
759
  mode: () => mode,
702
760
  minimize,
703
761
  restore,
762
+ closeFan,
704
763
  applyInitial() {
705
764
  if (!loadMinimized()) return;
706
765
  const apply = () => {
@@ -721,12 +780,18 @@ var Pinbox = (function(exports) {
721
780
  ui.puck.removeEventListener("pointerup", onPointerUp);
722
781
  ui.puck.removeEventListener("pointercancel", onPointerUp);
723
782
  ui.puck.removeEventListener("click", onClick);
783
+ ui.fan.removeEventListener("click", onFanClick);
784
+ win.document.removeEventListener("pointerdown", onDocPointerDown);
724
785
  win.removeEventListener("resize", onResize);
725
786
  if (raf !== 0) win.cancelAnimationFrame(raf);
726
787
  raf = 0;
727
788
  win.clearTimeout(holdTimer);
728
789
  win.clearTimeout(fadeTimer);
729
790
  win.clearTimeout(hideTimer);
791
+ win.clearTimeout(fanTimer);
792
+ ui.fan.hidden = true;
793
+ ui.fan.classList.remove("on");
794
+ fanOpen = false;
730
795
  }
731
796
  };
732
797
  }
@@ -774,6 +839,39 @@ var Pinbox = (function(exports) {
774
839
  });
775
840
  return video;
776
841
  }
842
+ /**
843
+ * The one live capture stream, reused across pins. getDisplayMedia MUST
844
+ * prompt on every call (spec — no persistent grant exists), so the only way
845
+ * to stop asking per pin is to never re-call it: prompt once, keep the track,
846
+ * and read a fresh frame from the still-playing video for every capture.
847
+ * The browser's "sharing this tab" indicator stays on while the track lives —
848
+ * honest, and the user ending it from there simply re-prompts on the next pin.
849
+ */
850
+ let liveCapture = null;
851
+ async function captureVideo(win, media) {
852
+ const track = liveCapture?.stream.getVideoTracks()[0];
853
+ if (liveCapture && track?.readyState === "live") return liveCapture.video;
854
+ releaseCapture();
855
+ const stream = await media.getDisplayMedia({
856
+ video: true,
857
+ audio: false,
858
+ preferCurrentTab: true
859
+ });
860
+ const video = await firstFrame(win, stream);
861
+ stream.getVideoTracks()[0]?.addEventListener("ended", releaseCapture, { once: true });
862
+ liveCapture = {
863
+ stream,
864
+ video
865
+ };
866
+ return video;
867
+ }
868
+ /** Stop the cached stream (toolbar disconnect; also the track-ended handler). */
869
+ function releaseCapture() {
870
+ if (liveCapture === null) return;
871
+ for (const t of liveCapture.stream.getTracks()) t.stop();
872
+ liveCapture.video.srcObject = null;
873
+ liveCapture = null;
874
+ }
777
875
  /** webp-encode a bitmap; also emit the ≤32px placeholder data URL. */
778
876
  async function encode(bmp) {
779
877
  const canvas = new OffscreenCanvas(bmp.width, bmp.height);
@@ -813,21 +911,14 @@ var Pinbox = (function(exports) {
813
911
  const crop = visibleCropRect(el);
814
912
  if (source === null || crop === null) return null;
815
913
  const { win, media } = source;
816
- let stream = null;
817
914
  try {
818
- stream = await media.getDisplayMedia({
819
- video: true,
820
- audio: false,
821
- preferCurrentTab: true
822
- });
823
- const video = await firstFrame(win, stream);
915
+ const video = await captureVideo(win, media);
824
916
  const sx = video.videoWidth / win.innerWidth;
825
917
  const sy = video.videoHeight / win.innerHeight;
826
918
  return await encode(await createImageBitmap(video, Math.round(crop.x * sx), Math.round(crop.y * sy), Math.max(1, Math.round(crop.width * sx)), Math.max(1, Math.round(crop.height * sy))));
827
919
  } catch {
920
+ releaseCapture();
828
921
  return null;
829
- } finally {
830
- if (stream) for (const track of stream.getTracks()) track.stop();
831
922
  }
832
923
  }
833
924
  /**
@@ -2079,12 +2170,25 @@ var Pinbox = (function(exports) {
2079
2170
  //#region src/ui/puck.ts
2080
2171
  /** The bar's ident mark, sized up for the 48px puck face. */
2081
2172
  const PUCK_ICON = "<svg width=\"17\" height=\"17\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><rect x=\"2.5\" y=\"1.5\" width=\"11\" height=\"7\" rx=\"1\"/><path d=\"M8 8.5v6\"/><circle cx=\"8\" cy=\"14.6\" r=\".9\" fill=\"currentColor\" stroke=\"none\"/></svg>";
2173
+ const FAN_PIN = "<svg width=\"15\" height=\"15\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><rect x=\"3\" y=\"1.5\" width=\"10\" height=\"6.5\" rx=\"1\"/><path d=\"M8 8v6.5\"/></svg>";
2174
+ const FAN_INBOX = "<svg width=\"15\" height=\"15\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><path d=\"M1.8 8.5h3.4l1 2h3.6l1-2h3.4\"/><path d=\"M2.6 3.2h10.8l1.2 5.3v4a1 1 0 01-1 1H2.4a1 1 0 01-1-1v-4z\"/></svg>";
2175
+ const FAN_THEME = "<svg width=\"15\" height=\"15\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><path d=\"M8 1.6a6.4 6.4 0 100 12.8A5 5 0 018 1.6z\"/></svg>";
2176
+ const FAN_EXPAND = "<svg width=\"15\" height=\"15\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\"><path d=\"M9.5 6.5v-4h4\"/><path d=\"M6.5 9.5v4h-4\"/></svg>";
2177
+ function fanItem(act, index, icon, label, key) {
2178
+ return `<button type="button" class="pb-fan-item" data-act="${act}" style="--i:${index}" aria-label="${label}">${icon}${act === "inbox" ? "<span class=\"badge\" data-ref=\"count\" hidden>0</span>" : ""}<span class="fl">${label.toUpperCase()}<i>${key}</i></span></button>`;
2179
+ }
2082
2180
  function createMinimizeUi(doc) {
2083
2181
  const puck = doc.createElement("button");
2084
2182
  puck.type = "button";
2085
2183
  puck.className = "pb-puck pb-ghost";
2086
- puck.setAttribute("aria-label", "Restore Pinbox toolbar");
2184
+ puck.setAttribute("aria-label", "Pinbox menu");
2185
+ puck.setAttribute("aria-haspopup", "menu");
2087
2186
  puck.innerHTML = `<span class="in">${PUCK_ICON}</span><span class="badge" data-ref="count" hidden>0</span><span class="cdot"></span>`;
2187
+ const fan = doc.createElement("div");
2188
+ fan.className = "pb-fan";
2189
+ fan.hidden = true;
2190
+ fan.setAttribute("role", "menu");
2191
+ fan.innerHTML = fanItem("pin", 0, FAN_PIN, "Drop a pin", "P") + fanItem("inbox", 1, FAN_INBOX, "Inbox", "I") + fanItem("theme", 2, FAN_THEME, "Theme", "D") + fanItem("expand", 3, FAN_EXPAND, "Expand", "M");
2088
2192
  const morphWrap = doc.createElement("div");
2089
2193
  morphWrap.className = "pb-morph-wrap";
2090
2194
  morphWrap.hidden = true;
@@ -2095,9 +2199,14 @@ var Pinbox = (function(exports) {
2095
2199
  carrier.innerHTML = `${PUCK_ICON}<span class="badge" data-ref="count" hidden>0</span>`;
2096
2200
  morphWrap.appendChild(surface);
2097
2201
  morphWrap.appendChild(carrier);
2098
- const badges = [puck.querySelector("[data-ref=\"count\"]"), carrier.querySelector("[data-ref=\"count\"]")];
2202
+ const badges = [
2203
+ puck.querySelector("[data-ref=\"count\"]"),
2204
+ carrier.querySelector("[data-ref=\"count\"]"),
2205
+ fan.querySelector("[data-ref=\"count\"]")
2206
+ ];
2099
2207
  return {
2100
2208
  puck,
2209
+ fan,
2101
2210
  morphWrap,
2102
2211
  surface,
2103
2212
  carrier,
@@ -2109,6 +2218,7 @@ var Pinbox = (function(exports) {
2109
2218
  }
2110
2219
  const degraded = state.connection === "offline" || state.connection === "incompatible";
2111
2220
  puck.classList.toggle("degraded", degraded);
2221
+ puck.classList.toggle("armed", state.mode === "placing");
2112
2222
  }
2113
2223
  };
2114
2224
  }
@@ -2395,10 +2505,28 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
2395
2505
  .pb-puck:active { cursor: grabbing; }
2396
2506
  .pb-puck .in { display: flex; transition: transform 160ms var(--pb-ease); }
2397
2507
  .pb-puck:hover .in { transform: scale(1.12); }
2398
- .pb-puck .badge, .pb-carrier .badge { position: absolute; top: -5px; right: -5px; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px; background: var(--pb-amber); color: var(--pb-amber-ink); font-family: var(--pb-font-mono); font-size: 9px; font-weight: 500; display: flex; align-items: center; justify-content: center; }
2508
+ .pb-puck .badge, .pb-carrier .badge, .pb-fan-item .badge { position: absolute; top: -5px; right: -5px; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px; background: var(--pb-amber); color: var(--pb-amber-ink); font-family: var(--pb-font-mono); font-size: 9px; font-weight: 500; display: flex; align-items: center; justify-content: center; }
2399
2509
  /* The bar says "· OFFLINE" in words; the puck's amber dot is the same signal. */
2400
2510
  .pb-puck .cdot { position: absolute; bottom: -1px; right: -1px; width: 9px; height: 9px; border-radius: 999px; background: var(--pb-amber); border: 2px solid var(--pb-canvas); display: none; }
2401
2511
  .pb-puck.degraded .cdot { display: block; }
2512
+ /* Placing armed from the fan: the bar's armed-ring is hidden with the bar. */
2513
+ .pb-puck.armed { border-color: var(--pb-amber); box-shadow: 0 0 0 4px var(--pb-amber-soft), var(--pb-shadow); }
2514
+
2515
+ /* puck fan menu (ui/puck.ts, minimize.ts) — tap the puck and a vertical
2516
+ quick-menu fans out of it; EXPAND is how the bar comes back. Items stagger
2517
+ from the puck; hovering slides a label + key chip toward screen center. */
2518
+ .pb-fan { position: fixed; z-index: 96; display: flex; flex-direction: column; gap: 6px; align-items: center; }
2519
+ .pb-fan-item { position: relative; width: 40px; height: 40px; border-radius: 999px; background: var(--pb-bar); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border: 1px solid var(--pb-line-2); box-shadow: var(--pb-shadow); color: var(--pb-fg2); display: flex; align-items: center; justify-content: center; opacity: 0; transform: var(--fan-from) scale(0.5); transition: transform 300ms var(--pb-ease), opacity 180ms linear, color 140ms linear, border-color 140ms linear; transition-delay: calc(var(--i) * 35ms); }
2520
+ .pb-fan.up { --fan-from: translateY(16px); }
2521
+ .pb-fan.down { --fan-from: translateY(-16px); }
2522
+ .pb-fan.on .pb-fan-item { opacity: 1; transform: none; }
2523
+ .pb-fan-item:hover { color: var(--pb-amber); border-color: var(--pb-amber); }
2524
+ .pb-fan-item .badge { pointer-events: none; }
2525
+ .pb-fan-item .fl { position: absolute; top: 50%; display: flex; align-items: center; gap: 6px; padding: 4px 8px; background: var(--pb-elev); border: 1px solid var(--pb-line-2); border-radius: 2px; box-shadow: var(--pb-shadow); font-family: var(--pb-font-mono); font-size: 9px; letter-spacing: .14em; color: var(--pb-fg1); white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 140ms linear, transform 200ms var(--pb-ease); }
2526
+ .pb-fan.labels-right .fl { left: calc(100% + 10px); transform: translateY(-50%) translateX(-6px); }
2527
+ .pb-fan.labels-left .fl { right: calc(100% + 10px); transform: translateY(-50%) translateX(6px); }
2528
+ .pb-fan-item:hover .fl { opacity: 1; transform: translateY(-50%) translateX(0); }
2529
+ .pb-fan-item .fl i { font-style: normal; padding: 1px 5px; border: 1px solid var(--pb-line-2); border-radius: 2px; background: var(--pb-sunken); color: var(--pb-fg3); }
2402
2530
 
2403
2531
  /* shortcuts modal (ui/shortcuts.ts) — prototype lines 226–232 */
2404
2532
  .pb-modal { position: fixed; inset: 0; z-index: 120; display: flex; align-items: center; justify-content: center; background: var(--pb-scrim); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); animation: pb-fade 200ms ease-out both; }
@@ -2545,6 +2673,7 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
2545
2673
  this.#aim = null;
2546
2674
  this.#min?.destroy();
2547
2675
  this.#min = null;
2676
+ releaseCapture();
2548
2677
  this.#unsubscribe?.();
2549
2678
  this.#unsubscribe = null;
2550
2679
  this.#pageStyle?.remove();
@@ -2605,6 +2734,7 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
2605
2734
  this.#minUi = createMinimizeUi(document);
2606
2735
  shadow.appendChild(this.#minUi.morphWrap);
2607
2736
  shadow.appendChild(this.#minUi.puck);
2737
+ shadow.appendChild(this.#minUi.fan);
2608
2738
  this.#drawer = createDrawer(document, {
2609
2739
  onActivate: (pinId) => this.#activateFromInbox(pinId),
2610
2740
  onClose: () => this.store.update({ inboxOpen: false })
@@ -2629,7 +2759,12 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
2629
2759
  storagePrefix: `pinbox:${this.config?.endpoint ?? ""}`,
2630
2760
  reduced: window.matchMedia("(prefers-reduced-motion: reduce)").matches,
2631
2761
  initialMinimized: this.config?.minimized === true,
2632
- onSettled: (minimized, keyboard) => this.#onMinimizeSettled(minimized, keyboard)
2762
+ onSettled: (minimized, keyboard) => this.#onMinimizeSettled(minimized, keyboard),
2763
+ onFanAction: (action) => {
2764
+ if (action === "pin") this.#togglePlacing();
2765
+ else if (action === "inbox") this.#toggleInbox();
2766
+ else this.#toggleTheme();
2767
+ }
2633
2768
  });
2634
2769
  this.#min.applyInitial();
2635
2770
  }
@@ -2656,11 +2791,6 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
2656
2791
  restore(keyboard = false) {
2657
2792
  this.#min?.restore(keyboard);
2658
2793
  }
2659
- /** Bar-surface shortcuts pressed while minimized restore the bar, then run. */
2660
- #surfaced(run) {
2661
- if (this.#min?.minimized() === true) this.restore(false);
2662
- run();
2663
- }
2664
2794
  /**
2665
2795
  * Task 8 wiring: WS events mutate the store, connection state renders in the
2666
2796
  * bar, card actions hit the hub. The mirror seeds pins so an offline reload
@@ -2886,9 +3016,12 @@ button { font: inherit; color: inherit; background: none; border: 0; cursor: poi
2886
3016
  };
2887
3017
  /** Prototype keyboard map (v2-command-bar.html lines 701–712) + M (v3 minimize). */
2888
3018
  #shortcuts = {
2889
- escape: () => this.#dismiss(),
2890
- p: () => this.#surfaced(() => this.#togglePlacing()),
2891
- i: () => this.#surfaced(() => this.#toggleInbox()),
3019
+ escape: () => {
3020
+ if (this.#min?.closeFan() === true) return;
3021
+ this.#dismiss();
3022
+ },
3023
+ p: () => this.#togglePlacing(),
3024
+ i: () => this.#toggleInbox(),
2892
3025
  d: () => this.#toggleTheme(),
2893
3026
  r: () => this.#resolveActive(),
2894
3027
  c: () => this.#copyOpenPins(),
package/dist/vue.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as defineToolbarElement, r as PinboxToolbarElement } from "./src-ChTPrRo5.js";
1
+ import { n as defineToolbarElement, r as PinboxToolbarElement } from "./src-C3zvwQrr.js";
2
2
  import { h } from "vue";
3
3
  //#region src/vue.ts
4
4
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autono/pinbox-toolbar",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -71,7 +71,7 @@
71
71
  }
72
72
  },
73
73
  "devDependencies": {
74
- "@autono/pinbox-core": "0.11.0",
74
+ "@autono/pinbox-core": "0.13.0",
75
75
  "@types/react": "^19.2.0",
76
76
  "typescript": "^7.0.0",
77
77
  "tsdown": "^0.22.0",