@loupekit/sdk 0.10.5 → 0.10.6-next.51

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 CHANGED
@@ -60,7 +60,7 @@ the moment of the comment, so the feedback stays actionable even after the UI ch
60
60
  | 💬 **Free comments** | Drop a page-level note anywhere with the **Note** mode — no element, no screenshot. |
61
61
  | ▭ **Free-region screenshots** | Drag a free-size box, screenshot exactly that area, comment on it. The region anchors to the element under its center, so it tracks responsive reflow and scrolling. |
62
62
  | ⏺ **Screen recording** | The **Record** tool drags the same box, then captures a screen video of it (via `getDisplayMedia` + canvas crop) as `.webm` — duration-capped with a Stop button. Recordings play back inline in the widget and the dashboard. |
63
- | 🧲 **Dockable control** | A DevTools-style panel: dock it to the left / right / bottom edge (which pushes your page over so it's never covered) or float it as a movable, resizable window. Light/dark theme, collapses to a small `◎` launcher, and becomes a bottom sheet on mobile. Position & theme persist. |
63
+ | 🧲 **Dockable control** | A DevTools-style panel: dock it to the left / right / bottom edge (which pushes your page over so it's never covered) or float it as a movable, resizable window. Light/dark theme, collapses to a `◎` FAB cluster (quick actions to pin a comment, drop a note, hide the markers, or open the Claude setup), and becomes a bottom sheet on mobile. Position, theme and marker visibility persist. |
64
64
  | 🔁 **Redeploy-surviving re-anchoring** | A multi-signal fingerprint (stable id/testid, CSS path, XPath, text, attributes, position) re-locates the element on the current page; if it can't, the pin **detaches** instead of pointing at the wrong thing. |
65
65
  | 📸 **Screenshot capture** | `[data-loupe-redact]` regions are painted over **before any pixels leave the browser**. |
66
66
  | 🧩 **Shadow-DOM isolation** | The widget's CSS never leaks into your page and vice-versa. |
@@ -93,8 +93,10 @@ init({
93
93
  A dockable control panel appears with a two-page sidebar — **Comments** (with the
94
94
  **Inspect**, **Note**, **Region**, and **Record** tools + the comment list) and
95
95
  **Connect Claude** (MCP setup steps). Use the header's dock controls to dock it left / right /
96
- bottom (which pushes your page over) or float it, toggle light/dark, or close it to a small
97
- `◎` launcher. Call `destroy()` to tear it down. `init()` is idempotent — safe to call more
96
+ bottom (which pushes your page over) or float it, toggle light/dark, or close it to the `◎`
97
+ FAB cluster — the primary button carries the comment count and expands four quick actions
98
+ (pin a comment, drop a note, hide the markers, open the Claude setup). Call `destroy()` to
99
+ tear it down. `init()` is idempotent — safe to call more
98
100
  than once. Pass `label` to change the brand name shown in the header.
99
101
 
100
102
  ### Offline mode (no backend)
@@ -100,25 +100,74 @@ var Loupe = (() => {
100
100
  .pin.free { background: var(--accent); border-radius: 50% 50% 2px 50%; }
101
101
  .pin.free.done { background: #10935a; }
102
102
  .pin.active { outline: 3px solid rgba(107,115,230,.45); }
103
+ /* The quick-action "Markers" toggle hides every pin without discarding it.
104
+ !important beats the inline display the pin positioner sets on each frame. */
105
+ .overlay.hide-pins .pin { display: none !important; }
103
106
 
104
- /* ------------------------------------------------------------------ launcher */
105
- /* The collapsed state: a small floating button that reopens the panel. */
106
- .launcher {
107
+ /* --------------------------------------------------------------- FAB cluster */
108
+ /* The collapsed state: a primary brand button (with the comment count) plus the
109
+ quick actions that expand out of it. Replaces the old single launcher. */
110
+ .fab-cluster {
107
111
  position: fixed; z-index: 2147483003; bottom: 20px; right: 20px;
108
- width: 46px; height: 46px; border-radius: 50%; padding: 0;
112
+ display: none; flex-direction: column; align-items: flex-end; gap: 10px;
113
+ }
114
+ .fab-cluster.show { display: flex; }
115
+
116
+ .fab-minis { display: none; flex-direction: column; align-items: flex-end; gap: 10px; }
117
+ .fab-cluster.expanded .fab-minis { display: flex; }
118
+ .fab-cluster.expanded .fab-minis .fab-mini { animation: loupe-fab-in 180ms cubic-bezier(.16, 1, .3, 1) both; }
119
+ .fab-cluster.expanded .fab-minis .fab-mini:nth-child(1) { animation-delay: 0ms; }
120
+ .fab-cluster.expanded .fab-minis .fab-mini:nth-child(2) { animation-delay: 30ms; }
121
+ .fab-cluster.expanded .fab-minis .fab-mini:nth-child(3) { animation-delay: 60ms; }
122
+ .fab-cluster.expanded .fab-minis .fab-mini:nth-child(4) { animation-delay: 90ms; }
123
+ @keyframes loupe-fab-in { from { opacity: 0; transform: translateY(8px) scale(.9); } to { opacity: 1; transform: none; } }
124
+
125
+ .fab-mini {
126
+ position: relative; width: 40px; height: 40px; border-radius: 50%; padding: 0;
127
+ border: 1px solid var(--line); background: var(--bg); color: var(--ink);
128
+ cursor: pointer; display: inline-flex; align-items: center; justify-content: center;
129
+ box-shadow: var(--shadow);
130
+ }
131
+ .fab-mini:hover { border-color: var(--accent); color: var(--accent); }
132
+ .fab-mini.on { background: var(--accent); border-color: var(--accent); color: #fff; }
133
+ .fab-mini.on:hover { color: #fff; }
134
+ .fab-mini svg { display: block; width: 16px; height: 16px; }
135
+ /* The action's label, revealed on hover so the collapsed cluster stays calm. */
136
+ .fab-mini .fab-tip {
137
+ position: absolute; right: calc(100% + 8px); top: 50%; transform: translateY(-50%);
138
+ background: var(--bg); color: var(--ink); border: 1px solid var(--line);
139
+ border-radius: 7px; padding: 4px 8px; font-size: 11px; font-weight: 600; white-space: nowrap;
140
+ box-shadow: var(--shadow); opacity: 0; pointer-events: none; transition: opacity 120ms ease;
141
+ }
142
+ .fab-mini:hover .fab-tip { opacity: 1; }
143
+
144
+ .launcher {
145
+ position: relative; width: 46px; height: 46px; border-radius: 50%; padding: 0;
109
146
  border: 1px solid var(--line); background: var(--bg-2); color: var(--ink);
110
- cursor: pointer; display: none; align-items: center; justify-content: center;
147
+ cursor: pointer; display: inline-flex; align-items: center; justify-content: center;
111
148
  box-shadow: var(--shadow);
112
149
  }
113
- .launcher.show { display: inline-flex; }
114
150
  .launcher:hover { border-color: var(--accent); }
115
151
  .launcher .logo { font-size: 24px; line-height: 1; color: var(--accent); }
152
+ /* Chevron pinned to the corner: up = actions are tucked away, down = they are out. */
153
+ .launcher .lchev {
154
+ position: absolute; right: -3px; bottom: -3px; width: 18px; height: 18px; border-radius: 50%;
155
+ background: var(--bg); border: 1px solid var(--line); color: var(--muted);
156
+ display: grid; place-items: center; transition: transform 160ms cubic-bezier(.16, 1, .3, 1);
157
+ }
158
+ .launcher .lchev svg { width: 11px; height: 11px; display: block; }
159
+ .fab-cluster.expanded .launcher .lchev { transform: rotate(180deg); }
116
160
  .launcher .lcount {
117
161
  position: absolute; top: -5px; right: -5px; background: var(--pin); color: #fff;
118
162
  font-size: 10px; font-weight: 700; line-height: 1; border-radius: 999px; padding: 3px 6px;
119
163
  border: 2px solid var(--bg);
120
164
  }
121
165
  .launcher .lcount:empty { display: none; }
166
+ @media (prefers-reduced-motion: reduce) {
167
+ .fab-cluster.expanded .fab-minis .fab-mini { animation: none; }
168
+ .launcher .lchev { transition: none; }
169
+ .fab-mini .fab-tip { transition: none; }
170
+ }
122
171
 
123
172
  /* --------------------------------------------------------------------- dock */
124
173
  /* The control panel. One container, four dock modes (left/right/bottom/float),
@@ -330,7 +379,7 @@ var Loupe = (() => {
330
379
  .resize { display: none !important; }
331
380
  .tools button { flex: 1; justify-content: center; } /* full-width tap targets */
332
381
  .dock.mode-bottom .list { grid-template-columns: 1fr; }
333
- .launcher { bottom: 16px; right: 16px; }
382
+ .fab-cluster { bottom: 16px; right: 16px; }
334
383
  /* While a tool is active, shrink the sheet to just header + tools so most of the
335
384
  page stays visible and tappable; the list returns when the tool closes. */
336
385
  .dock.inspecting { height: auto !important; }
@@ -2455,7 +2504,7 @@ var Loupe = (() => {
2455
2504
  };
2456
2505
 
2457
2506
  // src/app.ts
2458
- var SDK_VERSION = true ? "0.10.5" : "dev";
2507
+ var SDK_VERSION = true ? "0.10.6" : "dev";
2459
2508
  var DOCK_MODES = ["left", "right", "bottom", "float"];
2460
2509
  var RECORD_MAX_MS = 2e4;
2461
2510
  var MAX_FILES = 10;
@@ -2501,6 +2550,10 @@ var Loupe = (() => {
2501
2550
  }
2502
2551
  var LoupeApp = class {
2503
2552
  constructor(cfg) {
2553
+ /** Whether the quick-action minis are expanded out of the primary FAB. */
2554
+ this.fabExpanded = false;
2555
+ /** Whether pin markers are hidden on the page (a quick-action toggle, persisted). */
2556
+ this.markersHidden = false;
2504
2557
  this.comments = [];
2505
2558
  /** Free-text filter over the list (title / body / author). */
2506
2559
  this.search = "";
@@ -2802,9 +2855,9 @@ var Loupe = (() => {
2802
2855
  this.composer = el("div", "composer");
2803
2856
  this.shadow.appendChild(this.composer);
2804
2857
  this.dock = this.buildDock();
2805
- this.launcher = this.buildLauncher();
2858
+ this.fabCluster = this.buildFabCluster();
2806
2859
  this.recBar = this.buildRecBar();
2807
- this.shadow.append(this.dock, this.launcher, this.recBar);
2860
+ this.shadow.append(this.dock, this.fabCluster, this.recBar);
2808
2861
  }
2809
2862
  /**
2810
2863
  * The dockable control panel:
@@ -2968,14 +3021,82 @@ var Loupe = (() => {
2968
3021
  bar.onclick = () => this.stopRecording?.();
2969
3022
  return bar;
2970
3023
  }
2971
- buildLauncher() {
2972
- const b = el("button", "launcher");
2973
- b.title = `Open ${this.cfg.label ?? "Loupe"}`;
2974
- b.setAttribute("aria-label", "Open Loupe");
2975
- b.innerHTML = `<span class="logo">\u25CE</span><span class="lcount"></span>`;
2976
- this.launchCount = b.querySelector(".lcount");
2977
- b.onclick = () => this.openDock();
2978
- return b;
3024
+ /**
3025
+ * The collapsed-state FAB cluster. The primary brand button carries the comment
3026
+ * count and toggles the quick actions out and back; the actions are the four
3027
+ * ways into the product without opening the full panel first:
3028
+ * pin a comment, drop a note, hide/show the markers already on the page, and
3029
+ * jump straight to the Claude/MCP setup.
3030
+ */
3031
+ buildFabCluster() {
3032
+ const cluster = el("div", "fab-cluster");
3033
+ const minis = el("div", "fab-minis");
3034
+ const mini = (role, icon, label, title) => {
3035
+ const b = el("button", "fab-mini");
3036
+ b.dataset.fab = role;
3037
+ b.title = title;
3038
+ b.setAttribute("aria-label", title);
3039
+ b.innerHTML = `${icon}<span class="fab-tip">${label}</span>`;
3040
+ return b;
3041
+ };
3042
+ const comment = mini("comment", I_COMMENT, "Pin comment", "Pin feedback on any element");
3043
+ comment.onclick = () => {
3044
+ this.collapseFab();
3045
+ this.openDock();
3046
+ this.setMode("inspect");
3047
+ };
3048
+ const note = mini("note", I_NOTE, "Note", "Drop a note anywhere on the page");
3049
+ note.onclick = () => {
3050
+ this.collapseFab();
3051
+ this.openDock();
3052
+ this.setMode("free");
3053
+ };
3054
+ const markers = mini("markers", I_EYE, "Markers", "Show or hide the markers on this page");
3055
+ markers.onclick = () => this.toggleMarkers();
3056
+ const connect = mini("connect", I_PLUG, "Connect Claude", "Set up the Claude/MCP connection");
3057
+ connect.onclick = () => {
3058
+ this.collapseFab();
3059
+ this.openDock();
3060
+ this.setTab("connect");
3061
+ };
3062
+ minis.append(comment, note, markers, connect);
3063
+ const primary = el("button", "launcher");
3064
+ primary.title = `Open ${this.cfg.label ?? "Loupe"}`;
3065
+ primary.setAttribute("aria-label", `${this.cfg.label ?? "Loupe"} \u2014 quick actions`);
3066
+ primary.setAttribute("aria-expanded", "false");
3067
+ primary.innerHTML = `<span class="logo">\u25CE</span>${I_FAB_CHEVRON}<span class="lcount"></span>`;
3068
+ this.fabBadge = primary.querySelector(".lcount");
3069
+ primary.onclick = () => this.toggleFab();
3070
+ cluster.append(minis, primary);
3071
+ this.fabMinis = minis;
3072
+ return cluster;
3073
+ }
3074
+ /** Expand/collapse the quick actions out of the primary FAB. */
3075
+ toggleFab() {
3076
+ this.fabExpanded = !this.fabExpanded;
3077
+ this.applyFab();
3078
+ }
3079
+ collapseFab() {
3080
+ if (!this.fabExpanded) return;
3081
+ this.fabExpanded = false;
3082
+ this.applyFab();
3083
+ }
3084
+ /** Hide/show every pin on the page without losing them (persisted). */
3085
+ toggleMarkers() {
3086
+ this.markersHidden = !this.markersHidden;
3087
+ this.saveState();
3088
+ this.applyFab();
3089
+ }
3090
+ /** Reflect cluster expansion + marker visibility into the DOM. */
3091
+ applyFab() {
3092
+ this.fabCluster.classList.toggle("expanded", this.fabExpanded);
3093
+ const primary = this.fabCluster.querySelector(".launcher");
3094
+ primary?.setAttribute("aria-expanded", String(this.fabExpanded));
3095
+ primary?.setAttribute("aria-label", this.fabExpanded ? "Close quick actions" : `Open ${this.cfg.label ?? "Loupe"} quick actions`);
3096
+ const markers = this.fabCluster.querySelector('[data-fab="markers"]');
3097
+ markers?.classList.toggle("on", this.markersHidden);
3098
+ markers?.setAttribute("aria-pressed", String(this.markersHidden));
3099
+ this.overlay.classList.toggle("hide-pins", this.markersHidden);
2979
3100
  }
2980
3101
  /** A tool button with a uniform icon + label layout. `icon` may be an SVG string. */
2981
3102
  toolBtn(icon, label, role) {
@@ -3397,7 +3518,7 @@ var Loupe = (() => {
3397
3518
  updateCount() {
3398
3519
  const n = this.comments.length;
3399
3520
  this.countEl.textContent = String(n);
3400
- this.launchCount.textContent = n ? String(n) : "";
3521
+ this.fabBadge.textContent = n ? String(n) : "";
3401
3522
  }
3402
3523
  /**
3403
3524
  * The current viewport rect for a region comment. Prefers the element-relative
@@ -3437,6 +3558,7 @@ var Loupe = (() => {
3437
3558
  // ---- dock: open / close / mode / theme ------------------------------------
3438
3559
  openDock() {
3439
3560
  this.open = true;
3561
+ this.fabExpanded = false;
3440
3562
  this.saveState();
3441
3563
  this.applyDockLayout();
3442
3564
  this.renderList();
@@ -3482,6 +3604,7 @@ var Loupe = (() => {
3482
3604
  if (p?.theme === "light" || p?.theme === "dark") this.theme = p.theme;
3483
3605
  if (p?.tab === "comments" || p?.tab === "connect") this.tab = p.tab;
3484
3606
  if (p?.float && typeof p.float.w === "number") this.floatRect = { ...this.floatRect, ...p.float };
3607
+ if (typeof p?.markersHidden === "boolean") this.markersHidden = p.markersHidden;
3485
3608
  } catch {
3486
3609
  }
3487
3610
  }
@@ -3492,7 +3615,8 @@ var Loupe = (() => {
3492
3615
  open: this.open,
3493
3616
  theme: this.theme,
3494
3617
  tab: this.tab,
3495
- float: this.floatRect
3618
+ float: this.floatRect,
3619
+ markersHidden: this.markersHidden
3496
3620
  }));
3497
3621
  } catch {
3498
3622
  }
@@ -3525,10 +3649,11 @@ var Loupe = (() => {
3525
3649
  for (const p of ["left", "top", "right", "bottom", "width", "height"]) d.style[p] = "";
3526
3650
  }
3527
3651
  d.querySelectorAll(".dctl [data-dock]").forEach((b) => b.classList.toggle("on", b.dataset.dock === this.dockMode));
3528
- this.launcher.classList.toggle("show", !this.open);
3652
+ this.fabCluster.classList.toggle("show", !this.open);
3529
3653
  const leftSide = this.dockMode === "left";
3530
- this.launcher.style.left = leftSide ? "20px" : "auto";
3531
- this.launcher.style.right = leftSide ? "auto" : "20px";
3654
+ this.fabCluster.style.left = leftSide ? "20px" : "auto";
3655
+ this.fabCluster.style.right = leftSide ? "auto" : "20px";
3656
+ this.applyFab();
3532
3657
  this.pushPage();
3533
3658
  }
3534
3659
  /**
@@ -3726,6 +3851,19 @@ var Loupe = (() => {
3726
3851
  var I_CLOSE = svg(
3727
3852
  `<path d="M4.2 4.2l7.6 7.6M11.8 4.2l-7.6 7.6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>`
3728
3853
  );
3854
+ var I_COMMENT = svg(
3855
+ `<path d="M2 3.1h12v7.4H6.5l-3.3 2.6v-2.6H2z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/><path d="M8 5.1v3.4M6.3 6.8h3.4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>`
3856
+ );
3857
+ var I_NOTE = svg(
3858
+ `<path d="M2.6 2.6h10.8v7.2l-3.6 3.6H2.6z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/><path d="M13.4 9.8h-3.6v3.6" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/>`
3859
+ );
3860
+ var I_EYE = svg(
3861
+ `<path d="M1.4 8S3.9 4 8 4s6.6 4 6.6 4-2.5 4-6.6 4S1.4 8 1.4 8z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/><circle cx="8" cy="8" r="2" stroke="currentColor" stroke-width="1.3"/>`
3862
+ );
3863
+ var I_PLUG = svg(
3864
+ `<path d="M6 1.8v3.1M10 1.8v3.1" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><path d="M4.4 4.9h7.2v2.3a3.6 3.6 0 0 1-7.2 0z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/><path d="M8 10.8v3.4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>`
3865
+ );
3866
+ var I_FAB_CHEVRON = `<span class="lchev">${svg(`<path d="M4.4 9.6 8 6l3.6 3.6" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>`)}</span>`;
3729
3867
  var REGION_ICON = `<svg width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true"><rect x="1.5" y="2.5" width="12" height="10" rx="1.5" stroke="currentColor" stroke-width="1.4" stroke-dasharray="2.4 1.8"/></svg>`;
3730
3868
  var NOTE_ICON = `<svg width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true"><path d="M2 2.5h11v7.5H6l-3 2.5v-2.5H2z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>`;
3731
3869
  var RECORD_ICON = `<svg width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true"><rect x="1.5" y="2.5" width="12" height="10" rx="1.5" stroke="currentColor" stroke-width="1.4" stroke-dasharray="2.4 1.8"/><circle cx="7.5" cy="7.5" r="2.4" fill="currentColor"/></svg>`;