@loupekit/sdk 0.5.2 → 0.6.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/README.md +6 -4
- package/dist/index.global.js +428 -322
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +433 -327
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.global.js
CHANGED
|
@@ -32,27 +32,38 @@ var Loupe = (() => {
|
|
|
32
32
|
:host { all: initial; }
|
|
33
33
|
* { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; }
|
|
34
34
|
|
|
35
|
-
/*
|
|
36
|
-
|
|
35
|
+
/* Theme tokens live on :host (inside the Shadow DOM :root matches nothing).
|
|
36
|
+
Dark is the default; the host element gets .theme-light to flip to light. */
|
|
37
37
|
:host {
|
|
38
|
+
--accent: #6b73e6;
|
|
39
|
+
--pin: #ff5842;
|
|
40
|
+
--bg: #14161d;
|
|
41
|
+
--bg-2: #1b1e27;
|
|
42
|
+
--bg-3: #262a36;
|
|
43
|
+
--ink: #e7e9f0;
|
|
44
|
+
--muted: #9aa0af;
|
|
45
|
+
--line: #2b2f3b;
|
|
46
|
+
--shadow: 0 12px 48px rgba(0,0,0,.42);
|
|
47
|
+
}
|
|
48
|
+
:host(.theme-light) {
|
|
38
49
|
--accent: #4a55d6;
|
|
39
50
|
--pin: #ff5842;
|
|
51
|
+
--bg: #ffffff;
|
|
52
|
+
--bg-2: #f6f7fb;
|
|
53
|
+
--bg-3: #eceef4;
|
|
40
54
|
--ink: #16181f;
|
|
41
|
-
--panel: #ffffff;
|
|
42
|
-
--panel-2: #f4f5f9;
|
|
43
|
-
--line: #e2e5ee;
|
|
44
55
|
--muted: #6b7180;
|
|
56
|
+
--line: #e2e5ee;
|
|
57
|
+
--shadow: 0 12px 40px rgba(0,0,0,.22);
|
|
45
58
|
}
|
|
46
59
|
|
|
47
|
-
.overlay {
|
|
48
|
-
position: fixed; inset: 0; z-index: 2147483000; pointer-events: none;
|
|
49
|
-
}
|
|
60
|
+
.overlay { position: fixed; inset: 0; z-index: 2147483000; pointer-events: none; }
|
|
50
61
|
|
|
51
62
|
/* inspector highlight */
|
|
52
63
|
.hl {
|
|
53
64
|
position: fixed; pointer-events: none; z-index: 2147483001;
|
|
54
65
|
border: 2px solid var(--accent);
|
|
55
|
-
background: rgba(
|
|
66
|
+
background: rgba(107, 115, 230, 0.12);
|
|
56
67
|
border-radius: 4px; display: none;
|
|
57
68
|
transition: all 60ms linear;
|
|
58
69
|
}
|
|
@@ -65,7 +76,7 @@ var Loupe = (() => {
|
|
|
65
76
|
/* region selection (during drag) + active-comment outline */
|
|
66
77
|
.selbox {
|
|
67
78
|
position: fixed; pointer-events: none; z-index: 2147483001; display: none;
|
|
68
|
-
border: 2px dashed var(--accent); background: rgba(
|
|
79
|
+
border: 2px dashed var(--accent); background: rgba(107, 115, 230, 0.14); border-radius: 4px;
|
|
69
80
|
}
|
|
70
81
|
.region-box {
|
|
71
82
|
position: fixed; pointer-events: none; z-index: 2147483001; display: none;
|
|
@@ -88,121 +99,158 @@ var Loupe = (() => {
|
|
|
88
99
|
.pin.done { background: #10935a; }
|
|
89
100
|
.pin.free { background: var(--accent); border-radius: 50% 50% 2px 50%; }
|
|
90
101
|
.pin.free.done { background: #10935a; }
|
|
91
|
-
.pin.active { outline: 3px solid rgba(
|
|
102
|
+
.pin.active { outline: 3px solid rgba(107,115,230,.45); }
|
|
92
103
|
|
|
93
|
-
/*
|
|
94
|
-
.
|
|
95
|
-
|
|
96
|
-
z-index: 2147483003;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
104
|
+
/* ------------------------------------------------------------------ launcher */
|
|
105
|
+
/* The collapsed state: a small floating button that reopens the panel. */
|
|
106
|
+
.launcher {
|
|
107
|
+
position: fixed; z-index: 2147483003; bottom: 20px; right: 20px;
|
|
108
|
+
width: 46px; height: 46px; border-radius: 50%; padding: 0;
|
|
109
|
+
border: 1px solid var(--line); background: var(--bg-2); color: var(--ink);
|
|
110
|
+
cursor: pointer; display: none; align-items: center; justify-content: center;
|
|
111
|
+
box-shadow: var(--shadow);
|
|
100
112
|
}
|
|
101
|
-
|
|
102
|
-
.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
113
|
+
.launcher.show { display: inline-flex; }
|
|
114
|
+
.launcher:hover { border-color: var(--accent); }
|
|
115
|
+
.launcher .logo { font-size: 24px; line-height: 1; color: var(--accent); }
|
|
116
|
+
.launcher .lcount {
|
|
117
|
+
position: absolute; top: -5px; right: -5px; background: var(--pin); color: #fff;
|
|
118
|
+
font-size: 10px; font-weight: 700; line-height: 1; border-radius: 999px; padding: 3px 6px;
|
|
119
|
+
border: 2px solid var(--bg);
|
|
106
120
|
}
|
|
107
|
-
.
|
|
108
|
-
|
|
121
|
+
.launcher .lcount:empty { display: none; }
|
|
122
|
+
|
|
123
|
+
/* --------------------------------------------------------------------- dock */
|
|
124
|
+
/* The control panel. One container, four dock modes (left/right/bottom/float),
|
|
125
|
+
overlaying the host page (never reflows it). */
|
|
126
|
+
.dock {
|
|
127
|
+
position: fixed; z-index: 2147483003; pointer-events: auto;
|
|
128
|
+
display: none; flex-direction: column;
|
|
129
|
+
background: var(--bg); color: var(--ink);
|
|
130
|
+
border: 1px solid var(--line); box-shadow: var(--shadow);
|
|
131
|
+
overflow: hidden; font-size: 13px;
|
|
132
|
+
}
|
|
133
|
+
.dock.open { display: flex; }
|
|
134
|
+
.dock.mode-right { top: 0; right: 0; bottom: 0; width: 360px; border-width: 0 0 0 1px; }
|
|
135
|
+
.dock.mode-left { top: 0; left: 0; bottom: 0; width: 360px; border-width: 0 1px 0 0; }
|
|
136
|
+
.dock.mode-bottom { left: 0; right: 0; bottom: 0; height: 320px; border-width: 1px 0 0 0; }
|
|
137
|
+
.dock.mode-float { border-radius: 14px; /* left/top/width/height set inline */ }
|
|
138
|
+
|
|
139
|
+
/* header: brand + dock controls */
|
|
140
|
+
.dhead {
|
|
141
|
+
display: flex; align-items: center; gap: 8px; flex: none;
|
|
142
|
+
padding: 8px 10px; border-bottom: 1px solid var(--line); background: var(--bg-2);
|
|
109
143
|
}
|
|
110
|
-
.
|
|
111
|
-
.
|
|
112
|
-
.
|
|
113
|
-
.
|
|
114
|
-
.
|
|
115
|
-
.
|
|
116
|
-
.
|
|
117
|
-
|
|
144
|
+
.dock.mode-float .dhead { cursor: grab; }
|
|
145
|
+
.dock.mode-float.dragging .dhead { cursor: grabbing; }
|
|
146
|
+
.dock.dragging { user-select: none; }
|
|
147
|
+
.brand { display: flex; align-items: center; gap: 8px; font-weight: 700; letter-spacing: -.01em; }
|
|
148
|
+
.brand .logo { font-size: 16px; line-height: 1; color: var(--accent); flex: none; }
|
|
149
|
+
.brand .title { font-size: 13px; }
|
|
150
|
+
.dctl { display: flex; align-items: center; gap: 2px; margin-left: auto; }
|
|
151
|
+
.dctl button {
|
|
152
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
153
|
+
width: 26px; height: 26px; padding: 0; border: 0; border-radius: 6px;
|
|
154
|
+
background: transparent; color: var(--muted); cursor: pointer;
|
|
118
155
|
}
|
|
119
|
-
.
|
|
120
|
-
.
|
|
121
|
-
|
|
122
|
-
.
|
|
123
|
-
.toolbar.collapsed > *:not(.brand) { display: none; }
|
|
156
|
+
.dctl button:hover { background: var(--bg-3); color: var(--ink); }
|
|
157
|
+
.dctl button.on { background: var(--bg-3); color: var(--accent); }
|
|
158
|
+
.dctl svg { display: block; }
|
|
159
|
+
.dctl .gap { width: 1px; height: 16px; background: var(--line); margin: 0 4px; flex: none; }
|
|
124
160
|
|
|
125
|
-
/*
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
.toolbar.orient-v.flow-rev { flex-direction: column-reverse; }
|
|
132
|
-
.toolbar.orient-v button, .toolbar.orient-v .brand { justify-content: flex-start; }
|
|
133
|
-
.toolbar.orient-v .sep { width: 22px; height: 1px; margin: 2px auto; }
|
|
134
|
-
.toolbar.orient-v .count { margin-left: auto; }
|
|
135
|
-
.toolbar .count {
|
|
136
|
-
background: var(--pin); color: #fff; font-size: 11px; font-weight: 700;
|
|
137
|
-
border-radius: 999px; padding: 1px 7px; margin-left: 2px;
|
|
161
|
+
/* tools row */
|
|
162
|
+
.tools { display: flex; gap: 6px; flex: none; padding: 10px; border-bottom: 1px solid var(--line); flex-wrap: wrap; }
|
|
163
|
+
.tools button {
|
|
164
|
+
display: flex; align-items: center; gap: 6px; padding: 7px 10px; line-height: 1;
|
|
165
|
+
border: 1px solid var(--line); border-radius: 8px; background: var(--bg-2); color: var(--ink);
|
|
166
|
+
font-size: 12px; font-weight: 600; cursor: pointer;
|
|
138
167
|
}
|
|
168
|
+
.tools button:hover { background: var(--bg-3); }
|
|
169
|
+
.tools button.on { background: var(--accent); border-color: var(--accent); color: #fff; }
|
|
170
|
+
.tools .ico { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 15px; height: 15px; }
|
|
171
|
+
.tools .ico svg { width: 15px; height: 15px; display: block; }
|
|
139
172
|
|
|
140
|
-
/*
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
173
|
+
/* list */
|
|
174
|
+
.listhead {
|
|
175
|
+
display: flex; align-items: center; gap: 8px; flex: none; padding: 12px 12px 6px;
|
|
176
|
+
font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); font-weight: 700;
|
|
177
|
+
}
|
|
178
|
+
.count { background: var(--pin); color: #fff; font-size: 11px; font-weight: 700; line-height: 1; border-radius: 999px; padding: 3px 7px; }
|
|
179
|
+
.list { flex: 1; overflow-y: auto; padding: 6px 10px 12px; display: flex; flex-direction: column; gap: 8px; }
|
|
180
|
+
.empty { color: var(--muted); font-size: 13px; padding: 24px 12px; text-align: center; line-height: 1.5; }
|
|
181
|
+
/* bottom dock lays the list out in flowing columns so it isn't a tall single strip */
|
|
182
|
+
.dock.mode-bottom .list { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); align-content: start; }
|
|
183
|
+
|
|
184
|
+
.item { border: 1px solid var(--line); border-radius: 10px; padding: 10px; cursor: pointer; background: var(--bg-2); }
|
|
185
|
+
.item:hover { border-color: var(--accent); }
|
|
186
|
+
.item .top { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
|
187
|
+
.item .num { background: var(--pin); color: #fff; width: 20px; height: 20px; border-radius: 50%; font-size: 11px; font-weight: 700; display: grid; place-items: center; flex: none; }
|
|
188
|
+
.item .num.detached { background: #9aa0af; }
|
|
189
|
+
.item .num.done { background: #10935a; }
|
|
190
|
+
.item .who { font-size: 12px; font-weight: 600; }
|
|
191
|
+
.item .device { font-size: 10px; color: var(--muted); background: var(--bg-3); border-radius: 999px; padding: 1px 7px; white-space: nowrap; }
|
|
192
|
+
.item .body { font-size: 13px; line-height: 1.4; }
|
|
193
|
+
.item .meta { font-size: 11px; color: var(--muted); margin-top: 6px; font-family: ui-monospace, Menlo, monospace; word-break: break-all; }
|
|
194
|
+
.item .badge { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; padding: 1px 6px; border-radius: 5px; margin-left: auto; white-space: nowrap; }
|
|
195
|
+
.badge.detached { background: var(--bg-3); color: var(--muted); }
|
|
196
|
+
.badge.done { background: #d8f0e4; color: #10935a; }
|
|
197
|
+
.item .actions { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; }
|
|
198
|
+
.item .actions button { font-size: 11px; border: 1px solid var(--line); background: var(--bg-3); border-radius: 6px; padding: 4px 8px; cursor: pointer; color: var(--ink); }
|
|
199
|
+
.item .actions button:hover { border-color: var(--accent); }
|
|
200
|
+
.item img.shot { width: 100%; border-radius: 6px; margin-top: 8px; border: 1px solid var(--line); }
|
|
201
|
+
|
|
202
|
+
/* float resize grip (bottom-right corner) */
|
|
203
|
+
.resize { display: none; position: absolute; right: 0; bottom: 0; width: 16px; height: 16px; cursor: nwse-resize; z-index: 1; }
|
|
204
|
+
.dock.mode-float .resize { display: block; }
|
|
205
|
+
.resize::after {
|
|
206
|
+
content: ""; position: absolute; right: 3px; bottom: 3px; width: 7px; height: 7px;
|
|
207
|
+
border-right: 2px solid var(--muted); border-bottom: 2px solid var(--muted); opacity: .7;
|
|
148
208
|
}
|
|
149
209
|
|
|
150
210
|
/* composer popover */
|
|
151
211
|
.composer {
|
|
152
212
|
position: fixed; z-index: 2147483004; pointer-events: auto; width: 300px;
|
|
153
|
-
background: var(--
|
|
154
|
-
border-radius: 12px; box-shadow:
|
|
213
|
+
background: var(--bg); color: var(--ink); border: 1px solid var(--line);
|
|
214
|
+
border-radius: 12px; box-shadow: var(--shadow); padding: 12px; display: none;
|
|
155
215
|
}
|
|
156
216
|
.composer .target {
|
|
157
217
|
font-family: ui-monospace, Menlo, monospace; font-size: 11px; color: var(--accent);
|
|
158
|
-
background: var(--
|
|
218
|
+
background: var(--bg-2); border-radius: 6px; padding: 5px 8px; margin-bottom: 8px;
|
|
159
219
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
160
220
|
}
|
|
161
221
|
.composer textarea {
|
|
162
222
|
width: 100%; min-height: 68px; resize: vertical; border: 1px solid var(--line);
|
|
163
|
-
border-radius: 8px; padding: 8px; font-size: 13px; color: var(--ink); outline: none;
|
|
223
|
+
border-radius: 8px; padding: 8px; font-size: 13px; color: var(--ink); background: var(--bg-2); outline: none;
|
|
164
224
|
}
|
|
165
225
|
.composer textarea:focus { border-color: var(--accent); }
|
|
166
226
|
.composer .row { display: flex; align-items: center; justify-content: space-between; margin-top: 8px; gap: 8px; }
|
|
167
227
|
.composer label.chk { font-size: 12px; color: var(--muted); display: flex; align-items: center; gap: 6px; cursor: pointer; }
|
|
168
228
|
.composer .btns { display: flex; gap: 6px; }
|
|
169
|
-
.composer button {
|
|
170
|
-
border: 0; border-radius: 8px; font-size: 13px; font-weight: 600; padding: 7px 12px; cursor: pointer;
|
|
171
|
-
}
|
|
229
|
+
.composer button { border: 0; border-radius: 8px; font-size: 13px; font-weight: 600; padding: 7px 12px; cursor: pointer; }
|
|
172
230
|
.composer .primary { background: var(--accent); color: #fff; }
|
|
173
231
|
.composer .primary:disabled { opacity: .5; cursor: default; }
|
|
174
|
-
.composer .ghost { background: var(--
|
|
232
|
+
.composer .ghost { background: var(--bg-3); color: var(--ink); }
|
|
175
233
|
|
|
176
|
-
/*
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
.
|
|
187
|
-
.
|
|
188
|
-
.
|
|
189
|
-
|
|
234
|
+
/* Mobile: left/right/float docking is a desktop affordance. On small screens the
|
|
235
|
+
panel collapses to a bottom sheet that OVERLAYS the page (pushing a side dock
|
|
236
|
+
here would squeeze the page to a useless sliver), regardless of the chosen dock
|
|
237
|
+
mode. !important overrides the inline geometry JS applies for float mode. */
|
|
238
|
+
@media (max-width: 640px) {
|
|
239
|
+
.dock.open {
|
|
240
|
+
top: auto !important; left: 0 !important; right: 0 !important; bottom: 0 !important;
|
|
241
|
+
width: auto !important; height: 76vh !important;
|
|
242
|
+
border-width: 1px 0 0 0 !important; border-radius: 16px 16px 0 0 !important;
|
|
243
|
+
}
|
|
244
|
+
.dctl [data-dock], .dctl .gap { display: none; } /* dock positions don't apply on mobile */
|
|
245
|
+
.resize { display: none !important; }
|
|
246
|
+
.tools button { flex: 1; justify-content: center; } /* full-width tap targets */
|
|
247
|
+
.dock.mode-bottom .list { grid-template-columns: 1fr; }
|
|
248
|
+
.launcher { bottom: 16px; right: 16px; }
|
|
249
|
+
/* While a tool is active, shrink the sheet to just header + tools so most of the
|
|
250
|
+
page stays visible and tappable; the list returns when the tool closes. */
|
|
251
|
+
.dock.inspecting { height: auto !important; }
|
|
252
|
+
.dock.inspecting .listhead, .dock.inspecting .list { display: none; }
|
|
190
253
|
}
|
|
191
|
-
.item:hover { border-color: var(--accent); }
|
|
192
|
-
.item .top { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
|
193
|
-
.item .num { background: var(--pin); color:#fff; width:20px; height:20px; border-radius:50%; font-size:11px; font-weight:700; display:grid; place-items:center; }
|
|
194
|
-
.item .num.detached { background:#9aa0af; }
|
|
195
|
-
.item .num.done { background:#10935a; }
|
|
196
|
-
.item .who { font-size: 12px; font-weight: 600; }
|
|
197
|
-
.item .device { font-size: 10px; color: var(--muted); background: var(--panel-2); border-radius: 999px; padding: 1px 7px; white-space: nowrap; }
|
|
198
|
-
.item .body { font-size: 13px; line-height: 1.4; }
|
|
199
|
-
.item .meta { font-size: 11px; color: var(--muted); margin-top: 6px; font-family: ui-monospace, Menlo, monospace; }
|
|
200
|
-
.item .badge { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; padding: 1px 6px; border-radius: 5px; margin-left: auto; }
|
|
201
|
-
.badge.detached { background: #eceef2; color: #6b7180; }
|
|
202
|
-
.badge.done { background: #d8f0e4; color: #10935a; }
|
|
203
|
-
.item .actions { display: flex; gap: 6px; margin-top: 8px; }
|
|
204
|
-
.item .actions button { font-size: 11px; border: 1px solid var(--line); background: var(--panel-2); border-radius: 6px; padding: 4px 8px; cursor: pointer; color: var(--ink); }
|
|
205
|
-
.item img.shot { width: 100%; border-radius: 6px; margin-top: 8px; border: 1px solid var(--line); }
|
|
206
254
|
`
|
|
207
255
|
);
|
|
208
256
|
|
|
@@ -554,14 +602,14 @@ var Loupe = (() => {
|
|
|
554
602
|
}
|
|
555
603
|
var XMLNS = "http://www.w3.org/2000/svg";
|
|
556
604
|
function createSvg(width, height, ownerDocument) {
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
return
|
|
562
|
-
}
|
|
563
|
-
function svgToDataUrl(
|
|
564
|
-
let xhtml = new XMLSerializer().serializeToString(
|
|
605
|
+
const svg2 = getDocument(ownerDocument).createElementNS(XMLNS, "svg");
|
|
606
|
+
svg2.setAttributeNS(null, "width", width.toString());
|
|
607
|
+
svg2.setAttributeNS(null, "height", height.toString());
|
|
608
|
+
svg2.setAttributeNS(null, "viewBox", `0 0 ${width} ${height}`);
|
|
609
|
+
return svg2;
|
|
610
|
+
}
|
|
611
|
+
function svgToDataUrl(svg2, removeControlCharacter) {
|
|
612
|
+
let xhtml = new XMLSerializer().serializeToString(svg2);
|
|
565
613
|
if (removeControlCharacter) {
|
|
566
614
|
xhtml = xhtml.replace(/[\u0000-\u0008\v\f\u000E-\u001F\uD800-\uDFFF\uFFFE\uFFFF]/gu, "");
|
|
567
615
|
}
|
|
@@ -1812,8 +1860,8 @@ var Loupe = (() => {
|
|
|
1812
1860
|
return out;
|
|
1813
1861
|
}
|
|
1814
1862
|
var SVG_EXTERNAL_RESOURCE_REGEX = /\bx?link:?href\s*=\s*["'](?!data:)[^"']+["']/i;
|
|
1815
|
-
function svgHasExternalResources(
|
|
1816
|
-
return SVG_EXTERNAL_RESOURCE_REGEX.test(
|
|
1863
|
+
function svgHasExternalResources(svg2) {
|
|
1864
|
+
return SVG_EXTERNAL_RESOURCE_REGEX.test(svg2.innerHTML);
|
|
1817
1865
|
}
|
|
1818
1866
|
async function domToForeignObjectSvg(node, options) {
|
|
1819
1867
|
const context = await orCreateContext(node, options);
|
|
@@ -1873,35 +1921,35 @@ var Loupe = (() => {
|
|
|
1873
1921
|
await Promise.all([...Array.from({ length: 4 })].map(runTask));
|
|
1874
1922
|
log.timeEnd("embed node");
|
|
1875
1923
|
await onEmbedNode?.(clone);
|
|
1876
|
-
const
|
|
1877
|
-
svgDefsElement &&
|
|
1878
|
-
svgStyleElement &&
|
|
1924
|
+
const svg2 = createForeignObjectSvg(clone, context);
|
|
1925
|
+
svgDefsElement && svg2.insertBefore(svgDefsElement, svg2.children[0]);
|
|
1926
|
+
svgStyleElement && svg2.insertBefore(svgStyleElement, svg2.children[0]);
|
|
1879
1927
|
autoDestruct && destroyContext(context);
|
|
1880
|
-
await onCreateForeignObjectSvg?.(
|
|
1881
|
-
return
|
|
1928
|
+
await onCreateForeignObjectSvg?.(svg2);
|
|
1929
|
+
return svg2;
|
|
1882
1930
|
}
|
|
1883
1931
|
function createForeignObjectSvg(clone, context) {
|
|
1884
1932
|
const { width, height } = context;
|
|
1885
|
-
const
|
|
1886
|
-
const foreignObject =
|
|
1933
|
+
const svg2 = createSvg(width, height, clone.ownerDocument);
|
|
1934
|
+
const foreignObject = svg2.ownerDocument.createElementNS(svg2.namespaceURI, "foreignObject");
|
|
1887
1935
|
foreignObject.setAttributeNS(null, "x", "0%");
|
|
1888
1936
|
foreignObject.setAttributeNS(null, "y", "0%");
|
|
1889
1937
|
foreignObject.setAttributeNS(null, "width", "100%");
|
|
1890
1938
|
foreignObject.setAttributeNS(null, "height", "100%");
|
|
1891
1939
|
foreignObject.append(clone);
|
|
1892
|
-
|
|
1893
|
-
return
|
|
1940
|
+
svg2.appendChild(foreignObject);
|
|
1941
|
+
return svg2;
|
|
1894
1942
|
}
|
|
1895
1943
|
async function domToCanvas(node, options) {
|
|
1896
1944
|
const context = await orCreateContext(node, options);
|
|
1897
|
-
const
|
|
1898
|
-
const dataUrl = svgToDataUrl(
|
|
1945
|
+
const svg2 = await domToForeignObjectSvg(context);
|
|
1946
|
+
const dataUrl = svgToDataUrl(svg2, context.isEnable("removeControlCharacter"));
|
|
1899
1947
|
if (!context.autoDestruct) {
|
|
1900
1948
|
context.svgStyleElement = createStyleElement(context.ownerDocument);
|
|
1901
1949
|
context.svgDefsElement = context.ownerDocument?.createElementNS(XMLNS, "defs");
|
|
1902
1950
|
context.svgStyles.clear();
|
|
1903
1951
|
}
|
|
1904
|
-
const image = createImage(dataUrl,
|
|
1952
|
+
const image = createImage(dataUrl, svg2.ownerDocument);
|
|
1905
1953
|
return await imageToCanvas(image, context);
|
|
1906
1954
|
}
|
|
1907
1955
|
async function domToDataUrl(node, options) {
|
|
@@ -2190,6 +2238,7 @@ var Loupe = (() => {
|
|
|
2190
2238
|
};
|
|
2191
2239
|
|
|
2192
2240
|
// src/app.ts
|
|
2241
|
+
var DOCK_MODES = ["left", "right", "bottom", "float"];
|
|
2193
2242
|
var uid2 = () => crypto.randomUUID ? crypto.randomUUID() : "c_" + Math.abs(hash(String(performance.now()))).toString(36);
|
|
2194
2243
|
function hash(s) {
|
|
2195
2244
|
let h = 0;
|
|
@@ -2204,19 +2253,20 @@ var Loupe = (() => {
|
|
|
2204
2253
|
/** comment.id → pin element. */
|
|
2205
2254
|
this.pins = /* @__PURE__ */ new Map();
|
|
2206
2255
|
this.mode = "off";
|
|
2207
|
-
this.collapsed = false;
|
|
2208
2256
|
this.lastUrl = "";
|
|
2209
2257
|
this.targetOffset = { x: 0.5, y: 0.5 };
|
|
2210
2258
|
this.pending = null;
|
|
2211
2259
|
this.dragStart = null;
|
|
2212
2260
|
/** region comment whose outline is currently highlighted (tracks scroll). */
|
|
2213
2261
|
this.activeRegionId = null;
|
|
2214
|
-
|
|
2215
|
-
this.
|
|
2216
|
-
|
|
2217
|
-
this.
|
|
2218
|
-
/**
|
|
2219
|
-
this.
|
|
2262
|
+
// ---- control-panel state (persisted in localStorage `loupe:dock`) ----------
|
|
2263
|
+
this.dockMode = "right";
|
|
2264
|
+
this.open = true;
|
|
2265
|
+
this.theme = "dark";
|
|
2266
|
+
/** Float-mode window geometry; (x<=0 && y<=0) → placed on first layout. */
|
|
2267
|
+
this.floatRect = { x: 0, y: 0, w: 380, h: 540 };
|
|
2268
|
+
this.floatDrag = null;
|
|
2269
|
+
this.floatResize = null;
|
|
2220
2270
|
this.raf = 0;
|
|
2221
2271
|
// ---- inspector ------------------------------------------------------------
|
|
2222
2272
|
this.onMove = (e) => {
|
|
@@ -2260,6 +2310,8 @@ var Loupe = (() => {
|
|
|
2260
2310
|
// ---- region ("free-size screenshot") selection ----------------------------
|
|
2261
2311
|
this.onRegionDown = (e) => {
|
|
2262
2312
|
if (this.mode !== "region" || e.button !== 0) return;
|
|
2313
|
+
const t = e.target;
|
|
2314
|
+
if (t && (t.id === "loupe-root" || t.closest?.("#loupe-root"))) return;
|
|
2263
2315
|
e.preventDefault();
|
|
2264
2316
|
e.stopPropagation();
|
|
2265
2317
|
this.dragStart = { x: e.clientX, y: e.clientY };
|
|
@@ -2365,57 +2417,52 @@ var Loupe = (() => {
|
|
|
2365
2417
|
}
|
|
2366
2418
|
}
|
|
2367
2419
|
};
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
this.
|
|
2371
|
-
if (
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
this.
|
|
2375
|
-
this.
|
|
2376
|
-
|
|
2377
|
-
brand.setPointerCapture(e.pointerId);
|
|
2378
|
-
} catch {
|
|
2379
|
-
}
|
|
2380
|
-
brand.addEventListener("pointermove", this.onBarPointerMove);
|
|
2381
|
-
brand.addEventListener("pointerup", this.onBarPointerUp);
|
|
2382
|
-
brand.addEventListener("pointercancel", this.onBarPointerUp);
|
|
2420
|
+
this.onWinResize = () => this.applyDockLayout();
|
|
2421
|
+
// ---- float-mode drag + resize ---------------------------------------------
|
|
2422
|
+
this.onHeadPointerDown = (e) => {
|
|
2423
|
+
if (this.dockMode !== "float" || e.button !== 0 || this.isMobile()) return;
|
|
2424
|
+
if (e.target?.closest(".dctl")) return;
|
|
2425
|
+
this.floatDrag = { px: e.clientX, py: e.clientY, ox: this.floatRect.x, oy: this.floatRect.y };
|
|
2426
|
+
this.dock.classList.add("dragging");
|
|
2427
|
+
window.addEventListener("pointermove", this.onHeadPointerMove);
|
|
2428
|
+
window.addEventListener("pointerup", this.onHeadPointerUp);
|
|
2383
2429
|
};
|
|
2384
|
-
this.
|
|
2385
|
-
if (!this.
|
|
2386
|
-
const dx = e.clientX - this.barDrag.px;
|
|
2387
|
-
const dy = e.clientY - this.barDrag.py;
|
|
2388
|
-
if (!this.justDragged && Math.hypot(dx, dy) < 5) return;
|
|
2389
|
-
this.justDragged = true;
|
|
2430
|
+
this.onHeadPointerMove = (e) => {
|
|
2431
|
+
if (!this.floatDrag) return;
|
|
2390
2432
|
e.preventDefault();
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
const iw = brand.offsetWidth || 44, ih = brand.offsetHeight || 44;
|
|
2395
|
-
const ix = clampPx(this.barDrag.ix + dx, 6, vw - iw - 6);
|
|
2396
|
-
const iy = clampPx(this.barDrag.iy + dy, 6, vh - ih - 6);
|
|
2397
|
-
bar.classList.add("floating", "dragging");
|
|
2398
|
-
bar.classList.remove("orient-v", "flow-rev");
|
|
2399
|
-
bar.classList.add("orient-h");
|
|
2400
|
-
Object.assign(bar.style, { transform: "none", left: ix + "px", top: iy + "px", right: "auto", bottom: "auto" });
|
|
2401
|
-
this.barPos = { fx: ix / vw, fy: iy / vh };
|
|
2433
|
+
this.floatRect.x = this.floatDrag.ox + (e.clientX - this.floatDrag.px);
|
|
2434
|
+
this.floatRect.y = this.floatDrag.oy + (e.clientY - this.floatDrag.py);
|
|
2435
|
+
this.applyDockLayout();
|
|
2402
2436
|
};
|
|
2403
|
-
this.
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2437
|
+
this.onHeadPointerUp = () => {
|
|
2438
|
+
if (!this.floatDrag) return;
|
|
2439
|
+
this.floatDrag = null;
|
|
2440
|
+
this.dock.classList.remove("dragging");
|
|
2441
|
+
window.removeEventListener("pointermove", this.onHeadPointerMove);
|
|
2442
|
+
window.removeEventListener("pointerup", this.onHeadPointerUp);
|
|
2443
|
+
this.saveState();
|
|
2444
|
+
};
|
|
2445
|
+
this.onResizeDown = (e) => {
|
|
2446
|
+
if (this.dockMode !== "float") return;
|
|
2447
|
+
e.preventDefault();
|
|
2448
|
+
e.stopPropagation();
|
|
2449
|
+
this.floatResize = { px: e.clientX, py: e.clientY, ow: this.floatRect.w, oh: this.floatRect.h };
|
|
2450
|
+
window.addEventListener("pointermove", this.onResizeMove);
|
|
2451
|
+
window.addEventListener("pointerup", this.onResizeUp);
|
|
2452
|
+
};
|
|
2453
|
+
this.onResizeMove = (e) => {
|
|
2454
|
+
if (!this.floatResize) return;
|
|
2455
|
+
e.preventDefault();
|
|
2456
|
+
this.floatRect.w = this.floatResize.ow + (e.clientX - this.floatResize.px);
|
|
2457
|
+
this.floatRect.h = this.floatResize.oh + (e.clientY - this.floatResize.py);
|
|
2458
|
+
this.applyDockLayout();
|
|
2459
|
+
};
|
|
2460
|
+
this.onResizeUp = () => {
|
|
2461
|
+
if (!this.floatResize) return;
|
|
2462
|
+
this.floatResize = null;
|
|
2463
|
+
window.removeEventListener("pointermove", this.onResizeMove);
|
|
2464
|
+
window.removeEventListener("pointerup", this.onResizeUp);
|
|
2465
|
+
this.saveState();
|
|
2419
2466
|
};
|
|
2420
2467
|
this.cfg = cfg;
|
|
2421
2468
|
this.store = cfg.apiBase ? new HttpAdapter(cfg.apiBase, cfg.user, cfg.userHmac, cfg.headers, cfg.credentials) : new LocalStorageAdapter();
|
|
@@ -2424,13 +2471,14 @@ var Loupe = (() => {
|
|
|
2424
2471
|
return location.pathname + location.search;
|
|
2425
2472
|
}
|
|
2426
2473
|
async start() {
|
|
2427
|
-
this.
|
|
2474
|
+
this.loadState();
|
|
2428
2475
|
this.buildDom();
|
|
2429
|
-
this.
|
|
2476
|
+
if (this.cfg.autoOpen) this.open = true;
|
|
2477
|
+
this.applyDockLayout();
|
|
2430
2478
|
this.lastUrl = this.url;
|
|
2431
2479
|
this.comments = await this.store.list(this.cfg.projectKey, this.url);
|
|
2432
2480
|
this.renderPins();
|
|
2433
|
-
this.
|
|
2481
|
+
this.renderList();
|
|
2434
2482
|
this.observe();
|
|
2435
2483
|
this.watchNavigation();
|
|
2436
2484
|
if (this.cfg.autoOpen) this.setMode("inspect");
|
|
@@ -2460,7 +2508,7 @@ var Loupe = (() => {
|
|
|
2460
2508
|
try {
|
|
2461
2509
|
this.comments = await this.store.list(this.cfg.projectKey, this.url);
|
|
2462
2510
|
this.renderPins();
|
|
2463
|
-
this.
|
|
2511
|
+
this.renderList();
|
|
2464
2512
|
} catch {
|
|
2465
2513
|
}
|
|
2466
2514
|
}
|
|
@@ -2482,45 +2530,80 @@ var Loupe = (() => {
|
|
|
2482
2530
|
this.shadow.appendChild(this.overlay);
|
|
2483
2531
|
this.composer = el("div", "composer");
|
|
2484
2532
|
this.shadow.appendChild(this.composer);
|
|
2485
|
-
this.
|
|
2486
|
-
this.
|
|
2487
|
-
this.
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
const
|
|
2492
|
-
const
|
|
2493
|
-
brand
|
|
2494
|
-
brand.
|
|
2495
|
-
brand.
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2533
|
+
this.dock = this.buildDock();
|
|
2534
|
+
this.launcher = this.buildLauncher();
|
|
2535
|
+
this.shadow.append(this.dock, this.launcher);
|
|
2536
|
+
}
|
|
2537
|
+
/** The dockable control panel: header (brand + dock controls) → tools → list. */
|
|
2538
|
+
buildDock() {
|
|
2539
|
+
const dock = el("div", "dock");
|
|
2540
|
+
const head = el("div", "dhead");
|
|
2541
|
+
const brand = el("div", "brand");
|
|
2542
|
+
brand.innerHTML = `<span class="logo">\u25CE</span><span class="title"></span>`;
|
|
2543
|
+
brand.querySelector(".title").textContent = this.cfg.label ?? "Loupe";
|
|
2544
|
+
head.addEventListener("pointerdown", this.onHeadPointerDown);
|
|
2545
|
+
const ctl = el("div", "dctl");
|
|
2546
|
+
const dockBtn = (mode, icon, title) => {
|
|
2547
|
+
const b = el("button");
|
|
2548
|
+
b.dataset.dock = mode;
|
|
2549
|
+
b.title = title;
|
|
2550
|
+
b.setAttribute("aria-label", title);
|
|
2551
|
+
b.innerHTML = icon;
|
|
2552
|
+
b.onclick = () => this.setDock(mode);
|
|
2553
|
+
return b;
|
|
2502
2554
|
};
|
|
2503
|
-
|
|
2504
|
-
|
|
2555
|
+
ctl.append(
|
|
2556
|
+
dockBtn("left", I_DOCK_LEFT, "Dock to left"),
|
|
2557
|
+
dockBtn("bottom", I_DOCK_BOTTOM, "Dock to bottom"),
|
|
2558
|
+
dockBtn("right", I_DOCK_RIGHT, "Dock to right"),
|
|
2559
|
+
dockBtn("float", I_FLOAT, "Float"),
|
|
2560
|
+
el("span", "gap")
|
|
2561
|
+
);
|
|
2562
|
+
this.themeBtn = el("button");
|
|
2563
|
+
this.themeBtn.dataset.role = "theme";
|
|
2564
|
+
this.themeBtn.onclick = () => this.toggleTheme();
|
|
2565
|
+
const closeBtn = el("button");
|
|
2566
|
+
closeBtn.dataset.role = "close";
|
|
2567
|
+
closeBtn.title = "Close";
|
|
2568
|
+
closeBtn.setAttribute("aria-label", "Close");
|
|
2569
|
+
closeBtn.innerHTML = I_CLOSE;
|
|
2570
|
+
closeBtn.onclick = () => this.closeDock();
|
|
2571
|
+
ctl.append(this.themeBtn, closeBtn);
|
|
2572
|
+
head.append(brand, ctl);
|
|
2573
|
+
const tools = el("div", "tools");
|
|
2574
|
+
const inspectBtn = this.toolBtn("\u271B", "Inspect", "inspect");
|
|
2575
|
+
inspectBtn.title = "Inspect an element and comment on it";
|
|
2505
2576
|
inspectBtn.onclick = () => this.setMode(this.mode === "inspect" ? "off" : "inspect");
|
|
2506
2577
|
const freeBtn = this.toolBtn(NOTE_ICON, "Note", "free");
|
|
2507
2578
|
freeBtn.title = "Drop a note anywhere on the page \u2014 no element, no screenshot";
|
|
2508
2579
|
freeBtn.onclick = () => this.setMode(this.mode === "free" ? "off" : "free");
|
|
2509
|
-
const regionBtn = this.toolBtn(REGION_ICON, "Region
|
|
2580
|
+
const regionBtn = this.toolBtn(REGION_ICON, "Region", "region");
|
|
2510
2581
|
regionBtn.title = "Drag a free-size box, screenshot it, and comment";
|
|
2511
2582
|
regionBtn.onclick = () => this.setMode(this.mode === "region" ? "off" : "region");
|
|
2512
|
-
|
|
2583
|
+
tools.append(inspectBtn, freeBtn, regionBtn);
|
|
2584
|
+
const listHead = el("div", "listhead");
|
|
2585
|
+
listHead.append(document.createTextNode("Comments"));
|
|
2513
2586
|
this.countEl = el("span", "count", "0");
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2587
|
+
listHead.appendChild(this.countEl);
|
|
2588
|
+
this.listEl = el("div", "list");
|
|
2589
|
+
const resize = el("div", "resize");
|
|
2590
|
+
resize.addEventListener("pointerdown", this.onResizeDown);
|
|
2591
|
+
dock.append(head, tools, listHead, this.listEl, resize);
|
|
2592
|
+
return dock;
|
|
2593
|
+
}
|
|
2594
|
+
buildLauncher() {
|
|
2595
|
+
const b = el("button", "launcher");
|
|
2596
|
+
b.title = `Open ${this.cfg.label ?? "Loupe"}`;
|
|
2597
|
+
b.setAttribute("aria-label", "Open Loupe");
|
|
2598
|
+
b.innerHTML = `<span class="logo">\u25CE</span><span class="lcount"></span>`;
|
|
2599
|
+
this.launchCount = b.querySelector(".lcount");
|
|
2600
|
+
b.onclick = () => this.openDock();
|
|
2601
|
+
return b;
|
|
2518
2602
|
}
|
|
2519
|
-
/** A
|
|
2603
|
+
/** A tool button with a uniform icon + label layout. `icon` may be an SVG string. */
|
|
2520
2604
|
toolBtn(icon, label, role) {
|
|
2521
2605
|
const b = el("button");
|
|
2522
2606
|
b.dataset.role = role;
|
|
2523
|
-
b.title = label;
|
|
2524
2607
|
b.setAttribute("aria-label", label);
|
|
2525
2608
|
b.innerHTML = `<span class="ico">${icon}</span><span class="label">${label}</span>`;
|
|
2526
2609
|
return b;
|
|
@@ -2576,14 +2659,19 @@ var Loupe = (() => {
|
|
|
2576
2659
|
return elAt;
|
|
2577
2660
|
}
|
|
2578
2661
|
setMode(mode) {
|
|
2662
|
+
if (mode !== "off" && !this.open) {
|
|
2663
|
+
this.open = true;
|
|
2664
|
+
this.applyDockLayout();
|
|
2665
|
+
}
|
|
2579
2666
|
this.mode = mode;
|
|
2580
2667
|
this.hl.style.display = "none";
|
|
2581
2668
|
this.selbox.style.display = "none";
|
|
2582
2669
|
this.cancelDrag();
|
|
2583
2670
|
document.body.style.cursor = mode === "off" ? "" : "crosshair";
|
|
2584
|
-
this.
|
|
2585
|
-
this.
|
|
2586
|
-
this.
|
|
2671
|
+
this.dock.querySelector('[data-role="inspect"]')?.classList.toggle("on", mode === "inspect");
|
|
2672
|
+
this.dock.querySelector('[data-role="free"]')?.classList.toggle("on", mode === "free");
|
|
2673
|
+
this.dock.querySelector('[data-role="region"]')?.classList.toggle("on", mode === "region");
|
|
2674
|
+
this.dock.classList.toggle("inspecting", mode !== "off");
|
|
2587
2675
|
document.removeEventListener("mousemove", this.onMove, true);
|
|
2588
2676
|
document.removeEventListener("click", this.onClick, true);
|
|
2589
2677
|
document.removeEventListener("click", this.onFreeClick, true);
|
|
@@ -2705,7 +2793,7 @@ var Loupe = (() => {
|
|
|
2705
2793
|
this.resolved.set(comment.id, anchoredEl);
|
|
2706
2794
|
this.closeComposer();
|
|
2707
2795
|
this.renderPins();
|
|
2708
|
-
this.
|
|
2796
|
+
this.renderList();
|
|
2709
2797
|
this.flash(comment.id);
|
|
2710
2798
|
}
|
|
2711
2799
|
// ---- pins + re-anchoring --------------------------------------------------
|
|
@@ -2721,7 +2809,7 @@ var Loupe = (() => {
|
|
|
2721
2809
|
if (!pin) {
|
|
2722
2810
|
pin = el("button", "pin");
|
|
2723
2811
|
pin.onclick = () => {
|
|
2724
|
-
this.
|
|
2812
|
+
this.openDock();
|
|
2725
2813
|
this.flash(c.id);
|
|
2726
2814
|
};
|
|
2727
2815
|
this.overlay.appendChild(pin);
|
|
@@ -2731,9 +2819,14 @@ var Loupe = (() => {
|
|
|
2731
2819
|
pin.classList.toggle("done", c.status === "done");
|
|
2732
2820
|
pin.classList.toggle("free", c.kind === "free");
|
|
2733
2821
|
});
|
|
2734
|
-
this.
|
|
2822
|
+
this.updateCount();
|
|
2735
2823
|
this.position();
|
|
2736
2824
|
}
|
|
2825
|
+
updateCount() {
|
|
2826
|
+
const n = this.comments.length;
|
|
2827
|
+
this.countEl.textContent = String(n);
|
|
2828
|
+
this.launchCount.textContent = n ? String(n) : "";
|
|
2829
|
+
}
|
|
2737
2830
|
/**
|
|
2738
2831
|
* The current viewport rect for a region comment. Prefers the element-relative
|
|
2739
2832
|
* fractions (so it tracks reflow across viewports); falls back to the stored
|
|
@@ -2757,136 +2850,129 @@ var Loupe = (() => {
|
|
|
2757
2850
|
};
|
|
2758
2851
|
window.addEventListener("scroll", reposition, true);
|
|
2759
2852
|
window.addEventListener("resize", reposition);
|
|
2760
|
-
window.addEventListener("resize", this.
|
|
2853
|
+
window.addEventListener("resize", this.onWinResize);
|
|
2761
2854
|
let debounce = 0;
|
|
2762
2855
|
this.mo = new MutationObserver(() => {
|
|
2763
2856
|
clearTimeout(debounce);
|
|
2764
2857
|
debounce = window.setTimeout(() => {
|
|
2765
2858
|
this.position();
|
|
2766
|
-
this.
|
|
2859
|
+
this.renderList();
|
|
2767
2860
|
}, 120);
|
|
2768
2861
|
});
|
|
2769
2862
|
this.mo.observe(document.body, { childList: true, subtree: true, attributes: true, characterData: true });
|
|
2770
2863
|
this.tick = window.setInterval(this.position, 800);
|
|
2771
2864
|
}
|
|
2772
|
-
// ----
|
|
2773
|
-
|
|
2774
|
-
this.
|
|
2775
|
-
this.
|
|
2865
|
+
// ---- dock: open / close / mode / theme ------------------------------------
|
|
2866
|
+
openDock() {
|
|
2867
|
+
this.open = true;
|
|
2868
|
+
this.saveState();
|
|
2869
|
+
this.applyDockLayout();
|
|
2870
|
+
this.renderList();
|
|
2776
2871
|
}
|
|
2777
|
-
|
|
2778
|
-
this.
|
|
2779
|
-
this.
|
|
2872
|
+
closeDock() {
|
|
2873
|
+
this.open = false;
|
|
2874
|
+
this.setMode("off");
|
|
2875
|
+
this.closeComposer();
|
|
2876
|
+
this.saveState();
|
|
2877
|
+
this.applyDockLayout();
|
|
2780
2878
|
}
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
this.
|
|
2784
|
-
this.
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2879
|
+
setDock(mode) {
|
|
2880
|
+
this.dockMode = mode;
|
|
2881
|
+
this.open = true;
|
|
2882
|
+
this.saveState();
|
|
2883
|
+
this.applyDockLayout();
|
|
2884
|
+
}
|
|
2885
|
+
toggleTheme() {
|
|
2886
|
+
this.theme = this.theme === "dark" ? "light" : "dark";
|
|
2887
|
+
this.saveState();
|
|
2888
|
+
this.applyDockLayout();
|
|
2889
|
+
}
|
|
2890
|
+
/** Narrow viewports render the panel as a bottom sheet, not a side/float dock. */
|
|
2891
|
+
isMobile() {
|
|
2892
|
+
return window.innerWidth <= 640;
|
|
2795
2893
|
}
|
|
2796
|
-
|
|
2894
|
+
loadState() {
|
|
2797
2895
|
try {
|
|
2798
|
-
const s = localStorage.getItem("loupe:
|
|
2799
|
-
if (s)
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2896
|
+
const s = localStorage.getItem("loupe:dock");
|
|
2897
|
+
if (!s) return;
|
|
2898
|
+
const p = JSON.parse(s);
|
|
2899
|
+
if (DOCK_MODES.includes(p?.mode)) this.dockMode = p.mode;
|
|
2900
|
+
if (typeof p?.open === "boolean") this.open = p.open;
|
|
2901
|
+
if (p?.theme === "light" || p?.theme === "dark") this.theme = p.theme;
|
|
2902
|
+
if (p?.float && typeof p.float.w === "number") this.floatRect = { ...this.floatRect, ...p.float };
|
|
2803
2903
|
} catch {
|
|
2804
2904
|
}
|
|
2805
2905
|
}
|
|
2806
|
-
|
|
2906
|
+
saveState() {
|
|
2807
2907
|
try {
|
|
2808
|
-
|
|
2908
|
+
localStorage.setItem("loupe:dock", JSON.stringify({
|
|
2909
|
+
mode: this.dockMode,
|
|
2910
|
+
open: this.open,
|
|
2911
|
+
theme: this.theme,
|
|
2912
|
+
float: this.floatRect
|
|
2913
|
+
}));
|
|
2809
2914
|
} catch {
|
|
2810
2915
|
}
|
|
2811
2916
|
}
|
|
2812
|
-
|
|
2813
|
-
|
|
2917
|
+
/** Reflect all control-panel state (theme, dock mode, geometry, launcher) into the DOM. */
|
|
2918
|
+
applyDockLayout() {
|
|
2919
|
+
this.root.classList.toggle("theme-light", this.theme === "light");
|
|
2920
|
+
this.themeBtn.title = this.theme === "dark" ? "Switch to light theme" : "Switch to dark theme";
|
|
2921
|
+
this.themeBtn.innerHTML = this.theme === "dark" ? I_SUN : I_MOON;
|
|
2922
|
+
const d = this.dock;
|
|
2923
|
+
d.classList.toggle("open", this.open);
|
|
2924
|
+
for (const m of DOCK_MODES) d.classList.toggle("mode-" + m, this.dockMode === m);
|
|
2925
|
+
if (this.dockMode === "float") {
|
|
2926
|
+
const vw = window.innerWidth, vh = window.innerHeight;
|
|
2927
|
+
let { x, y, w, h } = this.floatRect;
|
|
2928
|
+
w = clampPx(w, 280, Math.min(760, vw - 24));
|
|
2929
|
+
h = clampPx(h, 220, vh - 24);
|
|
2930
|
+
if (x <= 0 && y <= 0) {
|
|
2931
|
+
x = Math.max(12, vw - w - 24);
|
|
2932
|
+
y = 64;
|
|
2933
|
+
}
|
|
2934
|
+
x = clampPx(x, 8, Math.max(8, vw - w - 8));
|
|
2935
|
+
y = clampPx(y, 8, Math.max(8, vh - h - 8));
|
|
2936
|
+
this.floatRect = { x, y, w, h };
|
|
2937
|
+
Object.assign(d.style, { left: x + "px", top: y + "px", width: w + "px", height: h + "px", right: "auto", bottom: "auto" });
|
|
2938
|
+
} else {
|
|
2939
|
+
for (const p of ["left", "top", "right", "bottom", "width", "height"]) d.style[p] = "";
|
|
2940
|
+
}
|
|
2941
|
+
d.querySelectorAll(".dctl [data-dock]").forEach((b) => b.classList.toggle("on", b.dataset.dock === this.dockMode));
|
|
2942
|
+
this.launcher.classList.toggle("show", !this.open);
|
|
2943
|
+
const leftSide = this.dockMode === "left";
|
|
2944
|
+
this.launcher.style.left = leftSide ? "20px" : "auto";
|
|
2945
|
+
this.launcher.style.right = leftSide ? "auto" : "20px";
|
|
2946
|
+
this.pushPage();
|
|
2814
2947
|
}
|
|
2815
2948
|
/**
|
|
2816
|
-
*
|
|
2817
|
-
*
|
|
2818
|
-
*
|
|
2819
|
-
*
|
|
2949
|
+
* Push the host page over so the docked panel never covers content (like real
|
|
2950
|
+
* DevTools). We shrink the <html> box with a margin on the docked edge — the
|
|
2951
|
+
* panel is `position: fixed` (relative to the viewport), so it sits in the
|
|
2952
|
+
* gutter the margin frees up. Float mode and the closed state reserve nothing.
|
|
2953
|
+
* Only inline styles are touched, so clearing them restores the host exactly.
|
|
2820
2954
|
*/
|
|
2821
|
-
|
|
2822
|
-
const
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
const cx = ix + iw / 2, cy = iy + ih / 2;
|
|
2834
|
-
const horizontalSide = cx < vw / 2 ? "left" : "right";
|
|
2835
|
-
const verticalSide = cy < vh / 2 ? "top" : "bottom";
|
|
2836
|
-
const vertical = Math.min(cx, vw - cx) < Math.min(cy, vh - cy);
|
|
2837
|
-
bar.classList.add("floating");
|
|
2838
|
-
bar.classList.remove("dragging");
|
|
2839
|
-
bar.classList.toggle("orient-v", vertical);
|
|
2840
|
-
bar.classList.toggle("orient-h", !vertical);
|
|
2841
|
-
bar.classList.toggle("flow-rev", vertical ? verticalSide === "bottom" : horizontalSide === "right");
|
|
2842
|
-
bar.style.transform = "none";
|
|
2843
|
-
if (horizontalSide === "left") {
|
|
2844
|
-
bar.style.left = ix + "px";
|
|
2845
|
-
bar.style.right = "auto";
|
|
2846
|
-
} else {
|
|
2847
|
-
bar.style.right = vw - (ix + iw) + "px";
|
|
2848
|
-
bar.style.left = "auto";
|
|
2849
|
-
}
|
|
2850
|
-
if (verticalSide === "top") {
|
|
2851
|
-
bar.style.top = iy + "px";
|
|
2852
|
-
bar.style.bottom = "auto";
|
|
2853
|
-
} else {
|
|
2854
|
-
bar.style.bottom = vh - (iy + ih) + "px";
|
|
2855
|
-
bar.style.top = "auto";
|
|
2856
|
-
}
|
|
2857
|
-
const r = bar.getBoundingClientRect();
|
|
2858
|
-
if (r.right > vw - 6 && bar.style.left !== "auto") {
|
|
2859
|
-
bar.style.left = "auto";
|
|
2860
|
-
bar.style.right = "6px";
|
|
2861
|
-
}
|
|
2862
|
-
if (r.left < 6 && bar.style.right !== "auto") {
|
|
2863
|
-
bar.style.right = "auto";
|
|
2864
|
-
bar.style.left = "6px";
|
|
2865
|
-
}
|
|
2866
|
-
if (r.bottom > vh - 6 && bar.style.top !== "auto") {
|
|
2867
|
-
bar.style.top = "auto";
|
|
2868
|
-
bar.style.bottom = "6px";
|
|
2869
|
-
}
|
|
2870
|
-
if (r.top < 6 && bar.style.bottom !== "auto") {
|
|
2871
|
-
bar.style.bottom = "auto";
|
|
2872
|
-
bar.style.top = "6px";
|
|
2873
|
-
}
|
|
2874
|
-
}
|
|
2875
|
-
renderPanel() {
|
|
2876
|
-
const p = this.panel;
|
|
2877
|
-
p.innerHTML = "";
|
|
2878
|
-
const head = el("div", "phead");
|
|
2879
|
-
head.append(document.createTextNode(`Comments \xB7 ${this.comments.length}`));
|
|
2880
|
-
const x = el("button", "x", "\xD7");
|
|
2881
|
-
x.onclick = () => p.classList.remove("open");
|
|
2882
|
-
head.appendChild(x);
|
|
2883
|
-
p.appendChild(head);
|
|
2884
|
-
const list = el("div", "list");
|
|
2955
|
+
pushPage() {
|
|
2956
|
+
const de = document.documentElement;
|
|
2957
|
+
de.style.marginLeft = de.style.marginRight = de.style.marginBottom = "";
|
|
2958
|
+
if (!this.open || this.dockMode === "float" || this.isMobile()) return;
|
|
2959
|
+
const r = this.dock.getBoundingClientRect();
|
|
2960
|
+
if (this.dockMode === "left") de.style.marginLeft = r.width + "px";
|
|
2961
|
+
else if (this.dockMode === "right") de.style.marginRight = r.width + "px";
|
|
2962
|
+
else if (this.dockMode === "bottom") de.style.marginBottom = r.height + "px";
|
|
2963
|
+
}
|
|
2964
|
+
// ---- comment list ---------------------------------------------------------
|
|
2965
|
+
renderList() {
|
|
2966
|
+
this.listEl.innerHTML = "";
|
|
2885
2967
|
if (!this.comments.length) {
|
|
2886
|
-
|
|
2968
|
+
this.listEl.appendChild(el(
|
|
2969
|
+
"div",
|
|
2970
|
+
"empty",
|
|
2971
|
+
"No comments yet. Use Inspect to pick an element, or Note to drop a comment anywhere on the page."
|
|
2972
|
+
));
|
|
2887
2973
|
}
|
|
2888
|
-
this.comments.forEach((c, i) =>
|
|
2889
|
-
|
|
2974
|
+
this.comments.forEach((c, i) => this.listEl.appendChild(this.itemView(c, i)));
|
|
2975
|
+
this.updateCount();
|
|
2890
2976
|
}
|
|
2891
2977
|
itemView(c, i) {
|
|
2892
2978
|
const detached = this.pins.get(c.id)?.classList.contains("detached") && c.status !== "done";
|
|
@@ -2919,7 +3005,7 @@ var Loupe = (() => {
|
|
|
2919
3005
|
c.status = status;
|
|
2920
3006
|
await this.store.update(c.id, { status });
|
|
2921
3007
|
this.renderPins();
|
|
2922
|
-
this.
|
|
3008
|
+
this.renderList();
|
|
2923
3009
|
};
|
|
2924
3010
|
const claudeBtn = el("button", "", "Copy for Claude");
|
|
2925
3011
|
claudeBtn.onclick = async (e) => {
|
|
@@ -2934,7 +3020,7 @@ var Loupe = (() => {
|
|
|
2934
3020
|
this.comments = this.comments.filter((x) => x.id !== c.id);
|
|
2935
3021
|
this.resolved.delete(c.id);
|
|
2936
3022
|
this.renderPins();
|
|
2937
|
-
this.
|
|
3023
|
+
this.renderList();
|
|
2938
3024
|
};
|
|
2939
3025
|
actions.append(doneBtn, claudeBtn, del);
|
|
2940
3026
|
item.appendChild(actions);
|
|
@@ -3015,8 +3101,14 @@ var Loupe = (() => {
|
|
|
3015
3101
|
this.mo?.disconnect();
|
|
3016
3102
|
if (this.tick) clearInterval(this.tick);
|
|
3017
3103
|
window.clearTimeout(this.regionTimer);
|
|
3018
|
-
window.removeEventListener("resize", this.
|
|
3104
|
+
window.removeEventListener("resize", this.onWinResize);
|
|
3105
|
+
window.removeEventListener("pointermove", this.onHeadPointerMove);
|
|
3106
|
+
window.removeEventListener("pointerup", this.onHeadPointerUp);
|
|
3107
|
+
window.removeEventListener("pointermove", this.onResizeMove);
|
|
3108
|
+
window.removeEventListener("pointerup", this.onResizeUp);
|
|
3019
3109
|
document.removeEventListener("keydown", this.onKey, true);
|
|
3110
|
+
const de = document.documentElement;
|
|
3111
|
+
de.style.marginLeft = de.style.marginRight = de.style.marginBottom = "";
|
|
3020
3112
|
this.root?.remove();
|
|
3021
3113
|
}
|
|
3022
3114
|
};
|
|
@@ -3026,17 +3118,31 @@ var Loupe = (() => {
|
|
|
3026
3118
|
if (text) n.textContent = text;
|
|
3027
3119
|
return n;
|
|
3028
3120
|
}
|
|
3029
|
-
function sep() {
|
|
3030
|
-
return el("span", "sep");
|
|
3031
|
-
}
|
|
3032
3121
|
function clamp(n) {
|
|
3033
3122
|
return Math.max(0, Math.min(1, n));
|
|
3034
3123
|
}
|
|
3035
3124
|
function clampPx(n, min, max) {
|
|
3036
3125
|
return Math.max(min, Math.min(max, n));
|
|
3037
3126
|
}
|
|
3038
|
-
var
|
|
3039
|
-
var
|
|
3127
|
+
var svg = (inner) => `<svg viewBox="0 0 16 16" width="15" height="15" fill="none" aria-hidden="true">${inner}</svg>`;
|
|
3128
|
+
var DOCK_FRAME = `<rect x="1.5" y="2.5" width="13" height="11" rx="1.6" stroke="currentColor" stroke-width="1.3"/>`;
|
|
3129
|
+
var I_DOCK_LEFT = svg(`${DOCK_FRAME}<rect x="2.2" y="3.2" width="4" height="9.6" rx="1" fill="currentColor"/>`);
|
|
3130
|
+
var I_DOCK_RIGHT = svg(`${DOCK_FRAME}<rect x="9.8" y="3.2" width="4" height="9.6" rx="1" fill="currentColor"/>`);
|
|
3131
|
+
var I_DOCK_BOTTOM = svg(`${DOCK_FRAME}<rect x="2.2" y="9" width="11.6" height="3.8" rx="1" fill="currentColor"/>`);
|
|
3132
|
+
var I_FLOAT = svg(
|
|
3133
|
+
`<rect x="2.5" y="3.5" width="11" height="9" rx="1.6" stroke="currentColor" stroke-width="1.3"/><path d="M2.5 6.1h11" stroke="currentColor" stroke-width="1.3"/>`
|
|
3134
|
+
);
|
|
3135
|
+
var I_SUN = svg(
|
|
3136
|
+
`<circle cx="8" cy="8" r="3" stroke="currentColor" stroke-width="1.3"/><g stroke="currentColor" stroke-width="1.2" stroke-linecap="round"><path d="M8 1.4v1.7"/><path d="M8 12.9v1.7"/><path d="M1.4 8h1.7"/><path d="M12.9 8h1.7"/><path d="M3.3 3.3l1.2 1.2"/><path d="M11.5 11.5l1.2 1.2"/><path d="M12.7 3.3l-1.2 1.2"/><path d="M4.5 11.5l-1.2 1.2"/></g>`
|
|
3137
|
+
);
|
|
3138
|
+
var I_MOON = svg(
|
|
3139
|
+
`<path d="M13 9.4A5.3 5.3 0 1 1 6.6 3 4.3 4.3 0 0 0 13 9.4z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>`
|
|
3140
|
+
);
|
|
3141
|
+
var I_CLOSE = svg(
|
|
3142
|
+
`<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"/>`
|
|
3143
|
+
);
|
|
3144
|
+
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>`;
|
|
3145
|
+
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>`;
|
|
3040
3146
|
function pageAnchor(point) {
|
|
3041
3147
|
return {
|
|
3042
3148
|
tag: "page",
|