@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 +5 -3
- package/dist/index.global.js +161 -23
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +161 -23
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
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
|
|
97
|
-
|
|
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)
|
package/dist/index.global.js
CHANGED
|
@@ -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
|
-
/*
|
|
105
|
-
/* The collapsed state: a
|
|
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
|
-
|
|
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:
|
|
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
|
-
.
|
|
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.
|
|
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.
|
|
2858
|
+
this.fabCluster = this.buildFabCluster();
|
|
2806
2859
|
this.recBar = this.buildRecBar();
|
|
2807
|
-
this.shadow.append(this.dock, this.
|
|
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
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
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.
|
|
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.
|
|
3652
|
+
this.fabCluster.classList.toggle("show", !this.open);
|
|
3529
3653
|
const leftSide = this.dockMode === "left";
|
|
3530
|
-
this.
|
|
3531
|
-
this.
|
|
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>`;
|