@loupekit/sdk 0.5.2 → 0.6.0-next.22
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.js
CHANGED
|
@@ -5,27 +5,38 @@ var STYLES = (
|
|
|
5
5
|
:host { all: initial; }
|
|
6
6
|
* { box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; }
|
|
7
7
|
|
|
8
|
-
/*
|
|
9
|
-
|
|
8
|
+
/* Theme tokens live on :host (inside the Shadow DOM :root matches nothing).
|
|
9
|
+
Dark is the default; the host element gets .theme-light to flip to light. */
|
|
10
10
|
:host {
|
|
11
|
+
--accent: #6b73e6;
|
|
12
|
+
--pin: #ff5842;
|
|
13
|
+
--bg: #14161d;
|
|
14
|
+
--bg-2: #1b1e27;
|
|
15
|
+
--bg-3: #262a36;
|
|
16
|
+
--ink: #e7e9f0;
|
|
17
|
+
--muted: #9aa0af;
|
|
18
|
+
--line: #2b2f3b;
|
|
19
|
+
--shadow: 0 12px 48px rgba(0,0,0,.42);
|
|
20
|
+
}
|
|
21
|
+
:host(.theme-light) {
|
|
11
22
|
--accent: #4a55d6;
|
|
12
23
|
--pin: #ff5842;
|
|
24
|
+
--bg: #ffffff;
|
|
25
|
+
--bg-2: #f6f7fb;
|
|
26
|
+
--bg-3: #eceef4;
|
|
13
27
|
--ink: #16181f;
|
|
14
|
-
--panel: #ffffff;
|
|
15
|
-
--panel-2: #f4f5f9;
|
|
16
|
-
--line: #e2e5ee;
|
|
17
28
|
--muted: #6b7180;
|
|
29
|
+
--line: #e2e5ee;
|
|
30
|
+
--shadow: 0 12px 40px rgba(0,0,0,.22);
|
|
18
31
|
}
|
|
19
32
|
|
|
20
|
-
.overlay {
|
|
21
|
-
position: fixed; inset: 0; z-index: 2147483000; pointer-events: none;
|
|
22
|
-
}
|
|
33
|
+
.overlay { position: fixed; inset: 0; z-index: 2147483000; pointer-events: none; }
|
|
23
34
|
|
|
24
35
|
/* inspector highlight */
|
|
25
36
|
.hl {
|
|
26
37
|
position: fixed; pointer-events: none; z-index: 2147483001;
|
|
27
38
|
border: 2px solid var(--accent);
|
|
28
|
-
background: rgba(
|
|
39
|
+
background: rgba(107, 115, 230, 0.12);
|
|
29
40
|
border-radius: 4px; display: none;
|
|
30
41
|
transition: all 60ms linear;
|
|
31
42
|
}
|
|
@@ -38,7 +49,7 @@ var STYLES = (
|
|
|
38
49
|
/* region selection (during drag) + active-comment outline */
|
|
39
50
|
.selbox {
|
|
40
51
|
position: fixed; pointer-events: none; z-index: 2147483001; display: none;
|
|
41
|
-
border: 2px dashed var(--accent); background: rgba(
|
|
52
|
+
border: 2px dashed var(--accent); background: rgba(107, 115, 230, 0.14); border-radius: 4px;
|
|
42
53
|
}
|
|
43
54
|
.region-box {
|
|
44
55
|
position: fixed; pointer-events: none; z-index: 2147483001; display: none;
|
|
@@ -61,121 +72,158 @@ var STYLES = (
|
|
|
61
72
|
.pin.done { background: #10935a; }
|
|
62
73
|
.pin.free { background: var(--accent); border-radius: 50% 50% 2px 50%; }
|
|
63
74
|
.pin.free.done { background: #10935a; }
|
|
64
|
-
.pin.active { outline: 3px solid rgba(
|
|
75
|
+
.pin.active { outline: 3px solid rgba(107,115,230,.45); }
|
|
65
76
|
|
|
66
|
-
/*
|
|
67
|
-
.
|
|
68
|
-
|
|
69
|
-
z-index: 2147483003;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
.
|
|
84
|
-
.toolbar .ico { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; }
|
|
85
|
-
.toolbar .ico svg { width: 16px; height: 16px; display: block; }
|
|
86
|
-
.toolbar .label { white-space: nowrap; }
|
|
87
|
-
.toolbar button.on { background: var(--accent); color: #fff; }
|
|
88
|
-
.toolbar .sep { width: 1px; height: 20px; background: #333846; margin: 0 2px; }
|
|
89
|
-
.toolbar .brand {
|
|
90
|
-
font-weight: 700; letter-spacing: -.01em; cursor: grab; user-select: none; touch-action: none;
|
|
91
|
-
}
|
|
92
|
-
.toolbar.dragging { transition: none; box-shadow: 0 12px 40px rgba(0,0,0,.5); }
|
|
93
|
-
.toolbar.dragging .brand { cursor: grabbing; }
|
|
94
|
-
/* collapsed \u2192 show only the brand/logo; click it again to expand */
|
|
95
|
-
.toolbar.collapsed { gap: 0; }
|
|
96
|
-
.toolbar.collapsed > *:not(.brand) { display: none; }
|
|
77
|
+
/* ------------------------------------------------------------------ launcher */
|
|
78
|
+
/* The collapsed state: a small floating button that reopens the panel. */
|
|
79
|
+
.launcher {
|
|
80
|
+
position: fixed; z-index: 2147483003; bottom: 20px; right: 20px;
|
|
81
|
+
width: 46px; height: 46px; border-radius: 50%; padding: 0;
|
|
82
|
+
border: 1px solid var(--line); background: var(--bg-2); color: var(--ink);
|
|
83
|
+
cursor: pointer; display: none; align-items: center; justify-content: center;
|
|
84
|
+
box-shadow: var(--shadow);
|
|
85
|
+
}
|
|
86
|
+
.launcher.show { display: inline-flex; }
|
|
87
|
+
.launcher:hover { border-color: var(--accent); }
|
|
88
|
+
.launcher .logo { font-size: 24px; line-height: 1; color: var(--accent); }
|
|
89
|
+
.launcher .lcount {
|
|
90
|
+
position: absolute; top: -5px; right: -5px; background: var(--pin); color: #fff;
|
|
91
|
+
font-size: 10px; font-weight: 700; line-height: 1; border-radius: 999px; padding: 3px 6px;
|
|
92
|
+
border: 2px solid var(--bg);
|
|
93
|
+
}
|
|
94
|
+
.launcher .lcount:empty { display: none; }
|
|
97
95
|
|
|
98
|
-
/*
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
96
|
+
/* --------------------------------------------------------------------- dock */
|
|
97
|
+
/* The control panel. One container, four dock modes (left/right/bottom/float),
|
|
98
|
+
overlaying the host page (never reflows it). */
|
|
99
|
+
.dock {
|
|
100
|
+
position: fixed; z-index: 2147483003; pointer-events: auto;
|
|
101
|
+
display: none; flex-direction: column;
|
|
102
|
+
background: var(--bg); color: var(--ink);
|
|
103
|
+
border: 1px solid var(--line); box-shadow: var(--shadow);
|
|
104
|
+
overflow: hidden; font-size: 13px;
|
|
105
|
+
}
|
|
106
|
+
.dock.open { display: flex; }
|
|
107
|
+
.dock.mode-right { top: 0; right: 0; bottom: 0; width: 360px; border-width: 0 0 0 1px; }
|
|
108
|
+
.dock.mode-left { top: 0; left: 0; bottom: 0; width: 360px; border-width: 0 1px 0 0; }
|
|
109
|
+
.dock.mode-bottom { left: 0; right: 0; bottom: 0; height: 320px; border-width: 1px 0 0 0; }
|
|
110
|
+
.dock.mode-float { border-radius: 14px; /* left/top/width/height set inline */ }
|
|
112
111
|
|
|
113
|
-
/*
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
112
|
+
/* header: brand + dock controls */
|
|
113
|
+
.dhead {
|
|
114
|
+
display: flex; align-items: center; gap: 8px; flex: none;
|
|
115
|
+
padding: 8px 10px; border-bottom: 1px solid var(--line); background: var(--bg-2);
|
|
116
|
+
}
|
|
117
|
+
.dock.mode-float .dhead { cursor: grab; }
|
|
118
|
+
.dock.mode-float.dragging .dhead { cursor: grabbing; }
|
|
119
|
+
.dock.dragging { user-select: none; }
|
|
120
|
+
.brand { display: flex; align-items: center; gap: 8px; font-weight: 700; letter-spacing: -.01em; }
|
|
121
|
+
.brand .logo { font-size: 16px; line-height: 1; color: var(--accent); flex: none; }
|
|
122
|
+
.brand .title { font-size: 13px; }
|
|
123
|
+
.dctl { display: flex; align-items: center; gap: 2px; margin-left: auto; }
|
|
124
|
+
.dctl button {
|
|
125
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
126
|
+
width: 26px; height: 26px; padding: 0; border: 0; border-radius: 6px;
|
|
127
|
+
background: transparent; color: var(--muted); cursor: pointer;
|
|
128
|
+
}
|
|
129
|
+
.dctl button:hover { background: var(--bg-3); color: var(--ink); }
|
|
130
|
+
.dctl button.on { background: var(--bg-3); color: var(--accent); }
|
|
131
|
+
.dctl svg { display: block; }
|
|
132
|
+
.dctl .gap { width: 1px; height: 16px; background: var(--line); margin: 0 4px; flex: none; }
|
|
133
|
+
|
|
134
|
+
/* tools row */
|
|
135
|
+
.tools { display: flex; gap: 6px; flex: none; padding: 10px; border-bottom: 1px solid var(--line); flex-wrap: wrap; }
|
|
136
|
+
.tools button {
|
|
137
|
+
display: flex; align-items: center; gap: 6px; padding: 7px 10px; line-height: 1;
|
|
138
|
+
border: 1px solid var(--line); border-radius: 8px; background: var(--bg-2); color: var(--ink);
|
|
139
|
+
font-size: 12px; font-weight: 600; cursor: pointer;
|
|
140
|
+
}
|
|
141
|
+
.tools button:hover { background: var(--bg-3); }
|
|
142
|
+
.tools button.on { background: var(--accent); border-color: var(--accent); color: #fff; }
|
|
143
|
+
.tools .ico { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 15px; height: 15px; }
|
|
144
|
+
.tools .ico svg { width: 15px; height: 15px; display: block; }
|
|
145
|
+
|
|
146
|
+
/* list */
|
|
147
|
+
.listhead {
|
|
148
|
+
display: flex; align-items: center; gap: 8px; flex: none; padding: 12px 12px 6px;
|
|
149
|
+
font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); font-weight: 700;
|
|
150
|
+
}
|
|
151
|
+
.count { background: var(--pin); color: #fff; font-size: 11px; font-weight: 700; line-height: 1; border-radius: 999px; padding: 3px 7px; }
|
|
152
|
+
.list { flex: 1; overflow-y: auto; padding: 6px 10px 12px; display: flex; flex-direction: column; gap: 8px; }
|
|
153
|
+
.empty { color: var(--muted); font-size: 13px; padding: 24px 12px; text-align: center; line-height: 1.5; }
|
|
154
|
+
/* bottom dock lays the list out in flowing columns so it isn't a tall single strip */
|
|
155
|
+
.dock.mode-bottom .list { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); align-content: start; }
|
|
156
|
+
|
|
157
|
+
.item { border: 1px solid var(--line); border-radius: 10px; padding: 10px; cursor: pointer; background: var(--bg-2); }
|
|
158
|
+
.item:hover { border-color: var(--accent); }
|
|
159
|
+
.item .top { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
|
160
|
+
.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; }
|
|
161
|
+
.item .num.detached { background: #9aa0af; }
|
|
162
|
+
.item .num.done { background: #10935a; }
|
|
163
|
+
.item .who { font-size: 12px; font-weight: 600; }
|
|
164
|
+
.item .device { font-size: 10px; color: var(--muted); background: var(--bg-3); border-radius: 999px; padding: 1px 7px; white-space: nowrap; }
|
|
165
|
+
.item .body { font-size: 13px; line-height: 1.4; }
|
|
166
|
+
.item .meta { font-size: 11px; color: var(--muted); margin-top: 6px; font-family: ui-monospace, Menlo, monospace; word-break: break-all; }
|
|
167
|
+
.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; }
|
|
168
|
+
.badge.detached { background: var(--bg-3); color: var(--muted); }
|
|
169
|
+
.badge.done { background: #d8f0e4; color: #10935a; }
|
|
170
|
+
.item .actions { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; }
|
|
171
|
+
.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); }
|
|
172
|
+
.item .actions button:hover { border-color: var(--accent); }
|
|
173
|
+
.item img.shot { width: 100%; border-radius: 6px; margin-top: 8px; border: 1px solid var(--line); }
|
|
174
|
+
|
|
175
|
+
/* float resize grip (bottom-right corner) */
|
|
176
|
+
.resize { display: none; position: absolute; right: 0; bottom: 0; width: 16px; height: 16px; cursor: nwse-resize; z-index: 1; }
|
|
177
|
+
.dock.mode-float .resize { display: block; }
|
|
178
|
+
.resize::after {
|
|
179
|
+
content: ""; position: absolute; right: 3px; bottom: 3px; width: 7px; height: 7px;
|
|
180
|
+
border-right: 2px solid var(--muted); border-bottom: 2px solid var(--muted); opacity: .7;
|
|
121
181
|
}
|
|
122
182
|
|
|
123
183
|
/* composer popover */
|
|
124
184
|
.composer {
|
|
125
185
|
position: fixed; z-index: 2147483004; pointer-events: auto; width: 300px;
|
|
126
|
-
background: var(--
|
|
127
|
-
border-radius: 12px; box-shadow:
|
|
186
|
+
background: var(--bg); color: var(--ink); border: 1px solid var(--line);
|
|
187
|
+
border-radius: 12px; box-shadow: var(--shadow); padding: 12px; display: none;
|
|
128
188
|
}
|
|
129
189
|
.composer .target {
|
|
130
190
|
font-family: ui-monospace, Menlo, monospace; font-size: 11px; color: var(--accent);
|
|
131
|
-
background: var(--
|
|
191
|
+
background: var(--bg-2); border-radius: 6px; padding: 5px 8px; margin-bottom: 8px;
|
|
132
192
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
133
193
|
}
|
|
134
194
|
.composer textarea {
|
|
135
195
|
width: 100%; min-height: 68px; resize: vertical; border: 1px solid var(--line);
|
|
136
|
-
border-radius: 8px; padding: 8px; font-size: 13px; color: var(--ink); outline: none;
|
|
196
|
+
border-radius: 8px; padding: 8px; font-size: 13px; color: var(--ink); background: var(--bg-2); outline: none;
|
|
137
197
|
}
|
|
138
198
|
.composer textarea:focus { border-color: var(--accent); }
|
|
139
199
|
.composer .row { display: flex; align-items: center; justify-content: space-between; margin-top: 8px; gap: 8px; }
|
|
140
200
|
.composer label.chk { font-size: 12px; color: var(--muted); display: flex; align-items: center; gap: 6px; cursor: pointer; }
|
|
141
201
|
.composer .btns { display: flex; gap: 6px; }
|
|
142
|
-
.composer button {
|
|
143
|
-
border: 0; border-radius: 8px; font-size: 13px; font-weight: 600; padding: 7px 12px; cursor: pointer;
|
|
144
|
-
}
|
|
202
|
+
.composer button { border: 0; border-radius: 8px; font-size: 13px; font-weight: 600; padding: 7px 12px; cursor: pointer; }
|
|
145
203
|
.composer .primary { background: var(--accent); color: #fff; }
|
|
146
204
|
.composer .primary:disabled { opacity: .5; cursor: default; }
|
|
147
|
-
.composer .ghost { background: var(--
|
|
205
|
+
.composer .ghost { background: var(--bg-3); color: var(--ink); }
|
|
148
206
|
|
|
149
|
-
/*
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
.
|
|
160
|
-
.
|
|
161
|
-
.
|
|
162
|
-
|
|
207
|
+
/* Mobile: left/right/float docking is a desktop affordance. On small screens the
|
|
208
|
+
panel collapses to a bottom sheet that OVERLAYS the page (pushing a side dock
|
|
209
|
+
here would squeeze the page to a useless sliver), regardless of the chosen dock
|
|
210
|
+
mode. !important overrides the inline geometry JS applies for float mode. */
|
|
211
|
+
@media (max-width: 640px) {
|
|
212
|
+
.dock.open {
|
|
213
|
+
top: auto !important; left: 0 !important; right: 0 !important; bottom: 0 !important;
|
|
214
|
+
width: auto !important; height: 76vh !important;
|
|
215
|
+
border-width: 1px 0 0 0 !important; border-radius: 16px 16px 0 0 !important;
|
|
216
|
+
}
|
|
217
|
+
.dctl [data-dock], .dctl .gap { display: none; } /* dock positions don't apply on mobile */
|
|
218
|
+
.resize { display: none !important; }
|
|
219
|
+
.tools button { flex: 1; justify-content: center; } /* full-width tap targets */
|
|
220
|
+
.dock.mode-bottom .list { grid-template-columns: 1fr; }
|
|
221
|
+
.launcher { bottom: 16px; right: 16px; }
|
|
222
|
+
/* While a tool is active, shrink the sheet to just header + tools so most of the
|
|
223
|
+
page stays visible and tappable; the list returns when the tool closes. */
|
|
224
|
+
.dock.inspecting { height: auto !important; }
|
|
225
|
+
.dock.inspecting .listhead, .dock.inspecting .list { display: none; }
|
|
163
226
|
}
|
|
164
|
-
.item:hover { border-color: var(--accent); }
|
|
165
|
-
.item .top { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
|
|
166
|
-
.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; }
|
|
167
|
-
.item .num.detached { background:#9aa0af; }
|
|
168
|
-
.item .num.done { background:#10935a; }
|
|
169
|
-
.item .who { font-size: 12px; font-weight: 600; }
|
|
170
|
-
.item .device { font-size: 10px; color: var(--muted); background: var(--panel-2); border-radius: 999px; padding: 1px 7px; white-space: nowrap; }
|
|
171
|
-
.item .body { font-size: 13px; line-height: 1.4; }
|
|
172
|
-
.item .meta { font-size: 11px; color: var(--muted); margin-top: 6px; font-family: ui-monospace, Menlo, monospace; }
|
|
173
|
-
.item .badge { font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; padding: 1px 6px; border-radius: 5px; margin-left: auto; }
|
|
174
|
-
.badge.detached { background: #eceef2; color: #6b7180; }
|
|
175
|
-
.badge.done { background: #d8f0e4; color: #10935a; }
|
|
176
|
-
.item .actions { display: flex; gap: 6px; margin-top: 8px; }
|
|
177
|
-
.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); }
|
|
178
|
-
.item img.shot { width: 100%; border-radius: 6px; margin-top: 8px; border: 1px solid var(--line); }
|
|
179
227
|
`
|
|
180
228
|
);
|
|
181
229
|
|
|
@@ -527,14 +575,14 @@ function getDocument(target) {
|
|
|
527
575
|
}
|
|
528
576
|
var XMLNS = "http://www.w3.org/2000/svg";
|
|
529
577
|
function createSvg(width, height, ownerDocument) {
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
return
|
|
535
|
-
}
|
|
536
|
-
function svgToDataUrl(
|
|
537
|
-
let xhtml = new XMLSerializer().serializeToString(
|
|
578
|
+
const svg2 = getDocument(ownerDocument).createElementNS(XMLNS, "svg");
|
|
579
|
+
svg2.setAttributeNS(null, "width", width.toString());
|
|
580
|
+
svg2.setAttributeNS(null, "height", height.toString());
|
|
581
|
+
svg2.setAttributeNS(null, "viewBox", `0 0 ${width} ${height}`);
|
|
582
|
+
return svg2;
|
|
583
|
+
}
|
|
584
|
+
function svgToDataUrl(svg2, removeControlCharacter) {
|
|
585
|
+
let xhtml = new XMLSerializer().serializeToString(svg2);
|
|
538
586
|
if (removeControlCharacter) {
|
|
539
587
|
xhtml = xhtml.replace(/[\u0000-\u0008\v\f\u000E-\u001F\uD800-\uDFFF\uFFFE\uFFFF]/gu, "");
|
|
540
588
|
}
|
|
@@ -1785,8 +1833,8 @@ function unwrapCssLayers(rules, out = []) {
|
|
|
1785
1833
|
return out;
|
|
1786
1834
|
}
|
|
1787
1835
|
var SVG_EXTERNAL_RESOURCE_REGEX = /\bx?link:?href\s*=\s*["'](?!data:)[^"']+["']/i;
|
|
1788
|
-
function svgHasExternalResources(
|
|
1789
|
-
return SVG_EXTERNAL_RESOURCE_REGEX.test(
|
|
1836
|
+
function svgHasExternalResources(svg2) {
|
|
1837
|
+
return SVG_EXTERNAL_RESOURCE_REGEX.test(svg2.innerHTML);
|
|
1790
1838
|
}
|
|
1791
1839
|
async function domToForeignObjectSvg(node, options) {
|
|
1792
1840
|
const context = await orCreateContext(node, options);
|
|
@@ -1846,35 +1894,35 @@ async function domToForeignObjectSvg(node, options) {
|
|
|
1846
1894
|
await Promise.all([...Array.from({ length: 4 })].map(runTask));
|
|
1847
1895
|
log.timeEnd("embed node");
|
|
1848
1896
|
await onEmbedNode?.(clone);
|
|
1849
|
-
const
|
|
1850
|
-
svgDefsElement &&
|
|
1851
|
-
svgStyleElement &&
|
|
1897
|
+
const svg2 = createForeignObjectSvg(clone, context);
|
|
1898
|
+
svgDefsElement && svg2.insertBefore(svgDefsElement, svg2.children[0]);
|
|
1899
|
+
svgStyleElement && svg2.insertBefore(svgStyleElement, svg2.children[0]);
|
|
1852
1900
|
autoDestruct && destroyContext(context);
|
|
1853
|
-
await onCreateForeignObjectSvg?.(
|
|
1854
|
-
return
|
|
1901
|
+
await onCreateForeignObjectSvg?.(svg2);
|
|
1902
|
+
return svg2;
|
|
1855
1903
|
}
|
|
1856
1904
|
function createForeignObjectSvg(clone, context) {
|
|
1857
1905
|
const { width, height } = context;
|
|
1858
|
-
const
|
|
1859
|
-
const foreignObject =
|
|
1906
|
+
const svg2 = createSvg(width, height, clone.ownerDocument);
|
|
1907
|
+
const foreignObject = svg2.ownerDocument.createElementNS(svg2.namespaceURI, "foreignObject");
|
|
1860
1908
|
foreignObject.setAttributeNS(null, "x", "0%");
|
|
1861
1909
|
foreignObject.setAttributeNS(null, "y", "0%");
|
|
1862
1910
|
foreignObject.setAttributeNS(null, "width", "100%");
|
|
1863
1911
|
foreignObject.setAttributeNS(null, "height", "100%");
|
|
1864
1912
|
foreignObject.append(clone);
|
|
1865
|
-
|
|
1866
|
-
return
|
|
1913
|
+
svg2.appendChild(foreignObject);
|
|
1914
|
+
return svg2;
|
|
1867
1915
|
}
|
|
1868
1916
|
async function domToCanvas(node, options) {
|
|
1869
1917
|
const context = await orCreateContext(node, options);
|
|
1870
|
-
const
|
|
1871
|
-
const dataUrl = svgToDataUrl(
|
|
1918
|
+
const svg2 = await domToForeignObjectSvg(context);
|
|
1919
|
+
const dataUrl = svgToDataUrl(svg2, context.isEnable("removeControlCharacter"));
|
|
1872
1920
|
if (!context.autoDestruct) {
|
|
1873
1921
|
context.svgStyleElement = createStyleElement(context.ownerDocument);
|
|
1874
1922
|
context.svgDefsElement = context.ownerDocument?.createElementNS(XMLNS, "defs");
|
|
1875
1923
|
context.svgStyles.clear();
|
|
1876
1924
|
}
|
|
1877
|
-
const image = createImage(dataUrl,
|
|
1925
|
+
const image = createImage(dataUrl, svg2.ownerDocument);
|
|
1878
1926
|
return await imageToCanvas(image, context);
|
|
1879
1927
|
}
|
|
1880
1928
|
async function domToDataUrl(node, options) {
|
|
@@ -2163,6 +2211,7 @@ var HttpAdapter = class {
|
|
|
2163
2211
|
};
|
|
2164
2212
|
|
|
2165
2213
|
// src/app.ts
|
|
2214
|
+
var DOCK_MODES = ["left", "right", "bottom", "float"];
|
|
2166
2215
|
var uid2 = () => crypto.randomUUID ? crypto.randomUUID() : "c_" + Math.abs(hash(String(performance.now()))).toString(36);
|
|
2167
2216
|
function hash(s) {
|
|
2168
2217
|
let h = 0;
|
|
@@ -2177,19 +2226,20 @@ var LoupeApp = class {
|
|
|
2177
2226
|
/** comment.id → pin element. */
|
|
2178
2227
|
this.pins = /* @__PURE__ */ new Map();
|
|
2179
2228
|
this.mode = "off";
|
|
2180
|
-
this.collapsed = false;
|
|
2181
2229
|
this.lastUrl = "";
|
|
2182
2230
|
this.targetOffset = { x: 0.5, y: 0.5 };
|
|
2183
2231
|
this.pending = null;
|
|
2184
2232
|
this.dragStart = null;
|
|
2185
2233
|
/** region comment whose outline is currently highlighted (tracks scroll). */
|
|
2186
2234
|
this.activeRegionId = null;
|
|
2187
|
-
|
|
2188
|
-
this.
|
|
2189
|
-
|
|
2190
|
-
this.
|
|
2191
|
-
/**
|
|
2192
|
-
this.
|
|
2235
|
+
// ---- control-panel state (persisted in localStorage `loupe:dock`) ----------
|
|
2236
|
+
this.dockMode = "right";
|
|
2237
|
+
this.open = true;
|
|
2238
|
+
this.theme = "dark";
|
|
2239
|
+
/** Float-mode window geometry; (x<=0 && y<=0) → placed on first layout. */
|
|
2240
|
+
this.floatRect = { x: 0, y: 0, w: 380, h: 540 };
|
|
2241
|
+
this.floatDrag = null;
|
|
2242
|
+
this.floatResize = null;
|
|
2193
2243
|
this.raf = 0;
|
|
2194
2244
|
// ---- inspector ------------------------------------------------------------
|
|
2195
2245
|
this.onMove = (e) => {
|
|
@@ -2233,6 +2283,8 @@ var LoupeApp = class {
|
|
|
2233
2283
|
// ---- region ("free-size screenshot") selection ----------------------------
|
|
2234
2284
|
this.onRegionDown = (e) => {
|
|
2235
2285
|
if (this.mode !== "region" || e.button !== 0) return;
|
|
2286
|
+
const t = e.target;
|
|
2287
|
+
if (t && (t.id === "loupe-root" || t.closest?.("#loupe-root"))) return;
|
|
2236
2288
|
e.preventDefault();
|
|
2237
2289
|
e.stopPropagation();
|
|
2238
2290
|
this.dragStart = { x: e.clientX, y: e.clientY };
|
|
@@ -2338,57 +2390,52 @@ var LoupeApp = class {
|
|
|
2338
2390
|
}
|
|
2339
2391
|
}
|
|
2340
2392
|
};
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
this.
|
|
2344
|
-
if (
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
this.
|
|
2348
|
-
this.
|
|
2349
|
-
|
|
2350
|
-
brand.setPointerCapture(e.pointerId);
|
|
2351
|
-
} catch {
|
|
2352
|
-
}
|
|
2353
|
-
brand.addEventListener("pointermove", this.onBarPointerMove);
|
|
2354
|
-
brand.addEventListener("pointerup", this.onBarPointerUp);
|
|
2355
|
-
brand.addEventListener("pointercancel", this.onBarPointerUp);
|
|
2393
|
+
this.onWinResize = () => this.applyDockLayout();
|
|
2394
|
+
// ---- float-mode drag + resize ---------------------------------------------
|
|
2395
|
+
this.onHeadPointerDown = (e) => {
|
|
2396
|
+
if (this.dockMode !== "float" || e.button !== 0 || this.isMobile()) return;
|
|
2397
|
+
if (e.target?.closest(".dctl")) return;
|
|
2398
|
+
this.floatDrag = { px: e.clientX, py: e.clientY, ox: this.floatRect.x, oy: this.floatRect.y };
|
|
2399
|
+
this.dock.classList.add("dragging");
|
|
2400
|
+
window.addEventListener("pointermove", this.onHeadPointerMove);
|
|
2401
|
+
window.addEventListener("pointerup", this.onHeadPointerUp);
|
|
2356
2402
|
};
|
|
2357
|
-
this.
|
|
2358
|
-
if (!this.
|
|
2359
|
-
const dx = e.clientX - this.barDrag.px;
|
|
2360
|
-
const dy = e.clientY - this.barDrag.py;
|
|
2361
|
-
if (!this.justDragged && Math.hypot(dx, dy) < 5) return;
|
|
2362
|
-
this.justDragged = true;
|
|
2403
|
+
this.onHeadPointerMove = (e) => {
|
|
2404
|
+
if (!this.floatDrag) return;
|
|
2363
2405
|
e.preventDefault();
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
const iw = brand.offsetWidth || 44, ih = brand.offsetHeight || 44;
|
|
2368
|
-
const ix = clampPx(this.barDrag.ix + dx, 6, vw - iw - 6);
|
|
2369
|
-
const iy = clampPx(this.barDrag.iy + dy, 6, vh - ih - 6);
|
|
2370
|
-
bar.classList.add("floating", "dragging");
|
|
2371
|
-
bar.classList.remove("orient-v", "flow-rev");
|
|
2372
|
-
bar.classList.add("orient-h");
|
|
2373
|
-
Object.assign(bar.style, { transform: "none", left: ix + "px", top: iy + "px", right: "auto", bottom: "auto" });
|
|
2374
|
-
this.barPos = { fx: ix / vw, fy: iy / vh };
|
|
2406
|
+
this.floatRect.x = this.floatDrag.ox + (e.clientX - this.floatDrag.px);
|
|
2407
|
+
this.floatRect.y = this.floatDrag.oy + (e.clientY - this.floatDrag.py);
|
|
2408
|
+
this.applyDockLayout();
|
|
2375
2409
|
};
|
|
2376
|
-
this.
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2410
|
+
this.onHeadPointerUp = () => {
|
|
2411
|
+
if (!this.floatDrag) return;
|
|
2412
|
+
this.floatDrag = null;
|
|
2413
|
+
this.dock.classList.remove("dragging");
|
|
2414
|
+
window.removeEventListener("pointermove", this.onHeadPointerMove);
|
|
2415
|
+
window.removeEventListener("pointerup", this.onHeadPointerUp);
|
|
2416
|
+
this.saveState();
|
|
2417
|
+
};
|
|
2418
|
+
this.onResizeDown = (e) => {
|
|
2419
|
+
if (this.dockMode !== "float") return;
|
|
2420
|
+
e.preventDefault();
|
|
2421
|
+
e.stopPropagation();
|
|
2422
|
+
this.floatResize = { px: e.clientX, py: e.clientY, ow: this.floatRect.w, oh: this.floatRect.h };
|
|
2423
|
+
window.addEventListener("pointermove", this.onResizeMove);
|
|
2424
|
+
window.addEventListener("pointerup", this.onResizeUp);
|
|
2425
|
+
};
|
|
2426
|
+
this.onResizeMove = (e) => {
|
|
2427
|
+
if (!this.floatResize) return;
|
|
2428
|
+
e.preventDefault();
|
|
2429
|
+
this.floatRect.w = this.floatResize.ow + (e.clientX - this.floatResize.px);
|
|
2430
|
+
this.floatRect.h = this.floatResize.oh + (e.clientY - this.floatResize.py);
|
|
2431
|
+
this.applyDockLayout();
|
|
2432
|
+
};
|
|
2433
|
+
this.onResizeUp = () => {
|
|
2434
|
+
if (!this.floatResize) return;
|
|
2435
|
+
this.floatResize = null;
|
|
2436
|
+
window.removeEventListener("pointermove", this.onResizeMove);
|
|
2437
|
+
window.removeEventListener("pointerup", this.onResizeUp);
|
|
2438
|
+
this.saveState();
|
|
2392
2439
|
};
|
|
2393
2440
|
this.cfg = cfg;
|
|
2394
2441
|
this.store = cfg.apiBase ? new HttpAdapter(cfg.apiBase, cfg.user, cfg.userHmac, cfg.headers, cfg.credentials) : new LocalStorageAdapter();
|
|
@@ -2397,13 +2444,14 @@ var LoupeApp = class {
|
|
|
2397
2444
|
return location.pathname + location.search;
|
|
2398
2445
|
}
|
|
2399
2446
|
async start() {
|
|
2400
|
-
this.
|
|
2447
|
+
this.loadState();
|
|
2401
2448
|
this.buildDom();
|
|
2402
|
-
this.
|
|
2449
|
+
if (this.cfg.autoOpen) this.open = true;
|
|
2450
|
+
this.applyDockLayout();
|
|
2403
2451
|
this.lastUrl = this.url;
|
|
2404
2452
|
this.comments = await this.store.list(this.cfg.projectKey, this.url);
|
|
2405
2453
|
this.renderPins();
|
|
2406
|
-
this.
|
|
2454
|
+
this.renderList();
|
|
2407
2455
|
this.observe();
|
|
2408
2456
|
this.watchNavigation();
|
|
2409
2457
|
if (this.cfg.autoOpen) this.setMode("inspect");
|
|
@@ -2433,7 +2481,7 @@ var LoupeApp = class {
|
|
|
2433
2481
|
try {
|
|
2434
2482
|
this.comments = await this.store.list(this.cfg.projectKey, this.url);
|
|
2435
2483
|
this.renderPins();
|
|
2436
|
-
this.
|
|
2484
|
+
this.renderList();
|
|
2437
2485
|
} catch {
|
|
2438
2486
|
}
|
|
2439
2487
|
}
|
|
@@ -2455,45 +2503,80 @@ var LoupeApp = class {
|
|
|
2455
2503
|
this.shadow.appendChild(this.overlay);
|
|
2456
2504
|
this.composer = el("div", "composer");
|
|
2457
2505
|
this.shadow.appendChild(this.composer);
|
|
2458
|
-
this.
|
|
2459
|
-
this.
|
|
2460
|
-
this.
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
const
|
|
2465
|
-
const
|
|
2466
|
-
brand
|
|
2467
|
-
brand.
|
|
2468
|
-
brand.
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2506
|
+
this.dock = this.buildDock();
|
|
2507
|
+
this.launcher = this.buildLauncher();
|
|
2508
|
+
this.shadow.append(this.dock, this.launcher);
|
|
2509
|
+
}
|
|
2510
|
+
/** The dockable control panel: header (brand + dock controls) → tools → list. */
|
|
2511
|
+
buildDock() {
|
|
2512
|
+
const dock = el("div", "dock");
|
|
2513
|
+
const head = el("div", "dhead");
|
|
2514
|
+
const brand = el("div", "brand");
|
|
2515
|
+
brand.innerHTML = `<span class="logo">\u25CE</span><span class="title"></span>`;
|
|
2516
|
+
brand.querySelector(".title").textContent = this.cfg.label ?? "Loupe";
|
|
2517
|
+
head.addEventListener("pointerdown", this.onHeadPointerDown);
|
|
2518
|
+
const ctl = el("div", "dctl");
|
|
2519
|
+
const dockBtn = (mode, icon, title) => {
|
|
2520
|
+
const b = el("button");
|
|
2521
|
+
b.dataset.dock = mode;
|
|
2522
|
+
b.title = title;
|
|
2523
|
+
b.setAttribute("aria-label", title);
|
|
2524
|
+
b.innerHTML = icon;
|
|
2525
|
+
b.onclick = () => this.setDock(mode);
|
|
2526
|
+
return b;
|
|
2475
2527
|
};
|
|
2476
|
-
|
|
2477
|
-
|
|
2528
|
+
ctl.append(
|
|
2529
|
+
dockBtn("left", I_DOCK_LEFT, "Dock to left"),
|
|
2530
|
+
dockBtn("bottom", I_DOCK_BOTTOM, "Dock to bottom"),
|
|
2531
|
+
dockBtn("right", I_DOCK_RIGHT, "Dock to right"),
|
|
2532
|
+
dockBtn("float", I_FLOAT, "Float"),
|
|
2533
|
+
el("span", "gap")
|
|
2534
|
+
);
|
|
2535
|
+
this.themeBtn = el("button");
|
|
2536
|
+
this.themeBtn.dataset.role = "theme";
|
|
2537
|
+
this.themeBtn.onclick = () => this.toggleTheme();
|
|
2538
|
+
const closeBtn = el("button");
|
|
2539
|
+
closeBtn.dataset.role = "close";
|
|
2540
|
+
closeBtn.title = "Close";
|
|
2541
|
+
closeBtn.setAttribute("aria-label", "Close");
|
|
2542
|
+
closeBtn.innerHTML = I_CLOSE;
|
|
2543
|
+
closeBtn.onclick = () => this.closeDock();
|
|
2544
|
+
ctl.append(this.themeBtn, closeBtn);
|
|
2545
|
+
head.append(brand, ctl);
|
|
2546
|
+
const tools = el("div", "tools");
|
|
2547
|
+
const inspectBtn = this.toolBtn("\u271B", "Inspect", "inspect");
|
|
2548
|
+
inspectBtn.title = "Inspect an element and comment on it";
|
|
2478
2549
|
inspectBtn.onclick = () => this.setMode(this.mode === "inspect" ? "off" : "inspect");
|
|
2479
2550
|
const freeBtn = this.toolBtn(NOTE_ICON, "Note", "free");
|
|
2480
2551
|
freeBtn.title = "Drop a note anywhere on the page \u2014 no element, no screenshot";
|
|
2481
2552
|
freeBtn.onclick = () => this.setMode(this.mode === "free" ? "off" : "free");
|
|
2482
|
-
const regionBtn = this.toolBtn(REGION_ICON, "Region
|
|
2553
|
+
const regionBtn = this.toolBtn(REGION_ICON, "Region", "region");
|
|
2483
2554
|
regionBtn.title = "Drag a free-size box, screenshot it, and comment";
|
|
2484
2555
|
regionBtn.onclick = () => this.setMode(this.mode === "region" ? "off" : "region");
|
|
2485
|
-
|
|
2556
|
+
tools.append(inspectBtn, freeBtn, regionBtn);
|
|
2557
|
+
const listHead = el("div", "listhead");
|
|
2558
|
+
listHead.append(document.createTextNode("Comments"));
|
|
2486
2559
|
this.countEl = el("span", "count", "0");
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2560
|
+
listHead.appendChild(this.countEl);
|
|
2561
|
+
this.listEl = el("div", "list");
|
|
2562
|
+
const resize = el("div", "resize");
|
|
2563
|
+
resize.addEventListener("pointerdown", this.onResizeDown);
|
|
2564
|
+
dock.append(head, tools, listHead, this.listEl, resize);
|
|
2565
|
+
return dock;
|
|
2566
|
+
}
|
|
2567
|
+
buildLauncher() {
|
|
2568
|
+
const b = el("button", "launcher");
|
|
2569
|
+
b.title = `Open ${this.cfg.label ?? "Loupe"}`;
|
|
2570
|
+
b.setAttribute("aria-label", "Open Loupe");
|
|
2571
|
+
b.innerHTML = `<span class="logo">\u25CE</span><span class="lcount"></span>`;
|
|
2572
|
+
this.launchCount = b.querySelector(".lcount");
|
|
2573
|
+
b.onclick = () => this.openDock();
|
|
2574
|
+
return b;
|
|
2491
2575
|
}
|
|
2492
|
-
/** A
|
|
2576
|
+
/** A tool button with a uniform icon + label layout. `icon` may be an SVG string. */
|
|
2493
2577
|
toolBtn(icon, label, role) {
|
|
2494
2578
|
const b = el("button");
|
|
2495
2579
|
b.dataset.role = role;
|
|
2496
|
-
b.title = label;
|
|
2497
2580
|
b.setAttribute("aria-label", label);
|
|
2498
2581
|
b.innerHTML = `<span class="ico">${icon}</span><span class="label">${label}</span>`;
|
|
2499
2582
|
return b;
|
|
@@ -2549,14 +2632,19 @@ var LoupeApp = class {
|
|
|
2549
2632
|
return elAt;
|
|
2550
2633
|
}
|
|
2551
2634
|
setMode(mode) {
|
|
2635
|
+
if (mode !== "off" && !this.open) {
|
|
2636
|
+
this.open = true;
|
|
2637
|
+
this.applyDockLayout();
|
|
2638
|
+
}
|
|
2552
2639
|
this.mode = mode;
|
|
2553
2640
|
this.hl.style.display = "none";
|
|
2554
2641
|
this.selbox.style.display = "none";
|
|
2555
2642
|
this.cancelDrag();
|
|
2556
2643
|
document.body.style.cursor = mode === "off" ? "" : "crosshair";
|
|
2557
|
-
this.
|
|
2558
|
-
this.
|
|
2559
|
-
this.
|
|
2644
|
+
this.dock.querySelector('[data-role="inspect"]')?.classList.toggle("on", mode === "inspect");
|
|
2645
|
+
this.dock.querySelector('[data-role="free"]')?.classList.toggle("on", mode === "free");
|
|
2646
|
+
this.dock.querySelector('[data-role="region"]')?.classList.toggle("on", mode === "region");
|
|
2647
|
+
this.dock.classList.toggle("inspecting", mode !== "off");
|
|
2560
2648
|
document.removeEventListener("mousemove", this.onMove, true);
|
|
2561
2649
|
document.removeEventListener("click", this.onClick, true);
|
|
2562
2650
|
document.removeEventListener("click", this.onFreeClick, true);
|
|
@@ -2678,7 +2766,7 @@ var LoupeApp = class {
|
|
|
2678
2766
|
this.resolved.set(comment.id, anchoredEl);
|
|
2679
2767
|
this.closeComposer();
|
|
2680
2768
|
this.renderPins();
|
|
2681
|
-
this.
|
|
2769
|
+
this.renderList();
|
|
2682
2770
|
this.flash(comment.id);
|
|
2683
2771
|
}
|
|
2684
2772
|
// ---- pins + re-anchoring --------------------------------------------------
|
|
@@ -2694,7 +2782,7 @@ var LoupeApp = class {
|
|
|
2694
2782
|
if (!pin) {
|
|
2695
2783
|
pin = el("button", "pin");
|
|
2696
2784
|
pin.onclick = () => {
|
|
2697
|
-
this.
|
|
2785
|
+
this.openDock();
|
|
2698
2786
|
this.flash(c.id);
|
|
2699
2787
|
};
|
|
2700
2788
|
this.overlay.appendChild(pin);
|
|
@@ -2704,9 +2792,14 @@ var LoupeApp = class {
|
|
|
2704
2792
|
pin.classList.toggle("done", c.status === "done");
|
|
2705
2793
|
pin.classList.toggle("free", c.kind === "free");
|
|
2706
2794
|
});
|
|
2707
|
-
this.
|
|
2795
|
+
this.updateCount();
|
|
2708
2796
|
this.position();
|
|
2709
2797
|
}
|
|
2798
|
+
updateCount() {
|
|
2799
|
+
const n = this.comments.length;
|
|
2800
|
+
this.countEl.textContent = String(n);
|
|
2801
|
+
this.launchCount.textContent = n ? String(n) : "";
|
|
2802
|
+
}
|
|
2710
2803
|
/**
|
|
2711
2804
|
* The current viewport rect for a region comment. Prefers the element-relative
|
|
2712
2805
|
* fractions (so it tracks reflow across viewports); falls back to the stored
|
|
@@ -2730,136 +2823,129 @@ var LoupeApp = class {
|
|
|
2730
2823
|
};
|
|
2731
2824
|
window.addEventListener("scroll", reposition, true);
|
|
2732
2825
|
window.addEventListener("resize", reposition);
|
|
2733
|
-
window.addEventListener("resize", this.
|
|
2826
|
+
window.addEventListener("resize", this.onWinResize);
|
|
2734
2827
|
let debounce = 0;
|
|
2735
2828
|
this.mo = new MutationObserver(() => {
|
|
2736
2829
|
clearTimeout(debounce);
|
|
2737
2830
|
debounce = window.setTimeout(() => {
|
|
2738
2831
|
this.position();
|
|
2739
|
-
this.
|
|
2832
|
+
this.renderList();
|
|
2740
2833
|
}, 120);
|
|
2741
2834
|
});
|
|
2742
2835
|
this.mo.observe(document.body, { childList: true, subtree: true, attributes: true, characterData: true });
|
|
2743
2836
|
this.tick = window.setInterval(this.position, 800);
|
|
2744
2837
|
}
|
|
2745
|
-
// ----
|
|
2746
|
-
|
|
2747
|
-
this.
|
|
2748
|
-
this.
|
|
2838
|
+
// ---- dock: open / close / mode / theme ------------------------------------
|
|
2839
|
+
openDock() {
|
|
2840
|
+
this.open = true;
|
|
2841
|
+
this.saveState();
|
|
2842
|
+
this.applyDockLayout();
|
|
2843
|
+
this.renderList();
|
|
2749
2844
|
}
|
|
2750
|
-
|
|
2751
|
-
this.
|
|
2752
|
-
this.
|
|
2845
|
+
closeDock() {
|
|
2846
|
+
this.open = false;
|
|
2847
|
+
this.setMode("off");
|
|
2848
|
+
this.closeComposer();
|
|
2849
|
+
this.saveState();
|
|
2850
|
+
this.applyDockLayout();
|
|
2753
2851
|
}
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
this.
|
|
2757
|
-
this.
|
|
2758
|
-
|
|
2759
|
-
this.setMode("off");
|
|
2760
|
-
this.panel.classList.remove("open");
|
|
2761
|
-
this.closeComposer();
|
|
2762
|
-
this.overlay.style.display = "none";
|
|
2763
|
-
} else {
|
|
2764
|
-
this.overlay.style.display = "";
|
|
2765
|
-
this.renderPins();
|
|
2766
|
-
}
|
|
2767
|
-
this.layoutBar();
|
|
2852
|
+
setDock(mode) {
|
|
2853
|
+
this.dockMode = mode;
|
|
2854
|
+
this.open = true;
|
|
2855
|
+
this.saveState();
|
|
2856
|
+
this.applyDockLayout();
|
|
2768
2857
|
}
|
|
2769
|
-
|
|
2858
|
+
toggleTheme() {
|
|
2859
|
+
this.theme = this.theme === "dark" ? "light" : "dark";
|
|
2860
|
+
this.saveState();
|
|
2861
|
+
this.applyDockLayout();
|
|
2862
|
+
}
|
|
2863
|
+
/** Narrow viewports render the panel as a bottom sheet, not a side/float dock. */
|
|
2864
|
+
isMobile() {
|
|
2865
|
+
return window.innerWidth <= 640;
|
|
2866
|
+
}
|
|
2867
|
+
loadState() {
|
|
2770
2868
|
try {
|
|
2771
|
-
const s = localStorage.getItem("loupe:
|
|
2772
|
-
if (s)
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2869
|
+
const s = localStorage.getItem("loupe:dock");
|
|
2870
|
+
if (!s) return;
|
|
2871
|
+
const p = JSON.parse(s);
|
|
2872
|
+
if (DOCK_MODES.includes(p?.mode)) this.dockMode = p.mode;
|
|
2873
|
+
if (typeof p?.open === "boolean") this.open = p.open;
|
|
2874
|
+
if (p?.theme === "light" || p?.theme === "dark") this.theme = p.theme;
|
|
2875
|
+
if (p?.float && typeof p.float.w === "number") this.floatRect = { ...this.floatRect, ...p.float };
|
|
2776
2876
|
} catch {
|
|
2777
2877
|
}
|
|
2778
2878
|
}
|
|
2779
|
-
|
|
2879
|
+
saveState() {
|
|
2780
2880
|
try {
|
|
2781
|
-
|
|
2881
|
+
localStorage.setItem("loupe:dock", JSON.stringify({
|
|
2882
|
+
mode: this.dockMode,
|
|
2883
|
+
open: this.open,
|
|
2884
|
+
theme: this.theme,
|
|
2885
|
+
float: this.floatRect
|
|
2886
|
+
}));
|
|
2782
2887
|
} catch {
|
|
2783
2888
|
}
|
|
2784
2889
|
}
|
|
2785
|
-
|
|
2786
|
-
|
|
2890
|
+
/** Reflect all control-panel state (theme, dock mode, geometry, launcher) into the DOM. */
|
|
2891
|
+
applyDockLayout() {
|
|
2892
|
+
this.root.classList.toggle("theme-light", this.theme === "light");
|
|
2893
|
+
this.themeBtn.title = this.theme === "dark" ? "Switch to light theme" : "Switch to dark theme";
|
|
2894
|
+
this.themeBtn.innerHTML = this.theme === "dark" ? I_SUN : I_MOON;
|
|
2895
|
+
const d = this.dock;
|
|
2896
|
+
d.classList.toggle("open", this.open);
|
|
2897
|
+
for (const m of DOCK_MODES) d.classList.toggle("mode-" + m, this.dockMode === m);
|
|
2898
|
+
if (this.dockMode === "float") {
|
|
2899
|
+
const vw = window.innerWidth, vh = window.innerHeight;
|
|
2900
|
+
let { x, y, w, h } = this.floatRect;
|
|
2901
|
+
w = clampPx(w, 280, Math.min(760, vw - 24));
|
|
2902
|
+
h = clampPx(h, 220, vh - 24);
|
|
2903
|
+
if (x <= 0 && y <= 0) {
|
|
2904
|
+
x = Math.max(12, vw - w - 24);
|
|
2905
|
+
y = 64;
|
|
2906
|
+
}
|
|
2907
|
+
x = clampPx(x, 8, Math.max(8, vw - w - 8));
|
|
2908
|
+
y = clampPx(y, 8, Math.max(8, vh - h - 8));
|
|
2909
|
+
this.floatRect = { x, y, w, h };
|
|
2910
|
+
Object.assign(d.style, { left: x + "px", top: y + "px", width: w + "px", height: h + "px", right: "auto", bottom: "auto" });
|
|
2911
|
+
} else {
|
|
2912
|
+
for (const p of ["left", "top", "right", "bottom", "width", "height"]) d.style[p] = "";
|
|
2913
|
+
}
|
|
2914
|
+
d.querySelectorAll(".dctl [data-dock]").forEach((b) => b.classList.toggle("on", b.dataset.dock === this.dockMode));
|
|
2915
|
+
this.launcher.classList.toggle("show", !this.open);
|
|
2916
|
+
const leftSide = this.dockMode === "left";
|
|
2917
|
+
this.launcher.style.left = leftSide ? "20px" : "auto";
|
|
2918
|
+
this.launcher.style.right = leftSide ? "auto" : "20px";
|
|
2919
|
+
this.pushPage();
|
|
2787
2920
|
}
|
|
2788
2921
|
/**
|
|
2789
|
-
*
|
|
2790
|
-
*
|
|
2791
|
-
*
|
|
2792
|
-
*
|
|
2922
|
+
* Push the host page over so the docked panel never covers content (like real
|
|
2923
|
+
* DevTools). We shrink the <html> box with a margin on the docked edge — the
|
|
2924
|
+
* panel is `position: fixed` (relative to the viewport), so it sits in the
|
|
2925
|
+
* gutter the margin frees up. Float mode and the closed state reserve nothing.
|
|
2926
|
+
* Only inline styles are touched, so clearing them restores the host exactly.
|
|
2793
2927
|
*/
|
|
2794
|
-
|
|
2795
|
-
const
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
const cx = ix + iw / 2, cy = iy + ih / 2;
|
|
2807
|
-
const horizontalSide = cx < vw / 2 ? "left" : "right";
|
|
2808
|
-
const verticalSide = cy < vh / 2 ? "top" : "bottom";
|
|
2809
|
-
const vertical = Math.min(cx, vw - cx) < Math.min(cy, vh - cy);
|
|
2810
|
-
bar.classList.add("floating");
|
|
2811
|
-
bar.classList.remove("dragging");
|
|
2812
|
-
bar.classList.toggle("orient-v", vertical);
|
|
2813
|
-
bar.classList.toggle("orient-h", !vertical);
|
|
2814
|
-
bar.classList.toggle("flow-rev", vertical ? verticalSide === "bottom" : horizontalSide === "right");
|
|
2815
|
-
bar.style.transform = "none";
|
|
2816
|
-
if (horizontalSide === "left") {
|
|
2817
|
-
bar.style.left = ix + "px";
|
|
2818
|
-
bar.style.right = "auto";
|
|
2819
|
-
} else {
|
|
2820
|
-
bar.style.right = vw - (ix + iw) + "px";
|
|
2821
|
-
bar.style.left = "auto";
|
|
2822
|
-
}
|
|
2823
|
-
if (verticalSide === "top") {
|
|
2824
|
-
bar.style.top = iy + "px";
|
|
2825
|
-
bar.style.bottom = "auto";
|
|
2826
|
-
} else {
|
|
2827
|
-
bar.style.bottom = vh - (iy + ih) + "px";
|
|
2828
|
-
bar.style.top = "auto";
|
|
2829
|
-
}
|
|
2830
|
-
const r = bar.getBoundingClientRect();
|
|
2831
|
-
if (r.right > vw - 6 && bar.style.left !== "auto") {
|
|
2832
|
-
bar.style.left = "auto";
|
|
2833
|
-
bar.style.right = "6px";
|
|
2834
|
-
}
|
|
2835
|
-
if (r.left < 6 && bar.style.right !== "auto") {
|
|
2836
|
-
bar.style.right = "auto";
|
|
2837
|
-
bar.style.left = "6px";
|
|
2838
|
-
}
|
|
2839
|
-
if (r.bottom > vh - 6 && bar.style.top !== "auto") {
|
|
2840
|
-
bar.style.top = "auto";
|
|
2841
|
-
bar.style.bottom = "6px";
|
|
2842
|
-
}
|
|
2843
|
-
if (r.top < 6 && bar.style.bottom !== "auto") {
|
|
2844
|
-
bar.style.bottom = "auto";
|
|
2845
|
-
bar.style.top = "6px";
|
|
2846
|
-
}
|
|
2847
|
-
}
|
|
2848
|
-
renderPanel() {
|
|
2849
|
-
const p = this.panel;
|
|
2850
|
-
p.innerHTML = "";
|
|
2851
|
-
const head = el("div", "phead");
|
|
2852
|
-
head.append(document.createTextNode(`Comments \xB7 ${this.comments.length}`));
|
|
2853
|
-
const x = el("button", "x", "\xD7");
|
|
2854
|
-
x.onclick = () => p.classList.remove("open");
|
|
2855
|
-
head.appendChild(x);
|
|
2856
|
-
p.appendChild(head);
|
|
2857
|
-
const list = el("div", "list");
|
|
2928
|
+
pushPage() {
|
|
2929
|
+
const de = document.documentElement;
|
|
2930
|
+
de.style.marginLeft = de.style.marginRight = de.style.marginBottom = "";
|
|
2931
|
+
if (!this.open || this.dockMode === "float" || this.isMobile()) return;
|
|
2932
|
+
const r = this.dock.getBoundingClientRect();
|
|
2933
|
+
if (this.dockMode === "left") de.style.marginLeft = r.width + "px";
|
|
2934
|
+
else if (this.dockMode === "right") de.style.marginRight = r.width + "px";
|
|
2935
|
+
else if (this.dockMode === "bottom") de.style.marginBottom = r.height + "px";
|
|
2936
|
+
}
|
|
2937
|
+
// ---- comment list ---------------------------------------------------------
|
|
2938
|
+
renderList() {
|
|
2939
|
+
this.listEl.innerHTML = "";
|
|
2858
2940
|
if (!this.comments.length) {
|
|
2859
|
-
|
|
2941
|
+
this.listEl.appendChild(el(
|
|
2942
|
+
"div",
|
|
2943
|
+
"empty",
|
|
2944
|
+
"No comments yet. Use Inspect to pick an element, or Note to drop a comment anywhere on the page."
|
|
2945
|
+
));
|
|
2860
2946
|
}
|
|
2861
|
-
this.comments.forEach((c, i) =>
|
|
2862
|
-
|
|
2947
|
+
this.comments.forEach((c, i) => this.listEl.appendChild(this.itemView(c, i)));
|
|
2948
|
+
this.updateCount();
|
|
2863
2949
|
}
|
|
2864
2950
|
itemView(c, i) {
|
|
2865
2951
|
const detached = this.pins.get(c.id)?.classList.contains("detached") && c.status !== "done";
|
|
@@ -2892,7 +2978,7 @@ var LoupeApp = class {
|
|
|
2892
2978
|
c.status = status;
|
|
2893
2979
|
await this.store.update(c.id, { status });
|
|
2894
2980
|
this.renderPins();
|
|
2895
|
-
this.
|
|
2981
|
+
this.renderList();
|
|
2896
2982
|
};
|
|
2897
2983
|
const claudeBtn = el("button", "", "Copy for Claude");
|
|
2898
2984
|
claudeBtn.onclick = async (e) => {
|
|
@@ -2907,7 +2993,7 @@ var LoupeApp = class {
|
|
|
2907
2993
|
this.comments = this.comments.filter((x) => x.id !== c.id);
|
|
2908
2994
|
this.resolved.delete(c.id);
|
|
2909
2995
|
this.renderPins();
|
|
2910
|
-
this.
|
|
2996
|
+
this.renderList();
|
|
2911
2997
|
};
|
|
2912
2998
|
actions.append(doneBtn, claudeBtn, del);
|
|
2913
2999
|
item.appendChild(actions);
|
|
@@ -2988,8 +3074,14 @@ var LoupeApp = class {
|
|
|
2988
3074
|
this.mo?.disconnect();
|
|
2989
3075
|
if (this.tick) clearInterval(this.tick);
|
|
2990
3076
|
window.clearTimeout(this.regionTimer);
|
|
2991
|
-
window.removeEventListener("resize", this.
|
|
3077
|
+
window.removeEventListener("resize", this.onWinResize);
|
|
3078
|
+
window.removeEventListener("pointermove", this.onHeadPointerMove);
|
|
3079
|
+
window.removeEventListener("pointerup", this.onHeadPointerUp);
|
|
3080
|
+
window.removeEventListener("pointermove", this.onResizeMove);
|
|
3081
|
+
window.removeEventListener("pointerup", this.onResizeUp);
|
|
2992
3082
|
document.removeEventListener("keydown", this.onKey, true);
|
|
3083
|
+
const de = document.documentElement;
|
|
3084
|
+
de.style.marginLeft = de.style.marginRight = de.style.marginBottom = "";
|
|
2993
3085
|
this.root?.remove();
|
|
2994
3086
|
}
|
|
2995
3087
|
};
|
|
@@ -2999,17 +3091,31 @@ function el(tag, cls = "", text = "") {
|
|
|
2999
3091
|
if (text) n.textContent = text;
|
|
3000
3092
|
return n;
|
|
3001
3093
|
}
|
|
3002
|
-
function sep() {
|
|
3003
|
-
return el("span", "sep");
|
|
3004
|
-
}
|
|
3005
3094
|
function clamp(n) {
|
|
3006
3095
|
return Math.max(0, Math.min(1, n));
|
|
3007
3096
|
}
|
|
3008
3097
|
function clampPx(n, min, max) {
|
|
3009
3098
|
return Math.max(min, Math.min(max, n));
|
|
3010
3099
|
}
|
|
3011
|
-
var
|
|
3012
|
-
var
|
|
3100
|
+
var svg = (inner) => `<svg viewBox="0 0 16 16" width="15" height="15" fill="none" aria-hidden="true">${inner}</svg>`;
|
|
3101
|
+
var DOCK_FRAME = `<rect x="1.5" y="2.5" width="13" height="11" rx="1.6" stroke="currentColor" stroke-width="1.3"/>`;
|
|
3102
|
+
var I_DOCK_LEFT = svg(`${DOCK_FRAME}<rect x="2.2" y="3.2" width="4" height="9.6" rx="1" fill="currentColor"/>`);
|
|
3103
|
+
var I_DOCK_RIGHT = svg(`${DOCK_FRAME}<rect x="9.8" y="3.2" width="4" height="9.6" rx="1" fill="currentColor"/>`);
|
|
3104
|
+
var I_DOCK_BOTTOM = svg(`${DOCK_FRAME}<rect x="2.2" y="9" width="11.6" height="3.8" rx="1" fill="currentColor"/>`);
|
|
3105
|
+
var I_FLOAT = svg(
|
|
3106
|
+
`<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"/>`
|
|
3107
|
+
);
|
|
3108
|
+
var I_SUN = svg(
|
|
3109
|
+
`<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>`
|
|
3110
|
+
);
|
|
3111
|
+
var I_MOON = svg(
|
|
3112
|
+
`<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"/>`
|
|
3113
|
+
);
|
|
3114
|
+
var I_CLOSE = svg(
|
|
3115
|
+
`<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"/>`
|
|
3116
|
+
);
|
|
3117
|
+
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>`;
|
|
3118
|
+
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>`;
|
|
3013
3119
|
function pageAnchor(point) {
|
|
3014
3120
|
return {
|
|
3015
3121
|
tag: "page",
|