@loupekit/sdk 0.5.2-next.21 → 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.
@@ -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
- /* Custom properties must live on :host \u2014 inside the Shadow DOM, :root matches
36
- nothing and no element carries a .loupe class, so they'd never resolve. */
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(74, 85, 214, 0.10);
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(74, 85, 214, 0.12); border-radius: 4px;
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(74,85,214,.4); }
102
+ .pin.active { outline: 3px solid rgba(107,115,230,.45); }
92
103
 
93
- /* toolbar */
94
- .toolbar {
95
- position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
96
- z-index: 2147483003; pointer-events: auto;
97
- display: flex; align-items: center; gap: 4px;
98
- background: #1b1e27; color: #fff; padding: 6px; border-radius: 12px;
99
- box-shadow: 0 8px 30px rgba(0,0,0,.4); border: 1px solid #2b2f3b;
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
- /* every toolbar item (brand + buttons) shares one icon+label layout so they align */
102
- .toolbar button, .toolbar .brand {
103
- display: flex; align-items: center; gap: 6px;
104
- padding: 8px 12px; border-radius: 8px;
105
- font-size: 13px; font-weight: 500; line-height: 1;
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
- .toolbar button {
108
- background: transparent; color: #cfd3de; border: 0; cursor: pointer;
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
- .toolbar button:hover, .toolbar .brand:hover { background: #2b2f3b; color: #fff; }
111
- .toolbar .ico { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; }
112
- .toolbar .ico svg { width: 16px; height: 16px; display: block; }
113
- .toolbar .label { white-space: nowrap; }
114
- .toolbar button.on { background: var(--accent); color: #fff; }
115
- .toolbar .sep { width: 1px; height: 20px; background: #333846; margin: 0 2px; }
116
- .toolbar .brand {
117
- font-weight: 700; letter-spacing: -.01em; cursor: grab; user-select: none; touch-action: none;
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
- .toolbar.dragging { transition: none; box-shadow: 0 12px 40px rgba(0,0,0,.5); }
120
- .toolbar.dragging .brand { cursor: grabbing; }
121
- /* collapsed \u2192 show only the brand/logo; click it again to expand */
122
- .toolbar.collapsed { gap: 0; }
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
- /* free-move: the bar is fixed-positioned by JS and expands *inward* from its
126
- docked edge \u2014 vertical (a column) against a left/right edge, horizontal
127
- otherwise \u2014 so it's always fully on-screen. */
128
- .toolbar.floating { transform: none; }
129
- .toolbar.orient-h.flow-rev { flex-direction: row-reverse; }
130
- .toolbar.orient-v { flex-direction: column; align-items: stretch; }
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
- /* mobile \u2192 compact, icon-only, pinned to the left */
141
- @media (max-width: 640px) {
142
- .toolbar { left: 12px; right: auto; transform: none; bottom: 12px; padding: 4px; gap: 2px; }
143
- .toolbar .label { display: none; }
144
- .toolbar button, .toolbar .brand { padding: 10px; }
145
- .toolbar .sep { display: none; }
146
- .toolbar .count { position: absolute; top: -4px; right: -4px; margin: 0; }
147
- .toolbar [data-role="comments"] { position: relative; }
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(--panel); color: var(--ink); border: 1px solid var(--line);
154
- border-radius: 12px; box-shadow: 0 12px 40px rgba(0,0,0,.22); padding: 12px; display: none;
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(--panel-2); border-radius: 6px; padding: 5px 8px; margin-bottom: 8px;
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(--panel-2); color: var(--ink); }
232
+ .composer .ghost { background: var(--bg-3); color: var(--ink); }
175
233
 
176
- /* panel (comment list) */
177
- .panel {
178
- position: fixed; right: 16px; bottom: 80px; top: 16px; width: 340px;
179
- z-index: 2147483003; pointer-events: auto; display: none; flex-direction: column;
180
- background: var(--panel); color: var(--ink); border: 1px solid var(--line);
181
- border-radius: 14px; box-shadow: 0 12px 40px rgba(0,0,0,.22); overflow: hidden;
182
- }
183
- .panel.open { display: flex; }
184
- .panel .phead { padding: 14px 16px; border-bottom: 1px solid var(--line); font-weight: 700; display:flex; justify-content:space-between; align-items:center; }
185
- .panel .phead .x { cursor:pointer; color: var(--muted); background:none; border:0; font-size:18px; }
186
- .panel .list { overflow-y: auto; padding: 8px; display: flex; flex-direction: column; gap: 8px; }
187
- .panel .empty { color: var(--muted); font-size: 13px; padding: 24px 12px; text-align: center; }
188
- .item {
189
- border: 1px solid var(--line); border-radius: 10px; padding: 10px; cursor: pointer; background: var(--panel);
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 svg = getDocument(ownerDocument).createElementNS(XMLNS, "svg");
558
- svg.setAttributeNS(null, "width", width.toString());
559
- svg.setAttributeNS(null, "height", height.toString());
560
- svg.setAttributeNS(null, "viewBox", `0 0 ${width} ${height}`);
561
- return svg;
562
- }
563
- function svgToDataUrl(svg, removeControlCharacter) {
564
- let xhtml = new XMLSerializer().serializeToString(svg);
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(svg) {
1816
- return SVG_EXTERNAL_RESOURCE_REGEX.test(svg.innerHTML);
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 svg = createForeignObjectSvg(clone, context);
1877
- svgDefsElement && svg.insertBefore(svgDefsElement, svg.children[0]);
1878
- svgStyleElement && svg.insertBefore(svgStyleElement, svg.children[0]);
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?.(svg);
1881
- return svg;
1928
+ await onCreateForeignObjectSvg?.(svg2);
1929
+ return svg2;
1882
1930
  }
1883
1931
  function createForeignObjectSvg(clone, context) {
1884
1932
  const { width, height } = context;
1885
- const svg = createSvg(width, height, clone.ownerDocument);
1886
- const foreignObject = svg.ownerDocument.createElementNS(svg.namespaceURI, "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
- svg.appendChild(foreignObject);
1893
- return svg;
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 svg = await domToForeignObjectSvg(context);
1898
- const dataUrl = svgToDataUrl(svg, context.isEnable("removeControlCharacter"));
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, svg.ownerDocument);
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
- /** Free-move toolbar position as viewport fractions, or null → default (bottom-center). */
2215
- this.barPos = null;
2216
- /** In-flight drag of the toolbar by its ◎ handle. */
2217
- this.barDrag = null;
2218
- /** True right after a drag so the trailing click doesn't also toggle collapse. */
2219
- this.justDragged = false;
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
- // ---- draggable, edge-aware floating toolbar --------------------------------
2369
- this.onResizeBar = () => this.layoutBar();
2370
- this.onBarPointerDown = (e) => {
2371
- if (typeof e.button === "number" && e.button !== 0) return;
2372
- const brand = this.brandEl();
2373
- const r = brand.getBoundingClientRect();
2374
- this.barDrag = { px: e.clientX, py: e.clientY, ix: r.left, iy: r.top };
2375
- this.justDragged = false;
2376
- try {
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.onBarPointerMove = (e) => {
2385
- if (!this.barDrag) return;
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
- const vw = window.innerWidth, vh = window.innerHeight;
2392
- const bar = this.toolbar;
2393
- const brand = this.brandEl();
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.onBarPointerUp = (e) => {
2404
- const brand = this.brandEl();
2405
- brand.removeEventListener("pointermove", this.onBarPointerMove);
2406
- brand.removeEventListener("pointerup", this.onBarPointerUp);
2407
- brand.removeEventListener("pointercancel", this.onBarPointerUp);
2408
- try {
2409
- brand.releasePointerCapture(e.pointerId);
2410
- } catch {
2411
- }
2412
- const dragged = this.justDragged;
2413
- this.barDrag = null;
2414
- this.toolbar.classList.remove("dragging");
2415
- if (dragged) {
2416
- this.saveBarPos();
2417
- this.layoutBar();
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.loadBarPos();
2474
+ this.loadState();
2428
2475
  this.buildDom();
2429
- this.layoutBar();
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.renderPanel();
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.renderPanel();
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.panel = el("div", "panel");
2486
- this.shadow.appendChild(this.panel);
2487
- this.toolbar = this.buildToolbar();
2488
- this.shadow.appendChild(this.toolbar);
2489
- }
2490
- buildToolbar() {
2491
- const bar = el("div", "toolbar");
2492
- const brand = el("span", "brand");
2493
- brand.innerHTML = `<span class="ico">\u25CE</span><span class="label">Loupe</span>`;
2494
- brand.title = "Drag to move \xB7 click to collapse / expand";
2495
- brand.setAttribute("role", "button");
2496
- brand.onclick = () => {
2497
- if (this.justDragged) {
2498
- this.justDragged = false;
2499
- return;
2500
- }
2501
- this.toggleCollapsed();
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
- brand.addEventListener("pointerdown", this.onBarPointerDown);
2504
- const inspectBtn = this.toolBtn("\u271B", "Inspect & comment", "inspect");
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 shot", "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
- const listBtn = this.toolBtn("\u2630", "Comments", "comments");
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
- listBtn.appendChild(this.countEl);
2515
- listBtn.onclick = () => this.togglePanel();
2516
- bar.append(brand, sep(), inspectBtn, freeBtn, regionBtn, listBtn);
2517
- return bar;
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 toolbar button with a uniform icon + label layout. `icon` may be an SVG string. */
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.toolbar.querySelector('[data-role="inspect"]')?.classList.toggle("on", mode === "inspect");
2585
- this.toolbar.querySelector('[data-role="free"]')?.classList.toggle("on", mode === "free");
2586
- this.toolbar.querySelector('[data-role="region"]')?.classList.toggle("on", mode === "region");
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.renderPanel();
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.openPanel();
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.countEl.textContent = String(this.comments.length);
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.onResizeBar);
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.renderPanel();
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
- // ---- panel ----------------------------------------------------------------
2773
- openPanel() {
2774
- this.panel.classList.add("open");
2775
- this.renderPanel();
2865
+ // ---- dock: open / close / mode / theme ------------------------------------
2866
+ openDock() {
2867
+ this.open = true;
2868
+ this.saveState();
2869
+ this.applyDockLayout();
2870
+ this.renderList();
2776
2871
  }
2777
- togglePanel() {
2778
- this.panel.classList.toggle("open");
2779
- this.renderPanel();
2872
+ closeDock() {
2873
+ this.open = false;
2874
+ this.setMode("off");
2875
+ this.closeComposer();
2876
+ this.saveState();
2877
+ this.applyDockLayout();
2780
2878
  }
2781
- /** Collapse the toolbar to just the logo (or expand it back). */
2782
- toggleCollapsed() {
2783
- this.collapsed = !this.collapsed;
2784
- this.toolbar.classList.toggle("collapsed", this.collapsed);
2785
- if (this.collapsed) {
2786
- this.setMode("off");
2787
- this.panel.classList.remove("open");
2788
- this.closeComposer();
2789
- this.overlay.style.display = "none";
2790
- } else {
2791
- this.overlay.style.display = "";
2792
- this.renderPins();
2793
- }
2794
- this.layoutBar();
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
- loadBarPos() {
2894
+ loadState() {
2797
2895
  try {
2798
- const s = localStorage.getItem("loupe:bar");
2799
- if (s) {
2800
- const p = JSON.parse(s);
2801
- if (typeof p?.fx === "number" && typeof p?.fy === "number") this.barPos = p;
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
- saveBarPos() {
2906
+ saveState() {
2807
2907
  try {
2808
- if (this.barPos) localStorage.setItem("loupe:bar", JSON.stringify(this.barPos));
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
- brandEl() {
2813
- return this.toolbar.querySelector(".brand");
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
- * Position the toolbar for its stored spot and make it expand *inward* so it's
2817
- * always fully on-screen: docked to a left/right edge vertical; to a
2818
- * top/bottom edge or a corner horizontal. No stored spot default
2819
- * (bottom-center, governed by CSS).
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
- layoutBar() {
2822
- const bar = this.toolbar;
2823
- if (!this.barPos) {
2824
- bar.classList.remove("floating", "orient-h", "orient-v", "flow-rev");
2825
- for (const p of ["left", "top", "right", "bottom", "transform"]) bar.style[p] = "";
2826
- return;
2827
- }
2828
- const vw = window.innerWidth, vh = window.innerHeight;
2829
- const brand = this.brandEl();
2830
- const iw = brand.offsetWidth || 44, ih = brand.offsetHeight || 44;
2831
- const ix = clampPx(this.barPos.fx * vw, 6, Math.max(6, vw - iw - 6));
2832
- const iy = clampPx(this.barPos.fy * vh, 6, Math.max(6, vh - ih - 6));
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
- list.appendChild(el("div", "empty", "No comments yet. Click \u201CInspect & comment\u201D and pick an element, or \u201CNote\u201D to drop a comment anywhere."));
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) => list.appendChild(this.itemView(c, i)));
2889
- p.appendChild(list);
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.renderPanel();
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.renderPanel();
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.onResizeBar);
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 REGION_ICON = `<svg class="ico" 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>`;
3039
- var NOTE_ICON = `<svg class="ico" 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>`;
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",