@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.
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
- /* Custom properties must live on :host \u2014 inside the Shadow DOM, :root matches
9
- nothing and no element carries a .loupe class, so they'd never resolve. */
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(74, 85, 214, 0.10);
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(74, 85, 214, 0.12); border-radius: 4px;
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(74,85,214,.4); }
75
+ .pin.active { outline: 3px solid rgba(107,115,230,.45); }
65
76
 
66
- /* toolbar */
67
- .toolbar {
68
- position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%);
69
- z-index: 2147483003; pointer-events: auto;
70
- display: flex; align-items: center; gap: 4px;
71
- background: #1b1e27; color: #fff; padding: 6px; border-radius: 12px;
72
- box-shadow: 0 8px 30px rgba(0,0,0,.4); border: 1px solid #2b2f3b;
73
- }
74
- /* every toolbar item (brand + buttons) shares one icon+label layout so they align */
75
- .toolbar button, .toolbar .brand {
76
- display: flex; align-items: center; gap: 6px;
77
- padding: 8px 12px; border-radius: 8px;
78
- font-size: 13px; font-weight: 500; line-height: 1;
79
- }
80
- .toolbar button {
81
- background: transparent; color: #cfd3de; border: 0; cursor: pointer;
82
- }
83
- .toolbar button:hover, .toolbar .brand:hover { background: #2b2f3b; color: #fff; }
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
- /* free-move: the bar is fixed-positioned by JS and expands *inward* from its
99
- docked edge \u2014 vertical (a column) against a left/right edge, horizontal
100
- otherwise \u2014 so it's always fully on-screen. */
101
- .toolbar.floating { transform: none; }
102
- .toolbar.orient-h.flow-rev { flex-direction: row-reverse; }
103
- .toolbar.orient-v { flex-direction: column; align-items: stretch; }
104
- .toolbar.orient-v.flow-rev { flex-direction: column-reverse; }
105
- .toolbar.orient-v button, .toolbar.orient-v .brand { justify-content: flex-start; }
106
- .toolbar.orient-v .sep { width: 22px; height: 1px; margin: 2px auto; }
107
- .toolbar.orient-v .count { margin-left: auto; }
108
- .toolbar .count {
109
- background: var(--pin); color: #fff; font-size: 11px; font-weight: 700;
110
- border-radius: 999px; padding: 1px 7px; margin-left: 2px;
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
- /* mobile \u2192 compact, icon-only, pinned to the left */
114
- @media (max-width: 640px) {
115
- .toolbar { left: 12px; right: auto; transform: none; bottom: 12px; padding: 4px; gap: 2px; }
116
- .toolbar .label { display: none; }
117
- .toolbar button, .toolbar .brand { padding: 10px; }
118
- .toolbar .sep { display: none; }
119
- .toolbar .count { position: absolute; top: -4px; right: -4px; margin: 0; }
120
- .toolbar [data-role="comments"] { position: relative; }
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(--panel); color: var(--ink); border: 1px solid var(--line);
127
- border-radius: 12px; box-shadow: 0 12px 40px rgba(0,0,0,.22); padding: 12px; display: none;
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(--panel-2); border-radius: 6px; padding: 5px 8px; margin-bottom: 8px;
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(--panel-2); color: var(--ink); }
205
+ .composer .ghost { background: var(--bg-3); color: var(--ink); }
148
206
 
149
- /* panel (comment list) */
150
- .panel {
151
- position: fixed; right: 16px; bottom: 80px; top: 16px; width: 340px;
152
- z-index: 2147483003; pointer-events: auto; display: none; flex-direction: column;
153
- background: var(--panel); color: var(--ink); border: 1px solid var(--line);
154
- border-radius: 14px; box-shadow: 0 12px 40px rgba(0,0,0,.22); overflow: hidden;
155
- }
156
- .panel.open { display: flex; }
157
- .panel .phead { padding: 14px 16px; border-bottom: 1px solid var(--line); font-weight: 700; display:flex; justify-content:space-between; align-items:center; }
158
- .panel .phead .x { cursor:pointer; color: var(--muted); background:none; border:0; font-size:18px; }
159
- .panel .list { overflow-y: auto; padding: 8px; display: flex; flex-direction: column; gap: 8px; }
160
- .panel .empty { color: var(--muted); font-size: 13px; padding: 24px 12px; text-align: center; }
161
- .item {
162
- border: 1px solid var(--line); border-radius: 10px; padding: 10px; cursor: pointer; background: var(--panel);
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 svg = getDocument(ownerDocument).createElementNS(XMLNS, "svg");
531
- svg.setAttributeNS(null, "width", width.toString());
532
- svg.setAttributeNS(null, "height", height.toString());
533
- svg.setAttributeNS(null, "viewBox", `0 0 ${width} ${height}`);
534
- return svg;
535
- }
536
- function svgToDataUrl(svg, removeControlCharacter) {
537
- let xhtml = new XMLSerializer().serializeToString(svg);
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(svg) {
1789
- return SVG_EXTERNAL_RESOURCE_REGEX.test(svg.innerHTML);
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 svg = createForeignObjectSvg(clone, context);
1850
- svgDefsElement && svg.insertBefore(svgDefsElement, svg.children[0]);
1851
- svgStyleElement && svg.insertBefore(svgStyleElement, svg.children[0]);
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?.(svg);
1854
- return svg;
1901
+ await onCreateForeignObjectSvg?.(svg2);
1902
+ return svg2;
1855
1903
  }
1856
1904
  function createForeignObjectSvg(clone, context) {
1857
1905
  const { width, height } = context;
1858
- const svg = createSvg(width, height, clone.ownerDocument);
1859
- const foreignObject = svg.ownerDocument.createElementNS(svg.namespaceURI, "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
- svg.appendChild(foreignObject);
1866
- return svg;
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 svg = await domToForeignObjectSvg(context);
1871
- const dataUrl = svgToDataUrl(svg, context.isEnable("removeControlCharacter"));
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, svg.ownerDocument);
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
- /** Free-move toolbar position as viewport fractions, or null → default (bottom-center). */
2188
- this.barPos = null;
2189
- /** In-flight drag of the toolbar by its ◎ handle. */
2190
- this.barDrag = null;
2191
- /** True right after a drag so the trailing click doesn't also toggle collapse. */
2192
- this.justDragged = false;
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
- // ---- draggable, edge-aware floating toolbar --------------------------------
2342
- this.onResizeBar = () => this.layoutBar();
2343
- this.onBarPointerDown = (e) => {
2344
- if (typeof e.button === "number" && e.button !== 0) return;
2345
- const brand = this.brandEl();
2346
- const r = brand.getBoundingClientRect();
2347
- this.barDrag = { px: e.clientX, py: e.clientY, ix: r.left, iy: r.top };
2348
- this.justDragged = false;
2349
- try {
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.onBarPointerMove = (e) => {
2358
- if (!this.barDrag) return;
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
- const vw = window.innerWidth, vh = window.innerHeight;
2365
- const bar = this.toolbar;
2366
- const brand = this.brandEl();
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.onBarPointerUp = (e) => {
2377
- const brand = this.brandEl();
2378
- brand.removeEventListener("pointermove", this.onBarPointerMove);
2379
- brand.removeEventListener("pointerup", this.onBarPointerUp);
2380
- brand.removeEventListener("pointercancel", this.onBarPointerUp);
2381
- try {
2382
- brand.releasePointerCapture(e.pointerId);
2383
- } catch {
2384
- }
2385
- const dragged = this.justDragged;
2386
- this.barDrag = null;
2387
- this.toolbar.classList.remove("dragging");
2388
- if (dragged) {
2389
- this.saveBarPos();
2390
- this.layoutBar();
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.loadBarPos();
2447
+ this.loadState();
2401
2448
  this.buildDom();
2402
- this.layoutBar();
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.renderPanel();
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.renderPanel();
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.panel = el("div", "panel");
2459
- this.shadow.appendChild(this.panel);
2460
- this.toolbar = this.buildToolbar();
2461
- this.shadow.appendChild(this.toolbar);
2462
- }
2463
- buildToolbar() {
2464
- const bar = el("div", "toolbar");
2465
- const brand = el("span", "brand");
2466
- brand.innerHTML = `<span class="ico">\u25CE</span><span class="label">Loupe</span>`;
2467
- brand.title = "Drag to move \xB7 click to collapse / expand";
2468
- brand.setAttribute("role", "button");
2469
- brand.onclick = () => {
2470
- if (this.justDragged) {
2471
- this.justDragged = false;
2472
- return;
2473
- }
2474
- this.toggleCollapsed();
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
- brand.addEventListener("pointerdown", this.onBarPointerDown);
2477
- const inspectBtn = this.toolBtn("\u271B", "Inspect & comment", "inspect");
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 shot", "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
- const listBtn = this.toolBtn("\u2630", "Comments", "comments");
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
- listBtn.appendChild(this.countEl);
2488
- listBtn.onclick = () => this.togglePanel();
2489
- bar.append(brand, sep(), inspectBtn, freeBtn, regionBtn, listBtn);
2490
- return bar;
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 toolbar button with a uniform icon + label layout. `icon` may be an SVG string. */
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.toolbar.querySelector('[data-role="inspect"]')?.classList.toggle("on", mode === "inspect");
2558
- this.toolbar.querySelector('[data-role="free"]')?.classList.toggle("on", mode === "free");
2559
- this.toolbar.querySelector('[data-role="region"]')?.classList.toggle("on", mode === "region");
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.renderPanel();
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.openPanel();
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.countEl.textContent = String(this.comments.length);
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.onResizeBar);
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.renderPanel();
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
- // ---- panel ----------------------------------------------------------------
2746
- openPanel() {
2747
- this.panel.classList.add("open");
2748
- this.renderPanel();
2838
+ // ---- dock: open / close / mode / theme ------------------------------------
2839
+ openDock() {
2840
+ this.open = true;
2841
+ this.saveState();
2842
+ this.applyDockLayout();
2843
+ this.renderList();
2749
2844
  }
2750
- togglePanel() {
2751
- this.panel.classList.toggle("open");
2752
- this.renderPanel();
2845
+ closeDock() {
2846
+ this.open = false;
2847
+ this.setMode("off");
2848
+ this.closeComposer();
2849
+ this.saveState();
2850
+ this.applyDockLayout();
2753
2851
  }
2754
- /** Collapse the toolbar to just the logo (or expand it back). */
2755
- toggleCollapsed() {
2756
- this.collapsed = !this.collapsed;
2757
- this.toolbar.classList.toggle("collapsed", this.collapsed);
2758
- if (this.collapsed) {
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
- loadBarPos() {
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:bar");
2772
- if (s) {
2773
- const p = JSON.parse(s);
2774
- if (typeof p?.fx === "number" && typeof p?.fy === "number") this.barPos = p;
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
- saveBarPos() {
2879
+ saveState() {
2780
2880
  try {
2781
- if (this.barPos) localStorage.setItem("loupe:bar", JSON.stringify(this.barPos));
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
- brandEl() {
2786
- return this.toolbar.querySelector(".brand");
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
- * Position the toolbar for its stored spot and make it expand *inward* so it's
2790
- * always fully on-screen: docked to a left/right edge vertical; to a
2791
- * top/bottom edge or a corner horizontal. No stored spot default
2792
- * (bottom-center, governed by CSS).
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
- layoutBar() {
2795
- const bar = this.toolbar;
2796
- if (!this.barPos) {
2797
- bar.classList.remove("floating", "orient-h", "orient-v", "flow-rev");
2798
- for (const p of ["left", "top", "right", "bottom", "transform"]) bar.style[p] = "";
2799
- return;
2800
- }
2801
- const vw = window.innerWidth, vh = window.innerHeight;
2802
- const brand = this.brandEl();
2803
- const iw = brand.offsetWidth || 44, ih = brand.offsetHeight || 44;
2804
- const ix = clampPx(this.barPos.fx * vw, 6, Math.max(6, vw - iw - 6));
2805
- const iy = clampPx(this.barPos.fy * vh, 6, Math.max(6, vh - ih - 6));
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
- list.appendChild(el("div", "empty", "No comments yet. Click \u201CInspect & comment\u201D and pick an element, or \u201CNote\u201D to drop a comment anywhere."));
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) => list.appendChild(this.itemView(c, i)));
2862
- p.appendChild(list);
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.renderPanel();
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.renderPanel();
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.onResizeBar);
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 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>`;
3012
- 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>`;
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",