@bitovi/vybit 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1121,499 +1121,279 @@
1121
1121
  });
1122
1122
  };
1123
1123
 
1124
- // overlay/src/ws.ts
1125
- var socket = null;
1126
- var connected = false;
1127
- var handlers = [];
1128
- function connect(url = "ws://localhost:3333") {
1129
- socket = new WebSocket(url);
1130
- socket.addEventListener("open", () => {
1131
- connected = true;
1132
- send({ type: "REGISTER", role: "overlay" });
1133
- window.dispatchEvent(new CustomEvent("overlay-ws-connected"));
1134
- });
1135
- socket.addEventListener("close", () => {
1136
- connected = false;
1137
- socket = null;
1138
- window.dispatchEvent(new CustomEvent("overlay-ws-disconnected"));
1139
- setTimeout(() => connect(url), 3e3);
1140
- });
1141
- socket.addEventListener("message", (event) => {
1142
- try {
1143
- const data = JSON.parse(event.data);
1144
- for (const handler of handlers) {
1145
- handler(data);
1146
- }
1147
- } catch (err) {
1148
- console.error("[tw-overlay] Failed to parse message:", err);
1149
- }
1150
- });
1151
- socket.addEventListener("error", (err) => {
1152
- console.error("[tw-overlay] WebSocket error:", err);
1153
- });
1154
- }
1155
- function send(data) {
1156
- if (connected && socket) {
1157
- socket.send(JSON.stringify(data));
1158
- } else {
1159
- console.warn("[tw-overlay] Cannot send \u2014 not connected");
1124
+ // overlay/src/containers/ModalContainer.ts
1125
+ var STORAGE_KEY = "tw-modal-bounds";
1126
+ function loadBounds() {
1127
+ try {
1128
+ const stored = localStorage.getItem(STORAGE_KEY);
1129
+ if (stored) return JSON.parse(stored);
1130
+ } catch {
1160
1131
  }
1132
+ return { top: 80, left: Math.max(0, window.innerWidth - 440), width: 400, height: 600 };
1161
1133
  }
1162
- function onMessage(handler) {
1163
- handlers.push(handler);
1164
- }
1165
- function sendTo(role, data) {
1166
- send({ ...data, to: role });
1167
- }
1168
-
1169
- // overlay/src/fiber.ts
1170
- function getFiber(domNode) {
1171
- const key = Object.keys(domNode).find((k) => k.startsWith("__reactFiber$"));
1172
- return key ? domNode[key] : null;
1173
- }
1174
- function findComponentBoundary(fiber) {
1175
- let current = fiber.return;
1176
- while (current) {
1177
- if (typeof current.type === "function") {
1178
- return {
1179
- componentType: current.type,
1180
- componentName: current.type.displayName || current.type.name || "Unknown",
1181
- componentFiber: current
1182
- };
1183
- }
1184
- current = current.return;
1134
+ function saveBounds(bounds) {
1135
+ try {
1136
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(bounds));
1137
+ } catch {
1185
1138
  }
1186
- return null;
1187
1139
  }
1188
- function getRootFiber() {
1189
- const candidateIds = ["root", "app", "__next"];
1190
- for (const id of candidateIds) {
1191
- const el = document.getElementById(id);
1192
- if (!el) continue;
1193
- const key = Object.keys(el).find((k) => k.startsWith("__reactContainer$"));
1194
- if (key) {
1195
- const container = el[key];
1196
- if (container?.stateNode?.current) {
1197
- return container.stateNode.current;
1198
- }
1199
- return container;
1200
- }
1140
+ var ModalContainer = class {
1141
+ constructor(shadowRoot2) {
1142
+ this.shadowRoot = shadowRoot2;
1201
1143
  }
1202
- const reactRoot = document.querySelector("[data-reactroot]");
1203
- if (reactRoot) {
1204
- return getFiber(reactRoot);
1144
+ name = "modal";
1145
+ host = null;
1146
+ bounds = loadBounds();
1147
+ open(panelUrl) {
1148
+ if (this.host) return;
1149
+ this.bounds = loadBounds();
1150
+ const host = document.createElement("div");
1151
+ host.className = "container-modal";
1152
+ this.applyBounds(host);
1153
+ host.style.position = "fixed";
1154
+ host.style.zIndex = "999999";
1155
+ host.style.background = "#1e1e2e";
1156
+ host.style.borderRadius = "8px";
1157
+ host.style.boxShadow = "0 8px 32px rgba(0,0,0,0.4)";
1158
+ host.style.display = "flex";
1159
+ host.style.flexDirection = "column";
1160
+ host.style.overflow = "hidden";
1161
+ host.style.pointerEvents = "auto";
1162
+ const handle = document.createElement("div");
1163
+ handle.style.cssText = `
1164
+ height: 28px;
1165
+ background: #181825;
1166
+ cursor: move;
1167
+ display: flex;
1168
+ align-items: center;
1169
+ justify-content: center;
1170
+ flex-shrink: 0;
1171
+ user-select: none;
1172
+ `;
1173
+ handle.innerHTML = `<svg width="32" height="6" viewBox="0 0 32 6" fill="none"><rect x="0" y="0" width="32" height="2" rx="1" fill="#585b70"/><rect x="0" y="4" width="32" height="2" rx="1" fill="#585b70"/></svg>`;
1174
+ this.setupDrag(handle, host);
1175
+ host.appendChild(handle);
1176
+ const iframe = document.createElement("iframe");
1177
+ iframe.src = panelUrl;
1178
+ iframe.allow = "microphone";
1179
+ iframe.style.cssText = "flex:1; border:none; width:100%;";
1180
+ host.appendChild(iframe);
1181
+ const gripper = document.createElement("div");
1182
+ gripper.style.cssText = `
1183
+ position: absolute;
1184
+ bottom: 0;
1185
+ right: 0;
1186
+ width: 16px;
1187
+ height: 16px;
1188
+ cursor: nwse-resize;
1189
+ `;
1190
+ gripper.innerHTML = '<span style="position:absolute;bottom:2px;right:4px;color:#585b70;font-size:10px;">\u25E2</span>';
1191
+ this.setupResize(gripper, host, iframe);
1192
+ host.appendChild(gripper);
1193
+ this.shadowRoot.appendChild(host);
1194
+ this.host = host;
1205
1195
  }
1206
- return null;
1207
- }
1208
- function findAllInstances(rootFiber, componentType) {
1209
- const results = [];
1210
- function walk(fiber) {
1211
- if (!fiber) return;
1212
- if (fiber.type === componentType) {
1213
- results.push(fiber);
1196
+ close() {
1197
+ if (this.host) {
1198
+ this.host.remove();
1199
+ this.host = null;
1214
1200
  }
1215
- walk(fiber.child);
1216
- walk(fiber.sibling);
1217
- }
1218
- walk(rootFiber);
1219
- return results;
1220
- }
1221
-
1222
- // overlay/src/grouping.ts
1223
- function cssEscape(cls) {
1224
- if (typeof CSS !== "undefined" && CSS.escape) return CSS.escape(cls);
1225
- return cls.replace(/([^\w-])/g, "\\$1");
1226
- }
1227
- function buildSelector(tag, classes) {
1228
- return tag.toLowerCase() + classes.map((c) => `.${cssEscape(c)}`).join("");
1229
- }
1230
- function parseClassList(className) {
1231
- if (typeof className !== "string") return [];
1232
- return className.trim().split(/\s+/).filter(Boolean).sort();
1233
- }
1234
- function classDiff(refClasses, candidateClasses) {
1235
- const added = [];
1236
- const removed = [];
1237
- for (const c of candidateClasses) {
1238
- if (!refClasses.has(c)) added.push(c);
1239
1201
  }
1240
- for (const c of refClasses) {
1241
- if (!candidateClasses.has(c)) removed.push(c);
1202
+ isOpen() {
1203
+ return this.host !== null;
1242
1204
  }
1243
- added.sort();
1244
- removed.sort();
1245
- return { added, removed };
1246
- }
1247
- function diffLabel(added, removed) {
1248
- const parts = [];
1249
- for (const a of added) parts.push(`+${a}`);
1250
- for (const r of removed) parts.push(`-${r}`);
1251
- return parts.join(" ");
1252
- }
1253
- var MAX_DIFF = 10;
1254
- var MAX_CANDIDATES = 200;
1255
- function findExactMatches(clickedEl, shadowHost2) {
1256
- const classes = parseClassList(typeof clickedEl.className === "string" ? clickedEl.className : "");
1257
- const tag = clickedEl.tagName;
1258
- const fiber = getFiber(clickedEl);
1259
- const boundary = fiber ? findComponentBoundary(fiber) : null;
1260
- const componentName = boundary?.componentName ?? null;
1261
- let exactMatches;
1262
- if (boundary) {
1263
- const rootFiber = getRootFiber();
1264
- const allNodes = rootFiber ? collectComponentDOMNodes(rootFiber, boundary.componentType, tag) : [];
1265
- exactMatches = allNodes.filter(
1266
- (n) => n.tagName === tag && n.className === clickedEl.className
1267
- );
1268
- } else {
1269
- if (classes.length === 0) {
1270
- exactMatches = Array.from(
1271
- document.querySelectorAll(tag.toLowerCase())
1272
- ).filter(
1273
- (n) => (typeof n.className === "string" ? n.className.trim() : "") === "" && !isInShadowHost(n, shadowHost2)
1274
- );
1275
- } else {
1276
- const selector = buildSelector(tag, classes);
1277
- exactMatches = Array.from(
1278
- document.querySelectorAll(selector)
1279
- ).filter(
1280
- (n) => n.className === clickedEl.className && !isInShadowHost(n, shadowHost2)
1281
- );
1282
- }
1205
+ applyBounds(el) {
1206
+ el.style.top = `${this.bounds.top}px`;
1207
+ el.style.left = `${this.bounds.left}px`;
1208
+ el.style.width = `${this.bounds.width}px`;
1209
+ el.style.height = `${this.bounds.height}px`;
1283
1210
  }
1284
- if (!exactMatches.includes(clickedEl)) {
1285
- exactMatches.unshift(clickedEl);
1211
+ setupDrag(handle, host) {
1212
+ let startX = 0, startY = 0, startLeft = 0, startTop = 0;
1213
+ const onMove = (e) => {
1214
+ this.bounds.left = startLeft + (e.clientX - startX);
1215
+ this.bounds.top = startTop + (e.clientY - startY);
1216
+ host.style.left = `${this.bounds.left}px`;
1217
+ host.style.top = `${this.bounds.top}px`;
1218
+ };
1219
+ const onUp = () => {
1220
+ document.removeEventListener("mousemove", onMove);
1221
+ document.removeEventListener("mouseup", onUp);
1222
+ saveBounds(this.bounds);
1223
+ };
1224
+ handle.addEventListener("mousedown", (e) => {
1225
+ e.preventDefault();
1226
+ startX = e.clientX;
1227
+ startY = e.clientY;
1228
+ startLeft = this.bounds.left;
1229
+ startTop = this.bounds.top;
1230
+ document.addEventListener("mousemove", onMove);
1231
+ document.addEventListener("mouseup", onUp);
1232
+ });
1286
1233
  }
1287
- return {
1288
- exactMatch: exactMatches,
1289
- nearGroups: [],
1290
- // Not computed yet lazy
1291
- componentName
1292
- };
1293
- }
1294
- function computeNearGroups(clickedEl, exactMatchSet, shadowHost2) {
1295
- const rawClassName = typeof clickedEl.className === "string" ? clickedEl.className : "";
1296
- const classes = parseClassList(rawClassName);
1297
- const tag = clickedEl.tagName;
1298
- const refSet = new Set(classes);
1299
- if (classes.length === 0) return [];
1300
- const fiber = getFiber(clickedEl);
1301
- const boundary = fiber ? findComponentBoundary(fiber) : null;
1302
- let candidates;
1303
- if (boundary) {
1304
- const rootFiber = getRootFiber();
1305
- candidates = rootFiber ? collectComponentDOMNodes(rootFiber, boundary.componentType, tag) : [];
1306
- candidates = candidates.filter((n) => !exactMatchSet.has(n));
1307
- } else {
1308
- const seen = new Set(exactMatchSet);
1309
- candidates = [];
1310
- for (const cls of classes) {
1311
- const sel = `${tag.toLowerCase()}.${cssEscape(cls)}`;
1312
- for (const n of document.querySelectorAll(sel)) {
1313
- if (!seen.has(n) && !isInShadowHost(n, shadowHost2)) {
1314
- seen.add(n);
1315
- candidates.push(n);
1316
- if (candidates.length >= MAX_CANDIDATES) break;
1317
- }
1318
- }
1319
- if (candidates.length >= MAX_CANDIDATES) break;
1320
- }
1321
- }
1322
- const groupMap = /* @__PURE__ */ new Map();
1323
- for (const el of candidates) {
1324
- const candidateClasses = new Set(parseClassList(typeof el.className === "string" ? el.className : ""));
1325
- const { added, removed } = classDiff(refSet, candidateClasses);
1326
- const totalDiff = added.length + removed.length;
1327
- if (totalDiff === 0 || totalDiff > MAX_DIFF) continue;
1328
- const key = `+${added.join(",")}|-${removed.join(",")}`;
1329
- const existing = groupMap.get(key);
1330
- if (existing) {
1331
- existing.elements.push(el);
1332
- } else {
1333
- groupMap.set(key, { added, removed, elements: [el] });
1334
- }
1335
- }
1336
- const groups = [];
1337
- for (const [, g] of groupMap) {
1338
- groups.push({
1339
- label: diffLabel(g.added, g.removed),
1340
- added: g.added,
1341
- removed: g.removed,
1342
- elements: g.elements
1234
+ setupResize(gripper, host, iframe) {
1235
+ let startX = 0, startY = 0, startW = 0, startH = 0;
1236
+ const onMove = (e) => {
1237
+ this.bounds.width = Math.max(300, startW + (e.clientX - startX));
1238
+ this.bounds.height = Math.max(200, startH + (e.clientY - startY));
1239
+ host.style.width = `${this.bounds.width}px`;
1240
+ host.style.height = `${this.bounds.height}px`;
1241
+ };
1242
+ const onUp = () => {
1243
+ iframe.style.pointerEvents = "";
1244
+ document.removeEventListener("mousemove", onMove);
1245
+ document.removeEventListener("mouseup", onUp);
1246
+ saveBounds(this.bounds);
1247
+ };
1248
+ gripper.addEventListener("mousedown", (e) => {
1249
+ e.preventDefault();
1250
+ iframe.style.pointerEvents = "none";
1251
+ startX = e.clientX;
1252
+ startY = e.clientY;
1253
+ startW = this.bounds.width;
1254
+ startH = this.bounds.height;
1255
+ document.addEventListener("mousemove", onMove);
1256
+ document.addEventListener("mouseup", onUp);
1343
1257
  });
1344
1258
  }
1345
- groups.sort((a, b) => {
1346
- const diffA = a.added.length + a.removed.length;
1347
- const diffB = b.added.length + b.removed.length;
1348
- if (diffA !== diffB) return diffA - diffB;
1349
- return b.elements.length - a.elements.length;
1350
- });
1351
- return groups;
1352
- }
1353
- function collectComponentDOMNodes(rootFiber, componentType, tagName) {
1354
- const instances = findAllInstances(rootFiber, componentType);
1355
- const results = [];
1356
- for (const inst of instances) {
1357
- collectHostNodes(inst.child, tagName, results);
1358
- }
1359
- return results;
1360
- }
1361
- function collectHostNodes(fiber, tagName, out) {
1362
- if (!fiber) return;
1363
- if (fiber.tag === 5 && fiber.stateNode instanceof HTMLElement) {
1364
- if (fiber.stateNode.tagName === tagName) {
1365
- out.push(fiber.stateNode);
1366
- }
1367
- }
1368
- collectHostNodes(fiber.child, tagName, out);
1369
- collectHostNodes(fiber.sibling, tagName, out);
1370
- }
1371
- function isInShadowHost(el, shadowHost2) {
1372
- if (!shadowHost2) return false;
1373
- return shadowHost2.contains(el);
1374
- }
1259
+ };
1375
1260
 
1376
- // overlay/src/context.ts
1377
- function buildContext(target, oldClass, newClass, originalClassMap) {
1378
- const ancestors = [];
1379
- let current = target;
1380
- while (current && current !== document.documentElement) {
1381
- ancestors.push(current);
1382
- current = current.parentElement;
1383
- }
1384
- ancestors.reverse();
1385
- return buildLevel(ancestors, 0, target, oldClass, newClass, originalClassMap, 0);
1386
- }
1387
- function buildLevel(ancestors, ancestorIndex, target, oldClass, newClass, originalClassMap, indent) {
1388
- const el = ancestors[ancestorIndex];
1389
- const pad = " ".repeat(indent);
1390
- const tag = el.tagName.toLowerCase();
1391
- let attrs = "";
1392
- if (el.id) attrs += ` id="${el.id}"`;
1393
- const originalClass = originalClassMap.get(el);
1394
- const classStr = originalClass != null ? originalClass.trim() : typeof el.className === "string" ? el.className.trim() : "";
1395
- if (classStr) {
1396
- attrs += ` class="${classStr}"`;
1397
- }
1398
- const isTarget = el === target;
1399
- if (isTarget) {
1400
- const text = getInnerText(el);
1401
- const textNode = text ? `
1402
- ${pad} ${text}` : "";
1403
- return `${pad}<${tag}${attrs}> <!-- TARGET: change ${oldClass} \u2192 ${newClass} -->${textNode}
1404
- ${pad}</${tag}>`;
1261
+ // overlay/src/containers/PopoverContainer.ts
1262
+ var PopoverContainer = class {
1263
+ constructor(shadowRoot2) {
1264
+ this.shadowRoot = shadowRoot2;
1405
1265
  }
1406
- if (ancestorIndex >= ancestors.length - 1) {
1407
- return `${pad}<${tag}${attrs} />`;
1266
+ name = "popover";
1267
+ host = null;
1268
+ open(panelUrl) {
1269
+ if (this.host) return;
1270
+ const host = document.createElement("div");
1271
+ host.className = "container-popover";
1272
+ host.style.cssText = `
1273
+ position: fixed;
1274
+ top: 0;
1275
+ right: 0;
1276
+ width: 400px;
1277
+ height: 100vh;
1278
+ z-index: 999999;
1279
+ background: #1e1e2e;
1280
+ box-shadow: -4px 0 24px rgba(0,0,0,0.3);
1281
+ pointer-events: auto;
1282
+ `;
1283
+ const iframe = document.createElement("iframe");
1284
+ iframe.src = panelUrl;
1285
+ iframe.allow = "microphone";
1286
+ iframe.style.cssText = "width:100%; height:100%; border:none;";
1287
+ host.appendChild(iframe);
1288
+ this.shadowRoot.appendChild(host);
1289
+ this.host = host;
1408
1290
  }
1409
- const nextAncestor = ancestors[ancestorIndex + 1];
1410
- const children = Array.from(el.children);
1411
- const relevantIndex = children.indexOf(nextAncestor);
1412
- let inner = "";
1413
- if (relevantIndex === -1) {
1414
- inner = buildLevel(ancestors, ancestorIndex + 1, target, oldClass, newClass, originalClassMap, indent + 1);
1415
- } else {
1416
- const start = Math.max(0, relevantIndex - 3);
1417
- const end = Math.min(children.length - 1, relevantIndex + 3);
1418
- if (start > 0) {
1419
- inner += `${pad} ...
1420
- `;
1421
- }
1422
- for (let i = start; i <= end; i++) {
1423
- if (i === relevantIndex) {
1424
- inner += buildLevel(ancestors, ancestorIndex + 1, target, oldClass, newClass, originalClassMap, indent + 1) + "\n";
1425
- } else {
1426
- inner += renderSiblingNode(children[i], indent + 1, originalClassMap) + "\n";
1427
- }
1428
- }
1429
- if (end < children.length - 1) {
1430
- inner += `${pad} ...
1431
- `;
1291
+ close() {
1292
+ if (this.host) {
1293
+ this.host.remove();
1294
+ this.host = null;
1432
1295
  }
1433
1296
  }
1434
- return `${pad}<${tag}${attrs}>
1435
- ${inner}${pad}</${tag}>`;
1436
- }
1437
- function renderSiblingNode(el, indent, originalClassMap) {
1438
- const pad = " ".repeat(indent);
1439
- const tag = el.tagName.toLowerCase();
1440
- let attrs = "";
1441
- if (el.id) attrs += ` id="${el.id}"`;
1442
- const originalClass = originalClassMap.get(el);
1443
- const classStr = originalClass != null ? originalClass.trim() : typeof el.className === "string" ? el.className.trim() : "";
1444
- if (classStr) {
1445
- attrs += ` class="${classStr}"`;
1446
- }
1447
- const text = getInnerText(el);
1448
- if (!el.id && (!el.className || !el.className.trim()) && !text) {
1449
- return `${pad}<${tag}>...</${tag}>`;
1450
- }
1451
- if (text) {
1452
- return `${pad}<${tag}${attrs}>
1453
- ${pad} ${text}
1454
- ${pad}</${tag}>`;
1297
+ isOpen() {
1298
+ return this.host !== null;
1455
1299
  }
1456
- if (el.children.length > 0) {
1457
- return `${pad}<${tag}${attrs}>
1458
- ${pad} ...
1459
- ${pad}</${tag}>`;
1300
+ };
1301
+
1302
+ // overlay/src/containers/PopupContainer.ts
1303
+ var PopupContainer = class {
1304
+ name = "popup";
1305
+ popup = null;
1306
+ open(panelUrl) {
1307
+ if (this.popup && !this.popup.closed) {
1308
+ this.popup.focus();
1309
+ return;
1310
+ }
1311
+ this.popup = window.open(panelUrl, "tw-panel", "popup,width=420,height=700");
1460
1312
  }
1461
- return `${pad}<${tag}${attrs} />`;
1462
- }
1463
- function getInnerText(el) {
1464
- let text = "";
1465
- for (const node of Array.from(el.childNodes)) {
1466
- if (node.nodeType === Node.TEXT_NODE) {
1467
- text += node.textContent || "";
1313
+ close() {
1314
+ if (this.popup && !this.popup.closed) {
1315
+ this.popup.close();
1468
1316
  }
1317
+ this.popup = null;
1469
1318
  }
1470
- text = text.trim();
1471
- if (text.length > 60) text = text.slice(0, 57) + "...";
1472
- return text;
1473
- }
1319
+ isOpen() {
1320
+ return this.popup !== null && !this.popup.closed;
1321
+ }
1322
+ };
1474
1323
 
1475
- // overlay/src/patcher.ts
1476
- var previewState = null;
1477
- var previewStyleEl = null;
1478
- var committedStyleEl = null;
1479
- var previewGeneration = 0;
1480
- async function applyPreview(elements, oldClass, newClass, serverOrigin) {
1481
- const gen = ++previewGeneration;
1482
- if (!previewState) {
1483
- previewState = {
1484
- elements,
1485
- originalClasses: elements.map((n) => n.className)
1486
- };
1487
- }
1488
- if (newClass) {
1489
- try {
1490
- const res = await fetch(`${serverOrigin}/css`, {
1491
- method: "POST",
1492
- headers: { "Content-Type": "application/json" },
1493
- body: JSON.stringify({ classes: [newClass] })
1494
- });
1495
- if (gen !== previewGeneration) return;
1496
- const { css } = await res.json();
1497
- if (gen !== previewGeneration) return;
1498
- if (!previewStyleEl) {
1499
- previewStyleEl = document.createElement("style");
1500
- previewStyleEl.setAttribute("data-tw-preview", "");
1501
- document.head.appendChild(previewStyleEl);
1502
- }
1503
- previewStyleEl.textContent = css;
1504
- } catch {
1505
- }
1506
- }
1507
- if (gen !== previewGeneration) return;
1508
- if (previewState) {
1509
- for (let i = 0; i < previewState.elements.length; i++) {
1510
- previewState.elements[i].className = previewState.originalClasses[i];
1511
- }
1512
- }
1513
- for (const node of elements) {
1514
- if (oldClass) node.classList.remove(oldClass);
1515
- if (newClass) node.classList.add(newClass);
1516
- }
1517
- }
1518
- async function applyPreviewBatch(elements, pairs, serverOrigin) {
1519
- const gen = ++previewGeneration;
1520
- if (!previewState) {
1521
- previewState = {
1522
- elements,
1523
- originalClasses: elements.map((n) => n.className)
1524
- };
1525
- }
1526
- const newClasses = pairs.map((p) => p.newClass).filter(Boolean);
1527
- if (newClasses.length > 0) {
1528
- try {
1529
- const res = await fetch(`${serverOrigin}/css`, {
1530
- method: "POST",
1531
- headers: { "Content-Type": "application/json" },
1532
- body: JSON.stringify({ classes: newClasses })
1533
- });
1534
- if (gen !== previewGeneration) return;
1535
- const { css } = await res.json();
1536
- if (gen !== previewGeneration) return;
1537
- if (!previewStyleEl) {
1538
- previewStyleEl = document.createElement("style");
1539
- previewStyleEl.setAttribute("data-tw-preview", "");
1540
- document.head.appendChild(previewStyleEl);
1541
- }
1542
- previewStyleEl.textContent = css;
1543
- } catch {
1544
- }
1545
- }
1546
- if (gen !== previewGeneration) return;
1547
- if (previewState) {
1548
- for (let i = 0; i < previewState.elements.length; i++) {
1549
- previewState.elements[i].className = previewState.originalClasses[i];
1550
- }
1551
- }
1552
- for (const node of elements) {
1553
- for (const { oldClass, newClass } of pairs) {
1554
- if (oldClass) node.classList.remove(oldClass);
1555
- if (newClass) node.classList.add(newClass);
1556
- }
1557
- }
1558
- }
1559
- function revertPreview() {
1560
- previewGeneration++;
1561
- if (previewState) {
1562
- for (let i = 0; i < previewState.elements.length; i++) {
1563
- previewState.elements[i].className = previewState.originalClasses[i];
1564
- }
1565
- previewState = null;
1566
- }
1567
- previewStyleEl?.remove();
1568
- previewStyleEl = null;
1569
- }
1570
- function getPreviewState() {
1571
- return previewState;
1572
- }
1573
- function commitPreview() {
1574
- previewGeneration++;
1575
- previewState = null;
1576
- if (previewStyleEl) {
1577
- const css = previewStyleEl.textContent || "";
1578
- if (css) {
1579
- if (!committedStyleEl) {
1580
- committedStyleEl = document.createElement("style");
1581
- committedStyleEl.setAttribute("data-tw-committed", "");
1582
- document.head.appendChild(committedStyleEl);
1583
- }
1584
- committedStyleEl.textContent += css;
1585
- }
1586
- previewStyleEl.remove();
1587
- previewStyleEl = null;
1588
- }
1589
- }
1590
-
1591
- // overlay/src/containers/PopoverContainer.ts
1592
- var PopoverContainer = class {
1324
+ // overlay/src/containers/SidebarContainer.ts
1325
+ var SidebarContainer = class {
1593
1326
  constructor(shadowRoot2) {
1594
1327
  this.shadowRoot = shadowRoot2;
1595
1328
  }
1596
- name = "popover";
1329
+ name = "sidebar";
1597
1330
  host = null;
1331
+ originalPadding = "";
1332
+ width = 380;
1333
+ pageWrapper = null;
1334
+ originalBodyOverflow = "";
1598
1335
  open(panelUrl) {
1599
1336
  if (this.host) return;
1337
+ this.originalBodyOverflow = document.body.style.overflow;
1338
+ const wrapper = document.createElement("div");
1339
+ wrapper.id = "tw-page-wrapper";
1340
+ wrapper.style.cssText = `
1341
+ position: fixed;
1342
+ top: 0;
1343
+ left: 0;
1344
+ right: ${this.width}px;
1345
+ bottom: 0;
1346
+ overflow: auto;
1347
+ -webkit-overflow-scrolling: touch;
1348
+ background: transparent;
1349
+ z-index: 0;
1350
+ `;
1351
+ const bodyChildren = Array.from(document.body.childNodes);
1352
+ for (const node of bodyChildren) {
1353
+ if (node.id === "tw-visual-editor-host") continue;
1354
+ wrapper.appendChild(node);
1355
+ }
1356
+ const shadowHost2 = document.getElementById("tw-visual-editor-host");
1357
+ if (shadowHost2 && shadowHost2.parentNode) {
1358
+ shadowHost2.parentNode.insertBefore(wrapper, shadowHost2);
1359
+ } else {
1360
+ document.body.appendChild(wrapper);
1361
+ }
1362
+ document.body.style.overflow = "hidden";
1363
+ this.pageWrapper = wrapper;
1600
1364
  const host = document.createElement("div");
1601
- host.className = "container-popover";
1365
+ host.className = "container-sidebar";
1602
1366
  host.style.cssText = `
1603
1367
  position: fixed;
1604
1368
  top: 0;
1605
1369
  right: 0;
1606
- width: 400px;
1370
+ width: ${this.width}px;
1607
1371
  height: 100vh;
1608
1372
  z-index: 999999;
1609
1373
  background: #1e1e2e;
1610
1374
  box-shadow: -4px 0 24px rgba(0,0,0,0.3);
1375
+ display: flex;
1611
1376
  pointer-events: auto;
1612
1377
  `;
1378
+ const resizeHandle = document.createElement("div");
1379
+ resizeHandle.style.cssText = `
1380
+ width: 6px;
1381
+ cursor: ew-resize;
1382
+ background: transparent;
1383
+ flex-shrink: 0;
1384
+ `;
1385
+ resizeHandle.addEventListener("mouseenter", () => {
1386
+ resizeHandle.style.background = "#45475a";
1387
+ });
1388
+ resizeHandle.addEventListener("mouseleave", () => {
1389
+ resizeHandle.style.background = "transparent";
1390
+ });
1391
+ this.setupResize(resizeHandle, host);
1392
+ host.appendChild(resizeHandle);
1613
1393
  const iframe = document.createElement("iframe");
1614
1394
  iframe.src = panelUrl;
1615
1395
  iframe.allow = "microphone";
1616
- iframe.style.cssText = "width:100%; height:100%; border:none;";
1396
+ iframe.style.cssText = "flex:1; border:none; height:100%;";
1617
1397
  host.appendChild(iframe);
1618
1398
  this.shadowRoot.appendChild(host);
1619
1399
  this.host = host;
@@ -1622,292 +1402,467 @@ ${pad}</${tag}>`;
1622
1402
  if (this.host) {
1623
1403
  this.host.remove();
1624
1404
  this.host = null;
1405
+ if (this.pageWrapper) {
1406
+ const children = Array.from(this.pageWrapper.childNodes);
1407
+ const shadowHost2 = document.getElementById("tw-visual-editor-host");
1408
+ for (const node of children) {
1409
+ if (shadowHost2 && shadowHost2.parentNode) {
1410
+ shadowHost2.parentNode.insertBefore(node, shadowHost2);
1411
+ } else {
1412
+ document.body.appendChild(node);
1413
+ }
1414
+ }
1415
+ this.pageWrapper.remove();
1416
+ this.pageWrapper = null;
1417
+ }
1418
+ document.body.style.overflow = this.originalBodyOverflow || "";
1625
1419
  }
1626
1420
  }
1627
1421
  isOpen() {
1628
1422
  return this.host !== null;
1629
1423
  }
1424
+ setupResize(handle, host) {
1425
+ let startX = 0, startW = 0;
1426
+ const onMove = (e) => {
1427
+ this.width = Math.max(280, startW - (e.clientX - startX));
1428
+ host.style.width = `${this.width}px`;
1429
+ document.documentElement.style.paddingRight = `${this.width}px`;
1430
+ };
1431
+ const onUp = () => {
1432
+ document.removeEventListener("mousemove", onMove);
1433
+ document.removeEventListener("mouseup", onUp);
1434
+ };
1435
+ handle.addEventListener("mousedown", (e) => {
1436
+ e.preventDefault();
1437
+ startX = e.clientX;
1438
+ startW = this.width;
1439
+ document.addEventListener("mousemove", onMove);
1440
+ document.addEventListener("mouseup", onUp);
1441
+ });
1442
+ }
1630
1443
  };
1631
1444
 
1632
- // overlay/src/containers/ModalContainer.ts
1633
- var STORAGE_KEY = "tw-modal-bounds";
1634
- function loadBounds() {
1635
- try {
1636
- const stored = localStorage.getItem(STORAGE_KEY);
1637
- if (stored) return JSON.parse(stored);
1638
- } catch {
1445
+ // overlay/src/context.ts
1446
+ function buildContext(target, oldClass, newClass, originalClassMap) {
1447
+ const ancestors = [];
1448
+ let current = target;
1449
+ while (current && current !== document.documentElement) {
1450
+ ancestors.push(current);
1451
+ current = current.parentElement;
1639
1452
  }
1640
- return { top: 80, left: Math.max(0, window.innerWidth - 440), width: 400, height: 600 };
1453
+ ancestors.reverse();
1454
+ return buildLevel(ancestors, 0, target, oldClass, newClass, originalClassMap, 0);
1641
1455
  }
1642
- function saveBounds(bounds) {
1643
- try {
1644
- localStorage.setItem(STORAGE_KEY, JSON.stringify(bounds));
1645
- } catch {
1456
+ function buildLevel(ancestors, ancestorIndex, target, oldClass, newClass, originalClassMap, indent) {
1457
+ const el = ancestors[ancestorIndex];
1458
+ const pad = " ".repeat(indent);
1459
+ const tag = el.tagName.toLowerCase();
1460
+ let attrs = "";
1461
+ if (el.id) attrs += ` id="${el.id}"`;
1462
+ const originalClass = originalClassMap.get(el);
1463
+ const classStr = originalClass != null ? originalClass.trim() : typeof el.className === "string" ? el.className.trim() : "";
1464
+ if (classStr) {
1465
+ attrs += ` class="${classStr}"`;
1646
1466
  }
1647
- }
1648
- var ModalContainer = class {
1649
- constructor(shadowRoot2) {
1650
- this.shadowRoot = shadowRoot2;
1467
+ const isTarget = el === target;
1468
+ if (isTarget) {
1469
+ const text = getInnerText(el);
1470
+ const textNode = text ? `
1471
+ ${pad} ${text}` : "";
1472
+ return `${pad}<${tag}${attrs}> <!-- TARGET: change ${oldClass} \u2192 ${newClass} -->${textNode}
1473
+ ${pad}</${tag}>`;
1651
1474
  }
1652
- name = "modal";
1653
- host = null;
1654
- bounds = loadBounds();
1655
- open(panelUrl) {
1656
- if (this.host) return;
1657
- this.bounds = loadBounds();
1658
- const host = document.createElement("div");
1659
- host.className = "container-modal";
1660
- this.applyBounds(host);
1661
- host.style.position = "fixed";
1662
- host.style.zIndex = "999999";
1663
- host.style.background = "#1e1e2e";
1664
- host.style.borderRadius = "8px";
1665
- host.style.boxShadow = "0 8px 32px rgba(0,0,0,0.4)";
1666
- host.style.display = "flex";
1667
- host.style.flexDirection = "column";
1668
- host.style.overflow = "hidden";
1669
- host.style.pointerEvents = "auto";
1670
- const handle = document.createElement("div");
1671
- handle.style.cssText = `
1672
- height: 28px;
1673
- background: #181825;
1674
- cursor: move;
1675
- display: flex;
1676
- align-items: center;
1677
- justify-content: center;
1678
- flex-shrink: 0;
1679
- user-select: none;
1680
- `;
1681
- handle.innerHTML = '<span style="color:#585b70;font-size:11px;letter-spacing:2px;">\u22EF\u22EF\u22EF</span>';
1682
- this.setupDrag(handle, host);
1683
- host.appendChild(handle);
1684
- const iframe = document.createElement("iframe");
1685
- iframe.src = panelUrl;
1686
- iframe.allow = "microphone";
1687
- iframe.style.cssText = "flex:1; border:none; width:100%;";
1688
- host.appendChild(iframe);
1689
- const gripper = document.createElement("div");
1690
- gripper.style.cssText = `
1691
- position: absolute;
1692
- bottom: 0;
1693
- right: 0;
1694
- width: 16px;
1695
- height: 16px;
1696
- cursor: nwse-resize;
1697
- `;
1698
- gripper.innerHTML = '<span style="position:absolute;bottom:2px;right:4px;color:#585b70;font-size:10px;">\u25E2</span>';
1699
- this.setupResize(gripper, host, iframe);
1700
- host.appendChild(gripper);
1701
- this.shadowRoot.appendChild(host);
1702
- this.host = host;
1475
+ if (ancestorIndex >= ancestors.length - 1) {
1476
+ return `${pad}<${tag}${attrs} />`;
1703
1477
  }
1704
- close() {
1705
- if (this.host) {
1706
- this.host.remove();
1707
- this.host = null;
1478
+ const nextAncestor = ancestors[ancestorIndex + 1];
1479
+ const children = Array.from(el.children);
1480
+ const relevantIndex = children.indexOf(nextAncestor);
1481
+ let inner = "";
1482
+ if (relevantIndex === -1) {
1483
+ inner = buildLevel(ancestors, ancestorIndex + 1, target, oldClass, newClass, originalClassMap, indent + 1);
1484
+ } else {
1485
+ const start = Math.max(0, relevantIndex - 3);
1486
+ const end = Math.min(children.length - 1, relevantIndex + 3);
1487
+ if (start > 0) {
1488
+ inner += `${pad} ...
1489
+ `;
1490
+ }
1491
+ for (let i = start; i <= end; i++) {
1492
+ if (i === relevantIndex) {
1493
+ inner += buildLevel(ancestors, ancestorIndex + 1, target, oldClass, newClass, originalClassMap, indent + 1) + "\n";
1494
+ } else {
1495
+ inner += renderSiblingNode(children[i], indent + 1, originalClassMap) + "\n";
1496
+ }
1497
+ }
1498
+ if (end < children.length - 1) {
1499
+ inner += `${pad} ...
1500
+ `;
1708
1501
  }
1709
1502
  }
1710
- isOpen() {
1711
- return this.host !== null;
1503
+ return `${pad}<${tag}${attrs}>
1504
+ ${inner}${pad}</${tag}>`;
1505
+ }
1506
+ function renderSiblingNode(el, indent, originalClassMap) {
1507
+ const pad = " ".repeat(indent);
1508
+ const tag = el.tagName.toLowerCase();
1509
+ let attrs = "";
1510
+ if (el.id) attrs += ` id="${el.id}"`;
1511
+ const originalClass = originalClassMap.get(el);
1512
+ const classStr = originalClass != null ? originalClass.trim() : typeof el.className === "string" ? el.className.trim() : "";
1513
+ if (classStr) {
1514
+ attrs += ` class="${classStr}"`;
1712
1515
  }
1713
- applyBounds(el) {
1714
- el.style.top = `${this.bounds.top}px`;
1715
- el.style.left = `${this.bounds.left}px`;
1716
- el.style.width = `${this.bounds.width}px`;
1717
- el.style.height = `${this.bounds.height}px`;
1516
+ const text = getInnerText(el);
1517
+ if (!el.id && (!el.className || !el.className.trim()) && !text) {
1518
+ return `${pad}<${tag}>...</${tag}>`;
1718
1519
  }
1719
- setupDrag(handle, host) {
1720
- let startX = 0, startY = 0, startLeft = 0, startTop = 0;
1721
- const onMove = (e) => {
1722
- this.bounds.left = startLeft + (e.clientX - startX);
1723
- this.bounds.top = startTop + (e.clientY - startY);
1724
- host.style.left = `${this.bounds.left}px`;
1725
- host.style.top = `${this.bounds.top}px`;
1726
- };
1727
- const onUp = () => {
1728
- document.removeEventListener("mousemove", onMove);
1729
- document.removeEventListener("mouseup", onUp);
1730
- saveBounds(this.bounds);
1731
- };
1732
- handle.addEventListener("mousedown", (e) => {
1733
- e.preventDefault();
1734
- startX = e.clientX;
1735
- startY = e.clientY;
1736
- startLeft = this.bounds.left;
1737
- startTop = this.bounds.top;
1738
- document.addEventListener("mousemove", onMove);
1739
- document.addEventListener("mouseup", onUp);
1740
- });
1520
+ if (text) {
1521
+ return `${pad}<${tag}${attrs}>
1522
+ ${pad} ${text}
1523
+ ${pad}</${tag}>`;
1741
1524
  }
1742
- setupResize(gripper, host, iframe) {
1743
- let startX = 0, startY = 0, startW = 0, startH = 0;
1744
- const onMove = (e) => {
1745
- this.bounds.width = Math.max(300, startW + (e.clientX - startX));
1746
- this.bounds.height = Math.max(200, startH + (e.clientY - startY));
1747
- host.style.width = `${this.bounds.width}px`;
1748
- host.style.height = `${this.bounds.height}px`;
1749
- };
1750
- const onUp = () => {
1751
- iframe.style.pointerEvents = "";
1752
- document.removeEventListener("mousemove", onMove);
1753
- document.removeEventListener("mouseup", onUp);
1754
- saveBounds(this.bounds);
1755
- };
1756
- gripper.addEventListener("mousedown", (e) => {
1757
- e.preventDefault();
1758
- iframe.style.pointerEvents = "none";
1759
- startX = e.clientX;
1760
- startY = e.clientY;
1761
- startW = this.bounds.width;
1762
- startH = this.bounds.height;
1763
- document.addEventListener("mousemove", onMove);
1764
- document.addEventListener("mouseup", onUp);
1765
- });
1525
+ if (el.children.length > 0) {
1526
+ return `${pad}<${tag}${attrs}>
1527
+ ${pad} ...
1528
+ ${pad}</${tag}>`;
1766
1529
  }
1767
- };
1530
+ return `${pad}<${tag}${attrs} />`;
1531
+ }
1532
+ function getInnerText(el) {
1533
+ let text = "";
1534
+ for (const node of Array.from(el.childNodes)) {
1535
+ if (node.nodeType === Node.TEXT_NODE) {
1536
+ text += node.textContent || "";
1537
+ }
1538
+ }
1539
+ text = text.trim();
1540
+ if (text.length > 60) text = text.slice(0, 57) + "...";
1541
+ return text;
1542
+ }
1768
1543
 
1769
- // overlay/src/containers/SidebarContainer.ts
1770
- var SidebarContainer = class {
1771
- constructor(shadowRoot2) {
1772
- this.shadowRoot = shadowRoot2;
1544
+ // overlay/src/fiber.ts
1545
+ function getFiber(domNode) {
1546
+ const key = Object.keys(domNode).find((k) => k.startsWith("__reactFiber$"));
1547
+ return key ? domNode[key] : null;
1548
+ }
1549
+ function findComponentBoundary(fiber) {
1550
+ let current = fiber.return;
1551
+ while (current) {
1552
+ if (typeof current.type === "function") {
1553
+ return {
1554
+ componentType: current.type,
1555
+ componentName: current.type.displayName || current.type.name || "Unknown",
1556
+ componentFiber: current
1557
+ };
1558
+ }
1559
+ current = current.return;
1773
1560
  }
1774
- name = "sidebar";
1775
- host = null;
1776
- originalPadding = "";
1777
- width = 380;
1778
- pageWrapper = null;
1779
- originalBodyOverflow = "";
1780
- open(panelUrl) {
1781
- if (this.host) return;
1782
- this.originalBodyOverflow = document.body.style.overflow;
1783
- const wrapper = document.createElement("div");
1784
- wrapper.id = "tw-page-wrapper";
1785
- wrapper.style.cssText = `
1786
- position: fixed;
1787
- top: 0;
1788
- left: 0;
1789
- right: ${this.width}px;
1790
- bottom: 0;
1791
- overflow: auto;
1792
- -webkit-overflow-scrolling: touch;
1793
- background: transparent;
1794
- z-index: 0;
1795
- `;
1796
- const bodyChildren = Array.from(document.body.childNodes);
1797
- for (const node of bodyChildren) {
1798
- if (node.id === "tw-visual-editor-host") continue;
1799
- wrapper.appendChild(node);
1561
+ return null;
1562
+ }
1563
+ function getRootFiber() {
1564
+ const candidateIds = ["root", "app", "__next"];
1565
+ for (const id of candidateIds) {
1566
+ const el = document.getElementById(id);
1567
+ if (!el) continue;
1568
+ const key = Object.keys(el).find((k) => k.startsWith("__reactContainer$"));
1569
+ if (key) {
1570
+ const container = el[key];
1571
+ if (container?.stateNode?.current) {
1572
+ return container.stateNode.current;
1573
+ }
1574
+ return container;
1800
1575
  }
1801
- const shadowHost2 = document.getElementById("tw-visual-editor-host");
1802
- if (shadowHost2 && shadowHost2.parentNode) {
1803
- shadowHost2.parentNode.insertBefore(wrapper, shadowHost2);
1576
+ }
1577
+ const reactRoot = document.querySelector("[data-reactroot]");
1578
+ if (reactRoot) {
1579
+ return getFiber(reactRoot);
1580
+ }
1581
+ return null;
1582
+ }
1583
+ function findAllInstances(rootFiber, componentType) {
1584
+ const results = [];
1585
+ function walk(fiber) {
1586
+ if (!fiber) return;
1587
+ if (fiber.type === componentType) {
1588
+ results.push(fiber);
1589
+ }
1590
+ walk(fiber.child);
1591
+ walk(fiber.sibling);
1592
+ }
1593
+ walk(rootFiber);
1594
+ return results;
1595
+ }
1596
+
1597
+ // overlay/src/grouping.ts
1598
+ function cssEscape(cls) {
1599
+ if (typeof CSS !== "undefined" && CSS.escape) return CSS.escape(cls);
1600
+ return cls.replace(/([^\w-])/g, "\\$1");
1601
+ }
1602
+ function buildSelector(tag, classes) {
1603
+ return tag.toLowerCase() + classes.map((c) => `.${cssEscape(c)}`).join("");
1604
+ }
1605
+ function parseClassList(className) {
1606
+ if (typeof className !== "string") return [];
1607
+ return className.trim().split(/\s+/).filter(Boolean).sort();
1608
+ }
1609
+ function classDiff(refClasses, candidateClasses) {
1610
+ const added = [];
1611
+ const removed = [];
1612
+ for (const c of candidateClasses) {
1613
+ if (!refClasses.has(c)) added.push(c);
1614
+ }
1615
+ for (const c of refClasses) {
1616
+ if (!candidateClasses.has(c)) removed.push(c);
1617
+ }
1618
+ added.sort();
1619
+ removed.sort();
1620
+ return { added, removed };
1621
+ }
1622
+ function diffLabel(added, removed) {
1623
+ const parts = [];
1624
+ for (const a of added) parts.push(`+${a}`);
1625
+ for (const r of removed) parts.push(`-${r}`);
1626
+ return parts.join(" ");
1627
+ }
1628
+ var MAX_DIFF = 10;
1629
+ var MAX_CANDIDATES = 200;
1630
+ function findExactMatches(clickedEl, shadowHost2) {
1631
+ const classes = parseClassList(typeof clickedEl.className === "string" ? clickedEl.className : "");
1632
+ const tag = clickedEl.tagName;
1633
+ const fiber = getFiber(clickedEl);
1634
+ const boundary = fiber ? findComponentBoundary(fiber) : null;
1635
+ const componentName = boundary?.componentName ?? null;
1636
+ let exactMatches;
1637
+ if (boundary) {
1638
+ const rootFiber = getRootFiber();
1639
+ const allNodes = rootFiber ? collectComponentDOMNodes(rootFiber, boundary.componentType, tag) : [];
1640
+ exactMatches = allNodes.filter(
1641
+ (n) => n.tagName === tag && n.className === clickedEl.className
1642
+ );
1643
+ } else {
1644
+ if (classes.length === 0) {
1645
+ exactMatches = Array.from(
1646
+ document.querySelectorAll(tag.toLowerCase())
1647
+ ).filter(
1648
+ (n) => (typeof n.className === "string" ? n.className.trim() : "") === "" && !isInShadowHost(n, shadowHost2)
1649
+ );
1804
1650
  } else {
1805
- document.body.appendChild(wrapper);
1651
+ const selector = buildSelector(tag, classes);
1652
+ exactMatches = Array.from(
1653
+ document.querySelectorAll(selector)
1654
+ ).filter(
1655
+ (n) => n.className === clickedEl.className && !isInShadowHost(n, shadowHost2)
1656
+ );
1806
1657
  }
1807
- document.body.style.overflow = "hidden";
1808
- this.pageWrapper = wrapper;
1809
- const host = document.createElement("div");
1810
- host.className = "container-sidebar";
1811
- host.style.cssText = `
1812
- position: fixed;
1813
- top: 0;
1814
- right: 0;
1815
- width: ${this.width}px;
1816
- height: 100vh;
1817
- z-index: 999999;
1818
- background: #1e1e2e;
1819
- box-shadow: -4px 0 24px rgba(0,0,0,0.3);
1820
- display: flex;
1821
- pointer-events: auto;
1822
- `;
1823
- const resizeHandle = document.createElement("div");
1824
- resizeHandle.style.cssText = `
1825
- width: 6px;
1826
- cursor: ew-resize;
1827
- background: transparent;
1828
- flex-shrink: 0;
1829
- `;
1830
- resizeHandle.addEventListener("mouseenter", () => {
1831
- resizeHandle.style.background = "#45475a";
1832
- });
1833
- resizeHandle.addEventListener("mouseleave", () => {
1834
- resizeHandle.style.background = "transparent";
1658
+ }
1659
+ if (!exactMatches.includes(clickedEl)) {
1660
+ exactMatches.unshift(clickedEl);
1661
+ }
1662
+ return {
1663
+ exactMatch: exactMatches,
1664
+ nearGroups: [],
1665
+ // Not computed yet — lazy
1666
+ componentName
1667
+ };
1668
+ }
1669
+ function computeNearGroups(clickedEl, exactMatchSet, shadowHost2) {
1670
+ const rawClassName = typeof clickedEl.className === "string" ? clickedEl.className : "";
1671
+ const classes = parseClassList(rawClassName);
1672
+ const tag = clickedEl.tagName;
1673
+ const refSet = new Set(classes);
1674
+ if (classes.length === 0) return [];
1675
+ const fiber = getFiber(clickedEl);
1676
+ const boundary = fiber ? findComponentBoundary(fiber) : null;
1677
+ let candidates;
1678
+ if (boundary) {
1679
+ const rootFiber = getRootFiber();
1680
+ candidates = rootFiber ? collectComponentDOMNodes(rootFiber, boundary.componentType, tag) : [];
1681
+ candidates = candidates.filter((n) => !exactMatchSet.has(n));
1682
+ } else {
1683
+ const seen = new Set(exactMatchSet);
1684
+ candidates = [];
1685
+ for (const cls of classes) {
1686
+ const sel = `${tag.toLowerCase()}.${cssEscape(cls)}`;
1687
+ for (const n of document.querySelectorAll(sel)) {
1688
+ if (!seen.has(n) && !isInShadowHost(n, shadowHost2)) {
1689
+ seen.add(n);
1690
+ candidates.push(n);
1691
+ if (candidates.length >= MAX_CANDIDATES) break;
1692
+ }
1693
+ }
1694
+ if (candidates.length >= MAX_CANDIDATES) break;
1695
+ }
1696
+ }
1697
+ const groupMap = /* @__PURE__ */ new Map();
1698
+ for (const el of candidates) {
1699
+ const candidateClasses = new Set(parseClassList(typeof el.className === "string" ? el.className : ""));
1700
+ const { added, removed } = classDiff(refSet, candidateClasses);
1701
+ const totalDiff = added.length + removed.length;
1702
+ if (totalDiff === 0 || totalDiff > MAX_DIFF) continue;
1703
+ const key = `+${added.join(",")}|-${removed.join(",")}`;
1704
+ const existing = groupMap.get(key);
1705
+ if (existing) {
1706
+ existing.elements.push(el);
1707
+ } else {
1708
+ groupMap.set(key, { added, removed, elements: [el] });
1709
+ }
1710
+ }
1711
+ const groups = [];
1712
+ for (const [, g] of groupMap) {
1713
+ groups.push({
1714
+ label: diffLabel(g.added, g.removed),
1715
+ added: g.added,
1716
+ removed: g.removed,
1717
+ elements: g.elements
1835
1718
  });
1836
- this.setupResize(resizeHandle, host);
1837
- host.appendChild(resizeHandle);
1838
- const iframe = document.createElement("iframe");
1839
- iframe.src = panelUrl;
1840
- iframe.allow = "microphone";
1841
- iframe.style.cssText = "flex:1; border:none; height:100%;";
1842
- host.appendChild(iframe);
1843
- this.shadowRoot.appendChild(host);
1844
- this.host = host;
1845
1719
  }
1846
- close() {
1847
- if (this.host) {
1848
- this.host.remove();
1849
- this.host = null;
1850
- if (this.pageWrapper) {
1851
- const children = Array.from(this.pageWrapper.childNodes);
1852
- const shadowHost2 = document.getElementById("tw-visual-editor-host");
1853
- for (const node of children) {
1854
- if (shadowHost2 && shadowHost2.parentNode) {
1855
- shadowHost2.parentNode.insertBefore(node, shadowHost2);
1856
- } else {
1857
- document.body.appendChild(node);
1858
- }
1859
- }
1860
- this.pageWrapper.remove();
1861
- this.pageWrapper = null;
1720
+ groups.sort((a, b) => {
1721
+ const diffA = a.added.length + a.removed.length;
1722
+ const diffB = b.added.length + b.removed.length;
1723
+ if (diffA !== diffB) return diffA - diffB;
1724
+ return b.elements.length - a.elements.length;
1725
+ });
1726
+ return groups;
1727
+ }
1728
+ function collectComponentDOMNodes(rootFiber, componentType, tagName) {
1729
+ const instances = findAllInstances(rootFiber, componentType);
1730
+ const results = [];
1731
+ for (const inst of instances) {
1732
+ collectHostNodes(inst.child, tagName, results);
1733
+ }
1734
+ return results;
1735
+ }
1736
+ function collectHostNodes(fiber, tagName, out) {
1737
+ if (!fiber) return;
1738
+ if (fiber.tag === 5 && fiber.stateNode instanceof HTMLElement) {
1739
+ if (fiber.stateNode.tagName === tagName) {
1740
+ out.push(fiber.stateNode);
1741
+ }
1742
+ }
1743
+ collectHostNodes(fiber.child, tagName, out);
1744
+ collectHostNodes(fiber.sibling, tagName, out);
1745
+ }
1746
+ function isInShadowHost(el, shadowHost2) {
1747
+ if (!shadowHost2) return false;
1748
+ return shadowHost2.contains(el);
1749
+ }
1750
+
1751
+ // overlay/src/patcher.ts
1752
+ var previewState = null;
1753
+ var previewStyleEl = null;
1754
+ var committedStyleEl = null;
1755
+ var previewGeneration = 0;
1756
+ async function applyPreview(elements, oldClass, newClass, serverOrigin) {
1757
+ const gen = ++previewGeneration;
1758
+ if (!previewState) {
1759
+ previewState = {
1760
+ elements,
1761
+ originalClasses: elements.map((n) => n.className)
1762
+ };
1763
+ }
1764
+ if (newClass) {
1765
+ try {
1766
+ const res = await fetch(`${serverOrigin}/css`, {
1767
+ method: "POST",
1768
+ headers: { "Content-Type": "application/json" },
1769
+ body: JSON.stringify({ classes: [newClass] })
1770
+ });
1771
+ if (gen !== previewGeneration) return;
1772
+ const { css } = await res.json();
1773
+ if (gen !== previewGeneration) return;
1774
+ if (!previewStyleEl) {
1775
+ previewStyleEl = document.createElement("style");
1776
+ previewStyleEl.setAttribute("data-tw-preview", "");
1777
+ document.head.appendChild(previewStyleEl);
1862
1778
  }
1863
- document.body.style.overflow = this.originalBodyOverflow || "";
1779
+ previewStyleEl.textContent = css;
1780
+ } catch {
1864
1781
  }
1865
1782
  }
1866
- isOpen() {
1867
- return this.host !== null;
1783
+ if (gen !== previewGeneration) return;
1784
+ if (previewState) {
1785
+ for (let i = 0; i < previewState.elements.length; i++) {
1786
+ previewState.elements[i].className = previewState.originalClasses[i];
1787
+ }
1868
1788
  }
1869
- setupResize(handle, host) {
1870
- let startX = 0, startW = 0;
1871
- const onMove = (e) => {
1872
- this.width = Math.max(280, startW - (e.clientX - startX));
1873
- host.style.width = `${this.width}px`;
1874
- document.documentElement.style.paddingRight = `${this.width}px`;
1875
- };
1876
- const onUp = () => {
1877
- document.removeEventListener("mousemove", onMove);
1878
- document.removeEventListener("mouseup", onUp);
1789
+ for (const node of elements) {
1790
+ if (oldClass) node.classList.remove(oldClass);
1791
+ if (newClass) node.classList.add(newClass);
1792
+ }
1793
+ }
1794
+ async function applyPreviewBatch(elements, pairs, serverOrigin) {
1795
+ const gen = ++previewGeneration;
1796
+ if (!previewState) {
1797
+ previewState = {
1798
+ elements,
1799
+ originalClasses: elements.map((n) => n.className)
1879
1800
  };
1880
- handle.addEventListener("mousedown", (e) => {
1881
- e.preventDefault();
1882
- startX = e.clientX;
1883
- startW = this.width;
1884
- document.addEventListener("mousemove", onMove);
1885
- document.addEventListener("mouseup", onUp);
1886
- });
1887
1801
  }
1888
- };
1889
-
1890
- // overlay/src/containers/PopupContainer.ts
1891
- var PopupContainer = class {
1892
- name = "popup";
1893
- popup = null;
1894
- open(panelUrl) {
1895
- if (this.popup && !this.popup.closed) {
1896
- this.popup.focus();
1897
- return;
1802
+ const newClasses = pairs.map((p) => p.newClass).filter(Boolean);
1803
+ if (newClasses.length > 0) {
1804
+ try {
1805
+ const res = await fetch(`${serverOrigin}/css`, {
1806
+ method: "POST",
1807
+ headers: { "Content-Type": "application/json" },
1808
+ body: JSON.stringify({ classes: newClasses })
1809
+ });
1810
+ if (gen !== previewGeneration) return;
1811
+ const { css } = await res.json();
1812
+ if (gen !== previewGeneration) return;
1813
+ if (!previewStyleEl) {
1814
+ previewStyleEl = document.createElement("style");
1815
+ previewStyleEl.setAttribute("data-tw-preview", "");
1816
+ document.head.appendChild(previewStyleEl);
1817
+ }
1818
+ previewStyleEl.textContent = css;
1819
+ } catch {
1898
1820
  }
1899
- this.popup = window.open(panelUrl, "tw-panel", "popup,width=420,height=700");
1900
1821
  }
1901
- close() {
1902
- if (this.popup && !this.popup.closed) {
1903
- this.popup.close();
1822
+ if (gen !== previewGeneration) return;
1823
+ if (previewState) {
1824
+ for (let i = 0; i < previewState.elements.length; i++) {
1825
+ previewState.elements[i].className = previewState.originalClasses[i];
1904
1826
  }
1905
- this.popup = null;
1906
1827
  }
1907
- isOpen() {
1908
- return this.popup !== null && !this.popup.closed;
1828
+ for (const node of elements) {
1829
+ for (const { oldClass, newClass } of pairs) {
1830
+ if (oldClass) node.classList.remove(oldClass);
1831
+ if (newClass) node.classList.add(newClass);
1832
+ }
1909
1833
  }
1910
- };
1834
+ }
1835
+ function revertPreview() {
1836
+ previewGeneration++;
1837
+ if (previewState) {
1838
+ for (let i = 0; i < previewState.elements.length; i++) {
1839
+ previewState.elements[i].className = previewState.originalClasses[i];
1840
+ }
1841
+ previewState = null;
1842
+ }
1843
+ previewStyleEl?.remove();
1844
+ previewStyleEl = null;
1845
+ }
1846
+ function getPreviewState() {
1847
+ return previewState;
1848
+ }
1849
+ function commitPreview() {
1850
+ previewGeneration++;
1851
+ previewState = null;
1852
+ if (previewStyleEl) {
1853
+ const css = previewStyleEl.textContent || "";
1854
+ if (css) {
1855
+ if (!committedStyleEl) {
1856
+ committedStyleEl = document.createElement("style");
1857
+ committedStyleEl.setAttribute("data-tw-committed", "");
1858
+ document.head.appendChild(committedStyleEl);
1859
+ }
1860
+ committedStyleEl.textContent += css;
1861
+ }
1862
+ previewStyleEl.remove();
1863
+ previewStyleEl = null;
1864
+ }
1865
+ }
1911
1866
 
1912
1867
  // node_modules/html-to-image/es/util.js
1913
1868
  function resolveUrl(url, baseUrl) {
@@ -2748,6 +2703,51 @@ ${pad}</${tag}>`;
2748
2703
  }
2749
2704
  }
2750
2705
 
2706
+ // overlay/src/ws.ts
2707
+ var socket = null;
2708
+ var connected = false;
2709
+ var handlers = [];
2710
+ function connect(url = "ws://localhost:3333") {
2711
+ socket = new WebSocket(url);
2712
+ socket.addEventListener("open", () => {
2713
+ connected = true;
2714
+ send({ type: "REGISTER", role: "overlay" });
2715
+ window.dispatchEvent(new CustomEvent("overlay-ws-connected"));
2716
+ });
2717
+ socket.addEventListener("close", () => {
2718
+ connected = false;
2719
+ socket = null;
2720
+ window.dispatchEvent(new CustomEvent("overlay-ws-disconnected"));
2721
+ setTimeout(() => connect(url), 3e3);
2722
+ });
2723
+ socket.addEventListener("message", (event) => {
2724
+ try {
2725
+ const data = JSON.parse(event.data);
2726
+ for (const handler of handlers) {
2727
+ handler(data);
2728
+ }
2729
+ } catch (err) {
2730
+ console.error("[tw-overlay] Failed to parse message:", err);
2731
+ }
2732
+ });
2733
+ socket.addEventListener("error", (err) => {
2734
+ console.error("[tw-overlay] WebSocket error:", err);
2735
+ });
2736
+ }
2737
+ function send(data) {
2738
+ if (connected && socket) {
2739
+ socket.send(JSON.stringify(data));
2740
+ } else {
2741
+ console.warn("[tw-overlay] Cannot send \u2014 not connected");
2742
+ }
2743
+ }
2744
+ function onMessage(handler) {
2745
+ handlers.push(handler);
2746
+ }
2747
+ function sendTo(role, data) {
2748
+ send({ ...data, to: role });
2749
+ }
2750
+
2751
2751
  // overlay/src/index.ts
2752
2752
  var shadowRoot;
2753
2753
  var shadowHost;
@@ -2930,11 +2930,40 @@ ${pad}</${tag}>`;
2930
2930
  z-index: 999998;
2931
2931
  }
2932
2932
  /* \u2500\u2500 Group picker popover (replaces instance picker) \u2500\u2500 */
2933
+ .el-group-exact {
2934
+ display: flex;
2935
+ align-items: center;
2936
+ gap: 8px;
2937
+ padding: 8px 12px;
2938
+ font-size: 11px;
2939
+ color: #A0ABAB;
2940
+ }
2941
+ .el-group-exact .el-count-chip {
2942
+ display: inline-flex;
2943
+ align-items: center;
2944
+ justify-content: center;
2945
+ min-width: 20px;
2946
+ padding: 1px 6px;
2947
+ font-size: 10px;
2948
+ font-weight: 600;
2949
+ color: #fff;
2950
+ background: #00848B;
2951
+ border-radius: 9999px;
2952
+ }
2953
+ .el-group-divider {
2954
+ padding: 6px 12px 4px;
2955
+ font-size: 10px;
2956
+ font-weight: 700;
2957
+ text-transform: uppercase;
2958
+ letter-spacing: 0.05em;
2959
+ color: #687879;
2960
+ border-top: 1px solid #DFE2E2;
2961
+ }
2933
2962
  .el-group-empty {
2934
2963
  padding: 12px 14px;
2935
2964
  font-size: 11px;
2936
2965
  color: #687879;
2937
- text-align: center;
2966
+ text-align: left;
2938
2967
  }
2939
2968
  .el-group-row {
2940
2969
  display: flex;
@@ -3231,11 +3260,8 @@ ${pad}</${tag}>`;
3231
3260
  const label = boundary?.componentName ?? target.tagName.toLowerCase();
3232
3261
  showHoverPreview(target, label);
3233
3262
  }
3234
- var PENCIL_SVG = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
3235
- <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
3236
- <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
3237
- </svg>`;
3238
- var RESELECT_SVG = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M8 12l2.5 6 1.5-3 3-1.5z"/></svg>`;
3263
+ var PENCIL_SVG = `<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M15,0H1C0.448,0,0,0.448,0,1v9c0,0.552,0.448,1,1,1h2.882l-1.776,3.553c-0.247,0.494-0.047,1.095,0.447,1.342C2.696,15.966,2.849,16,2.999,16c0.367,0,0.72-0.202,0.896-0.553L4.618,14h6.764l0.724,1.447C12.281,15.798,12.634,16,13.001,16c0.15,0,0.303-0.034,0.446-0.105c0.494-0.247,0.694-0.848,0.447-1.342L12.118,11H15c0.552,0,1-0.448,1-1V1C16,0.448,15.552,0,15,0z M5.618,12l0.5-1h3.764l0.5,1H5.618z M14,9H2V2h12V9z"/></svg>`;
3264
+ var RESELECT_SVG = `<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M14,0H2C.895,0,0,.895,0,2V14c0,1.105,.895,2,2,2H6c.552,0,1-.448,1-1h0c0-.552-.448-1-1-1H2V2H14V6c0,.552,.448,1,1,1h0c.552,0,1-.448,1-1V2c0-1.105-.895-2-2-2Z"/><path d="M12.043,10.629l2.578-.644c.268-.068,.43-.339,.362-.607-.043-.172-.175-.308-.345-.358l-7-2c-.175-.051-.363-.002-.492,.126-.128,.129-.177,.317-.126,.492l2,7c.061,.214,.257,.362,.48,.362h.009c.226-.004,.421-.16,.476-.379l.644-2.578,3.664,3.664c.397,.384,1.03,.373,1.414-.025,.374-.388,.374-1.002,0-1.389l-3.664-3.664Z"/></svg>`;
3239
3265
  async function positionWithFlip(anchor, floating, placement = "top-start") {
3240
3266
  const { x, y } = await computePosition2(anchor, floating, {
3241
3267
  placement,
@@ -3311,7 +3337,9 @@ ${pad}</${tag}>`;
3311
3337
  }
3312
3338
  function showGroupPicker(anchorBtn, onClose, onCountChange) {
3313
3339
  if (pickerCloseHandler) {
3314
- document.removeEventListener("click", pickerCloseHandler, { capture: true });
3340
+ document.removeEventListener("click", pickerCloseHandler, {
3341
+ capture: true
3342
+ });
3315
3343
  pickerCloseHandler = null;
3316
3344
  }
3317
3345
  pickerEl?.remove();
@@ -3330,13 +3358,28 @@ ${pad}</${tag}>`;
3330
3358
  header.className = "el-picker-header";
3331
3359
  const title = document.createElement("span");
3332
3360
  title.className = "el-picker-title";
3333
- title.textContent = "Similar elements";
3361
+ title.textContent = "Selection";
3334
3362
  header.appendChild(title);
3335
3363
  picker.appendChild(header);
3364
+ const exactCount = currentEquivalentNodes.length;
3365
+ const exactRow = document.createElement("div");
3366
+ exactRow.className = "el-group-exact";
3367
+ const chip = document.createElement("span");
3368
+ chip.className = "el-count-chip";
3369
+ chip.textContent = String(exactCount);
3370
+ exactRow.appendChild(chip);
3371
+ const exactLabel = document.createElement("span");
3372
+ exactLabel.textContent = `exact match${exactCount !== 1 ? "es" : ""} selected`;
3373
+ exactRow.appendChild(exactLabel);
3374
+ picker.appendChild(exactRow);
3375
+ const divider = document.createElement("div");
3376
+ divider.className = "el-group-divider";
3377
+ divider.textContent = "Similar";
3378
+ picker.appendChild(divider);
3336
3379
  if (groups.length === 0) {
3337
3380
  const empty = document.createElement("div");
3338
3381
  empty.className = "el-group-empty";
3339
- empty.textContent = "No similar elements found";
3382
+ empty.textContent = "No additional similar elements found";
3340
3383
  picker.appendChild(empty);
3341
3384
  } else {
3342
3385
  let clearPreviewHighlights2 = function() {
@@ -3385,8 +3428,10 @@ ${pad}</${tag}>`;
3385
3428
  const diff = document.createElement("span");
3386
3429
  diff.className = "el-group-diff";
3387
3430
  const parts = [];
3388
- for (const a of group.added) parts.push(`<span class="diff-add">+${a}</span>`);
3389
- for (const r of group.removed) parts.push(`<span class="diff-rem">-${r}</span>`);
3431
+ for (const a of group.added)
3432
+ parts.push(`<span class="diff-add">+${a}</span>`);
3433
+ for (const r of group.removed)
3434
+ parts.push(`<span class="diff-rem">-${r}</span>`);
3390
3435
  diff.innerHTML = parts.join(" ");
3391
3436
  row.appendChild(cb);
3392
3437
  row.appendChild(count);
@@ -3414,7 +3459,9 @@ ${pad}</${tag}>`;
3414
3459
  const removePicker = () => {
3415
3460
  shadowRoot.querySelectorAll(".highlight-preview").forEach((el) => el.remove());
3416
3461
  if (pickerCloseHandler) {
3417
- document.removeEventListener("click", pickerCloseHandler, { capture: true });
3462
+ document.removeEventListener("click", pickerCloseHandler, {
3463
+ capture: true
3464
+ });
3418
3465
  pickerCloseHandler = null;
3419
3466
  }
3420
3467
  pickerEl?.remove();
@@ -3473,7 +3520,7 @@ ${pad}</${tag}>`;
3473
3520
  const screenshotRow = document.createElement("button");
3474
3521
  screenshotRow.className = "draw-popover-item";
3475
3522
  screenshotRow.innerHTML = `
3476
- <span class="draw-popover-icon">\u{1F4F7}</span>
3523
+ <span class="draw-popover-icon"><svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><rect x="4" y="4" width="8" height="8" rx="1" ry="1"/><path d="M2,2H6V0H2C.895,0,0,.895,0,2V6H2V2Z"/><path d="M14,0h-4V2h4V6h2V2c0-1.105-.895-2-2-2Z"/><path d="M14,14h-4v2h4c1.105,0,2-.895,2-2v-4h-2v4Z"/><path d="M2,10H0v4c0,1.105,.895,2,2,2H6v-2H2v-4Z"/></svg></span>
3477
3524
  <span class="draw-popover-label">Screenshot & Annotate</span>
3478
3525
  `;
3479
3526
  screenshotRow.addEventListener("click", (e) => {
@@ -3527,7 +3574,10 @@ ${pad}</${tag}>`;
3527
3574
  async function clickHandler(e) {
3528
3575
  const composed = e.composedPath();
3529
3576
  if (composed.some((el) => el === shadowHost)) return;
3530
- if (composed.some((el) => el instanceof HTMLElement && el.hasAttribute("data-tw-design-canvas"))) return;
3577
+ if (composed.some(
3578
+ (el) => el instanceof HTMLElement && el.hasAttribute("data-tw-design-canvas")
3579
+ ))
3580
+ return;
3531
3581
  e.preventDefault();
3532
3582
  e.stopPropagation();
3533
3583
  const target = e.target;
@@ -3539,7 +3589,6 @@ ${pad}</${tag}>`;
3539
3589
  for (const node of result.exactMatch) {
3540
3590
  highlightElement(node);
3541
3591
  }
3542
- console.log(`[overlay] ${componentName} \u2014 ${result.exactMatch.length} exact matches highlighted`);
3543
3592
  const config = await fetchTailwindConfig();
3544
3593
  currentEquivalentNodes = result.exactMatch;
3545
3594
  currentTargetEl = targetEl;
@@ -3747,7 +3796,12 @@ ${pad}</${tag}>`;
3747
3796
  default:
3748
3797
  targetEl.insertAdjacentElement("beforebegin", wrapper);
3749
3798
  }
3750
- designCanvasWrappers.push({ wrapper, replacedNodes: null, parent: null, anchor: null });
3799
+ designCanvasWrappers.push({
3800
+ wrapper,
3801
+ replacedNodes: null,
3802
+ parent: null,
3803
+ anchor: null
3804
+ });
3751
3805
  iframe.addEventListener("load", () => {
3752
3806
  const contextMsg = {
3753
3807
  type: "ELEMENT_CONTEXT",
@@ -3776,23 +3830,36 @@ ${pad}</${tag}>`;
3776
3830
  return;
3777
3831
  }
3778
3832
  if (!areSiblings(currentEquivalentNodes)) {
3779
- showToast("Screenshot & Annotate requires all selected elements to be siblings in the DOM.");
3833
+ showToast(
3834
+ "Screenshot & Annotate requires all selected elements to be siblings in the DOM."
3835
+ );
3780
3836
  return;
3781
3837
  }
3782
3838
  let screenshot;
3783
3839
  let screenshotWidth;
3784
3840
  let screenshotHeight;
3785
3841
  try {
3786
- ({ dataUrl: screenshot, width: screenshotWidth, height: screenshotHeight } = await captureRegion(currentEquivalentNodes));
3842
+ ({
3843
+ dataUrl: screenshot,
3844
+ width: screenshotWidth,
3845
+ height: screenshotHeight
3846
+ } = await captureRegion(currentEquivalentNodes));
3787
3847
  } catch (err) {
3788
3848
  showToast("Screenshot capture failed");
3789
3849
  console.error("[overlay] captureRegion error:", err);
3790
3850
  return;
3791
3851
  }
3792
3852
  const parent = currentEquivalentNodes[0].parentElement;
3793
- const anchor = currentEquivalentNodes[0].nextSibling;
3853
+ if (!parent) {
3854
+ showToast("Cannot find parent element");
3855
+ return;
3856
+ }
3857
+ const marker = document.createComment("tw-placeholder");
3858
+ parent.insertBefore(marker, currentEquivalentNodes[0]);
3794
3859
  const firstStyle = getComputedStyle(currentEquivalentNodes[0]);
3795
- const lastStyle = getComputedStyle(currentEquivalentNodes[currentEquivalentNodes.length - 1]);
3860
+ const lastStyle = getComputedStyle(
3861
+ currentEquivalentNodes[currentEquivalentNodes.length - 1]
3862
+ );
3796
3863
  const marginTop = firstStyle.marginTop;
3797
3864
  const marginBottom = lastStyle.marginBottom;
3798
3865
  const marginLeft = firstStyle.marginLeft;
@@ -3834,12 +3901,9 @@ ${pad}</${tag}>`;
3834
3901
  display: block;
3835
3902
  `;
3836
3903
  wrapper.appendChild(iframe);
3837
- if (anchor) {
3838
- parent.insertBefore(wrapper, anchor);
3839
- } else {
3840
- parent.appendChild(wrapper);
3841
- }
3842
- designCanvasWrappers.push({ wrapper, replacedNodes, parent, anchor });
3904
+ parent.insertBefore(wrapper, marker);
3905
+ marker.remove();
3906
+ designCanvasWrappers.push({ wrapper, replacedNodes, parent, anchor: wrapper.nextSibling });
3843
3907
  iframe.addEventListener("load", () => {
3844
3908
  const contextMsg = {
3845
3909
  type: "ELEMENT_CONTEXT",
@@ -3912,11 +3976,18 @@ ${pad}</${tag}>`;
3912
3976
  setSelectMode(false);
3913
3977
  }
3914
3978
  } else if (msg.type === "PATCH_PREVIEW" && currentEquivalentNodes.length > 0) {
3915
- applyPreview(currentEquivalentNodes, msg.oldClass, msg.newClass, SERVER_ORIGIN);
3979
+ applyPreview(
3980
+ currentEquivalentNodes,
3981
+ msg.oldClass,
3982
+ msg.newClass,
3983
+ SERVER_ORIGIN
3984
+ );
3916
3985
  } else if (msg.type === "PATCH_PREVIEW_BATCH" && currentEquivalentNodes.length > 0) {
3917
3986
  applyPreviewBatch(currentEquivalentNodes, msg.pairs, SERVER_ORIGIN);
3918
3987
  } else if (msg.type === "PATCH_REVERT") {
3919
3988
  revertPreview();
3989
+ } else if (msg.type === "PATCH_REVERT_STAGED" && currentEquivalentNodes.length > 0) {
3990
+ applyPreview(currentEquivalentNodes, msg.oldClass, msg.newClass, SERVER_ORIGIN).then(() => commitPreview());
3920
3991
  } else if (msg.type === "PATCH_STAGE" && currentTargetEl && currentBoundary) {
3921
3992
  const state = getPreviewState();
3922
3993
  const originalClassMap = /* @__PURE__ */ new Map();
@@ -3927,7 +3998,12 @@ ${pad}</${tag}>`;
3927
3998
  }
3928
3999
  const targetElIndex = currentEquivalentNodes.indexOf(currentTargetEl);
3929
4000
  const originalClassString = state && targetElIndex !== -1 ? state.originalClasses[targetElIndex] : currentTargetEl.className;
3930
- const context = buildContext(currentTargetEl, msg.oldClass, msg.newClass, originalClassMap);
4001
+ const context = buildContext(
4002
+ currentTargetEl,
4003
+ msg.oldClass,
4004
+ msg.newClass,
4005
+ originalClassMap
4006
+ );
3931
4007
  send({
3932
4008
  type: "PATCH_STAGED",
3933
4009
  patch: {
@@ -4002,6 +4078,12 @@ ${pad}</${tag}>`;
4002
4078
  }
4003
4079
  }
4004
4080
  last.wrapper.remove();
4081
+ if (currentTargetEl && currentEquivalentNodes.length > 0) {
4082
+ for (const n of currentEquivalentNodes) {
4083
+ highlightElement(n);
4084
+ }
4085
+ showDrawButton(currentTargetEl);
4086
+ }
4005
4087
  }
4006
4088
  }
4007
4089
  });
@@ -4014,15 +4096,19 @@ ${pad}</${tag}>`;
4014
4096
  positionWithFlip(currentTargetEl, toolbarEl);
4015
4097
  }
4016
4098
  });
4017
- window.addEventListener("scroll", () => {
4018
- if (currentEquivalentNodes.length > 0) {
4019
- shadowRoot.querySelectorAll(".highlight-overlay").forEach((el) => el.remove());
4020
- currentEquivalentNodes.forEach((n) => highlightElement(n));
4021
- }
4022
- if (toolbarEl && currentTargetEl) {
4023
- positionWithFlip(currentTargetEl, toolbarEl);
4024
- }
4025
- }, { capture: true, passive: true });
4099
+ window.addEventListener(
4100
+ "scroll",
4101
+ () => {
4102
+ if (currentEquivalentNodes.length > 0) {
4103
+ shadowRoot.querySelectorAll(".highlight-overlay").forEach((el) => el.remove());
4104
+ currentEquivalentNodes.forEach((n) => highlightElement(n));
4105
+ }
4106
+ if (toolbarEl && currentTargetEl) {
4107
+ positionWithFlip(currentTargetEl, toolbarEl);
4108
+ }
4109
+ },
4110
+ { capture: true, passive: true }
4111
+ );
4026
4112
  if (sessionStorage.getItem(PANEL_OPEN_KEY) === "1") {
4027
4113
  active = true;
4028
4114
  btn.classList.add("active");