Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
One-time setup that gathers your project's design context and saves it to CLAUDE.md for future sessions.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/live-browser.js
1/**2* Impeccable Live Variant Mode - Browser Script3*4* Injected into the user's page via <script src="http://localhost:PORT/live.js">.5* The server prepends window.__IMPECCABLE_TOKEN__ and window.__IMPECCABLE_PORT__6* before this code.7*8* UI: a single floating bar that morphs between three states -9* configure (pick action + go), generating (progressive dots), and cycling10* (prev/next + accept/discard). Feels like Spotlight, not a modal.11*/12(function () {13'use strict';14if (typeof window === 'undefined') return;1516// Guard against double-init. Bun's HTML loader may process the <script> tag17// and create a bundled copy alongside the external load, or HMR may re-execute.18// Check BEFORE reading token/port to catch all cases.19if (window.__IMPECCABLE_LIVE_INIT__) return;20window.__IMPECCABLE_LIVE_INIT__ = true;2122const TOKEN = window.__IMPECCABLE_TOKEN__;23const PORT = window.__IMPECCABLE_PORT__;24if (!TOKEN || !PORT) {25window.__IMPECCABLE_LIVE_INIT__ = false; // reset so the real load can init26return;27}2829//30// Design tokens31//3233// Brand kinpaku (gold) is pinned to the site's neo-kinpaku tokens34// (see site/styles/kinpaku-tokens.css) so Accept / knobs / cycle-dots /35// the selection outline / the comment tag all match the site's accent,36// not a washed theme-adjusted one. These mirror the kit's picker37// colors in site/styles/kinpaku-kit.css; keep them in sync by hand.38const C = {39brand: 'oklch(84% 0.19 80.46)', // kinpaku gold40brandHov: 'oklch(86% 0.07 84)', // kinpaku-pale (hover lift)41brandSoft: 'oklch(84% 0.19 80.46 / 0.18)', // kinpaku-dim42ink: 'oklch(4% 0.004 95)', // lacquer-deep43ash: 'oklch(55% 0.018 82)', // warm muted text44paper: 'oklch(98% 0.005 95 / 0.92)', // light overlay on user pages45paperSolid:'oklch(98% 0.005 95)',46mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline47white: 'oklch(99% 0 0)',48};49// Picker bar chrome - mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.50// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand51// mark and the active control, not the container outline).52const PICKER_SHADOW =53'0 16px 36px -12px oklch(0% 0 0 / 0.6)';54const FONT = 'system-ui, -apple-system, sans-serif';55const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';56// z-index: detect overlays use 99999, so our UI must be above them57const Z = { highlight: 100001, bar: 100005, picker: 100007, toast: 100010 };58const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'; // ease-out-quint59const PREFIX = 'impeccable-live';60const PICK_CURSOR_CLASS = PREFIX + '-pick-cursor';61const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000;62const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({63prefix: PREFIX,64storage: localStorage,65idFactory: () => crypto.randomUUID().replace(/-/g, '').slice(0, 8),66});67if (!sessionState) {68console.error('[impeccable] live-browser-session.js was not loaded. Live mode cannot start safely.');69window.__IMPECCABLE_LIVE_INIT__ = false;70return;71}72const HIGHLIGHT_TRANSITION =73'top 140ms ' + EASE +74', left 140ms ' + EASE +75', width 140ms ' + EASE +76', height 140ms ' + EASE +77', opacity 150ms ease';78const TOOLTIP_TRANSITION =79'top 140ms ' + EASE + ', left 140ms ' + EASE + ', opacity 150ms ease';8081const SKIP_TAGS = new Set([82'html', 'head', 'body', 'script', 'style', 'link', 'meta', 'noscript', 'br', 'wbr',83]);8485// Command vocabulary (values + labels + icons) comes from the canonical source,86// skill/scripts/live/vocabulary.mjs, which live-server.mjs serializes into87// window.__IMPECCABLE_VOCAB__ when it serves /live.js (same injection path as88// the token/port above, so it is always present here). The icons stack above89// each chip label and recolor to C.brand when selected (strokes use90// currentColor). ACTIONS drives the picker grid; ICONS maps value -> svg.91const VOCAB = Array.isArray(window.__IMPECCABLE_VOCAB__) ? window.__IMPECCABLE_VOCAB__ : [];92const ICONS = {};93const ACTIONS = VOCAB.map((c) => {94ICONS[c.value] = c.icon;95return { value: c.value, label: c.label };96});9798const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions'];99const LIVE_UI_SURFACES = [100{ key: 'global-bottom-bar', ids: [PREFIX + '-global-bar', PREFIX + '-global-bar-brand', PREFIX + '-pick-toggle', PREFIX + '-insert-toggle', PREFIX + '-detect-toggle', PREFIX + '-detect-badge', PREFIX + '-design-toggle', PREFIX + '-page-chat', PREFIX + '-page-chat-input', PREFIX + '-page-chat-voice'] },101{ key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] },102{ key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] },103{ key: 'action-picker', ids: [PREFIX + '-picker'] },104{ key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] },105{ key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] },106{ key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] },107{ key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] },108{ key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] },109{ key: 'insert-mode-chrome', ids: [PREFIX + '-insert-line', PREFIX + '-insert-placeholder', PREFIX + '-placeholder-resize', PREFIX + '-insert-input', PREFIX + '-insert-voice', PREFIX + '-insert-create', PREFIX + '-insert-create-tooltip'] },110{ key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] },111{ key: 'design-system-panel', ids: [PREFIX + '-design-host'] },112{ key: 'toasts-and-errors', ids: [PREFIX + '-toast'] },113{ key: 'css-isolation-boundary', ids: [PREFIX + '-root'] },114];115const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))];116117//118// State119//120121let state = 'IDLE';122let hoveredElement = null;123let selectedElement = null;124let currentSessionId = null;125let expectedVariants = 0;126let arrivedVariants = 0;127let visibleVariant = 0;128let svelteComponentSession = null;129let svelteRuntimePromise = null;130let pendingSvelteComponentRetryObserver = null;131let currentSourceFile = null;132let currentPreviewFile = null;133let currentPreviewMode = null;134let recoveryWaitingForAnchor = false;135let pickedAnchorSnapshot = null;136let pendingVariantAnchorRetryObserver = null;137let pendingAcceptedSession = null;138let variantObserver = null;139let variantSelectionInFlight = false;140let variantSelectionPromise = null;141let recoveringEmptyCycling = false;142let hasProjectContext = false;143let selectedAction = 'impeccable';144let selectedCount = 3;145const browserOwner = sessionState.owner;146let checkpointTimer = null;147148// Scroll lock - holds window.scrollY at a fixed value while the session is149// active, so HMR DOM patches and variant swaps can't drift the page. See150// startScrollLock / stopScrollLock below.151let scrollLockObserver = null;152let scrollLockTargetY = null;153let scrollLockRaf = null;154let scrollLockAbort = null;155156// Dedicated key for scroll position - SEPARATE from LS_KEY so that157// saveSession's state updates don't clobber a carefully-captured scrollY.158// (Previously: saveSession wrote scrollY alongside state, so every call159// during resume overwrote the pre-reload value with whatever the browser160// had landed on, typically 0.)161function writeScrollY(y) { sessionState.writeScrollY(y); }162function readScrollY() { return sessionState.readScrollY(); }163function clearScrollY() { sessionState.clearScrollY(); }164165// Pre-empt the browser: apply manual scroll restoration and jump to the166// saved scrollY at script-parse time. Retries on fonts.ready and load167// are essential: scrollTo(y) clamps to the current document.scrollHeight,168// which is often hundreds of pixels short of the final value until169// async-loaded fonts swap in and reflow.170try {171history.scrollRestoration = 'manual';172const savedY = readScrollY();173if (savedY != null) {174const apply = () => {175if (Math.abs(window.scrollY - savedY) > 0.5) {176window.scrollTo(0, savedY);177}178};179apply();180if (document.fonts?.ready) document.fonts.ready.then(apply).catch(() => {});181window.addEventListener('load', apply, { once: true });182}183} catch {}184185// UI refs186let highlightEl = null;187let tooltipEl = null;188let barEl = null;189let barHideSeq = 0;190let pickerEl = null;191let toastEl = null;192let scrollRaf = null;193let editBadgeEl = null;194let editBadgeProxyRoot = null;195let editBadgeProxyByTarget = new Map();196197//198// Helpers199//200201const domHelpers = window.__IMPECCABLE_LIVE_DOM__?.createLiveBrowserDomHelpers({202prefix: PREFIX,203skipTags: SKIP_TAGS,204document,205});206if (!domHelpers) {207console.error('[impeccable] live-browser-dom.js was not loaded. Live mode cannot start safely.');208window.__IMPECCABLE_LIVE_INIT__ = false;209return;210}211const {212own,213pickable,214desc,215rectIsUsableAnchor,216makeFrozenAnchor,217id8,218cssId,219liveUiRoot,220uiAppend,221uiAppendStyle,222uiGetById,223activeElementDeep,224defangOutsideHandlers,225} = domHelpers;226227window.__IMPECCABLE_LIVE_CHROME_CORE__ = {228version: 1,229adapter: window.__IMPECCABLE_LIVE_ADAPTER__ || 'dom',230mountContract: LIVE_CHROME_MOUNT_CONTRACT,231surfaces: LIVE_UI_SURFACES,232componentIds: LIVE_UI_COMPONENT_IDS,233root: liveUiRoot,234append: uiAppend,235appendStyle: uiAppendStyle,236getById: uiGetById,237activeElementDeep,238debugState: () => ({239state,240currentSessionId,241expectedVariants,242arrivedVariants,243visibleVariant,244savedSession: loadSession(),245sourceFile: currentSourceFile,246previewFile: currentPreviewFile,247previewMode: currentPreviewMode,248barText: barEl?.textContent || null,249barConnected: !!barEl?.isConnected,250hasSvelteComponentSession: !!svelteComponentSession,251mountedSvelteVariant: svelteComponentSession?.mountedVariant || 0,252pendingSvelteComponentRetry: !!pendingSvelteComponentRetryObserver,253recoveryWaitingForAnchor,254evtSourceReadyState: evtSource ? evtSource.readyState : null,255}),256};257258//259// Highlight overlay260//261262function initHighlight() {263highlightEl = document.createElement('div');264highlightEl.id = PREFIX + '-highlight';265Object.assign(highlightEl.style, {266position: 'fixed', top: '0', left: '0', width: '0', height: '0',267border: '2px solid ' + C.brand, borderRadius: '3px',268pointerEvents: 'none', zIndex: Z.highlight, boxSizing: 'border-box',269transition: HIGHLIGHT_TRANSITION,270display: 'none', opacity: '0',271});272uiAppend(highlightEl);273274tooltipEl = document.createElement('div');275tooltipEl.id = PREFIX + '-tooltip';276Object.assign(tooltipEl.style, {277position: 'fixed',278background: C.ink, color: C.white,279fontFamily: MONO, fontSize: '10px', fontWeight: '500',280padding: '2px 6px', borderRadius: '3px',281zIndex: Z.highlight + 1, pointerEvents: 'none',282whiteSpace: 'nowrap', display: 'none',283letterSpacing: '0.02em',284transition: TOOLTIP_TRANSITION,285});286uiAppend(tooltipEl);287}288289function shouldShowHighlightTagTooltip() {290// Configure/edit carry the tag in the bar selection pill, so keep only the outline.291return state !== 'CONFIGURING' && state !== 'EDITING';292}293294function hideHighlightTagTooltip() {295if (!tooltipEl) return;296tooltipEl.style.opacity = '0';297tooltipEl.style.display = 'none';298}299300function showHighlight(el) {301if (!el || !highlightEl) return;302if (el.hasAttribute?.('data-impeccable-insert-placeholder')) return;303const r = el.getBoundingClientRect();304const top = (r.top - 2) + 'px', left = (r.left - 2) + 'px';305const width = (r.width + 4) + 'px', height = (r.height + 4) + 'px';306const showTagTooltip = shouldShowHighlightTagTooltip();307308const hiWasHidden = highlightEl.style.display === 'none' || highlightEl.style.opacity === '0';309if (hiWasHidden) {310// Snap to first target without animating from (0,0), then fade in.311highlightEl.style.transition = 'none';312Object.assign(highlightEl.style, { top, left, width, height, display: 'block' });313void highlightEl.offsetWidth;314highlightEl.style.transition = HIGHLIGHT_TRANSITION;315highlightEl.style.opacity = '1';316} else {317Object.assign(highlightEl.style, { top, left, width, height, display: 'block', opacity: '1' });318}319320if (!showTagTooltip) {321hideHighlightTagTooltip();322return;323}324325const tipTop = r.top - 20;326const tipY = (tipTop < 4 ? r.bottom + 4 : tipTop) + 'px';327const tipX = Math.max(4, r.left) + 'px';328tooltipEl.textContent = desc(el);329if (hiWasHidden) {330tooltipEl.style.transition = 'none';331Object.assign(tooltipEl.style, { top: tipY, left: tipX, display: 'block' });332void tooltipEl.offsetWidth;333tooltipEl.style.transition = TOOLTIP_TRANSITION;334tooltipEl.style.opacity = '1';335} else {336Object.assign(tooltipEl.style, { top: tipY, left: tipX, display: 'block', opacity: '1' });337}338}339340function hideHighlight() {341if (highlightEl) { highlightEl.style.opacity = '0'; highlightEl.style.display = 'none'; }342if (tooltipEl) { tooltipEl.style.opacity = '0'; tooltipEl.style.display = 'none'; }343}344345//346// Annotation overlay (comment pins + kinpaku strokes)347//348// Active while state === 'CONFIGURING'. The overlay is a fixed-positioned349// sibling of <body> mirroring selectedElement's bounding rect. Click (no350// drag) drops a comment pin; drag paints a kinpaku SVG stroke. All coords351// are stored in element-local CSS px so they survive scroll / resize and352// correlate directly with the captured PNG.353//354355const DRAG_THRESHOLD = 5; // px - below this, treat pointerup as a click356const PIN_DBL_CLICK_MS = 300; // two clicks on the same pin within this delete it357let annotOverlayEl = null;358let annotSvgEl = null;359let annotPinsEl = null;360let annotClearChipEl = null;361let annotState = { comments: [], strokes: [] };362let annotActive = false;363// `annotPointer` is either:364// { kind: 'new', x0, y0, moved, strokeEl, strokePoints } creating a stroke/pin365// { kind: 'pin', idx, startPointer, startPin, moved } dragging an existing pin366let annotPointer = null;367let annotEditing = null; // { idx, input, wrapEl }368let annotLastPinClick = { idx: -1, time: 0 }; // for click-click-to-delete369let placeholderResizeLayerEl = null;370let placeholderResizeDrag = null;371372function initAnnotOverlay() {373annotOverlayEl = document.createElement('div');374annotOverlayEl.id = PREFIX + '-annot';375Object.assign(annotOverlayEl.style, {376position: 'fixed', top: '0', left: '0', width: '0', height: '0',377pointerEvents: 'auto', zIndex: Z.highlight + 2,378display: 'none', overflow: 'visible',379cursor: 'crosshair', touchAction: 'none',380});381382annotSvgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');383annotSvgEl.id = PREFIX + '-annot-svg';384Object.assign(annotSvgEl.style, {385position: 'absolute', top: '0', left: '0',386width: '100%', height: '100%',387// The SVG itself doesn't absorb clicks; individual hit-paths opt-in via388// pointer-events=stroke so gaps still fall through to the overlay.389pointerEvents: 'none', overflow: 'visible',390});391annotOverlayEl.appendChild(annotSvgEl);392393annotPinsEl = document.createElement('div');394annotPinsEl.id = PREFIX + '-annot-pins';395Object.assign(annotPinsEl.style, {396position: 'absolute', inset: '0',397pointerEvents: 'none',398});399annotOverlayEl.appendChild(annotPinsEl);400401annotClearChipEl = document.createElement('div');402annotClearChipEl.id = PREFIX + '-annot-clear';403annotClearChipEl.dataset.annotClear = 'true';404annotClearChipEl.textContent = 'Clear';405Object.assign(annotClearChipEl.style, {406position: 'absolute', top: '8px', right: '8px',407background: C.ink, color: C.white,408fontFamily: FONT, fontSize: '10px', fontWeight: '500',409letterSpacing: '0.08em', textTransform: 'uppercase',410padding: '5px 12px', borderRadius: '999px',411cursor: 'pointer', pointerEvents: 'auto',412display: 'none', userSelect: 'none',413boxShadow: '0 1px 3px rgba(0,0,0,0.2)',414});415annotOverlayEl.appendChild(annotClearChipEl);416417placeholderResizeLayerEl = document.createElement('div');418placeholderResizeLayerEl.id = PREFIX + '-placeholder-resize';419Object.assign(placeholderResizeLayerEl.style, {420position: 'absolute',421inset: '0',422pointerEvents: 'none',423display: 'none',424zIndex: '2',425});426annotOverlayEl.appendChild(placeholderResizeLayerEl);427428annotOverlayEl.addEventListener('pointerdown', onAnnotDown);429annotOverlayEl.addEventListener('pointermove', onAnnotMove);430annotOverlayEl.addEventListener('pointerup', onAnnotUp);431annotOverlayEl.addEventListener('pointercancel', onAnnotUp);432uiAppend(annotOverlayEl);433// Modal-host friendliness: pointer-events is already 'auto' on this434// overlay; we only need to silence the host's outside-interaction435// listeners. Don't override pointer-events here (the overlay toggles436// visibility via display:none, which is fine).437defangOutsideHandlers(annotOverlayEl, { setPointerEvents: false });438}439440function updateClearChip() {441if (!annotClearChipEl) return;442const hasAny = annotState.comments.length > 0 || annotState.strokes.length > 0;443annotClearChipEl.style.display = hasAny ? 'block' : 'none';444}445446function showAnnotOverlay(el) {447if (!annotOverlayEl || !el) return;448annotActive = true;449positionAnnotOverlay(el);450annotOverlayEl.style.display = 'block';451syncPlaceholderResizeHandles();452}453454function hideAnnotOverlay() {455annotActive = false;456placeholderResizeDrag = null;457if (annotOverlayEl) annotOverlayEl.style.display = 'none';458syncPlaceholderResizeHandles();459// Drop any in-progress edit without touching annotState - clearAnnotations460// (if the caller is exiting configure mode) handles state reset.461annotEditing = null;462}463464function positionAnnotOverlay(el) {465if (!annotOverlayEl || !el) return;466const r = el.getBoundingClientRect();467Object.assign(annotOverlayEl.style, {468top: r.top + 'px', left: r.left + 'px',469width: r.width + 'px', height: r.height + 'px',470});471annotSvgEl.setAttribute('viewBox', '0 0 ' + r.width + ' ' + r.height);472syncPlaceholderResizeHandles();473}474475function clearAnnotations() {476annotState.comments = [];477annotState.strokes = [];478if (annotSvgEl) while (annotSvgEl.firstChild) annotSvgEl.removeChild(annotSvgEl.firstChild);479if (annotPinsEl) annotPinsEl.innerHTML = '';480annotPointer = null;481annotEditing = null;482annotLastPinClick = { idx: -1, time: 0 };483updateClearChip();484}485486// Rebuild the SVG layer. Each stroke gets a wider invisible hit path487// beneath the visible kinpaku path so clicks register on thin lines.488function redrawStrokes() {489while (annotSvgEl.firstChild) annotSvgEl.removeChild(annotSvgEl.firstChild);490annotState.strokes.forEach((s, idx) => {491const d = pointsToPath(s.points);492const hit = document.createElementNS('http://www.w3.org/2000/svg', 'path');493hit.setAttribute('d', d);494hit.setAttribute('stroke', 'transparent');495hit.setAttribute('stroke-width', '16');496hit.setAttribute('stroke-linecap', 'round');497hit.setAttribute('stroke-linejoin', 'round');498hit.setAttribute('fill', 'none');499hit.setAttribute('pointer-events', 'stroke');500hit.style.cursor = 'pointer';501hit.dataset.annotStroke = String(idx);502annotSvgEl.appendChild(hit);503const visible = document.createElementNS('http://www.w3.org/2000/svg', 'path');504visible.setAttribute('d', d);505visible.setAttribute('stroke', C.brand);506visible.setAttribute('stroke-width', '3');507visible.setAttribute('stroke-linecap', 'round');508visible.setAttribute('stroke-linejoin', 'round');509visible.setAttribute('fill', 'none');510visible.setAttribute('pointer-events', 'none');511annotSvgEl.appendChild(visible);512});513updateClearChip();514}515516function localCoords(e) {517const rect = annotOverlayEl.getBoundingClientRect();518return { x: e.clientX - rect.left, y: e.clientY - rect.top };519}520521function onAnnotDown(e) {522if (!annotActive) return;523524// 0) Insert placeholder edge resize - wins over draw / pins.525const resizeEdge = e.target.closest?.('[data-impeccable-placeholder-resize]')?.dataset.impeccablePlaceholderResize;526if (resizeEdge && configureKind === 'insert' && placeholderElement) {527startPlaceholderEdgeResize(resizeEdge, e);528return;529}530531// 1) Clear chip → wipe all annotations532if (e.target.closest?.('[data-annot-clear]')) {533if (annotEditing) annotEditing = null;534clearAnnotations();535renderAllPins();536redrawStrokes();537e.stopPropagation(); e.preventDefault();538return;539}540541// 2) Stroke hit path → delete that stroke542const strokeHit = e.target.closest?.('[data-annot-stroke]');543if (strokeHit) {544const idx = parseInt(strokeHit.dataset.annotStroke, 10);545if (Number.isInteger(idx)) {546annotState.strokes.splice(idx, 1);547redrawStrokes();548}549e.stopPropagation(); e.preventDefault();550return;551}552553// 3) Pin → drag, edit, or delete-on-double-click554const pinWrap = e.target.closest?.('[data-annot-pin]');555if (pinWrap) {556const idx = parseInt(pinWrap.dataset.annotPin, 10);557if (!Number.isInteger(idx)) return;558// Double-click (two pointerdowns on the same pin within window) → delete.559const now = Date.now();560if (annotLastPinClick.idx === idx && now - annotLastPinClick.time < PIN_DBL_CLICK_MS) {561if (annotEditing && annotEditing.idx === idx) annotEditing = null;562annotState.comments.splice(idx, 1);563annotLastPinClick = { idx: -1, time: 0 };564renderAllPins();565e.stopPropagation(); e.preventDefault();566return;567}568annotLastPinClick = { idx, time: now };569// If editing a different pin, commit that edit before starting here.570if (annotEditing && annotEditing.idx !== idx) finalizeEditingPin();571// If already editing THIS pin and the user clicked the dot, let the572// input keep focus (don't start a drag - the click wasn't meant as one).573if (annotEditing && annotEditing.idx === idx) return;574const p = localCoords(e);575const pin = annotState.comments[idx];576annotPointer = {577kind: 'pin', idx,578startPointer: p,579startPin: { x: pin.x, y: pin.y },580moved: false,581};582try { annotOverlayEl.setPointerCapture(e.pointerId); } catch {}583e.stopPropagation(); e.preventDefault();584return;585}586587// 4) Empty area → commit any open edit, then start new annotation588if (annotEditing) {589finalizeEditingPin();590e.stopPropagation(); e.preventDefault();591return;592}593const p = localCoords(e);594annotPointer = { kind: 'new', x0: p.x, y0: p.y, moved: false, strokeEl: null, strokePoints: null };595try { annotOverlayEl.setPointerCapture(e.pointerId); } catch {}596e.stopPropagation(); e.preventDefault();597}598599function onAnnotMove(e) {600if (!annotActive) return;601602if (placeholderResizeDrag) {603const d = placeholderResizeDrag;604const next = resizePlaceholderFromEdge(605d.start,606d.edge,607e.clientX - d.startX,608e.clientY - d.startY,609d.parentWidth,610);611applyPlaceholderDimensions(next);612e.stopPropagation();613return;614}615616if (!annotPointer) return;617const p = localCoords(e);618619if (annotPointer.kind === 'pin') {620const dx = p.x - annotPointer.startPointer.x;621const dy = p.y - annotPointer.startPointer.y;622if (!annotPointer.moved) {623if (Math.hypot(dx, dy) < DRAG_THRESHOLD) return;624annotPointer.moved = true;625}626const pin = annotState.comments[annotPointer.idx];627if (!pin) { annotPointer = null; return; }628pin.x = annotPointer.startPin.x + dx;629pin.y = annotPointer.startPin.y + dy;630renderAllPins();631e.stopPropagation();632return;633}634635// kind === 'new'636const dx = p.x - annotPointer.x0, dy = p.y - annotPointer.y0;637if (!annotPointer.moved) {638if (Math.hypot(dx, dy) < DRAG_THRESHOLD) return;639annotPointer.moved = true;640const strokeEl = document.createElementNS('http://www.w3.org/2000/svg', 'path');641strokeEl.setAttribute('stroke', C.brand);642strokeEl.setAttribute('stroke-width', '3');643strokeEl.setAttribute('stroke-linecap', 'round');644strokeEl.setAttribute('stroke-linejoin', 'round');645strokeEl.setAttribute('fill', 'none');646strokeEl.setAttribute('pointer-events', 'none');647annotSvgEl.appendChild(strokeEl);648annotPointer.strokeEl = strokeEl;649annotPointer.strokePoints = [[annotPointer.x0, annotPointer.y0]];650}651annotPointer.strokePoints.push([p.x, p.y]);652annotPointer.strokeEl.setAttribute('d', pointsToPath(annotPointer.strokePoints));653e.stopPropagation();654}655656function pointsToPath(points) {657if (!points || points.length === 0) return '';658let d = 'M' + points[0][0].toFixed(1) + ' ' + points[0][1].toFixed(1);659for (let i = 1; i < points.length; i++) {660d += ' L' + points[i][0].toFixed(1) + ' ' + points[i][1].toFixed(1);661}662return d;663}664665function onAnnotUp(e) {666if (placeholderResizeDrag) {667try { annotOverlayEl.releasePointerCapture(e.pointerId); } catch {}668placeholderResizeDrag = null;669e.stopPropagation();670return;671}672if (!annotActive || !annotPointer) return;673674if (annotPointer.kind === 'pin') {675const wasDrag = annotPointer.moved;676const idx = annotPointer.idx;677try { annotOverlayEl.releasePointerCapture(e.pointerId); } catch {}678annotPointer = null;679if (wasDrag) {680// A drag is an intentional reposition; a follow-up click shouldn't be681// interpreted as a double-click-to-delete.682annotLastPinClick = { idx: -1, time: 0 };683} else {684beginEditPin(idx);685}686e.stopPropagation();687return;688}689690// kind === 'new'691const wasDrag = annotPointer.moved;692if (wasDrag) {693annotState.strokes.push({ points: annotPointer.strokePoints });694// Swap the temporary preview SVG path for the full render with hit paths.695redrawStrokes();696} else {697const idx = annotState.comments.length;698annotState.comments.push({ x: annotPointer.x0, y: annotPointer.y0, text: '' });699renderAllPins();700beginEditPin(idx);701}702try { annotOverlayEl.releasePointerCapture(e.pointerId); } catch {}703annotPointer = null;704if (configureKind === 'insert') syncInsertCreateButton();705e.stopPropagation();706}707708function renderAllPins() {709annotPinsEl.innerHTML = '';710annotState.comments.forEach((c, idx) => {711annotPinsEl.appendChild(buildPinElement(c, idx));712});713updateClearChip();714}715716function buildPinElement(comment, idx) {717const interactive = idx >= 0;718const wrap = document.createElement('div');719if (interactive) wrap.dataset.annotPin = String(idx);720Object.assign(wrap.style, {721position: 'absolute',722left: (comment.x - 7) + 'px', top: (comment.y - 7) + 'px',723pointerEvents: interactive ? 'auto' : 'none',724display: 'flex', alignItems: 'flex-start', gap: '6px',725cursor: interactive ? 'grab' : 'default',726touchAction: 'none',727});728const dot = document.createElement('div');729Object.assign(dot.style, {730width: '14px', height: '14px', borderRadius: '50%',731background: C.brand, border: '2px solid ' + C.white,732boxShadow: '0 1px 3px rgba(0,0,0,0.25)',733flexShrink: '0',734});735wrap.appendChild(dot);736737if (comment.text) {738const bubble = document.createElement('div');739bubble.textContent = comment.text;740Object.assign(bubble.style, {741background: C.ink, color: C.white,742fontFamily: FONT, fontSize: '12px', lineHeight: '1.4',743padding: '4px 8px', borderRadius: '3px',744marginTop: '-2px', maxWidth: '220px',745pointerEvents: 'none', whiteSpace: 'pre-wrap',746wordBreak: 'break-word',747});748wrap.appendChild(bubble);749}750return wrap;751}752753function beginEditPin(idx) {754const wrapEl = annotPinsEl.querySelector('[data-annot-pin="' + idx + '"]');755if (!wrapEl) return;756// Strip any existing bubble (but keep the dot)757wrapEl.querySelectorAll('div:not(:first-child)').forEach(n => n.remove());758const input = document.createElement('input');759input.type = 'text';760input.placeholder = 'Note…';761Object.assign(input.style, {762background: C.ink, color: C.white,763fontFamily: FONT, fontSize: '12px', lineHeight: '1.4',764padding: '4px 8px', borderRadius: '3px',765border: '1px solid ' + C.brand,766outline: 'none', marginTop: '-2px',767width: '220px', pointerEvents: 'auto',768});769const originalText = annotState.comments[idx].text || '';770input.value = originalText;771wrapEl.appendChild(input);772annotEditing = { idx, input, wrapEl, originalText };773input.addEventListener('keydown', onAnnotInputKey, true);774input.addEventListener('blur', () => {775// Fires on both focus-loss and programmatic blur; commit unless we776// already handled it.777if (annotEditing && annotEditing.input === input) finalizeEditingPin();778});779// Stop clicks/pointerdowns inside the input from bubbling to the overlay780['pointerdown', 'click'].forEach(ev => {781input.addEventListener(ev, e => e.stopPropagation());782});783setTimeout(() => input.focus(), 0);784}785786function onAnnotInputKey(e) {787if (e.key === 'Enter') {788e.preventDefault(); e.stopPropagation();789finalizeEditingPin();790} else if (e.key === 'Escape') {791e.preventDefault(); e.stopPropagation();792cancelEditingPin();793} else {794// Keep arrows / backspace from hitting global handlers795e.stopPropagation();796}797}798799function finalizeEditingPin() {800if (!annotEditing) return;801const { idx, input } = annotEditing;802const text = input.value.trim();803annotEditing = null;804if (text) annotState.comments[idx].text = text;805else annotState.comments.splice(idx, 1);806renderAllPins();807}808809function cancelEditingPin() {810if (!annotEditing) return;811const { idx, originalText } = annotEditing;812annotEditing = null;813// If the pin had text before this edit, restore it. If it was a814// just-created empty pin, Escape removes it.815if (originalText) {816annotState.comments[idx].text = originalText;817} else {818annotState.comments.splice(idx, 1);819}820renderAllPins();821}822823// Build a detached annotation subtree suitable for injection into the clone824// modern-screenshot creates. Coordinates are element-local so this slots825// straight into an element that's been made position:relative. Takes an826// explicit snapshot so it works after annotState has been cleared.827function buildAnnotationsForCapture(rect, snapshot) {828const comments = snapshot ? snapshot.comments : annotState.comments;829const strokes = snapshot ? snapshot.strokes : annotState.strokes;830if (comments.length === 0 && strokes.length === 0) return null;831const wrap = document.createElement('div');832Object.assign(wrap.style, {833position: 'absolute', top: '0', left: '0',834width: rect.width + 'px', height: rect.height + 'px',835pointerEvents: 'none', overflow: 'visible',836});837if (strokes.length > 0) {838const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');839svg.setAttribute('viewBox', '0 0 ' + rect.width + ' ' + rect.height);840Object.assign(svg.style, {841position: 'absolute', top: '0', left: '0',842width: '100%', height: '100%', overflow: 'visible',843});844for (const s of strokes) {845const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');846path.setAttribute('stroke', C.brand);847path.setAttribute('stroke-width', '3');848path.setAttribute('stroke-linecap', 'round');849path.setAttribute('stroke-linejoin', 'round');850path.setAttribute('fill', 'none');851path.setAttribute('d', pointsToPath(s.points));852svg.appendChild(path);853}854wrap.appendChild(svg);855}856for (const c of comments) {857// idx=-1 means non-interactive; pointerEvents stay off in the clone858wrap.appendChild(buildPinElement(c, -1));859}860return wrap;861}862863//864// Element context extraction865//866867function stripManualEditRuntimeState(root) {868if (!root || root.nodeType !== 1) return;869unwrapMixedContentTextNodes(root);870const nodes = [root, ...root.querySelectorAll('[data-impeccable-editable], [data-impeccable-original-text], [data-impeccable-text-wrap]')];871for (const node of nodes) {872const runtimeEditable = node.hasAttribute('data-impeccable-editable')873|| node.hasAttribute('data-impeccable-original-text');874node.removeAttribute('data-impeccable-editable');875node.removeAttribute('data-impeccable-original-text');876node.removeAttribute('data-impeccable-text-wrap');877if (runtimeEditable) {878node.removeAttribute('contenteditable');879if (node.style) {880node.style.userSelect = '';881node.style.cursor = '';882node.style.outline = '';883node.style.webkitUserModify = '';884if (!node.getAttribute('style')?.trim()) node.removeAttribute('style');885}886}887}888}889890function sanitizedContextOuterHTML(el, maxLength) {891if (!el || !el.cloneNode) return '';892const clone = el.cloneNode(true);893stripManualEditRuntimeState(clone);894return clone.outerHTML ? clone.outerHTML.slice(0, maxLength) : '';895}896897function extractContext(el) {898const cs = getComputedStyle(el);899const r = el.getBoundingClientRect();900const props = {};901for (const sheet of document.styleSheets) {902try {903for (const rule of sheet.cssRules) {904if (rule.style) for (let i = 0; i < rule.style.length; i++) {905const p = rule.style[i];906if (p.startsWith('--') && !props[p]) {907const v = cs.getPropertyValue(p).trim();908if (v) props[p] = v;909}910}911}912} catch { /* cross-origin */ }913}914return {915tagName: el.tagName.toLowerCase(), id: el.id || null,916classes: [...el.classList],917textContent: (el.textContent || '').slice(0, 500),918outerHTML: sanitizedContextOuterHTML(el, 10000),919computedStyles: {920'font-family': cs.fontFamily, 'font-size': cs.fontSize,921'font-weight': cs.fontWeight, 'line-height': cs.lineHeight,922'color': cs.color, 'background': cs.background,923'background-color': cs.backgroundColor,924'padding': cs.padding, 'margin': cs.margin,925'display': cs.display, 'position': cs.position,926'gap': cs.gap, 'border-radius': cs.borderRadius,927'box-shadow': cs.boxShadow,928},929cssCustomProperties: props,930parentContext: el.parentElement931? '<' + el.parentElement.tagName.toLowerCase()932+ (el.parentElement.id ? ' id="' + el.parentElement.id + '"' : '')933+ (el.parentElement.className ? ' class="' + el.parentElement.className + '"' : '')934+ '>'935: null,936boundingRect: { width: Math.round(r.width), height: Math.round(r.height) },937};938}939940const MANUAL_CONTEXT_SKIP = { script: 1, style: 1, template: 1, noscript: 1, svg: 1, code: 1, pre: 1 };941942function contextElementForManualEdit(selectedEl, rows, ops) {943if (!selectedEl) return selectedEl;944const leafOnly =945rows && rows.length === 1 && rows[0] && rows[0].el === selectedEl;946if (!leafOnly) return selectedEl;947948const editedTexts = new Set();949for (const row of rows || []) addManualContextText(editedTexts, row.text);950for (const op of ops || []) {951addManualContextText(editedTexts, op.originalText);952addManualContextText(editedTexts, op.newText);953}954955let cur = selectedEl.parentElement;956let depth = 0;957while (cur && cur !== document.body && cur !== document.documentElement && depth < 4) {958if (own(cur)) break;959if (isUsefulManualEditContext(cur, selectedEl, editedTexts)) return cur;960cur = cur.parentElement;961depth++;962}963return selectedEl;964}965966function isUsefulManualEditContext(candidate, leafEl, editedTexts) {967if (!candidate || !candidate.contains(leafEl)) return false;968if (!candidate.id && candidate.classList.length === 0 && candidate.children.length < 2) return false;969return collectManualContextPieces(candidate, editedTexts).length > 0;970}971972function collectManualContextPieces(rootEl, editedTexts) {973const pieces = [];974function walk(node) {975if (!node) return;976if (node.nodeType === 3) {977const text = normalizeManualContextText(node.nodeValue);978if (isMeaningfulManualContextPiece(text, editedTexts)) pieces.push(text);979return;980}981if (node.nodeType !== 1) return;982const tag = node.tagName.toLowerCase();983if (MANUAL_CONTEXT_SKIP[tag]) return;984if (node !== rootEl && own(node)) return;985for (const child of node.childNodes) walk(child);986}987walk(rootEl);988return pieces.slice(0, 12);989}990991function addManualContextText(set, value) {992const text = normalizeManualContextText(value);993if (text) set.add(text);994}995996function isMeaningfulManualContextPiece(text, editedTexts) {997if (!text || text.length < 3 || text.length > 160) return false;998if (/^[\d.,+\-%\s]+$/.test(text)) return false;999return !editedTexts.has(text);1000}10011002function normalizeManualContextText(value) {1003return String(value || '').replace(/\s+/g, ' ').trim();1004}10051006//1007// The Bar - one floating element, three modes1008//10091010// Contextual-bar palette. Cached at init so every build*Row reads a1011// consistent set of colors; detectPageTheme runs once rather than on every1012// phase transition.1013let BP = null;10141015// Bar shadow variants. The default projects down + subtle around. When1016// the Tune popover opens below the bar, a downward shadow lands on the1017// dark popover and reads as a bright ghost line. We swap to UP-only while1018// tune is open below so the popover's top edge is clean.1019const BAR_SHADOW_DEFAULT = '0 4px 20px oklch(0% 0 0 / 0.08), 0 1px 3px oklch(0% 0 0 / 0.06)';1020const BAR_SHADOW_UP = '0 -4px 20px oklch(0% 0 0 / 0.08), 0 -1px 3px oklch(0% 0 0 / 0.06)';1021const BAR_SHADOW_DOWN = BAR_SHADOW_DEFAULT;10221023function initBar() {1024BP = barPaletteForTheme(detectPageTheme());1025barEl = document.createElement('div');1026barEl.id = PREFIX + '-bar';1027Object.assign(barEl.style, {1028position: 'fixed', zIndex: Z.bar,1029display: 'none', opacity: '0',1030transform: 'translateY(6px)',1031transition: 'opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,1032background: BP.surface,1033border: '1px solid ' + BP.border,1034borderRadius: '8px',1035boxShadow: BP.shadow,1036transition: 'box-shadow 0.2s ease, opacity 0.25s ' + EASE + ', transform 0.3s ' + EASE,1037fontFamily: FONT, fontSize: '13px', color: BP.text,1038padding: '5px',1039maxWidth: '560px', minWidth: '340px',1040});1041uiAppend(barEl);1042defangOutsideHandlers(barEl);1043}10441045function positionBar() {1046if (!barEl) return;1047const barH = barEl.offsetHeight || 44;1048const barW = barEl.offsetWidth || 380;1049const GLOBAL_BAR_RESERVE = 64; // global bar height + bottom margin + breathing room1050const GAP = 8;10511052// Recovery pins to document.body when the picked element is off-screen or1053// missing. Center the generating bar above the global bar instead of1054// stacking a duplicate toast in the same slot.1055if (recoveryWaitingForAnchor) {1056const barRect = globalBarEl?.getBoundingClientRect();1057const reserve = barRect && barRect.height > 01058? Math.max(GLOBAL_BAR_RESERVE, window.innerHeight - barRect.top + 12)1059: GLOBAL_BAR_RESERVE;1060const top = window.innerHeight - barH - reserve;1061const left = Math.max(GAP, (window.innerWidth - barW) / 2);1062Object.assign(barEl.style, { top: top + 'px', left: left + 'px' });1063return;1064}10651066const anchor = resolveBarAnchor();1067if (!anchor) return;1068const r = anchor.getBoundingClientRect();10691070// Prefer below the element; fall back to above; if neither fits (element1071// taller than viewport), pin to a stable viewport anchor so the bar1072// doesn't teleport between top and bottom as the user scrolls.1073let top;1074const belowTop = r.bottom + GAP;1075const aboveTop = r.top - barH - GAP;1076if (belowTop + barH + GAP <= window.innerHeight - GLOBAL_BAR_RESERVE) {1077top = belowTop;1078} else if (aboveTop >= GAP) {1079top = aboveTop;1080} else {1081top = window.innerHeight - barH - GLOBAL_BAR_RESERVE;1082}10831084let left = r.left + (r.width - barW) / 2;1085if (left < GAP) left = GAP;1086if (left + barW > window.innerWidth - GAP) left = window.innerWidth - barW - GAP;1087Object.assign(barEl.style, { top: top + 'px', left: left + 'px' });1088}10891090function showBar(mode) {1091barHideSeq += 1;1092if (mode === 'cycling' && !ensureCyclingRenderable('show-bar')) return;1093barEl.innerHTML = '';1094if (mode === 'configure') {1095barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());1096if (configureKind === 'insert') syncInsertCreateButton();1097applyConfigureBarChrome();1098} else {1099restorePickerBarChrome();1100if (mode === 'generating') {1101if (recoveryWaitingForAnchor) dismissToast();1102barEl.appendChild(buildGeneratingRow());1103} else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());1104}1105barEl.style.display = 'block';1106positionBar();1107requestAnimationFrame(() => {1108barEl.style.opacity = '1';1109barEl.style.transform = 'translateY(0)';1110syncPageChatFocus('show-bar');1111});1112}11131114function hideBar() {1115if (!barEl) return;1116const hideSeq = ++barHideSeq;1117stopVoice({ suppressSubmit: true });1118if (configureKind === 'insert') clearInsertPicking();1119barEl.style.opacity = '0';1120barEl.style.transform = 'translateY(6px)';1121setTimeout(() => { if (barEl && hideSeq === barHideSeq) barEl.style.display = 'none'; }, 250);1122hideActionPicker();1123closeTunePopover();1124hideConfigureBarTooltip();1125if (state === 'EDITING') restoreInlineEditDrafts();1126disableInlineEdit();1127}11281129function updateBarContent(mode) {1130if (!barEl || barEl.style.display === 'none') return;1131if (mode === 'cycling' && !ensureCyclingRenderable('update-bar')) return;1132barEl.innerHTML = '';1133if (mode === 'configure') {1134barEl.appendChild(configureKind === 'insert' ? buildInsertConfigureRow() : buildConfigureRow());1135if (configureKind === 'insert') syncInsertCreateButton();1136applyConfigureBarChrome();1137} else {1138restorePickerBarChrome();1139if (mode === 'generating') barEl.appendChild(buildGeneratingRow());1140else if (mode === 'cycling') barEl.appendChild(buildCyclingRow());1141else if (mode === 'saving') barEl.appendChild(buildSavingRow());1142else if (mode === 'confirmed') {1143barEl.appendChild(buildConfirmedRow());1144barEl.style.background = 'oklch(95% 0.05 145)';1145barEl.style.border = '1px solid oklch(75% 0.12 145 / 0.4)';1146}1147}1148syncPageChatFocus('update-bar-content');1149}11501151// Configure row: the floating bar surface IS the input; modifier pills sit left of the field.11521153const CONFIGURE_BAR_H = '36px';1154// Compact selection pill + 7px inset balances vertical centering in the 36px bar.1155const CONFIGURE_BAR_INSET = '7px';1156const CONFIGURE_PILL_RADIUS = '7px';1157const CONFIGURE_SELECTION_PILL_BORDER = '1px solid oklch(70% 0.12 188)';1158const CONFIGURE_SELECTION_PILL_PAD = '1px 4px';1159const CONFIGURE_ROW_FONT_SIZE = '12px';1160const CONFIGURE_ROW_TRACK_H = '18px';1161const CONFIGURE_PILL_PAD_Y = '3px';1162const CONFIGURE_BAR_SURFACE = 'oklch(15% 0.008 95)';1163const CONFIGURE_PILL_TEXT = 'oklch(94% 0.02 82)';1164const ICON_CONFIGURE_SUBMIT =1165'<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>';11661167function applyConfigureBarChrome() {1168if (!barEl) return;1169barEl.dataset.configureSurface = 'true';1170barEl.style.padding = '0';1171barEl.style.background = CONFIGURE_BAR_SURFACE;1172barEl.style.overflow = 'hidden';1173syncConfigureInputChrome();1174}11751176function restorePickerBarChrome() {1177if (!barEl) return;1178barEl.dataset.configureSurface = 'false';1179barEl.removeAttribute('data-input-focused');1180barEl.removeAttribute('data-voice-listening');1181barEl.style.padding = '5px';1182barEl.style.background = BP.surface;1183barEl.style.overflow = '';1184barEl.style.border = '1px solid ' + BP.border;1185barEl.style.borderColor = BP.border;1186barEl.style.boxShadow = BP.shadow;1187}11881189function syncConfigureInputChrome() {1190const input = uiGetById(PREFIX + '-input') || uiGetById(PREFIX + '-insert-input');1191const surface = barEl?.dataset.configureSurface === 'true' ? barEl : null;1192if (!surface || !input) return;1193const focused = activeElementDeep() === input;1194const listening = voiceListening && voiceCtx?.mode === 'configure';1195surface.dataset.inputFocused = focused ? 'true' : 'false';1196surface.dataset.voiceListening = listening ? 'true' : 'false';1197surface.style.borderColor = listening1198? BP.patinaSoft1199: (focused ? BP.accentSoft : BP.border);1200surface.style.boxShadow = BP.shadow;1201}12021203function configureBarPalette() {1204return BP || barPaletteForTheme(detectPageTheme());1205}12061207function configureRowTextMetrics(extra = {}) {1208return {1209fontFamily: FONT,1210fontSize: CONFIGURE_ROW_FONT_SIZE,1211fontWeight: '500',1212lineHeight: CONFIGURE_ROW_TRACK_H,1213...extra,1214};1215}12161217function configureInputFieldStyle(extra = {}) {1218return {1219flex: '1', minWidth: '0', width: '100%',1220padding: '0', margin: '0',1221border: 'none', background: 'transparent',1222boxSizing: 'border-box',1223height: CONFIGURE_ROW_TRACK_H,1224color: CONFIGURE_PILL_TEXT,1225caretColor: CONFIGURE_PILL_TEXT,1226outline: 'none',1227...configureRowTextMetrics(),1228...extra,1229};1230}12311232function configureInputShellStyle() {1233return {1234display: 'flex', alignItems: 'center', gap: '6px',1235flex: '1', minWidth: '0', height: '100%',1236padding: '0 6px 0 ' + CONFIGURE_BAR_INSET,1237};1238}12391240function configureSelectionPillStyle(extra = {}) {1241const P = configureBarPalette();1242return {1243display: 'inline-flex', alignItems: 'center', justifyContent: 'center',1244gap: '2px', height: 'auto', flexShrink: '0',1245padding: CONFIGURE_SELECTION_PILL_PAD,1246boxSizing: 'border-box',1247border: CONFIGURE_SELECTION_PILL_BORDER,1248borderRadius: CONFIGURE_PILL_RADIUS,1249background: 'transparent',1250color: P.patina,1251cursor: 'pointer',1252transition: 'background 0.15s ease, color 0.15s ease, border-color 0.15s ease',1253whiteSpace: 'nowrap',1254...configureRowTextMetrics({1255fontFamily: MONO, fontWeight: '600', letterSpacing: '-0.01em',1256}),1257...extra,1258};1259}12601261function configureModifierPillStyle(extra = {}) {1262const P = configureBarPalette();1263return {1264display: 'inline-flex', alignItems: 'center', justifyContent: 'center',1265gap: '2px', height: 'auto', minHeight: CONFIGURE_ROW_TRACK_H,1266padding: CONFIGURE_PILL_PAD_Y + ' 8px', flexShrink: '0',1267boxSizing: 'border-box',1268border: '1px solid transparent',1269borderRadius: CONFIGURE_PILL_RADIUS,1270background: 'transparent',1271color: P.textDim, cursor: 'pointer',1272transition: 'background 0.15s ease, color 0.15s ease, border-color 0.15s ease',1273whiteSpace: 'nowrap',1274...configureRowTextMetrics(),1275...extra,1276};1277}12781279function configureInlineControlStyle(extra = {}) {1280const P = configureBarPalette();1281return {1282display: 'inline-flex', alignItems: 'center', justifyContent: 'center',1283gap: '2px', height: CONFIGURE_ROW_TRACK_H, flexShrink: '0',1284padding: '0', margin: '0',1285boxSizing: 'border-box',1286border: 'none', borderRadius: '0',1287background: 'transparent',1288color: P.textDim, cursor: 'pointer',1289transition: 'color 0.12s ease, background 0.12s ease',1290whiteSpace: 'nowrap',1291...configureRowTextMetrics(),1292...extra,1293};1294}12951296function bindConfigureInlineControlHover(btn, controlsLocked) {1297btn.addEventListener('mouseenter', () => {1298if (controlsLocked) return;1299const P = configureBarPalette();1300btn.style.color = P.text;1301});1302btn.addEventListener('mouseleave', () => {1303if (controlsLocked) return;1304btn.style.color = configureBarPalette().textDim;1305});1306}13071308function bindConfigureModifierPillHover(btn, controlsLocked) {1309btn.addEventListener('mouseenter', () => {1310if (controlsLocked) return;1311const P = configureBarPalette();1312btn.style.color = P.text;1313btn.style.background = P.toggleActive;1314});1315btn.addEventListener('mouseleave', () => {1316if (controlsLocked) return;1317const P = configureBarPalette();1318btn.style.color = P.textDim;1319btn.style.background = 'transparent';1320});1321}13221323let configureBarTooltipEl = null;13241325function ensureConfigureBarTooltip() {1326if (configureBarTooltipEl) return configureBarTooltipEl;1327const P = configureBarPalette();1328configureBarTooltipEl = el('div', {1329position: 'fixed',1330display: 'none',1331zIndex: String(Z.bar + 7),1332pointerEvents: 'none',1333maxWidth: 'min(360px, calc(100vw - 16px))',1334padding: '6px 9px',1335borderRadius: '7px',1336background: P.chatSurface,1337border: '1px solid ' + P.hairline,1338boxShadow: P.shadow,1339color: P.text,1340fontFamily: FONT,1341fontSize: '11px',1342fontWeight: '500',1343lineHeight: '1.35',1344letterSpacing: '0.01em',1345whiteSpace: 'normal',1346wordBreak: 'break-word',1347});1348configureBarTooltipEl.id = PREFIX + '-configure-bar-tooltip';1349uiAppend(configureBarTooltipEl);1350return configureBarTooltipEl;1351}13521353function showConfigureBarTooltip(anchor, message) {1354if (!anchor || !message) return;1355const tip = ensureConfigureBarTooltip();1356tip.textContent = message;1357tip.style.transition = 'none';1358tip.style.display = 'block';1359tip.style.opacity = '1';1360const r = anchor.getBoundingClientRect();1361const tipW = tip.offsetWidth;1362const tipH = tip.offsetHeight;1363const left = Math.max(8, Math.min(window.innerWidth - tipW - 8, r.left + r.width / 2 - tipW / 2));1364const top = Math.max(8, r.top - tipH - 8);1365tip.style.left = left + 'px';1366tip.style.top = top + 'px';1367}13681369function hideConfigureBarTooltip() {1370if (!configureBarTooltipEl) return;1371configureBarTooltipEl.style.display = 'none';1372configureBarTooltipEl.style.opacity = '0';1373}13741375function selectionTagLabel(el) {1376if (!el) return '';1377if (el.hasAttribute?.('data-impeccable-insert-placeholder')) return 'slot';1378return el.tagName.toLowerCase();1379}13801381function elementPath(el, maxDepth = 8) {1382if (!el) return '';1383const parts = [];1384let node = el;1385while (node && node.nodeType === 1 && node !== document.body) {1386let part = node.tagName.toLowerCase();1387if (node.id) part += '#' + node.id;1388else if (node.classList?.length) part += '.' + [...node.classList].slice(0, 2).join('.');1389parts.unshift(part);1390node = node.parentElement;1391if (parts.length >= maxDepth) break;1392}1393return parts.join(' \u203a ');1394}13951396function variantCountTooltipText(count) {1397const n = Number(count) || selectedCount;1398const word = n === 1 ? 'variant' : 'variants';1399return 'Click to change \u00b7 ' + n + ' ' + word;1400}14011402function removeConfigureSelection() {1403hideConfigureBarTooltip();1404if (configureKind === 'insert') {1405cancelInsertConfigure();1406return;1407}1408selectedElement = null;1409exitConfigureToPicking('selection-pill-remove', { clearHover: true });1410}14111412function buildSelectionPill({ el: targetEl, controlsLocked }) {1413const tag = selectionTagLabel(targetEl);1414const path = elementPath(targetEl);1415const P = configureBarPalette();1416const pill = el('button', configureSelectionPillStyle({ minWidth: '32px' }));1417pill.id = PREFIX + '-selection-pill';1418pill.type = 'button';1419pill.setAttribute('aria-label', 'Selected element: ' + tag);1420pill.disabled = controlsLocked;1421pill.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';1422pill.style.opacity = controlsLocked ? '0.58' : '1';1423pill.style.flexShrink = '0';14241425const faceStack = el('span', {1426display: 'grid', placeItems: 'center',1427width: '100%', minWidth: '1.25em',1428lineHeight: CONFIGURE_ROW_TRACK_H,1429});1430const tagFace = el('span', {1431gridArea: '1 / 1',1432transition: 'opacity 0.12s ease',1433color: P.patina,1434});1435const clearFace = el('span', {1436gridArea: '1 / 1',1437opacity: '0',1438transition: 'opacity 0.12s ease',1439color: 'oklch(58% 0.15 35)',1440});1441tagFace.textContent = tag;1442clearFace.textContent = '\u00D7';1443faceStack.appendChild(tagFace);1444faceStack.appendChild(clearFace);1445pill.appendChild(faceStack);14461447const setArmed = (armed) => {1448tagFace.style.opacity = armed ? '0' : '1';1449clearFace.style.opacity = armed ? '1' : '0';1450pill.style.background = armed ? P.toggleActive : 'transparent';1451pill.style.border = CONFIGURE_SELECTION_PILL_BORDER;1452pill.setAttribute('aria-label', armed ? 'Clear selection' : 'Selected element: ' + tag);1453};1454const arm = () => {1455if (controlsLocked) {1456showConfigureBarTooltip(pill, 'Apply is still running');1457return;1458}1459setArmed(true);1460if (path) showConfigureBarTooltip(pill, path);1461};1462const disarm = () => {1463hideConfigureBarTooltip();1464setArmed(false);1465};1466pill.addEventListener('mouseenter', arm);1467pill.addEventListener('mouseleave', disarm);1468pill.addEventListener('focus', arm);1469pill.addEventListener('blur', disarm);1470pill.addEventListener('click', (e) => {1471e.stopPropagation();1472if (controlsLocked) { showManualApplyBusyToast(); return; }1473removeConfigureSelection();1474});1475return pill;1476}14771478function bindConfigureCountPillTooltip(count, controlsLocked) {1479count.removeAttribute('title');1480count.addEventListener('mouseenter', () => {1481if (controlsLocked) {1482showConfigureBarTooltip(count, 'Apply is still running');1483return;1484}1485showConfigureBarTooltip(count, variantCountTooltipText(selectedCount));1486});1487count.addEventListener('mouseleave', hideConfigureBarTooltip);1488}14891490function buildConfigureActionControl({ controlsLocked, onClick }) {1491const control = el('button', configureInlineControlStyle());1492const label = document.createElement('span');1493label.textContent = actionLabel();1494const caret = el('span', {1495fontSize: '10px', lineHeight: '1',1496marginLeft: '2px', pointerEvents: 'none',1497color: 'inherit',1498});1499caret.textContent = '\u25BE';1500caret.setAttribute('aria-hidden', 'true');1501control.appendChild(label);1502control.appendChild(caret);1503control.disabled = controlsLocked;1504control.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';1505control.style.opacity = controlsLocked ? '0.58' : '1';1506bindConfigureInlineControlHover(control, controlsLocked);1507control.addEventListener('click', onClick);1508return control;1509}15101511const VARIANT_COUNT_MIN = 1;1512const VARIANT_COUNT_MAX = 4;15131514function cycleSelectedCount() {1515if (selectedCount >= VARIANT_COUNT_MAX) selectedCount = VARIANT_COUNT_MIN;1516else selectedCount += 1;1517return selectedCount;1518}15191520function buildConfigureCountControl({ controlsLocked, onClick }) {1521const count = el('button', configureInlineControlStyle({1522fontFamily: MONO, fontWeight: '600', letterSpacing: '0',1523}));1524count.textContent = '\u00D7' + selectedCount;1525count.disabled = controlsLocked;1526count.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';1527count.style.opacity = controlsLocked ? '0.58' : '1';1528bindConfigureInlineControlHover(count, controlsLocked);1529bindConfigureCountPillTooltip(count, controlsLocked);1530count.addEventListener('click', onClick);1531return count;1532}15331534function buildConfigureVoiceButton({ id, controlsLocked, onClick }) {1535const voiceBtn = el('button', {1536display: 'inline-flex', alignItems: 'center', justifyContent: 'center',1537boxSizing: 'border-box',1538width: CONFIGURE_BAR_H, height: '100%', flexShrink: '0',1539padding: '0', margin: '0',1540border: 'none', borderRight: '1px solid ' + BP.hairline,1541borderRadius: '0', background: 'transparent',1542color: BP.textDim, cursor: 'pointer',1543transition: 'color 0.12s ease, background 0.12s ease',1544});1545voiceBtn.id = id;1546voiceBtn.type = 'button';1547voiceBtn.setAttribute('aria-label', 'Voice input');1548voiceBtn.innerHTML = ICON_PAGE_VOICE;1549voiceBtn.disabled = controlsLocked;1550voiceBtn.style.cursor = controlsLocked ? 'not-allowed' : 'pointer';1551voiceBtn.style.opacity = controlsLocked ? '0.58' : '1';1552voiceBtn.addEventListener('mousedown', (e) => e.stopPropagation());1553voiceBtn.addEventListener('click', onClick);1554return voiceBtn;1555}15561557function buildConfigureTrailingCluster(controls, voiceBtn, submitBtn) {1558const cluster = el('div', {1559display: 'inline-flex', alignItems: 'stretch', flexShrink: '0',1560height: '100%', borderLeft: '1px solid ' + BP.hairline,1561});1562if (controls.length) {1563const controlsWrap = el('div', {1564display: 'inline-flex', alignItems: 'center', gap: '8px',1565padding: '0 10px', flexShrink: '0', height: '100%',1566});1567controls.forEach((control) => controlsWrap.appendChild(control));1568cluster.appendChild(controlsWrap);1569}1570voiceBtn.style.borderLeft = '1px solid ' + BP.hairline;1571cluster.appendChild(voiceBtn);1572cluster.appendChild(submitBtn);1573return cluster;1574}15751576function buildConfigureSubmitButton({ controlsLocked, onClick, ariaLabel }) {1577const btn = el('button', {1578display: 'inline-flex', alignItems: 'center', justifyContent: 'center',1579boxSizing: 'border-box', width: CONFIGURE_BAR_H, height: CONFIGURE_BAR_H,1580padding: '0', flexShrink: '0',1581border: 'none', borderLeft: '1px solid ' + BP.hairline,1582borderRadius: '0',1583background: BP.accent, color: C.ink,1584cursor: controlsLocked ? 'not-allowed' : 'pointer',1585transition: 'filter 0.12s ease, transform 0.1s ease',1586});1587btn.type = 'button';1588btn.setAttribute('aria-label', ariaLabel);1589btn.innerHTML = ICON_CONFIGURE_SUBMIT;1590btn.disabled = controlsLocked;1591btn.style.opacity = controlsLocked ? '0.58' : '1';1592if (controlsLocked) btn.title = 'Apply is still running';1593btn.addEventListener('mouseenter', () => { if (!controlsLocked) btn.style.filter = 'brightness(1.1)'; });1594btn.addEventListener('mouseleave', () => btn.style.filter = 'none');1595btn.addEventListener('mousedown', () => { if (!controlsLocked) btn.style.transform = 'scale(0.97)'; });1596btn.addEventListener('mouseup', () => btn.style.transform = 'scale(1)');1597btn.addEventListener('click', onClick);1598return btn;1599}16001601// Insert mode helpers (mirrors skill/scripts/live/insert-ui.mjs)16021603function detectInsertAxisFromStyle(style) {1604const display = style?.display || 'block';1605if (display.includes('flex')) {1606const dir = style.flexDirection || 'row';1607return dir.startsWith('row') ? 'row' : 'column';1608}1609if (display === 'grid' || display === 'inline-grid') {1610const flow = style.gridAutoFlow || 'row';1611if (flow.includes('column')) return 'column';1612const cols = (style.gridTemplateColumns || '').trim();1613if (cols && cols !== 'none') {1614const colCount = cols.split(/\s+/).filter(Boolean).length;1615if (colCount > 1) return 'row';1616}1617return 'row';1618}1619return 'column';1620}16211622function detectInsertAxis(parent) {1623if (!parent || parent.nodeType !== 1) return 'column';1624const st = getComputedStyle(parent);1625return detectInsertAxisFromStyle({1626display: st.display,1627flexDirection: st.flexDirection,1628gridTemplateColumns: st.gridTemplateColumns,1629gridAutoFlow: st.gridAutoFlow,1630});1631}16321633function layoutFlowChildren(parent) {1634if (!parent) return [];1635return [...parent.children]1636.filter(pickable)1637.map((el) => ({ el, rect: el.getBoundingClientRect() }));1638}16391640function computeInsertPosition(clientX, clientY, rect, axis) {1641axis = axis || 'column';1642if (!rect) return 'after';1643if (axis === 'row') {1644if (!Number.isFinite(rect.width) || rect.width <= 0) return 'after';1645return clientX < rect.left + rect.width / 2 ? 'before' : 'after';1646}1647if (!Number.isFinite(rect.height) || rect.height <= 0) return 'after';1648return clientY < rect.top + rect.height / 2 ? 'before' : 'after';1649}16501651function groupSiblingRows(siblings, rowThreshold) {1652rowThreshold = rowThreshold ?? 8;1653const sorted = [...siblings].sort((a, b) => a.rect.top - b.rect.top || a.rect.left - b.rect.left);1654const rows = [];1655for (const entry of sorted) {1656let placed = false;1657for (const row of rows) {1658if (Math.abs(entry.rect.top - row[0].rect.top) <= rowThreshold) {1659row.push(entry);1660placed = true;1661break;1662}1663}1664if (!placed) rows.push([entry]);1665}1666return rows;1667}16681669function horizontalOverlap(a, b) {1670const left = Math.max(a.left, b.left);1671const right = Math.min(a.right, b.right);1672return Math.max(0, right - left);1673}16741675function hitSiblingInsertGap(clientX, clientY, siblings, opts) {1676opts = opts || {};1677if (!siblings || siblings.length < 2) return null;1678const slop = opts.slop ?? 12;1679const minOverlap = opts.minOverlap ?? 0.25;16801681for (const row of groupSiblingRows(siblings)) {1682if (row.length < 2) continue;1683const sorted = [...row].sort((a, b) => a.rect.left - b.rect.left);1684for (let i = 0; i < sorted.length - 1; i++) {1685const a = sorted[i];1686const b = sorted[i + 1];1687const aRight = a.rect.right;1688const bLeft = b.rect.left;1689if (bLeft <= aRight) continue;1690const top = Math.max(a.rect.top, b.rect.top);1691const bottom = Math.min(a.rect.bottom, b.rect.bottom);1692const span = bottom - top;1693const minH = Math.min(a.rect.height, b.rect.height);1694if (span < minH * minOverlap) continue;1695const inX = clientX >= aRight - slop && clientX <= bLeft + slop;1696const inY = clientY >= top - slop && clientY <= bottom + slop;1697if (!inX || !inY) continue;1698return {1699anchor: b.el,1700position: 'before',1701axis: 'row',1702line: { axis: 'row', left: (aRight + bLeft) / 2, top, width: 0, height: span },1703};1704}1705}17061707const sortedCol = [...siblings].sort((a, b) => a.rect.top - b.rect.top || a.rect.left - b.rect.left);1708for (let i = 0; i < sortedCol.length - 1; i++) {1709const a = sortedCol[i];1710const b = sortedCol[i + 1];1711const overlap = horizontalOverlap(a.rect, b.rect);1712const minW = Math.min(a.rect.width, b.rect.width);1713if (overlap < minW * minOverlap) continue;1714const gapTop = a.rect.bottom;1715const gapBottom = b.rect.top;1716if (gapBottom <= gapTop) continue;1717const overlapLeft = Math.max(a.rect.left, b.rect.left);1718const overlapRight = Math.min(a.rect.right, b.rect.right);1719const inY = clientY >= gapTop - slop && clientY <= gapBottom + slop;1720const inX = clientX >= overlapLeft - slop && clientX <= overlapRight + slop;1721if (!inY || !inX) continue;1722return {1723anchor: b.el,1724position: 'before',1725axis: 'column',1726line: { axis: 'column', top: (gapTop + gapBottom) / 2, left: overlapLeft, width: overlap, height: 0 },1727};1728}1729return null;1730}17311732function insertLineCoords(rect, position, axis) {1733axis = axis || 'column';1734if (axis === 'row') {1735const x = position === 'before' ? rect.left - 2 : rect.right + 2;1736return { axis: 'row', top: rect.top, left: x, width: 0, height: rect.height };1737}1738const y = position === 'before' ? rect.top - 2 : rect.bottom + 2;1739return { axis: 'column', top: y, left: rect.left, width: rect.width, height: 0 };1740}17411742function resolveInsertHover({ clientX, clientY, target, rect, axis, siblings }) {1743const gap = hitSiblingInsertGap(clientX, clientY, siblings);1744if (gap) return gap;1745const position = computeInsertPosition(clientX, clientY, rect, axis);1746const line = insertLineCoords(rect, position, axis);1747return { anchor: target, position, axis, line };1748}17491750function cursorForInsertAxis(axis) {1751return axis === 'row' ? 'ew-resize' : 'ns-resize';1752}17531754function placeholderSizing({ axis, parentDisplay, parentWidth, anchorFlex }) {1755const display = parentDisplay || 'block';1756const w = Number.isFinite(parentWidth) ? parentWidth : 0;1757if (axis === 'row') {1758if (display.includes('flex')) {1759const flex = anchorFlex && anchorFlex !== 'none' && anchorFlex !== '0 1 auto'1760? anchorFlex1761: '1 1 0';1762return { kind: 'flex', flex, minWidth: 0 };1763}1764if (display === 'grid' || display === 'inline-grid') return { kind: 'auto' };1765}1766if (w >= PLACEHOLDER_MIN_WIDTH) return { kind: 'percent' };1767return {1768kind: 'explicit',1769width: Math.max(PLACEHOLDER_MIN_WIDTH, w || PLACEHOLDER_MIN_WIDTH),1770};1771}17721773function placeholderWidthIsImplicit(kind) {1774return kind === 'flex' || kind === 'percent' || kind === 'auto';1775}17761777function applyPlaceholderSizingStyles(placeholder, sizing) {1778placeholder.dataset.impeccablePlaceholderWidth = sizing.kind;1779placeholder.style.flex = '';1780placeholder.style.minWidth = '';1781placeholder.style.maxWidth = '';1782placeholder.style.width = '';1783if (sizing.kind === 'flex') {1784placeholder.style.flex = sizing.flex;1785placeholder.style.minWidth = sizing.minWidth + 'px';1786} else if (sizing.kind === 'percent') {1787placeholder.style.width = '100%';1788placeholder.style.maxWidth = '100%';1789} else if (sizing.kind === 'explicit') {1790placeholder.style.width = sizing.width + 'px';1791}1792}17931794function materializePlaceholderWidth(placeholder) {1795if (!placeholder) return;1796const kind = placeholder.dataset.impeccablePlaceholderWidth;1797if (!placeholderWidthIsImplicit(kind)) return;1798const w = Math.max(PLACEHOLDER_MIN_WIDTH, Math.round(placeholder.offsetWidth));1799placeholder.style.flex = '';1800placeholder.style.minWidth = '';1801placeholder.style.maxWidth = '';1802placeholder.style.width = w + 'px';1803placeholder.dataset.impeccablePlaceholderWidth = 'explicit';1804}18051806function canCreateInsert({ prompt, comments, strokes }) {1807const hasPrompt = typeof prompt === 'string' && prompt.trim().length > 0;1808const hasComments = Array.isArray(comments) && comments.length > 0;1809const hasStrokes = Array.isArray(strokes) && strokes.some(1810(s) => Array.isArray(s?.points) && s.points.length >= 2,1811);1812return hasPrompt || hasComments || hasStrokes;1813}18141815function insertCreateDisabledReason({ prompt, comments, strokes }) {1816if (canCreateInsert({ prompt, comments, strokes })) return null;1817return 'Add a prompt or annotate the placeholder to create';1818}18191820function clampPlaceholderSize(width, height, parentWidth) {1821const maxW = Math.max(PLACEHOLDER_MIN_WIDTH, parentWidth || PLACEHOLDER_MIN_WIDTH);1822return {1823width: Math.min(maxW, Math.max(PLACEHOLDER_MIN_WIDTH, Math.round(width))),1824height: Math.max(PLACEHOLDER_MIN_HEIGHT, Math.round(height)),1825};1826}18271828function cursorForPlaceholderEdge(edge) {1829if (edge === 'n' || edge === 's') return 'ns-resize';1830if (edge === 'e' || edge === 'w') return 'ew-resize';1831return 'default';1832}18331834function resizePlaceholderFromEdge(start, edge, dx, dy, parentWidth) {1835const base = {1836width: start.width,1837height: start.height,1838marginLeft: start.marginLeft ?? 0,1839marginTop: start.marginTop ?? 0,1840};1841if (edge === 'e') base.width = start.width + dx;1842else if (edge === 'w') {1843base.width = start.width - dx;1844base.marginLeft = start.marginLeft + dx;1845} else if (edge === 's') base.height = start.height + dy;1846else if (edge === 'n') {1847base.height = start.height - dy;1848base.marginTop = start.marginTop + dy;1849}1850const clamped = clampPlaceholderSize(base.width, base.height, parentWidth);1851if (edge === 'w') base.marginLeft = start.marginLeft + start.width - clamped.width;1852else if (edge === 'n') base.marginTop = start.marginTop + start.height - clamped.height;1853return {1854width: clamped.width,1855height: clamped.height,1856marginLeft: Math.round(base.marginLeft),1857marginTop: Math.round(base.marginTop),1858};1859}18601861function ensureInsertLine() {1862if (insertLineEl) return insertLineEl;1863insertLineEl = document.createElement('div');1864insertLineEl.id = PREFIX + '-insert-line';1865Object.assign(insertLineEl.style, {1866position: 'fixed',1867zIndex: String(Z.highlight),1868height: '0',1869borderTop: '2px dotted ' + C.brand,1870pointerEvents: 'none',1871display: 'none',1872opacity: '0.9',1873});1874uiAppend(insertLineEl);1875defangOutsideHandlers(insertLineEl);1876return insertLineEl;1877}18781879function showInsertLine(resolved) {1880if (!resolved?.anchor || !resolved.line) return;1881const line = ensureInsertLine();1882const coords = resolved.line;1883if (coords.axis === 'row') {1884Object.assign(line.style, {1885display: 'block',1886top: coords.top + 'px',1887left: coords.left + 'px',1888width: '0',1889height: coords.height + 'px',1890borderTop: 'none',1891borderLeft: '2px dotted ' + C.brand,1892});1893} else {1894Object.assign(line.style, {1895display: 'block',1896top: coords.top + 'px',1897left: coords.left + 'px',1898width: coords.width + 'px',1899height: '0',1900borderLeft: 'none',1901borderTop: '2px dotted ' + C.brand,1902});1903}1904insertHoverAnchor = resolved.anchor;1905insertHoverPosition = resolved.position;1906insertHoverAxis = resolved.axis || 'column';1907}19081909function hideInsertLine() {1910if (!insertLineEl) return;1911insertLineEl.style.display = 'none';1912insertHoverAnchor = null;1913insertHoverPosition = null;1914insertHoverAxis = null;1915syncPageInteractionCursor();1916}19171918let pageInteractionCursorActive = false;19191920function ensurePickCursorStyle() {1921if (document.getElementById(PREFIX + '-pick-cursor-style')) return;1922const style = document.createElement('style');1923style.id = PREFIX + '-pick-cursor-style';1924style.textContent =1925'html.' + PICK_CURSOR_CLASS + ' * { cursor: crosshair !important; }\n'1926+ 'html.' + PICK_CURSOR_CLASS + ' [id^="' + PREFIX + '"],\n'1927+ 'html.' + PICK_CURSOR_CLASS + ' [id^="' + PREFIX + '"] * { cursor: revert !important; }';1928// Styles the host page, not the chrome - inside the adapter's shadow UI1929// root (uiAppendStyle's target) these selectors would match nothing.1930document.head.appendChild(style);1931}19321933/** Page-level cursor while pick or insert mode is targeting page elements. */1934function syncPageInteractionCursor() {1935const pickCursor = state === 'PICKING' && pickActive && !insertActive;1936let axisCursor = '';1937if (state === 'PICKING' && insertActive) {1938axisCursor = insertHoverAnchor ? cursorForInsertAxis(insertHoverAxis || 'column') : '';1939}19401941if (pickCursor) {1942ensurePickCursorStyle();1943document.documentElement.classList.add(PICK_CURSOR_CLASS);1944document.documentElement.style.cursor = '';1945pageInteractionCursorActive = true;1946return;1947}19481949document.documentElement.classList.remove(PICK_CURSOR_CLASS);1950if (axisCursor) {1951document.documentElement.style.cursor = axisCursor;1952pageInteractionCursorActive = true;1953} else if (pageInteractionCursorActive) {1954document.documentElement.style.cursor = '';1955pageInteractionCursorActive = false;1956}1957}19581959/**1960* Single entry point for interaction-state transitions. The pick-mode1961* crosshair is derived from `state`, so a bare `state = ...` assignment1962* leaves the page cursor out of sync with the mode it advertises.1963*/1964function setLiveState(next) {1965state = next;1966syncPageInteractionCursor();1967}19681969/** Element used to position the floating bar / shader during a session. */1970function resolveBarAnchor() {1971if (svelteComponentSession?.sessionId === currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) {1972const anchor = resolveSvelteComponentAnchor();1973if (anchor) return anchor;1974}1975if (currentSessionId && (state === 'GENERATING' || state === 'CYCLING')) {1976const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');1977if (wrapper) {1978const variantCount = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])').length;1979if (variantCount > 0 && visibleVariant > 0) {1980const visEl = pickVariantContent(wrapper, visibleVariant);1981if (visEl) return visEl;1982}1983if (state === 'GENERATING') {1984const ph = ensureInsertPlaceholder();1985if (ph) return ph;1986if (insertAnchorElement && document.body.contains(insertAnchorElement)) return insertAnchorElement;1987}1988}1989}1990if (selectedElement && document.body.contains(selectedElement)) return selectedElement;1991if (placeholderElement && document.body.contains(placeholderElement)) return placeholderElement;1992if (insertAnchorElement && document.body.contains(insertAnchorElement)) return insertAnchorElement;1993return null;1994}19951996function removeInsertPlaceholderDom() {1997if (placeholderElement) {1998placeholderElement.remove();1999placeholderElement = null;2000}2001placeholderResizeDrag = null;2002syncPlaceholderResizeHandles();2003}20042005function finalizeInsertSession() {2006removeInsertPlaceholderDom();2007insertAnchorElement = null;2008insertAnchorPosition = null;2009insertAnchorLayoutAxis = null;2010insertPlaceholderSnapshot = null;2011if (configureKind === 'insert') configureKind = 'replace';2012}20132014function buildInsertPlaceholderSnapshotFromDom(anchor, placeholder) {2015return {2016width: Math.round(placeholder.offsetWidth || 0),2017height: Math.round(placeholder.offsetHeight || PLACEHOLDER_DEFAULT_HEIGHT),2018marginLeft: parseFloat(placeholder.style.marginLeft) || 0,2019marginTop: parseFloat(placeholder.style.marginTop) || 0,2020position: insertAnchorPosition || 'before',2021layoutAxis: insertAnchorLayoutAxis || 'column',2022anchorTag: anchor.tagName || 'DIV',2023anchorClasses: anchor.className || '',2024anchorText: (anchor.textContent || '').trim().slice(0, 120),2025};2026}20272028function findInsertAnchorInDom() {2029if (insertAnchorElement && document.body.contains(insertAnchorElement)) return insertAnchorElement;2030const snap = insertPlaceholderSnapshot;2031if (!snap) return null;2032const tag = (snap.anchorTag || 'div').toLowerCase();2033const cls = (snap.anchorClasses || '').split(/\s+/).filter(Boolean)[0];2034const needle = snap.anchorText || '';2035const sel = cls ? tag + '.' + cls : tag;2036const candidates = document.querySelectorAll(sel);2037for (const candidate of candidates) {2038if (own(candidate)) continue;2039if (needle && !(candidate.textContent || '').includes(needle.slice(0, 40))) continue;2040return candidate;2041}2042return null;2043}20442045function isInsertGeneratingSession() {2046if (state !== 'GENERATING' || !currentSessionId) return false;2047const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');2048return !!wrapper && wrapper.dataset.impeccableMode === 'insert';2049}20502051/** Recreate the dotted placeholder if Astro/Vite HMR removed it mid-generation. */2052function ensureInsertPlaceholder() {2053if (!isInsertGeneratingSession()) return placeholderElement;2054const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');2055const variantCount = wrapper.querySelectorAll('[data-impeccable-variant]:not([data-impeccable-variant="original"])').length;2056if (variantCount > 0) return placeholderElement;2057if (placeholderElement && document.body.contains(placeholderElement)) return placeholderElement;20582059const anchor = findInsertAnchorInDom();2060if (!anchor) return null;20612062insertAnchorElement = anchor;2063const position = insertPlaceholderSnapshot?.position || insertAnchorPosition || 'before';2064const axis = insertPlaceholderSnapshot?.layoutAxis || insertAnchorLayoutAxis;2065const ph = createInsertPlaceholder(anchor, position, axis);2066if (!ph) return null;20672068if (insertPlaceholderSnapshot) {2069applyPlaceholderDimensions({2070width: insertPlaceholderSnapshot.width,2071height: insertPlaceholderSnapshot.height,2072marginLeft: insertPlaceholderSnapshot.marginLeft,2073marginTop: insertPlaceholderSnapshot.marginTop,2074});2075}2076selectedElement = ph;2077return ph;2078}20792080function applyPlaceholderDimensions({ width, height, marginLeft, marginTop }) {2081const ph = placeholderElement;2082if (!ph) return;2083materializePlaceholderWidth(ph);2084ph.style.width = width + 'px';2085ph.style.height = height + 'px';2086ph.style.marginLeft = marginLeft ? marginLeft + 'px' : '';2087ph.style.marginTop = marginTop ? marginTop + 'px' : '';2088positionAnnotOverlay(ph);2089positionBar();2090}20912092function showOrUpdateCyclingBar() {2093if (barEl && barEl.style.display !== 'none') updateBarContent('cycling');2094else showBar('cycling');2095}20962097function buildPlaceholderResizeHandles() {2098if (!placeholderResizeLayerEl) return;2099placeholderResizeLayerEl.innerHTML = '';2100const hit = 10;2101const half = hit / 2;2102const specs = [2103{ edge: 'n', top: -half, left: 0, right: 0, height: hit },2104{ edge: 's', bottom: -half, left: 0, right: 0, height: hit },2105{ edge: 'e', top: 0, bottom: 0, right: -half, width: hit },2106{ edge: 'w', top: 0, bottom: 0, left: -half, width: hit },2107];2108for (const spec of specs) {2109const handle = el('div', {2110position: 'absolute',2111pointerEvents: 'auto',2112cursor: cursorForPlaceholderEdge(spec.edge),2113});2114if (spec.top != null) handle.style.top = spec.top + 'px';2115if (spec.bottom != null) handle.style.bottom = spec.bottom + 'px';2116if (spec.left != null) handle.style.left = spec.left + 'px';2117if (spec.right != null) handle.style.right = spec.right + 'px';2118if (spec.width != null) handle.style.width = spec.width + 'px';2119if (spec.height != null) handle.style.height = spec.height + 'px';2120handle.dataset.impeccablePlaceholderResize = spec.edge;2121handle.setAttribute('aria-label', 'Resize placeholder');2122handle.title = 'Drag to resize';2123placeholderResizeLayerEl.appendChild(handle);2124}2125}21262127function syncPlaceholderResizeHandles() {2128if (!placeholderResizeLayerEl) return;2129const show = configureKind === 'insert' && annotActive && !!placeholderElement && state === 'CONFIGURING';2130placeholderResizeLayerEl.style.display = show ? 'block' : 'none';2131if (!show) {2132placeholderResizeLayerEl.innerHTML = '';2133return;2134}2135if (!placeholderResizeLayerEl.childElementCount) buildPlaceholderResizeHandles();2136}21372138function startPlaceholderEdgeResize(edge, e) {2139const ph = placeholderElement;2140if (!ph || configureKind !== 'insert') return;2141materializePlaceholderWidth(ph);2142placeholderResizeDrag = {2143edge,2144startX: e.clientX,2145startY: e.clientY,2146start: {2147width: ph.offsetWidth,2148height: ph.offsetHeight,2149marginLeft: parseFloat(ph.style.marginLeft) || 0,2150marginTop: parseFloat(ph.style.marginTop) || 0,2151},2152parentWidth: ph.parentNode?.getBoundingClientRect().width || PLACEHOLDER_MIN_WIDTH,2153pointerId: e.pointerId,2154};2155try { annotOverlayEl.setPointerCapture(e.pointerId); } catch {}2156e.stopPropagation();2157e.preventDefault();2158}21592160function createInsertPlaceholder(anchor, position, layoutAxis) {2161removeInsertPlaceholderDom();2162const parent = anchor.parentNode;2163if (!parent) return null;2164const axis = layoutAxis || detectInsertAxis(parent);2165const pst = getComputedStyle(parent);2166const ast = getComputedStyle(anchor);2167const sizing = placeholderSizing({2168axis,2169parentDisplay: pst.display,2170parentWidth: parent.getBoundingClientRect().width,2171anchorFlex: ast.flex,2172});2173const placeholder = document.createElement('div');2174placeholder.id = PREFIX + '-insert-placeholder';2175placeholder.setAttribute('data-impeccable-insert-placeholder', 'true');2176placeholder.setAttribute('aria-hidden', 'true');2177Object.assign(placeholder.style, {2178boxSizing: 'border-box',2179height: PLACEHOLDER_DEFAULT_HEIGHT + 'px',2180minHeight: PLACEHOLDER_MIN_HEIGHT + 'px',2181border: '2px dotted ' + BP.accent,2182borderRadius: '0',2183background: 'transparent',2184opacity: '1',2185position: 'relative',2186marginLeft: '',2187marginTop: '',2188});2189applyPlaceholderSizingStyles(placeholder, sizing);2190if (position === 'before') parent.insertBefore(placeholder, anchor);2191else parent.insertBefore(placeholder, anchor.nextSibling);2192placeholderElement = placeholder;2193insertAnchorElement = anchor;2194insertAnchorPosition = position;2195insertAnchorLayoutAxis = axis;2196return placeholder;2197}21982199function clearInsertPicking() {2200hideInsertLine();2201finalizeInsertSession();2202}22032204function isInsertCreateEnabled(btn) {2205btn = btn || uiGetById(PREFIX + '-insert-create');2206return !!btn && btn.getAttribute('aria-disabled') !== 'true';2207}22082209let insertCreateTooltipEl = null;22102211function ensureInsertCreateTooltip() {2212if (insertCreateTooltipEl) return insertCreateTooltipEl;2213insertCreateTooltipEl = el('div', {2214position: 'fixed',2215display: 'none',2216zIndex: String(Z.bar + 7),2217pointerEvents: 'none',2218maxWidth: '240px',2219padding: '6px 9px',2220borderRadius: '7px',2221background: BP.chatSurface,2222border: '1px solid ' + BP.hairline,2223boxShadow: BP.shadow,2224color: BP.text,2225fontFamily: FONT,2226fontSize: '11px',2227fontWeight: '500',2228lineHeight: '1.35',2229});2230insertCreateTooltipEl.id = PREFIX + '-insert-create-tooltip';2231uiAppend(insertCreateTooltipEl);2232return insertCreateTooltipEl;2233}22342235function showInsertCreateTooltip(anchor, message) {2236if (!anchor || !message) return;2237const tip = ensureInsertCreateTooltip();2238tip.textContent = message;2239tip.style.display = 'block';2240const r = anchor.getBoundingClientRect();2241const tipW = tip.offsetWidth;2242const tipH = tip.offsetHeight;2243const left = Math.max(8, Math.min(window.innerWidth - tipW - 8, r.left + r.width / 2 - tipW / 2));2244const top = Math.max(8, r.top - tipH - 8);2245tip.style.left = left + 'px';2246tip.style.top = top + 'px';2247}22482249function hideInsertCreateTooltip() {2250if (!insertCreateTooltipEl) return;2251insertCreateTooltipEl.style.display = 'none';2252}22532254function insertCreateGateState(input) {2255return {2256prompt: input?.value ?? '',2257comments: annotState.comments,2258strokes: annotState.strokes,2259};2260}22612262function syncInsertCreateButton(btn, input) {2263btn = btn || uiGetById(PREFIX + '-insert-create');2264input = input || uiGetById(PREFIX + '-insert-input');2265if (!btn || !input) return;2266const gate = insertCreateGateState(input);2267const ok = canCreateInsert(gate);2268const reason = ok ? 'Create variants' : insertCreateDisabledReason(gate);2269btn.setAttribute('aria-disabled', ok ? 'false' : 'true');2270btn.setAttribute('aria-label', reason);2271if (ok) {2272hideInsertCreateTooltip();2273btn.style.background = BP.accent;2274btn.style.color = C.ink;2275btn.style.border = 'none';2276btn.style.opacity = '1';2277btn.style.cursor = 'pointer';2278} else {2279btn.style.background = 'transparent';2280btn.style.color = BP.textDim;2281btn.style.border = '1px solid ' + BP.hairline;2282btn.style.opacity = '0.72';2283btn.style.cursor = 'not-allowed';2284}2285}22862287/** Stylesheet shared by the replace and insert configure rows. */2288function ensureConfigureInputStyle() {2289if (uiGetById(PREFIX + '-configure-input-style')) return;2290const s = document.createElement('style');2291s.id = PREFIX + '-configure-input-style';2292s.textContent =2293'@keyframes impeccable-configure-voice-pulse { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }' +2294'#' + PREFIX + '-input, #' + PREFIX + '-insert-input { box-sizing: border-box; height: ' + CONFIGURE_ROW_TRACK_H + '; line-height: ' + CONFIGURE_ROW_TRACK_H + '; padding: 0; margin: 0; caret-color: ' + CONFIGURE_PILL_TEXT + '; }' +2295'#' + PREFIX + '-input::placeholder, #' + PREFIX + '-insert-input::placeholder { color: ' + BP.textDim + '; opacity: 1; }' +2296'#' + PREFIX + '-configure-voice[data-listening="true"] svg, #' + PREFIX + '-insert-voice[data-listening="true"] svg { animation: impeccable-configure-voice-pulse 1.1s ease-in-out infinite; }' +2297'@media (prefers-reduced-motion: reduce) { #' + PREFIX + '-configure-voice[data-listening="true"] svg, #' + PREFIX + '-insert-voice[data-listening="true"] svg { animation: none; opacity: 1; } }' +2298'#' + PREFIX + '-configure-voice:hover, #' + PREFIX + '-insert-voice:hover { background: oklch(27% 0 0); color: ' + BP.accent + '; }';2299uiAppendStyle(s);2300}23012302function buildConfigureRow() {2303const controlsLocked = pendingApplyInFlight === true;2304const row = el('div', {2305display: 'flex', alignItems: 'stretch', width: '100%', height: CONFIGURE_BAR_H,2306});23072308const inputShell = el('div', configureInputShellStyle());23092310const input = document.createElement('input');2311input.id = PREFIX + '-input';2312input.type = 'text';2313input.placeholder = '';2314input.setAttribute('aria-label', 'Describe the change');2315Object.assign(input.style, configureInputFieldStyle());2316input.disabled = controlsLocked;2317if (controlsLocked) {2318input.placeholder = 'apply is running...';2319input.style.cursor = 'not-allowed';2320input.style.opacity = '0.58';2321}23222323const action = buildConfigureActionControl({2324controlsLocked,2325onClick: (e) => {2326e.stopPropagation();2327if (controlsLocked) { showManualApplyBusyToast(); return; }2328toggleActionPicker();2329},2330});23312332const count = buildConfigureCountControl({2333controlsLocked,2334onClick: (e) => {2335e.stopPropagation();2336if (controlsLocked) { showManualApplyBusyToast(); return; }2337count.textContent = '\u00D7' + cycleSelectedCount();2338if (count.matches(':hover')) {2339showConfigureBarTooltip(count, variantCountTooltipText(selectedCount));2340}2341},2342});23432344inputShell.appendChild(buildSelectionPill({ el: selectedElement, controlsLocked }));2345inputShell.appendChild(input);23462347ensureConfigureInputStyle();23482349input.addEventListener('focus', () => syncConfigureInputChrome());2350input.addEventListener('blur', () => syncConfigureInputChrome());2351input.addEventListener('keydown', (e) => {2352if (e.key === 'Enter') { e.stopPropagation(); e.preventDefault(); handleGo(); return; }2353if (e.key === 'Escape') {2354e.stopPropagation();2355e.preventDefault();2356input.blur();2357exitConfigureToPicking('configure-input-escape');2358return;2359}2360if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && !input.value) return;2361e.stopPropagation();2362});23632364const voiceBtn = buildConfigureVoiceButton({2365id: PREFIX + '-configure-voice',2366controlsLocked,2367onClick: (e) => {2368e.stopPropagation();2369if (controlsLocked) { showManualApplyBusyToast(); return; }2370toggleConfigureVoice();2371},2372});23732374const go = buildConfigureSubmitButton({2375controlsLocked,2376ariaLabel: 'Generate variants',2377onClick: (e) => { e.stopPropagation(); handleGo(); },2378});23792380row.appendChild(inputShell);2381row.appendChild(buildConfigureTrailingCluster([action, count], voiceBtn, go));2382syncConfigureInputChrome();23832384if (!controlsLocked) setTimeout(() => input.focus(), 60);23852386return row;2387}23882389function buildInsertConfigureRow() {2390const controlsLocked = pendingApplyInFlight === true;2391const row = el('div', {2392display: 'flex', alignItems: 'stretch', width: '100%', height: CONFIGURE_BAR_H,2393});2394row.addEventListener('pointerdown', (e) => e.stopPropagation());2395row.addEventListener('mousedown', (e) => e.stopPropagation());2396row.addEventListener('click', (e) => e.stopPropagation());23972398const inputShell = el('div', configureInputShellStyle());23992400const input = document.createElement('input');2401input.id = PREFIX + '-insert-input';2402input.type = 'text';2403input.placeholder = '';2404input.setAttribute('aria-label', 'Describe the new element');2405Object.assign(input.style, configureInputFieldStyle());2406input.disabled = controlsLocked;2407if (controlsLocked) {2408input.placeholder = 'apply is running...';2409input.style.cursor = 'not-allowed';2410input.style.opacity = '0.58';2411}24122413const count = buildConfigureCountControl({2414controlsLocked,2415onClick: (e) => {2416e.stopPropagation();2417if (controlsLocked) { showManualApplyBusyToast(); return; }2418count.textContent = '\u00D7' + cycleSelectedCount();2419if (count.matches(':hover')) {2420showConfigureBarTooltip(count, variantCountTooltipText(selectedCount));2421}2422},2423});24242425inputShell.appendChild(buildSelectionPill({ el: selectedElement, controlsLocked }));2426inputShell.appendChild(input);24272428ensureConfigureInputStyle();24292430input.addEventListener('input', () => syncInsertCreateButton());2431input.addEventListener('pointerdown', (e) => e.stopPropagation());2432input.addEventListener('mousedown', (e) => e.stopPropagation());2433input.addEventListener('click', (e) => {2434e.stopPropagation();2435try { input.focus({ preventScroll: true }); } catch { input.focus(); }2436});2437input.addEventListener('keydown', (e) => {2438if (e.key === 'Enter') {2439e.stopPropagation(); e.preventDefault();2440if (isInsertCreateEnabled()) handleInsertCreate();2441return;2442}2443if (e.key === 'Escape') {2444e.stopPropagation(); e.preventDefault();2445cancelInsertConfigure();2446return;2447}2448e.stopPropagation();2449});2450input.addEventListener('focus', () => syncConfigureInputChrome());2451input.addEventListener('blur', () => syncConfigureInputChrome());24522453const voiceBtn = buildConfigureVoiceButton({2454id: PREFIX + '-insert-voice',2455controlsLocked,2456onClick: (e) => {2457e.stopPropagation();2458if (controlsLocked) { showManualApplyBusyToast(); return; }2459toggleConfigureVoice();2460},2461});24622463const create = buildConfigureSubmitButton({2464controlsLocked,2465ariaLabel: 'Create variants',2466onClick: (e) => {2467e.preventDefault();2468e.stopPropagation();2469if (controlsLocked) { showManualApplyBusyToast(); return; }2470if (!isInsertCreateEnabled(create)) return;2471handleInsertCreate();2472},2473});2474create.id = PREFIX + '-insert-create';2475create.addEventListener('mouseenter', () => {2476if (controlsLocked) return;2477if (isInsertCreateEnabled(create)) {2478hideInsertCreateTooltip();2479return;2480}2481showInsertCreateTooltip(create, insertCreateDisabledReason(insertCreateGateState(input)));2482});2483create.addEventListener('mouseleave', hideInsertCreateTooltip);2484row.appendChild(inputShell);2485row.appendChild(buildConfigureTrailingCluster([count], voiceBtn, create));2486syncInsertCreateButton(create, input);2487syncConfigureInputChrome();2488if (!controlsLocked) setTimeout(() => input.focus(), 60);2489return row;2490}24912492// Generating row24932494function buildGeneratingRow() {2495const row = el('div', {2496display: 'flex', alignItems: 'center', gap: '8px',2497padding: '2px 4px',2498});24992500// Action label2501const label = el('span', {2502fontWeight: '600', fontSize: '12px', color: BP.text,2503flexShrink: '0', whiteSpace: 'nowrap',2504});2505label.textContent = configureKind === 'insert' ? 'Insert' : actionLabel();2506row.appendChild(label);25072508// Dots2509row.appendChild(buildDots(false));25102511// Status2512const status = el('span', {2513fontSize: '11px', color: BP.textDim, whiteSpace: 'nowrap',2514marginLeft: 'auto',2515});2516// Variants currently arrive atomically in a single file edit, so a2517// per-variant counter would lie. Say what's true.2518status.textContent = recoveryWaitingForAnchor2519? 'Variants ready. Reveal the selected element to resume.'2520: (arrivedVariants < expectedVariants2521? 'Generating ' + expectedVariants + ' variants...'2522: 'Done');2523row.appendChild(status);25242525return row;2526}25272528// Cycling row25292530const TUNE_ICON_SVG = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" style="flex-shrink:0"><line x1="4" y1="8" x2="20" y2="8"/><circle cx="14" cy="8" r="2.4" fill="currentColor" stroke="none"/><line x1="4" y1="16" x2="20" y2="16"/><circle cx="10" cy="16" r="2.4" fill="currentColor" stroke="none"/></svg>';25312532function buildCyclingRow() {2533if (!ensureCyclingRenderable('build-cycling-row')) {2534return el('div', { display: 'none' });2535}2536const row = el('div', {2537display: 'flex', alignItems: 'center', gap: '6px',2538padding: '1px 2px',2539});25402541// Prev2542const prev = navBtn('\u2190');2543prev.id = PREFIX + '-variant-prev';2544prev.addEventListener('click', (e) => { e.stopPropagation(); cycleVariant(-1); });2545if (visibleVariant <= 1) prev.style.opacity = '0.3';2546row.appendChild(prev);25472548// Dots (clickable)2549row.appendChild(buildDots(true));25502551// Counter2552const counter = el('span', {2553fontFamily: MONO, fontSize: '11px', fontWeight: '500',2554color: BP.textDim, minWidth: '24px', textAlign: 'center',2555});2556counter.id = PREFIX + '-variant-counter';2557counter.textContent = visibleVariant + '/' + arrivedVariants;2558row.appendChild(counter);25592560// Next2561const next = navBtn('\u2192');2562next.id = PREFIX + '-variant-next';2563next.addEventListener('click', (e) => { e.stopPropagation(); cycleVariant(1); });2564if (visibleVariant >= arrivedVariants) next.style.opacity = '0.3';2565row.appendChild(next);25662567// Tune chip - only when the visible variant exposes params2568const visParams = parseVariantParams(getVisibleVariantEl());2569const hasParams = visParams.length > 0;2570if (hasParams) {2571const tune = el('button', {2572display: 'inline-flex', alignItems: 'center', gap: '6px',2573padding: '4px 10px', borderRadius: '5px',2574border: '1px solid transparent',2575background: tuneOpen ? BP.accentSoft : 'transparent',2576color: tuneOpen ? BP.accent : BP.text,2577fontFamily: FONT, fontSize: '11px', fontWeight: '500',2578cursor: 'pointer',2579transition: 'color 0.12s ease, background 0.12s ease',2580whiteSpace: 'nowrap',2581});2582tune.innerHTML = TUNE_ICON_SVG;2583const tuneLabel = document.createElement('span');2584tuneLabel.textContent = 'Tune';2585tune.appendChild(tuneLabel);2586const tuneBadge = document.createElement('span');2587Object.assign(tuneBadge.style, {2588display: 'inline-flex', alignItems: 'center', justifyContent: 'center',2589minWidth: '16px', height: '16px', padding: '0 4px',2590borderRadius: '999px',2591background: tuneOpen ? C.brand : BP.hairline,2592color: tuneOpen ? 'oklch(98% 0 0)' : 'inherit',2593fontFamily: MONO, fontSize: '9.5px', fontWeight: '600',2594lineHeight: '1',2595boxSizing: 'border-box',2596});2597tuneBadge.textContent = String(visParams.length);2598tune.appendChild(tuneBadge);2599tune.title = 'Tune this variant (' + visParams.length + ' knob' + (visParams.length === 1 ? '' : 's') + ')';2600tune.addEventListener('mouseenter', () => {2601if (!tuneOpen) tune.style.background = BP.accentSoft;2602});2603tune.addEventListener('mouseleave', () => {2604if (!tuneOpen) tune.style.background = 'transparent';2605});2606tune.addEventListener('click', (e) => { e.stopPropagation(); toggleTunePopover(); });2607tune.dataset.iceqTune = '1';2608row.appendChild(tune);2609}26102611// Spacer2612row.appendChild(el('div', { flex: '1' }));26132614// Accept - primary action, kinpaku gold + lacquer-deep (matches demo .live-demo-ctx-accept)2615const accept = el('button', {2616padding: '5px 14px', borderRadius: '5px',2617border: 'none', background: C.brand, color: C.ink,2618fontFamily: FONT, fontSize: '11px', fontWeight: '600',2619cursor: 'pointer', transition: 'filter 0.12s ease, transform 0.1s ease',2620whiteSpace: 'nowrap',2621});2622accept.textContent = '\u2713 Accept';2623accept.addEventListener('mouseenter', () => accept.style.filter = 'brightness(1.08)');2624accept.addEventListener('mouseleave', () => accept.style.filter = 'none');2625accept.addEventListener('mousedown', () => accept.style.transform = 'scale(0.97)');2626accept.addEventListener('mouseup', () => accept.style.transform = 'scale(1)');2627accept.addEventListener('click', (e) => { e.stopPropagation(); handleAccept(); });2628if (arrivedVariants === 0) { accept.style.opacity = '0.3'; accept.style.pointerEvents = 'none'; }2629row.appendChild(accept);26302631// Discard2632const discard = el('button', {2633padding: '4px 6px', borderRadius: '5px',2634border: '1px solid ' + BP.hairline, background: 'transparent',2635fontFamily: FONT, fontSize: '11px', color: BP.textDim,2636cursor: 'pointer', transition: 'color 0.12s ease, border-color 0.12s ease',2637});2638discard.textContent = '\u2715';2639discard.title = 'Discard all variants';2640discard.addEventListener('mouseenter', () => { discard.style.color = BP.text; discard.style.borderColor = BP.text; });2641discard.addEventListener('mouseleave', () => { discard.style.color = BP.textDim; discard.style.borderColor = BP.hairline; });2642discard.addEventListener('click', (e) => { e.stopPropagation(); handleDiscard(); });2643row.appendChild(discard);26442645return row;2646}26472648// Shared UI builders26492650// Saving row (waiting for agent to process accept/discard)26512652function buildSavingRow() {2653const row = el('div', {2654display: 'flex', alignItems: 'center', gap: '8px',2655padding: '2px 8px',2656});2657const spinner = el('div', {2658width: '14px', height: '14px', borderRadius: '50%',2659border: '2px solid ' + BP.hairline,2660borderTopColor: BP.accent,2661animation: 'impeccable-spin 0.6s linear infinite',2662flexShrink: '0',2663});2664row.appendChild(spinner);2665const label = el('span', {2666fontSize: '12px', color: BP.textDim, fontWeight: '500',2667});2668label.textContent = 'Applying variant...';2669row.appendChild(label);26702671ensureSpinKeyframes();2672return row;2673}26742675// Confirmed row (green success, auto-dismisses)26762677function buildConfirmedRow() {2678const row = el('div', {2679display: 'flex', alignItems: 'center', gap: '8px',2680padding: '2px 8px',2681});2682const check = el('span', {2683fontSize: '15px', lineHeight: '1', flexShrink: '0',2684color: 'oklch(45% 0.18 145)',2685});2686check.textContent = '\u2713';2687row.appendChild(check);2688const label = el('span', {2689fontSize: '12px', color: 'oklch(49% 0.08 188)', fontWeight: '600',2690});2691label.textContent = 'Variant applied';2692row.appendChild(label);2693return row;2694}26952696// Shared UI builders26972698function buildDots(clickable) {2699const container = el('div', {2700display: 'flex', alignItems: 'center', gap: '4px',2701});2702for (let i = 1; i <= expectedVariants; i++) {2703const arrived = i <= arrivedVariants;2704const active = i === visibleVariant;2705// active: solid site-brand kinpaku dot. arrived+inactive: muted neutral.2706// pending (not yet arrived): faint outline ring. No borders on arrived2707// dots - the previous "accent ring + ash fill" combo read as noisy2708// kinpaku chips, especially when all variants had arrived and every2709// dot wore an accent ring.2710const dotBg = active ? C.brand2711: arrived ? BP.textDim2712: 'transparent';2713const dotBorder = arrived ? 'none' : '1.5px solid ' + BP.hairline;2714const dot = el('div', {2715width: active ? '8px' : '6px',2716height: active ? '8px' : '6px',2717borderRadius: '50%',2718background: dotBg,2719border: dotBorder,2720boxSizing: 'border-box',2721transition: 'all 0.2s ' + EASE,2722cursor: (clickable && arrived) ? 'pointer' : 'default',2723transform: arrived ? 'scale(1)' : 'scale(0.85)',2724opacity: arrived ? (active ? '1' : '0.6') : '0.4',2725});2726if (clickable && arrived) {2727const idx = i;2728dot.addEventListener('click', (e) => {2729e.stopPropagation();2730selectVariant(idx, 'variant_changed');2731});2732}2733container.appendChild(dot);2734}2735return container;2736}27372738function navBtn(text) {2739const b = el('button', {2740width: '26px', height: '26px', borderRadius: '5px',2741border: '1px solid ' + BP.hairline, background: 'transparent',2742color: BP.text, fontFamily: FONT, fontSize: '13px',2743cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center',2744transition: 'border-color 0.12s ease, background 0.12s ease',2745padding: '0', lineHeight: '1',2746});2747b.textContent = text;2748b.addEventListener('mouseenter', () => { b.style.borderColor = BP.text; });2749b.addEventListener('mouseleave', () => { b.style.borderColor = BP.hairline; });2750return b;2751}27522753function actionLabel() {2754const a = ACTIONS.find(a => a.value === selectedAction);2755return a ? a.label : 'Freeform';2756}27572758function el(tag, styles) {2759const e = document.createElement(tag);2760if (String(tag).toLowerCase() === 'button') e.type = 'button';2761if (styles) Object.assign(e.style, styles);2762return e;2763}27642765//2766// Action picker popover2767//27682769function initActionPicker() {2770const P = barPaletteForTheme(detectPageTheme());2771pickerEl = document.createElement('div');2772pickerEl.id = PREFIX + '-picker';2773Object.assign(pickerEl.style, {2774position: 'fixed', zIndex: Z.picker,2775display: 'none', opacity: '0',2776transform: 'scale(0.96) translateY(4px)',2777transformOrigin: 'bottom right',2778transition: 'opacity 0.18s ' + EASE + ', transform 0.2s ' + EASE,2779background: P.surface,2780border: '1px solid ' + P.border,2781borderRadius: '8px',2782boxShadow: P.shadow,2783padding: '6px',2784fontFamily: FONT,2785});27862787// Build the chip grid2788const grid = el('div', {2789display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '3px',2790});27912792ACTIONS.forEach(action => {2793const chip = el('button', {2794display: 'flex', flexDirection: 'column', alignItems: 'center',2795gap: '4px',2796padding: '8px 6px', borderRadius: '6px',2797border: 'none',2798background: action.value === selectedAction ? P.accentSoft : 'transparent',2799color: action.value === selectedAction ? P.accent : P.text,2800fontFamily: FONT, fontSize: '11px', fontWeight: '500',2801cursor: 'pointer',2802transition: 'background 0.1s ease, color 0.1s ease',2803textAlign: 'center', whiteSpace: 'nowrap',2804});2805const iconWrap = el('span', {2806display: 'flex', alignItems: 'center', justifyContent: 'center',2807height: '20px', opacity: '0.9',2808});2809iconWrap.innerHTML = ICONS[action.value] || '';2810const labelEl = el('span', { lineHeight: '1' });2811labelEl.textContent = action.label;2812chip.appendChild(iconWrap);2813chip.appendChild(labelEl);2814chip.dataset.action = action.value;2815chip.addEventListener('mouseenter', () => {2816if (action.value !== selectedAction) chip.style.background = P.accentSoft;2817});2818chip.addEventListener('mouseleave', () => {2819chip.style.background = action.value === selectedAction ? P.accentSoft : 'transparent';2820});2821chip.addEventListener('click', (e) => {2822e.preventDefault();2823e.stopPropagation();2824const prompt = uiGetById(PREFIX + '-input')?.value || '';2825selectedAction = action.value;2826hideActionPicker();2827updateBarContent('configure');2828const input = uiGetById(PREFIX + '-input');2829if (input && prompt) input.value = prompt;2830});2831grid.appendChild(chip);2832});28332834pickerEl.appendChild(grid);2835uiAppend(pickerEl);2836defangOutsideHandlers(pickerEl);28372838// Cache the palette on the picker so toggleActionPicker's state refresh2839// uses the same theme-aware colors when it repaints chips.2840pickerEl.__iceq_palette = P;2841}28422843function toggleActionPicker() {2844if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }2845if (pickerEl.style.display !== 'none') { hideActionPicker(); return; }2846// Rebuild chips to reflect current selection2847const P = pickerEl.__iceq_palette || barPaletteForTheme(detectPageTheme());2848pickerEl.querySelectorAll('button').forEach(chip => {2849const isActive = chip.dataset.action === selectedAction;2850chip.style.background = isActive ? P.accentSoft : 'transparent';2851chip.style.color = isActive ? P.accent : P.text;2852});2853// Position above the bar, right-aligned to the configure bar edge.2854const barRect = barEl.getBoundingClientRect();2855const pickerH = 170; // approximate; grows with icon + label rows2856let top = barRect.top - pickerH - 6;2857if (top < 8) top = barRect.bottom + 6;2858pickerEl.style.display = 'block';2859const pickerW = pickerEl.offsetWidth;2860let left = barRect.right - pickerW;2861left = Math.max(8, Math.min(left, window.innerWidth - pickerW - 8));2862Object.assign(pickerEl.style, {2863top: top + 'px',2864left: left + 'px',2865});2866requestAnimationFrame(() => {2867pickerEl.style.opacity = '1';2868pickerEl.style.transform = 'scale(1) translateY(0)';2869});2870}28712872function hideActionPicker() {2873if (!pickerEl) return;2874pickerEl.style.opacity = '0';2875pickerEl.style.transform = 'scale(0.96) translateY(4px)';2876setTimeout(() => { if (pickerEl) pickerEl.style.display = 'none'; }, 180);2877}28782879function ensureCyclingRenderable(reason) {2880if (arrivedVariants > 0) {2881if (visibleVariant < 1 || visibleVariant > arrivedVariants) visibleVariant = 1;2882return true;2883}2884recoverEmptyCycling(reason);2885return false;2886}28872888function recoverEmptyCycling(reason) {2889if (recoveringEmptyCycling) return;2890recoveringEmptyCycling = true;2891try {2892console.warn('[impeccable] Refusing to render empty variant cycling state:', reason);2893const message = 'No variants were mounted. Please try again.';2894if (svelteComponentSession?.sessionId === currentSessionId) {2895abortSvelteComponentInjection(currentSessionId, message);2896return;2897}2898cleanup();2899showToast(message, 5000);2900} finally {2901recoveringEmptyCycling = false;2902}2903}29042905//2906// Params panel (per-variant coarse controls)2907//2908// Variants may declare a parameter manifest via a JSON attribute on the2909// variant wrapper:2910//2911// <div data-impeccable-variant="1"2912// data-impeccable-params='[{"id":"density","kind":"steps",...}]'>2913//2914// The panel docks to the right edge of the outline during CYCLING and2915// exposes 2-5 coarse knobs. Values apply to the variant wrapper so scoped2916// CSS can respond instantly without regeneration:2917//2918// range / numeric toggle -> CSS custom property used by variant styles2919// steps / boolean toggle → data-p-<id> attribute used via :scope[data-p-foo="..."]2920//2921// On variant switch, values reset to that variant's declared defaults.2922// On accept, current values are sent in the event payload so the agent2923// can bake them into the source-file write.2924//29252926let paramsPanelEl = null; // outer wrapper (overflow:hidden, clips the slide)2927let paramsPanelInner = null; // translating content (carries bg, padding, knobs)2928let paramsPanelBody = null; // grid holding the knob cells2929let paramsCurrentValues = {}; // {paramId: value} - mirror of the visible variant's live values2930let tuneOpen = false; // whether the Tune popover is open right now29312932// Theme-aware Tune popover. Appears as a drawer that slides out from the2933// contextual bar's bar-facing edge (below if the bar sits below the2934// element, above otherwise). Same width as the bar. Auto-wraps to extra2935// rows when the knobs exceed one row. The bar's border-radius on the2936// popover side goes flat while open so the two shapes read as one.2937let paramsPanelPalette = null;29382939function initParamsPanel() {2940paramsPanelPalette = barPaletteForTheme(detectPageTheme());2941const P = paramsPanelPalette;29422943// Single element, always in the DOM. The slide animation is a CSS mask2944// with mask-size growing from 0% to 100% along the bar-facing axis - no2945// display toggle, no opacity toggle, no transform trickery. The mask2946// hides everything initially; as it grows, content is revealed from2947// the bar edge outward.2948paramsPanelEl = document.createElement('div');2949paramsPanelEl.id = PREFIX + '-params-panel';2950Object.assign(paramsPanelEl.style, {2951position: 'fixed', zIndex: String(Z.bar - 1),2952background: P.surfaceDeep,2953color: P.text,2954fontFamily: FONT,2955padding: '14px 18px',2956boxSizing: 'border-box',2957borderRadius: '0 0 10px 10px',2958pointerEvents: 'none',29592960// clip-path is the same conceptual reveal as mask but with rock-solid2961// transition support across engines. Closed state clips from the far2962// edge; open = inset(0) shows everything.2963clipPath: 'inset(0 0 100% 0)',2964transition: 'clip-path 0.44s ' + EASE,29652966// Park off-screen until positionParamsPanel places it. These are NOT2967// in the transition list, so they snap instantly - no fly-in from the2968// top-left when first shown.2969top: '-9999px', left: '-9999px', width: '0',2970});29712972paramsPanelBody = el('div', {2973display: 'grid',2974gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))',2975gap: '12px 16px',2976});29772978paramsPanelEl.appendChild(paramsPanelBody);2979uiAppend(paramsPanelEl);2980// Don't override pointer-events: the panel toggles between 'none' (closed,2981// click-through) and 'auto' (open) on its own. Just silence the host's2982// outside-interaction listeners while the panel is open.2983defangOutsideHandlers(paramsPanelEl, { setPointerEvents: false });2984paramsPanelInner = paramsPanelEl; // compatibility alias for the rest of the code2985}298629872988function getMountedSvelteComponentAnchor(session = svelteComponentSession) {2989const el = session?.mountTargetEl?.firstElementChild || null;2990if (!el || !document.body.contains(el)) return null;2991return rectIsUsableAnchor(el.getBoundingClientRect()) ? el : null;2992}29932994function resolveSvelteComponentAnchor(session = svelteComponentSession) {2995return getMountedSvelteComponentAnchor(session)2996|| session?.swapAnchor2997|| null;2998}29993000function getVisibleVariantEl() {3001if (!currentSessionId) return null;3002if (svelteComponentSession?.sessionId === currentSessionId) {3003return resolveSvelteComponentAnchor()3004|| svelteComponentSession.wrapperEl3005|| null;3006}3007const wrapper = document.querySelector('[data-impeccable-variants="' + currentSessionId + '"]');3008if (!wrapper) return null;3009return wrapper.querySelector('[data-impeccable-variant="' + visibleVariant + '"]');3010}30113012function parseVariantParams(variantEl) {3013// Svelte component variants can't carry a `data-impeccable-params` attribute:3014// the compiler reads `{` inside attribute values as expression delimiters, so3015// JSON-with-braces breaks the build. For that path the params live in a sidecar3016// params.json keyed by variant number, loaded into the session at mount time.3017if (svelteComponentSession?.sessionId === currentSessionId) {3018const byVariant = svelteComponentSession.paramsByVariant || {};3019const params = byVariant[String(visibleVariant)] || byVariant[visibleVariant];3020return Array.isArray(params) ? params : [];3021}3022if (!variantEl) return [];3023const raw = variantEl.getAttribute('data-impeccable-params');3024if (!raw) return [];3025try {3026const parsed = JSON.parse(raw);3027return Array.isArray(parsed) ? parsed : [];3028} catch (err) {3029console.warn('[impeccable] Invalid data-impeccable-params JSON:', err.message);3030return [];3031}3032}30333034function applyParamValue(variantEl, param, value) {3035if (!variantEl) return;3036const attr = 'data-p-' + param.id;3037if (param.kind === 'range') {3038variantEl.style.setProperty('--p-' + param.id, String(value));3039} else if (param.kind === 'toggle') {3040const on = !!value;3041variantEl.style.setProperty('--p-' + param.id, on ? '1' : '0');3042if (on) variantEl.setAttribute(attr, 'on');3043else variantEl.removeAttribute(attr);3044} else if (param.kind === 'steps') {3045variantEl.setAttribute(attr, String(value));3046}3047}30483049function applyParamDefaults(variantEl, params) {3050paramsCurrentValues = {};3051for (const p of params) {3052paramsCurrentValues[p.id] = p.default;3053applyParamValue(variantEl, p, p.default);3054}3055}30563057function formatRangeValue(input) {3058const max = parseFloat(input.max), min = parseFloat(input.min);3059const v = parseFloat(input.value);3060if (!isFinite(v)) return input.value;3061return (max - min) <= 2 ? v.toFixed(2) : String(Math.round(v));3062}30633064function buildParamsPanel(variantEl, params) {3065const P = paramsPanelPalette || barPaletteForTheme(detectPageTheme());3066paramsPanelBody.innerHTML = '';3067for (const p of params) {3068const row = el('div', { display: 'flex', flexDirection: 'column', gap: '6px' });3069const labelRow = el('div', {3070display: 'flex', justifyContent: 'space-between',3071alignItems: 'baseline', gap: '8px',3072});3073const lbl = el('span', {3074fontSize: '10.5px', fontWeight: '600', color: P.text,3075letterSpacing: '0.03em',3076});3077lbl.textContent = p.label || p.id;3078labelRow.appendChild(lbl);3079const readout = el('span', {3080fontSize: '10.5px', color: P.textDim,3081fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',3082});3083labelRow.appendChild(readout);3084row.appendChild(labelRow);30853086if (p.kind === 'range') {3087const input = document.createElement('input');3088input.type = 'range';3089input.min = String(p.min != null ? p.min : 0);3090input.max = String(p.max != null ? p.max : 1);3091input.step = String(p.step != null ? p.step : 0.05);3092input.value = String(p.default);3093Object.assign(input.style, {3094width: '100%', accentColor: C.brand, cursor: 'pointer',3095});3096readout.textContent = formatRangeValue(input);3097input.addEventListener('input', (e) => {3098e.stopPropagation();3099const v = parseFloat(input.value);3100paramsCurrentValues[p.id] = v;3101readout.textContent = formatRangeValue(input);3102applyParamValue(variantEl, p, v);3103queueCheckpoint('param_changed');3104});3105row.appendChild(input);3106} else if (p.kind === 'toggle') {3107const initial = !!p.default;3108readout.textContent = initial ? 'On' : 'Off';3109const track = el('button', {3110position: 'relative', width: '36px', height: '20px',3111borderRadius: '10px', border: 'none', padding: '0',3112cursor: 'pointer',3113background: initial ? C.brand : P.hairline,3114transition: 'background 0.15s ease',3115alignSelf: 'flex-start',3116});3117const knob = el('span', {3118position: 'absolute', top: '2px',3119left: initial ? '18px' : '2px',3120width: '16px', height: '16px', borderRadius: '50%',3121background: 'oklch(98% 0 0)',3122transition: 'left 0.18s ' + EASE,3123boxShadow: '0 1px 2px oklch(0% 0 0 / 0.2)',3124});3125track.appendChild(knob);3126track.addEventListener('click', (e) => {3127e.stopPropagation();3128const next = !paramsCurrentValues[p.id];3129paramsCurrentValues[p.id] = next;3130track.style.background = next ? C.brand : P.hairline;3131knob.style.left = next ? '18px' : '2px';3132readout.textContent = next ? 'On' : 'Off';3133applyParamValue(variantEl, p, next);3134queueCheckpoint('param_changed');3135});3136row.appendChild(track);3137} else if (p.kind === 'steps') {3138const opts = (p.options || []).map(o =>3139typeof o === 'string' ? { value: o, label: o } : o3140);3141const activeOpt = opts.find(o => o.value === p.default) || opts[0];3142readout.textContent = activeOpt ? activeOpt.label : String(p.default);3143const segRow = el('div', {3144display: 'grid',3145gridTemplateColumns: 'repeat(' + opts.length + ', 1fr)',3146gap: '1px', padding: '2px',3147background: P.hairline, borderRadius: '5px',3148});3149const segBtns = [];3150opts.forEach(o => {3151const active = o.value === p.default;3152const b = el('button', {3153padding: '5px 4px', border: 'none', borderRadius: '3px',3154background: active ? C.brand : 'transparent',3155color: active ? 'oklch(98% 0 0)' : P.text,3156fontFamily: FONT, fontSize: '10.5px', fontWeight: '500',3157cursor: 'pointer', whiteSpace: 'nowrap',3158transition: 'background 0.1s ease, color 0.1s ease',3159});3160b.textContent = o.label;3161b.addEventListener('click', (e) => {3162e.stopPropagation();3163paramsCurrentValues[p.id] = o.value;3164readout.textContent = o.label;3165segBtns.forEach(({ btn, val }) => {3166const on = val === o.value;3167btn.style.background = on ? C.brand : 'transparent';3168btn.style.color = on ? 'oklch(98% 0 0)' : P.text;3169});3170applyParamValue(variantEl, p, o.value);3171queueCheckpoint('param_changed');3172});3173segRow.appendChild(b);3174segBtns.push({ btn: b, val: o.value });3175});3176row.appendChild(segRow);3177}31783179paramsPanelBody.appendChild(row);3180}3181}31823183//3184// Inline text editing - makes pure-text descendants of the picked element3185// directly contenteditable. Save stages copy edits in the live buffer; the3186// Apply copy edits dock later asks the AI to apply the staged batch.3187//31883189let inlineEditRows = [];3190let inlineEditDrafts = new Map();31913192// Mixed-content elements (e.g. <p>text<code>x</code>text</p>) skip the row3193// walker's "all-children-are-text-nodes" rule. Wrap each non-whitespace direct3194// text-node child in a marker span so the walker emits a row for it. The3195// wrappers are inline display by default and inherit styles, so the page3196// shouldn't visually shift. We unwrap in disableInlineEdit.3197const MIXED_WRAP_SKIP = { script: 1, style: 1, template: 1, noscript: 1, svg: 1, code: 1, pre: 1 };31983199function collectEditableTextRows(rootEl, opts) {3200if (!rootEl || rootEl.nodeType !== 1) return [];3201const isOwn = (opts && opts.isOwn) || (() => false);3202const rows = [];32033204function visit(el) {3205if (!el || el.nodeType !== 1) return;3206const tag = el.tagName.toLowerCase();3207if (MIXED_WRAP_SKIP[tag]) return;3208if (el.hasAttribute && el.hasAttribute('contenteditable')) return;3209if (el !== rootEl && isOwn(el)) return;32103211const children = Array.from(el.childNodes);3212const textNodes = [];3213let allText = children.length > 0;3214let hasNonWhitespaceText = false;3215for (const node of children) {3216if (node.nodeType === 3) {3217textNodes.push(node);3218if (node.nodeValue && /\S/.test(node.nodeValue)) hasNonWhitespaceText = true;3219} else {3220allText = false;3221}3222}3223if (allText && hasNonWhitespaceText) {3224rows.push({3225el,3226ref: documentRefForElement(el) || el.tagName.toLowerCase(),3227text: textNodes.map((node) => node.nodeValue).join(''),3228textNodes,3229});3230}32313232for (const child of children) {3233if (child.nodeType === 1) visit(child);3234}3235}32363237visit(rootEl);3238return rows;3239}32403241function wrapMixedContentTextNodes(rootEl) {3242if (!rootEl || rootEl.nodeType !== 1) return;3243const tag = rootEl.tagName.toLowerCase();3244if (MIXED_WRAP_SKIP[tag]) return;3245if (rootEl.hasAttribute('contenteditable')) return;3246const children = Array.from(rootEl.childNodes);3247const hasText = children.some((n) => n.nodeType === 3 && /\S/.test(n.nodeValue || ''));3248const hasElement = children.some((n) => n.nodeType === 1);3249if (hasText && hasElement) {3250for (const node of children) {3251if (node.nodeType === 3 && /\S/.test(node.nodeValue || '')) {3252const wrap = document.createElement('span');3253wrap.dataset.impeccableTextWrap = 'true';3254wrap.textContent = node.nodeValue;3255rootEl.insertBefore(wrap, node);3256rootEl.removeChild(node);3257}3258}3259}3260for (const child of Array.from(rootEl.children)) {3261if (!child.dataset || !child.dataset.impeccableTextWrap) {3262wrapMixedContentTextNodes(child);3263}3264}3265}3266function unwrapMixedContentTextNodes(rootEl) {3267if (!rootEl || rootEl.nodeType !== 1) return;3268const wraps = rootEl.querySelectorAll('[data-impeccable-text-wrap="true"]');3269for (const wrap of wraps) {3270const parent = wrap.parentNode;3271if (!parent) continue;3272const textNode = document.createTextNode(wrap.textContent);3273parent.replaceChild(textNode, wrap);3274parent.normalize();3275}3276}3277let inlineEditRoot = null;32783279function enableInlineEdit(targetEl) {3280if (!targetEl) return;3281inlineEditRoot = targetEl;3282wrapMixedContentTextNodes(targetEl);3283const rows = collectEditableTextRows(targetEl, { isOwn: own });3284inlineEditRows = rows;3285inlineEditDrafts = new Map();3286for (const row of rows) {3287row.inlineWhiteSpace = row.el.style.whiteSpace;3288row.el.style.whiteSpace = getComputedStyle(row.el).whiteSpace;3289row.el.setAttribute('contenteditable', 'true');3290row.el.dataset.impeccableEditable = 'true';3291row.el.dataset.impeccableOriginalText = row.text;3292row.el.style.userSelect = 'text';3293row.el.style.cursor = 'text';3294row.el.style.outline = 'none';3295row.el.addEventListener('input', onInlineInput);3296}3297}32983299function disableInlineEdit(opts = {}) {3300for (const row of inlineEditRows) {3301if (activeElementDeep() === row.el) row.el.blur();3302row.el.removeAttribute('contenteditable');3303delete row.el.dataset.impeccableEditable;3304delete row.el.dataset.impeccableOriginalText;3305row.el.style.whiteSpace = row.inlineWhiteSpace || '';3306row.el.style.userSelect = '';3307row.el.style.cursor = '';3308row.el.style.outline = '';3309row.el.removeEventListener('input', onInlineInput);3310}3311inlineEditRows = [];3312inlineEditDrafts = new Map();3313if (inlineEditRoot && !opts.preserveMixedWraps) {3314unwrapMixedContentTextNodes(inlineEditRoot);3315inlineEditRoot = null;3316}3317}33183319function onInlineInput(e) {3320inlineEditDrafts.set(e.currentTarget, e.currentTarget.textContent);3321}33223323function hasTextRows(el) {3324if (!el) return false;3325// Lightweight: any descendant outside SKIP_SUBTREE_TAGS with at least one3326// non-whitespace direct text-node child means we have something editable3327// (mixed-content paragraphs included). Mirrors what the wrap+walk path3328// will produce in enableInlineEdit.3329function check(node) {3330if (!node || node.nodeType !== 1) return false;3331const tag = node.tagName.toLowerCase();3332if (MIXED_WRAP_SKIP[tag]) return false;3333if (node !== el && own(node)) return false;3334for (const child of node.childNodes) {3335if (child.nodeType === 3 && /\S/.test(child.nodeValue || '')) return true;3336}3337for (const child of node.children) {3338if (check(child)) return true;3339}3340return false;3341}3342return check(el);3343}33443345function enterEditingMode() {3346if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }3347setLiveState('EDITING');3348hideBar();3349hideAnnotOverlay();3350renderEditBadge('editing');3351enableInlineEdit(selectedElement);3352// Focus first editable element and position cursor at end3353if (inlineEditRows.length > 0) {3354const firstEditable = inlineEditRows[0] && inlineEditRows[0].el;3355setTimeout(() => {3356const el = firstEditable;3357if (!el || !el.isConnected || state !== 'EDITING') return;3358el.focus();3359const range = document.createRange();3360const sel = window.getSelection();3361range.selectNodeContents(el);3362range.collapse(false);3363sel.removeAllRanges();3364sel.addRange(range);3365}, 50);3366}3367}33683369function restoreInlineEditDrafts() {3370for (const row of inlineEditRows) {3371if (inlineEditDrafts.has(row.el)) {3372row.el.textContent = row.el.dataset.impeccableOriginalText;3373}3374}3375}33763377function cancelEditing() {3378restoreInlineEditDrafts();3379disableInlineEdit();3380setLiveState('CONFIGURING');3381showBar('configure');3382showAnnotOverlay(selectedElement);3383renderEditBadge('idle');3384}33853386function cancelEditingToPicking() {3387restoreInlineEditDrafts();3388disableInlineEdit();3389hideBar();3390stopScrollTracking();3391hideAnnotOverlay();3392clearAnnotations();3393renderEditBadge('hidden');3394setLiveState('PICKING');3395hoveredElement = null;3396hideHighlight();3397syncPageChatFocus('editing-outside-click');3398}33993400function teardownConfigureChrome() {3401hideConfigureBarTooltip();3402// hideBar() restores unsaved EDITING drafts before it disables inline3403// edit; disabling here first would wipe the draft metadata it needs.3404hideBar();3405stopScrollTracking();3406hideAnnotOverlay();3407clearAnnotations();3408renderEditBadge('hidden');3409}34103411function exitConfigureToPicking(reason, opts = {}) {3412teardownConfigureChrome();3413setLiveState('PICKING');3414if (opts.clearHover) {3415hoveredElement = null;3416hideHighlight();3417}3418syncPageChatFocus(reason);3419}34203421// Prefer the leaf's own id/class; if it has neither (e.g. a bare <em>),3422// climb to the nearest ancestor with one. The CLI uses tag+class together,3423// so tag must come from the same node as the locator.3424function buildLocatorForLeaf(leafEl, fallbackEl) {3425if (leafEl && (leafEl.id || leafEl.classList.length > 0)) {3426return {3427tag: leafEl.tagName.toLowerCase(),3428elementId: leafEl.id || null,3429classes: [...leafEl.classList],3430};3431}3432let cur = leafEl?.parentElement;3433while (cur && cur !== document.body) {3434if (cur.id || cur.classList.length > 0) {3435return {3436tag: cur.tagName.toLowerCase(),3437elementId: cur.id || null,3438classes: [...cur.classList],3439};3440}3441cur = cur.parentElement;3442}3443return {3444tag: (fallbackEl || leafEl).tagName.toLowerCase(),3445elementId: (fallbackEl || leafEl).id || null,3446classes: [...((fallbackEl || leafEl).classList || [])],3447};3448}34493450function sourceHintForElement(el) {3451if (!el || !el.getAttribute) return null;3452const file = el.getAttribute('data-astro-source-file');3453const loc = el.getAttribute('data-astro-source-loc');3454if (file || loc) {3455const parsed = parseSourceLoc(loc);3456return {3457file: file || '',3458loc: loc || '',3459line: parsed.line,3460column: parsed.column,3461};3462}3463return null;3464}34653466function parseSourceLoc(loc) {3467const match = String(loc || '').match(/^(\d+)(?::(\d+))?/);3468return {3469line: match ? Number(match[1]) : null,3470column: match && match[2] ? Number(match[2]) : null,3471};3472}34733474function documentRefForElement(el) {3475if (!el || el.nodeType !== 1) return null;3476const parts = [];3477let cur = el;3478while (cur && cur.nodeType === 1) {3479const tag = cur.tagName.toLowerCase();3480if (tag === 'html') break;3481if (tag === 'body') {3482parts.unshift('body');3483break;3484}3485parts.unshift(documentRefSegment(cur));3486cur = cur.parentElement;3487}3488return parts.join('>') || null;3489}34903491function documentRefSegment(el) {3492const tag = el.tagName.toLowerCase();3493return tag + documentRefIdSuffix(el) + documentRefClassSuffix(el) + ':nth-of-type(' + indexAmongSameTag(el) + ')';3494}34953496function documentRefIdSuffix(el) {3497return el.id ? '#' + normalizeDocumentRefToken(el.id) : '';3498}34993500function documentRefClassSuffix(el) {3501if (!el.classList || el.classList.length === 0) return '';3502const classes = [];3503for (const cls of el.classList) {3504if (!cls || cls.indexOf('impeccable-') === 0) continue;3505classes.push(normalizeDocumentRefToken(cls));3506if (classes.length === 2) break;3507}3508return classes.length ? '.' + classes.join('.') : '';3509}35103511function normalizeDocumentRefToken(value) {3512return String(value || '').replace(/[>\s]+/g, '_');3513}35143515function indexAmongSameTag(el) {3516const parent = el.parentElement;3517if (!parent) return 1;3518const tag = el.tagName.toLowerCase();3519let n = 0;3520for (const sib of parent.children) {3521if (sib.tagName.toLowerCase() === tag) {3522n++;3523if (sib === el) return n;3524}3525}3526return 1;3527}35283529function copyEditLeafContext(el, originalText, newText) {3530if (!el) return null;3531return {3532ref: documentRefForElement(el),3533tagName: el.tagName ? el.tagName.toLowerCase() : null,3534id: el.id || null,3535classes: el.classList ? [...el.classList].filter((cls) => cls.indexOf('impeccable-') !== 0) : [],3536originalText,3537newText,3538textContent: (el.textContent || '').slice(0, 500),3539outerHTML: sanitizedContextOuterHTML(el, 3000) || null,3540};3541}35423543function nearbyEditableTextsForManualEdit(rows, activeEl, originalText, newText) {3544const out = [];3545const seen = new Set();3546const skip = new Set([normalizeManualContextText(originalText), normalizeManualContextText(newText)]);3547for (const row of rows || []) {3548if (!row || row.el === activeEl) continue;3549const text = normalizeManualContextText(row.text);3550if (!text || text.length < 2 || seen.has(text) || skip.has(text)) continue;3551seen.add(text);3552out.push({3553ref: documentRefForElement(row.el),3554tag: row.el?.tagName ? row.el.tagName.toLowerCase() : null,3555classes: row.el?.classList ? [...row.el.classList].filter((cls) => cls.indexOf('impeccable-') !== 0) : [],3556text,3557});3558if (out.length >= 12) break;3559}3560return out;3561}35623563function copyEditContainerContext(el) {3564if (!el) return null;3565return {3566ref: documentRefForElement(el),3567tagName: el.tagName ? el.tagName.toLowerCase() : null,3568id: el.id || null,3569classes: el.classList ? [...el.classList].filter((cls) => cls.indexOf('impeccable-') !== 0) : [],3570textContent: (el.textContent || '').slice(0, 1000),3571outerHTML: sanitizedContextOuterHTML(el, 10000) || null,3572};3573}35743575function forbiddenManualTextChars(text) {3576const out = [];3577for (const ch of ['<', '{', '}', '`']) {3578if (String(text || '').includes(ch)) out.push(ch);3579}3580return out;3581}35823583async function applyEditing() {3584if (pendingApplyInFlight) { showManualApplyBusyToast(); return; }3585const ops = [];3586for (const row of inlineEditRows) {3587const newText = inlineEditDrafts.get(row.el);3588if (newText !== undefined && newText !== row.text) {3589if (String(newText || '').trim() === '') {3590showToast('Save rejected: copy edits cannot be empty.', 5500);3591return;3592}3593const forbidden = forbiddenManualTextChars(newText);3594if (forbidden.length > 0) {3595showToast('Save rejected: newText cannot contain ' + forbidden.join(' ') + ' (plain text only; ask the AI to insert markup)', 5500);3596return;3597}3598const locator = buildLocatorForLeaf(row.el, selectedElement);3599const op = {3600ref: row.ref,3601tag: locator.tag,3602elementId: locator.elementId,3603classes: locator.classes,3604originalText: row.text,3605newText,3606};3607op.leaf = copyEditLeafContext(row.el, row.text, newText);3608op.nearbyEditableTexts = nearbyEditableTextsForManualEdit(inlineEditRows, row.el, row.text, newText);3609const restoreHint = mixedTextWrapRestoreHint(row.el);3610if (restoreHint) op.restore = restoreHint;3611const sourceHint = sourceHintForElement(row.el);3612if (sourceHint) op.sourceHint = sourceHint;3613ops.push(op);3614}3615}3616if (ops.length === 0) { cancelEditing(); return; }3617const contextElement = contextElementForManualEdit(selectedElement, inlineEditRows, ops);3618const contextRef = documentRefForElement(contextElement);3619if (contextRef) for (const op of ops) op.contextRef = contextRef;3620const container = copyEditContainerContext(contextElement);3621if (container) for (const op of ops) op.container = container;3622try {3623const res = await fetch('http://localhost:' + PORT + '/manual-edit-stash', {3624method: 'POST',3625headers: { 'Content-Type': 'application/json' },3626body: JSON.stringify({3627token: TOKEN,3628id: id8(),3629pageUrl: location.pathname,3630element: extractContext(contextElement),3631ops,3632}),3633});3634if (!res.ok) {3635const errBody = await res.json().catch(() => ({}));3636throw new Error(errBody.error || ('HTTP ' + res.status));3637}3638const stashResult = await res.json();3639updatePendingCounter(stashResult.pendingCount || 0);3640maybeShowFirstSaveToast();3641disableInlineEdit();3642setLiveState('CONFIGURING');3643showBar('configure');3644showAnnotOverlay(selectedElement);3645renderEditBadge('idle');3646} catch (err) {3647console.error('[impeccable] manual edit stash failed:', err);3648const detail = String(err?.message || '');3649if (detail.includes('newText cannot contain') || detail.includes('newText cannot be empty')) {3650showToast('Save rejected: ' + detail.replace(/^manual_edits:\s*/, ''), 5500);3651} else {3652showToast('Save failed - retry or cancel', 4000);3653}3654}3655}36563657function schedulePendingDockPosition() {3658if (!pendingDockEl || !globalBarEl) return;3659requestAnimationFrame(positionPendingDock);3660}36613662function positionPendingDock() {3663if (!pendingDockEl || !globalBarEl) return;3664const width = globalBarEl.offsetWidth;3665const height = globalBarEl.offsetHeight;3666if (!width || !height) return;3667pendingDockEl.style.left = Math.round((window.innerWidth / 2) - (width / 2) - 18) + 'px';3668pendingDockEl.style.top = 'auto';3669pendingDockEl.style.bottom = Math.round(14 + (height / 2)) + 'px';3670}36713672function playPendingIntroAnimation() {3673if (!pendingPillEl || !pendingPillEl.animate || (matchMedia?.('(prefers-reduced-motion: reduce)').matches)) return;3674if (pendingIntroAnimation) pendingIntroAnimation.cancel();3675pendingIntroAnimation = pendingPillEl.animate([3676{3677opacity: 0,3678transform: 'scale(0.82)',3679filter: 'brightness(1.2)',3680boxShadow: '0 0 0 0 oklch(84% 0.19 80.46 / 0.45), 0 8px 24px oklch(0% 0 0 / 0.16)',3681},3682{3683opacity: 1,3684transform: 'scale(1.08)',3685filter: 'brightness(1.15)',3686boxShadow: '0 0 0 12px oklch(84% 0.19 80.46 / 0), 0 12px 34px oklch(0% 0 0 / 0.22)',3687offset: 0.55,3688},3689{3690opacity: 1,3691transform: 'scale(1)',3692filter: 'none',3693boxShadow: '0 4px 16px oklch(0% 0 0 / 0.16), 0 1px 3px oklch(0% 0 0 / 0.1)',3694},3695], { duration: 620, easing: EASE });3696pendingIntroAnimation.addEventListener('finish', () => { pendingIntroAnimation = null; }, { once: true });3697}36983699function ensureSpinKeyframes() {3700if (uiGetById(PREFIX + '-keyframes')) return;3701const style = document.createElement('style');3702style.id = PREFIX + '-keyframes';3703style.textContent = '@keyframes impeccable-spin { to { transform: rotate(360deg); } }';3704uiAppendStyle(style);3705}37063707function pendingApplyLabel(count) {3708return count === 1 ? 'Apply copy edit' : 'Apply copy edits';3709}37103711function showManualApplyBusyToast() {3712showToast('Apply is still running. Wait for it to finish.', 2800);3713}37143715function manualApplyStateKey() {3716return PREFIX + ':manual-apply:' + PORT + ':' + TOKEN + ':' + location.pathname;3717}37183719function readStoredManualApplyState() {3720try {3721const raw = sessionStorage.getItem(manualApplyStateKey());3722if (!raw) return null;3723const storedState = JSON.parse(raw);3724if (!storedState || storedState.pageUrl !== location.pathname || Date.now() > Number(storedState.expiresAt || 0)) {3725sessionStorage.removeItem(manualApplyStateKey());3726return null;3727}3728return storedState;3729} catch {3730return null;3731}3732}37333734function writeManualApplyState(applyState) {3735try {3736sessionStorage.setItem(manualApplyStateKey(), JSON.stringify({3737...applyState,3738pageUrl: location.pathname,3739updatedAt: Date.now(),3740expiresAt: Date.now() + MANUAL_APPLY_STATE_TTL_MS,3741}));3742} catch {3743// Best-effort only. The in-memory flag still covers non-reload flows.3744}3745}37463747function storeManualApplyState(count, patch) {3748const currentCount = Number(count) || 0;3749const existing = readStoredManualApplyState() || {};3750const totalOps = Number(existing.totalOps) || Number(existing.count) || currentCount;3751if (totalOps <= 0 && currentCount <= 0) return;3752writeManualApplyState({3753count: Number(existing.count) || currentCount || totalOps,3754totalOps: totalOps || currentCount,3755completedOps: Number(existing.completedOps) || 0,3756remainingCount: Number.isFinite(Number(existing.remainingCount)) ? Number(existing.remainingCount) : currentCount,3757phase: existing.phase || 'applying',3758startedAt: Number(existing.startedAt) || Date.now(),3759...(patch || {}),3760});3761}37623763function clearStoredManualApplyState() {3764try {3765sessionStorage.removeItem(manualApplyStateKey());3766} catch {3767// Ignore storage failures; UI state can still clear in memory.3768}3769}37703771function shouldResumeManualApplyLoading(count) {3772return Number(count) > 0 && readStoredManualApplyState() !== null;3773}37743775function manualApplyLoadingText(fallbackCount) {3776const stored = readStoredManualApplyState();3777if (stored?.phase === 'repair-decision') return 'Apply needs attention';3778if (stored?.phase === 'repairing') {3779const attempt = Number(stored.repairAttempt) || 1;3780const max = Number(stored.repairMaxAttempts) || 3;3781return 'Fixing apply issue, attempt ' + attempt + '/' + max;3782}3783if (stored?.phase === 'verifying') return 'Verifying copy edits';3784const remaining = Number.isFinite(Number(stored?.remainingCount))3785? Number(stored.remainingCount)3786: Number(fallbackCount) || 0;3787return remaining > 03788? 'Applying ' + remaining + ' copy edit' + (remaining === 1 ? '' : 's')3789: 'Verifying copy edits';3790}37913792function resetManualApplyProgress(count) {3793const total = Number(count) || 0;3794if (total <= 0) return;3795writeManualApplyState({3796count: total,3797totalOps: total,3798completedOps: 0,3799remainingCount: total,3800phase: 'applying',3801startedAt: Date.now(),3802});3803}38043805function updateManualApplyProgressFromChunk(chunk) {3806if (!chunk || !pendingApplyInFlight) return;3807const stored = readStoredManualApplyState() || {};3808const totalOps = Number(chunk.totalOpCount) || Number(stored.totalOps) || Number(stored.count) || parseInt(pendingPillEl?.dataset.count || '0', 10) || 0;3809const completedOps = Math.min(totalOps, (Number(stored.completedOps) || 0) + (Number(chunk.opCount) || 0));3810const remainingCount = Math.max(0, totalOps - completedOps);3811storeManualApplyState(Number(stored.count) || totalOps, {3812totalOps,3813completedOps,3814remainingCount,3815phase: remainingCount > 0 ? 'applying' : 'verifying',3816});3817setPendingApplyLoading(true, remainingCount);3818}38193820function updateManualApplyRepairState(repair, phase) {3821const count = parseInt(pendingPillEl?.dataset.count || '0', 10) || Number(readStoredManualApplyState()?.count) || 0;3822if (count <= 0) return;3823storeManualApplyState(count, {3824phase,3825repairAttempt: Number(repair?.attempt || repair?.attempts) || 1,3826repairMaxAttempts: Number(repair?.maxAttempts) || 3,3827});3828setPendingApplyLoading(true, count);3829}38303831function refreshLiveControlsForManualApply() {3832if (pendingApplyInFlight) {3833hideActionPicker();3834closeTunePopover();3835}3836if (barEl && barEl.style.display !== 'none' && state === 'CONFIGURING') {3837const input = uiGetById(PREFIX + '-input');3838const prompt = input ? input.value : '';3839updateBarContent('configure');3840const nextInput = uiGetById(PREFIX + '-input');3841if (nextInput) nextInput.value = prompt;3842}3843if (editBadgeEl && editBadgeEl.style.display !== 'none') {3844if (pendingApplyInFlight) renderEditBadge('idle-disabled');3845else if (state === 'CONFIGURING' && selectedElement && hasTextRows(selectedElement)) renderEditBadge('idle');3846}3847updateGlobalBarState();3848}38493850function hidePendingApplyDock() {3851pendingApplyInFlight = false;3852clearStoredManualApplyState();3853if (pendingIntroAnimation) { pendingIntroAnimation.cancel(); pendingIntroAnimation = null; }3854if (pendingDockEl) pendingDockEl.style.display = 'none';3855if (pendingPillEl) {3856pendingPillEl.dataset.count = '0';3857pendingPillEl.style.display = 'none';3858pendingPillEl.disabled = false;3859pendingPillEl.setAttribute('aria-busy', 'false');3860pendingPillEl.setAttribute('aria-label', 'Apply copy edits to source');3861pendingPillEl.style.cursor = 'pointer';3862pendingPillEl.style.filter = 'none';3863pendingPillEl.style.transform = 'scale(1)';3864}3865if (pendingPillSpinnerEl) pendingPillSpinnerEl.style.display = 'none';3866if (pendingPillLabelEl) pendingPillLabelEl.textContent = pendingApplyLabel(0);3867if (pendingPillCountEl) {3868pendingPillCountEl.textContent = '0';3869pendingPillCountEl.style.display = 'inline-flex';3870}3871if (pendingTrashBtn) {3872pendingTrashBtn.style.display = 'none';3873pendingTrashBtn.disabled = false;3874pendingTrashBtn.style.cursor = 'pointer';3875pendingTrashBtn.style.opacity = '1';3876}3877if (pendingKeepFixingBtn) pendingKeepFixingBtn.style.display = 'none';3878if (pendingRollbackBtn) pendingRollbackBtn.style.display = 'none';3879refreshLiveControlsForManualApply();3880}38813882function setPendingApplyLoading(loading, count) {3883if (!pendingPillEl || !pendingPillLabelEl || !pendingPillCountEl || !pendingTrashBtn) return;3884pendingApplyInFlight = loading === true;3885const currentCount = count || parseInt(pendingPillEl.dataset.count || '0', 10) || 0;3886if (pendingApplyInFlight) storeManualApplyState(currentCount);3887else clearStoredManualApplyState();3888if (pendingPillSpinnerEl) pendingPillSpinnerEl.style.display = pendingApplyInFlight ? 'inline-block' : 'none';3889pendingPillLabelEl.textContent = pendingApplyInFlight3890? manualApplyLoadingText(currentCount)3891: pendingApplyLabel(currentCount);3892pendingPillCountEl.style.display = pendingApplyInFlight ? 'none' : 'inline-flex';3893pendingPillEl.disabled = pendingApplyInFlight;3894pendingPillEl.setAttribute('aria-busy', pendingApplyInFlight ? 'true' : 'false');3895pendingPillEl.style.cursor = pendingApplyInFlight ? 'wait' : 'pointer';3896pendingPillEl.style.filter = pendingApplyInFlight ? 'brightness(0.98)' : 'none';3897pendingPillEl.style.transform = 'scale(1)';3898pendingTrashBtn.disabled = pendingApplyInFlight;3899pendingTrashBtn.style.cursor = pendingApplyInFlight ? 'not-allowed' : 'pointer';3900pendingTrashBtn.style.opacity = pendingApplyInFlight ? '0.58' : '1';3901if (pendingApplyInFlight) {3902if (pendingKeepFixingBtn) pendingKeepFixingBtn.style.display = 'none';3903if (pendingRollbackBtn) pendingRollbackBtn.style.display = 'none';3904pendingTrashBtn.style.display = 'inline-flex';3905}3906schedulePendingDockPosition();3907refreshLiveControlsForManualApply();3908}39093910function updatePendingCounter(currentPageCount) {3911if (!pendingDockEl || !pendingPillEl || !pendingPillLabelEl || !pendingPillCountEl || !pendingTrashBtn) return;3912const previousCount = parseInt(pendingPillEl.dataset.count || '0', 10);3913if (!currentPageCount || currentPageCount <= 0) {3914hidePendingApplyDock();3915return;3916}3917pendingPillLabelEl.textContent = pendingApplyLabel(currentPageCount);3918pendingPillCountEl.textContent = String(currentPageCount);3919pendingPillEl.setAttribute('aria-label', 'Apply ' + currentPageCount + ' copy edit' + (currentPageCount === 1 ? '' : 's') + ' to source');3920pendingPillEl.style.display = 'inline-flex';3921pendingTrashBtn.style.display = 'inline-flex';3922pendingDockEl.style.display = 'inline-flex';3923pendingPillEl.dataset.count = String(currentPageCount);3924if (pendingApplyInFlight || shouldResumeManualApplyLoading(currentPageCount)) setPendingApplyLoading(true, currentPageCount);3925schedulePendingDockPosition();3926if (previousCount <= 0) playPendingIntroAnimation();3927}39283929function maybeShowFirstSaveToast() {3930if (!firstSaveOfSession) return;3931firstSaveOfSession = false;3932showToast('Saved. Click "Apply copy edits" to write changes.', 4500);3933}39343935async function fetchPendingCount() {3936try {3937const res = await fetch(3938'http://localhost:' + PORT + '/manual-edit-stash?token=' + encodeURIComponent(TOKEN) + '&pageUrl=' + encodeURIComponent(location.pathname),3939);3940if (!res.ok) return;3941const data = await res.json();3942updatePendingCounter(data.count || 0);3943} catch (err) {3944console.warn('[impeccable] failed to fetch pending count:', err);3945}3946}39473948async function onPendingPillClick() {3949const count = parseInt(pendingPillEl?.dataset.count || '0', 10);3950if (count <= 0 || pendingApplyInFlight) return;3951const ok = confirm('Apply ' + count + ' copy edit' + (count === 1 ? '' : 's') + ' to source?');3952if (!ok) return;3953let waitForSseCompletion = false;3954resetManualApplyProgress(count);3955setPendingApplyLoading(true, count);3956try {3957const res = await fetch(3958'http://localhost:' + PORT + '/manual-edit-commit?token=' + encodeURIComponent(TOKEN) + '&pageUrl=' + encodeURIComponent(location.pathname) + '&async=1',3959{ method: 'POST', keepalive: true },3960);3961if (!res.ok) {3962const errBody = await res.json().catch(() => ({}));3963throw new Error(errBody.error || ('HTTP ' + res.status));3964}3965const result = await res.json();3966if (res.status === 202 || result.status === 'started') {3967waitForSseCompletion = true;3968return;3969}3970const remaining = remainingManualEditCount(result);3971updatePendingCounter(remaining);3972if (result.failed && result.failed.length > 0) {3973console.warn('[impeccable] some copy edits failed:', result.failed);3974showToast('Applied ' + (result.applied?.length || 0) + ', ' + result.failed.length + ' failed - see console', 5000);3975} else {3976const n = Array.isArray(result.applied) ? result.applied.length : (result.cleared || 0);3977if (n > 0) {3978showToast('Applied ' + n + ' edit' + (n === 1 ? '' : 's'), 2500);3979} else {3980console.warn('[impeccable] apply returned no verified edits:', result);3981showToast('No edits applied - see console', 4000);3982}3983}3984} catch (err) {3985console.error('[impeccable] commit failed:', err);3986showToast('Apply failed - see console', 4000);3987} finally {3988if (waitForSseCompletion) return;3989const remainingCount = parseInt(pendingPillEl?.dataset.count || '0', 10) || 0;3990if (remainingCount > 0) setPendingApplyLoading(false);3991else hidePendingApplyDock();3992}3993}39943995async function onPendingTrashClick() {3996const count = parseInt(pendingPillEl?.dataset.count || '0', 10);3997if (count <= 0 || pendingApplyInFlight) return;3998const ok = confirm('Discard ' + count + ' copy edit' + (count === 1 ? '' : 's') + ' on this page?');3999if (!ok) return;4000try {4001const res = await fetch(4002'http://localhost:' + PORT + '/manual-edit-discard?token=' + encodeURIComponent(TOKEN) + '&pageUrl=' + encodeURIComponent(location.pathname),4003{ method: 'POST' },4004);4005if (!res.ok) throw new Error('HTTP ' + res.status);4006const result = await res.json().catch(() => ({}));4007const restoreFailures = restoreDiscardedManualEdits(result.entries || []);4008updatePendingCounter(0);4009if (restoreFailures > 0) {4010showToast('Discarded ' + count + ' copy edit' + (count === 1 ? '' : 's') + ' - refresh to reset ' + restoreFailures, 4000);4011} else {4012showToast('Discarded ' + count + ' copy edit' + (count === 1 ? '' : 's'), 2500);4013}4014} catch (err) {4015console.error('[impeccable] discard failed:', err);4016showToast('Discard failed - see console', 4000);4017}4018}40194020function showManualApplyDecision(msg) {4021const count = parseInt(pendingPillEl?.dataset.count || '0', 10) || numberOrNull(msg?.remainingCount) || 0;4022pendingApplyInFlight = false;4023storeManualApplyState(count, {4024phase: 'repair-decision',4025repairAttempt: numberOrNull(msg?.repair?.attempts) || numberOrNull(msg?.repair?.attempt) || 3,4026repairMaxAttempts: numberOrNull(msg?.repair?.maxAttempts) || 3,4027});4028if (pendingPillSpinnerEl) pendingPillSpinnerEl.style.display = 'none';4029if (pendingPillLabelEl) pendingPillLabelEl.textContent = 'Apply needs attention';4030if (pendingPillCountEl) pendingPillCountEl.style.display = 'none';4031if (pendingPillEl) {4032pendingPillEl.disabled = true;4033pendingPillEl.setAttribute('aria-busy', 'false');4034pendingPillEl.style.cursor = 'default';4035pendingPillEl.style.display = 'inline-flex';4036}4037if (pendingTrashBtn) pendingTrashBtn.style.display = 'none';4038if (pendingKeepFixingBtn) pendingKeepFixingBtn.style.display = 'inline-flex';4039if (pendingRollbackBtn) pendingRollbackBtn.style.display = 'inline-flex';4040if (pendingDockEl) pendingDockEl.style.display = 'inline-flex';4041schedulePendingDockPosition();4042refreshLiveControlsForManualApply();4043}40444045async function onPendingKeepFixingClick() {4046const count = parseInt(pendingPillEl?.dataset.count || '0', 10) || numberOrNull(readStoredManualApplyState()?.count) || 0;4047if (count <= 0) return;4048updateManualApplyRepairState({ attempt: 1, maxAttempts: 3 }, 'repairing');4049try {4050const res = await fetch(4051'http://localhost:' + PORT + '/manual-edit-commit?token=' + encodeURIComponent(TOKEN) + '&pageUrl=' + encodeURIComponent(location.pathname) + '&async=1&repair=1',4052{ method: 'POST', keepalive: true },4053);4054if (!res.ok) throw new Error('HTTP ' + res.status);4055if (pendingKeepFixingBtn) pendingKeepFixingBtn.style.display = 'none';4056if (pendingRollbackBtn) pendingRollbackBtn.style.display = 'none';4057if (pendingTrashBtn) pendingTrashBtn.style.display = 'inline-flex';4058} catch (err) {4059console.error('[impeccable] repair retry failed:', err);4060showToast('Repair retry failed - see console', 4000);4061showManualApplyDecision({ remainingCount: count, repair: readStoredManualApplyState() });4062}4063}40644065async function onPendingRollbackClick() {4066const ok = confirm('Rollback source files to before this Apply and keep the edits staged?');4067if (!ok) return;4068try {4069const res = await fetch(4070'http://localhost:' + PORT + '/manual-edit-repair-decision?token=' + encodeURIComponent(TOKEN) + '&pageUrl=' + encodeURIComponent(location.pathname),4071{4072method: 'POST',4073headers: { 'Content-Type': 'application/json' },4074body: JSON.stringify({ token: TOKEN, pageUrl: location.pathname, action: 'rollback' }),4075},4076);4077if (!res.ok) throw new Error('HTTP ' + res.status);4078const result = await res.json().catch(() => ({}));4079clearStoredManualApplyState();4080updatePendingCounter(numberOrNull(result.remainingCount) || 0);4081showToast('Rolled back source; copy edits are still staged.', 3500);4082} catch (err) {4083console.error('[impeccable] manual Apply rollback failed:', err);4084showToast('Rollback failed - see console', 4000);4085}4086}40874088function manualEditEventForCurrentPage(msg) {4089return !msg?.pageUrl || msg.pageUrl === location.pathname;4090}40914092function numberOrNull(value) {4093const n = Number(value);4094return Number.isFinite(n) ? n : null;4095}40964097function remainingManualEditCount(payload) {4098const perPageCount = numberOrNull(payload?.perPage?.[location.pathname]);4099if (perPageCount !== null) return perPageCount;4100const remainingCount = numberOrNull(payload?.remainingCount);4101if (remainingCount !== null) return remainingCount;4102const totalCount = numberOrNull(payload?.totalCount);4103if (totalCount === 0) return 0;4104return null;4105}41064107function handleManualEditActivity(msg) {4108if (!manualEditEventForCurrentPage(msg)) return;41094110if (msg.type === 'manual_edit_stashed') {4111const pendingCount = numberOrNull(msg.pendingCount);4112if (pendingCount !== null) updatePendingCounter(pendingCount);4113return;4114}41154116if (msg.type === 'manual_edit_commit_started') {4117const pendingCount = numberOrNull(msg.pendingCount);4118if (pendingCount !== null && pendingCount > 0) updatePendingCounter(pendingCount);4119if (!msg.repairOnly && pendingCount !== null && pendingCount > 0) resetManualApplyProgress(pendingCount);4120if (msg.repairOnly) updateManualApplyRepairState({ attempt: 1, maxAttempts: 3 }, 'repairing');4121setPendingApplyLoading(true, pendingCount || undefined);4122return;4123}41244125if (msg.type === 'manual_edit_apply_reply_received') {4126if (msg.chunk) updateManualApplyProgressFromChunk(msg.chunk);4127if (msg.repair) updateManualApplyRepairState(msg.repair, 'repairing');4128return;4129}41304131if (msg.type === 'manual_edit_apply_dispatched' && msg.repair) {4132updateManualApplyRepairState(msg.repair, 'repairing');4133return;4134}41354136if (msg.type === 'manual_edit_repair_needs_decision') {4137showManualApplyDecision(msg);4138return;4139}41404141if (msg.type === 'manual_edit_repair_rollback_done') {4142clearStoredManualApplyState();4143fetchPendingCount();4144return;4145}41464147if (msg.type === 'manual_edit_commit_done') {4148if (msg.reason === 'manual_edit_repair_needs_decision' || msg.needsManualDecision === true) {4149showManualApplyDecision(msg);4150return;4151}4152// Clear the in-flight flag BEFORE updating the counter. updatePendingCounter4153// re-asserts setPendingApplyLoading(true) whenever the flag is still set and4154// edits remain (failed entries stay staged), which would otherwise leave the4155// picker frozen forever after a partial/failed apply.4156const wasApplying = pendingApplyInFlight;4157setPendingApplyLoading(false);4158const remainingCount = remainingManualEditCount(msg);4159updatePendingCounter(remainingCount === null ? 0 : remainingCount);4160if (wasApplying) {4161const failedCount = numberOrNull(msg.failedCount) || 0;4162const appliedCount = numberOrNull(msg.appliedCount) || numberOrNull(msg.cleared) || 0;4163if (failedCount > 0) {4164showToast('Applied ' + appliedCount + ', ' + failedCount + ' failed - see console', 5000);4165} else if (appliedCount > 0) {4166showToast('Applied ' + appliedCount + ' edit' + (appliedCount === 1 ? '' : 's'), 2500);4167}4168}4169return;4170}41714172if (msg.type === 'manual_edit_commit_failed') {4173setPendingApplyLoading(false);4174fetchPendingCount();4175return;4176}41774178if (msg.type === 'manual_edit_discarded') {4179fetchPendingCount();4180}4181}41824183function restoreDiscardedManualEdits(entries) {4184let failures = 0;4185for (const entry of entries || []) {4186for (const op of entry.ops || []) {4187if (restoreMixedTextNodeManualEdit(op)) continue;4188const el = findManualEditRestoreElement(op);4189if (!el || typeof op.originalText !== 'string' || !canRestoreManualEditElement(el, op)) {4190failures += 1;4191continue;4192}4193el.textContent = op.originalText;4194}4195}4196if (failures > 0) {4197console.warn('[impeccable] skipped unsafe copy edit DOM restore for', failures, 'edit(s). Refresh to reset the page DOM.');4198}4199return failures;4200}42014202function canRestoreManualEditElement(el, op) {4203if (!el || typeof op?.originalText !== 'string') return false;4204if (el.children && el.children.length > 0) return false;4205return normalizeManualContextText(el.textContent) === normalizeManualContextText(op.newText);4206}42074208function mixedTextWrapRestoreHint(el) {4209if (!el || !el.dataset || el.dataset.impeccableTextWrap !== 'true' || !el.parentElement) return null;4210const siblings = directMixedTextRestoreNodes(el.parentElement);4211const textIndex = siblings.indexOf(el);4212return {4213kind: 'mixedTextNode',4214parentRef: documentRefForElement(el.parentElement),4215textIndex,4216};4217}42184219function restoreMixedTextNodeManualEdit(op) {4220const restore = op?.restore;4221if (!restore || restore.kind !== 'mixedTextNode' || typeof op?.originalText !== 'string') return false;4222const parent = queryManualEditRef(restore.parentRef);4223if (!parent) return false;4224const textNodes = directMixedTextRestoreNodes(parent).filter((node) => node.nodeType === 3);4225const newText = normalizeManualContextText(op.newText);4226const byIndex = textNodes[Number(restore.textIndex)];4227if (byIndex && normalizeManualContextText(byIndex.nodeValue) === newText) {4228byIndex.nodeValue = op.originalText;4229return true;4230}4231const matches = textNodes.filter((node) => normalizeManualContextText(node.nodeValue) === newText);4232if (matches.length !== 1) return false;4233matches[0].nodeValue = op.originalText;4234return true;4235}42364237function directMixedTextRestoreNodes(parent) {4238return Array.from(parent?.childNodes || []).filter((node) => {4239if (node.nodeType === 3) return /\S/.test(node.nodeValue || '');4240return node.nodeType === 14241&& node.dataset4242&& node.dataset.impeccableTextWrap === 'true'4243&& /\S/.test(node.textContent || '');4244});4245}42464247function findManualEditRestoreElement(op) {4248for (const ref of [op?.ref, op?.leaf?.ref]) {4249const byRef = queryManualEditRef(ref);4250if (byRef) return byRef;4251}4252const tag = op?.tag || op?.leaf?.tagName || '*';4253const classes = Array.isArray(op?.classes) ? op.classes : (Array.isArray(op?.leaf?.classes) ? op.leaf.classes : []);4254const selector = (tag === '*' ? '' : tag) + classes.map((cls) => '.' + cssIdent(cls)).join('') || '*';4255let matches = [];4256try {4257matches = Array.from(document.querySelectorAll(selector));4258} catch {4259matches = [];4260}4261const newText = normalizeManualContextText(op?.newText);4262const filtered = matches.filter((el) => normalizeManualContextText(el.textContent) === newText);4263return filtered.length === 1 ? filtered[0] : null;4264}42654266function queryManualEditRef(ref) {4267if (!ref || typeof ref !== 'string') return null;4268const parts = ref.split('>').map((part) => part.trim()).filter(Boolean);4269let current = null;4270for (let index = 0; index < parts.length; index += 1) {4271const segment = parseManualEditRefSegment(parts[index]);4272if (!segment) return null;4273if (index === 0 && segment.tag === 'body') {4274current = document.body;4275if (!elementMatchesManualRefSegment(current, segment)) return null;4276continue;4277}4278const scope = current || document.body;4279const children = Array.from(scope.children || []);4280current = children.find((child) => elementMatchesManualRefSegment(child, segment)) || null;4281if (!current) return null;4282}4283return current;4284}42854286function parseManualEditRefSegment(segment) {4287const nthMatch = String(segment || '').match(/:nth-of-type\((\d+)\)$/);4288const nth = nthMatch ? Number(nthMatch[1]) : null;4289const base = nthMatch ? segment.slice(0, nthMatch.index) : segment;4290const tagMatch = base.match(/^[^#.:\s]+/);4291const tag = tagMatch ? tagMatch[0].toLowerCase() : null;4292if (!tag) return null;4293const idMatch = base.match(/#([^#.]+)/);4294const classes = base4295.slice(tag.length)4296.replace(/#[^#.]+/, '')4297.split('.')4298.filter(Boolean);4299return { tag, id: idMatch ? idMatch[1] : null, classes, nth };4300}43014302function elementMatchesManualRefSegment(el, segment) {4303if (!el || !segment) return false;4304if (el.tagName.toLowerCase() !== segment.tag) return false;4305if (segment.id && el.id !== segment.id) return false;4306for (const cls of segment.classes) {4307if (!el.classList || !el.classList.contains(cls)) return false;4308}4309if (segment.nth && indexAmongSameTag(el) !== segment.nth) return false;4310return true;4311}43124313function cssIdent(value) {4314if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(String(value));4315return String(value).replace(/[^a-zA-Z0-9_-]/g, '\\$&');4316}43174318//4319// Edit content badge - floating button at element top-right to enter EDITING mode4320//43214322const EDIT_COPY_LABEL = 'Edit copy';4323const EDIT_COPY_ICON =4324'<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +4325'<path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/>' +4326'</svg>';43274328function usesShadowChromeRoot() {4329const root = liveUiRoot();4330return root && root !== document.body && root.host && root.host.id === PREFIX + '-root';4331}43324333function setImportantStyle(el, name, value) {4334el.style.setProperty(name, value, 'important');4335}43364337function initEditBadgeHitProxies() {4338if (!usesShadowChromeRoot() || editBadgeProxyRoot) return;4339editBadgeProxyRoot = document.createElement('div');4340editBadgeProxyRoot.id = PREFIX + '-edit-badge-hit-proxies';4341editBadgeProxyRoot.setAttribute('aria-hidden', 'true');4342const styles = {4343all: 'initial',4344position: 'fixed',4345inset: '0',4346width: '100vw',4347height: '100vh',4348zIndex: String(Z.toast + 1),4349pointerEvents: 'none',4350background: 'transparent',4351overflow: 'visible',4352};4353for (const [name, value] of Object.entries(styles)) {4354setImportantStyle(editBadgeProxyRoot, name.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()), value);4355}4356document.body.appendChild(editBadgeProxyRoot);4357}43584359function styleEditBadgeProxy(proxy, target) {4360const rect = target.getBoundingClientRect();4361const cursor = getComputedStyle(target).cursor || 'pointer';4362const styles = {4363all: 'initial',4364position: 'fixed',4365left: rect.left + 'px',4366top: rect.top + 'px',4367width: rect.width + 'px',4368height: rect.height + 'px',4369margin: '0',4370padding: '0',4371border: '0',4372borderRadius: '0',4373background: 'transparent',4374color: 'transparent',4375opacity: '0.001',4376pointerEvents: 'auto',4377cursor,4378zIndex: String(Z.toast + 2),4379};4380for (const [name, value] of Object.entries(styles)) {4381setImportantStyle(proxy, name.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()), value);4382}4383}43844385function proxyMouseEvent(type, source, target) {4386let event;4387try {4388event = new MouseEvent(type, {4389bubbles: type !== 'mouseenter' && type !== 'mouseleave',4390cancelable: true,4391composed: true,4392clientX: source.clientX,4393clientY: source.clientY,4394screenX: source.screenX,4395screenY: source.screenY,4396button: source.button || 0,4397buttons: source.buttons || 0,4398ctrlKey: source.ctrlKey,4399metaKey: source.metaKey,4400shiftKey: source.shiftKey,4401altKey: source.altKey,4402});4403target.dispatchEvent(event);4404} catch {}4405}44064407function bindEditBadgeProxy(proxy, target) {4408const stop = (event) => {4409event.preventDefault();4410event.stopPropagation();4411};4412proxy.addEventListener('mouseenter', (event) => {4413stop(event);4414proxyMouseEvent('mouseenter', event, target);4415proxyMouseEvent('mouseover', event, target);4416});4417proxy.addEventListener('mouseleave', (event) => {4418stop(event);4419proxyMouseEvent('mouseleave', event, target);4420proxyMouseEvent('mouseout', event, target);4421});4422proxy.addEventListener('mousedown', (event) => {4423stop(event);4424target.focus?.({ preventScroll: true });4425proxyMouseEvent('mousedown', event, target);4426});4427proxy.addEventListener('mouseup', (event) => {4428stop(event);4429proxyMouseEvent('mouseup', event, target);4430});4431proxy.addEventListener('click', (event) => {4432stop(event);4433target.click();4434syncEditBadgeHitProxies();4435});4436}44374438function editBadgeProxyTargets() {4439if (!usesShadowChromeRoot() || !editBadgeEl || editBadgeEl.style.display === 'none') return [];4440return [...editBadgeEl.querySelectorAll('button')].filter((target) => {4441if (target.disabled) return false;4442const rect = target.getBoundingClientRect();4443if (rect.width < 1 || rect.height < 1) return false;4444const style = getComputedStyle(target);4445return style.display !== 'none' && style.visibility !== 'hidden';4446});4447}44484449function syncEditBadgeHitProxies() {4450if (!usesShadowChromeRoot()) {4451if (editBadgeProxyRoot) editBadgeProxyRoot.remove();4452editBadgeProxyRoot = null;4453editBadgeProxyByTarget = new Map();4454return;4455}4456initEditBadgeHitProxies();4457if (!editBadgeProxyRoot) return;4458const targets = editBadgeProxyTargets();4459const active = new Set(targets);4460for (const [target, proxy] of editBadgeProxyByTarget) {4461if (!active.has(target) || !target.isConnected) {4462proxy.remove();4463editBadgeProxyByTarget.delete(target);4464}4465}4466for (const target of targets) {4467let proxy = editBadgeProxyByTarget.get(target);4468if (!proxy) {4469proxy = document.createElement('button');4470proxy.type = 'button';4471proxy.tabIndex = -1;4472proxy.dataset.impeccableEditBadgeProxy = 'true';4473proxy.setAttribute('aria-hidden', 'true');4474bindEditBadgeProxy(proxy, target);4475editBadgeProxyRoot.appendChild(proxy);4476editBadgeProxyByTarget.set(target, proxy);4477}4478proxy.title = target.title || target.getAttribute('aria-label') || target.textContent || EDIT_COPY_LABEL;4479styleEditBadgeProxy(proxy, target);4480}4481}44824483function initEditBadge() {4484editBadgeEl = document.createElement('div');4485editBadgeEl.id = PREFIX + '-edit-badge';4486Object.assign(editBadgeEl.style, {4487position: 'fixed',4488zIndex: String(Z.highlight + 1),4489cursor: 'default',4490display: 'none',4491userSelect: 'none',4492});4493uiAppend(editBadgeEl);4494initEditBadgeHitProxies();44954496// Remove focus rings on edit badge buttons + contenteditable elements4497if (!uiGetById(PREFIX + '-edit-badge-focus-style')) {4498const s = document.createElement('style');4499s.id = PREFIX + '-edit-badge-focus-style';4500s.textContent =4501'#' + PREFIX + '-edit-badge button { outline: none !important; box-shadow: 0 2px 8px rgba(0,0,0,0.1) !important; }' +4502'#' + PREFIX + '-edit-badge button:focus { outline: none !important; }' +4503'#' + PREFIX + '-edit-badge button:focus-visible { outline: none !important; }' +4504'[data-impeccable-editable="true"] { outline: none !important; box-shadow: none !important; }' +4505'[data-impeccable-editable="true"]:focus { outline: none !important; box-shadow: none !important; }' +4506'[data-impeccable-editable="true"]:focus-visible { outline: none !important; box-shadow: none !important; }';4507uiAppendStyle(s);4508}4509}45104511function positionEditBadge() {4512if (!selectedElement || !editBadgeEl || editBadgeEl.style.display === 'none') {4513syncEditBadgeHitProxies();4514return;4515}4516const r = selectedElement.getBoundingClientRect();4517const bw = editBadgeEl.offsetWidth;4518// Match showHighlight's 2px outset so the badge right edge lines up with the outline.4519const outlineRight = r.right + 2;4520editBadgeEl.style.top = Math.max(4, r.top - 28) + 'px';4521editBadgeEl.style.left = Math.min(window.innerWidth - bw - 4, outlineRight - bw) + 'px';4522syncEditBadgeHitProxies();4523}45244525function renderEditBadge(mode) {4526if (mode === 'hidden' || !editBadgeEl) {4527hideConfigureBarTooltip();4528if (editBadgeEl) editBadgeEl.style.display = 'none';4529syncEditBadgeHitProxies();4530return;4531}4532editBadgeEl.style.display = 'flex';4533editBadgeEl.style.alignItems = 'center';4534editBadgeEl.style.cursor = 'default';4535const P = BP || barPaletteForTheme(detectPageTheme());4536const ACCENT = P.accent;4537const PRIMARY_TEXT = C.ink;4538const SURFACE = P.chatSurface;4539const MUTED = P.textDim;4540const HAIRLINE = P.hairline;4541const calloutStyle = (color, borderColor) => ({4542fontFamily: FONT,4543fontSize: '10px',4544fontWeight: '600',4545lineHeight: '16px',4546letterSpacing: '0.06em',4547color: color,4548background: SURFACE,4549padding: '2px 8px',4550border: '1px solid ' + (borderColor || color),4551borderRadius: '6px',4552boxSizing: 'border-box',4553minHeight: '22px',4554margin: '0',4555appearance: 'none',4556whiteSpace: 'nowrap',4557boxShadow: '0 4px 16px oklch(0% 0 0 / 0.16), 0 1px 3px oklch(0% 0 0 / 0.08)',4558cursor: 'pointer',4559transition: 'background 0.18s ease, color 0.18s ease, border-color 0.18s ease, filter 0.18s ease',4560});4561if (mode === 'idle' || mode === 'idle-disabled') {4562const disabled = mode === 'idle-disabled';4563editBadgeEl.innerHTML = '';4564const btn = document.createElement('button');4565btn.type = 'button';4566btn.innerHTML = EDIT_COPY_ICON;4567btn.setAttribute('aria-label', EDIT_COPY_LABEL);4568Object.assign(btn.style, calloutStyle(4569disabled ? MUTED : PRIMARY_TEXT,4570disabled ? HAIRLINE : ACCENT,4571));4572Object.assign(btn.style, {4573padding: '4px',4574minWidth: '22px',4575width: '22px',4576height: '22px',4577minHeight: '22px',4578display: 'inline-flex',4579alignItems: 'center',4580justifyContent: 'center',4581lineHeight: '0',4582letterSpacing: '0',4583background: disabled ? SURFACE : ACCENT,4584});4585if (disabled) {4586btn.style.cursor = 'not-allowed';4587btn.style.opacity = '0.55';4588btn.disabled = true;4589const disabledTip = EDIT_COPY_LABEL + ' is disabled while the current copy edit is applying';4590btn.addEventListener('mouseenter', () => showConfigureBarTooltip(btn, disabledTip));4591btn.addEventListener('mouseleave', hideConfigureBarTooltip);4592} else {4593btn.addEventListener('mouseenter', () => showConfigureBarTooltip(btn, EDIT_COPY_LABEL));4594btn.addEventListener('mouseleave', hideConfigureBarTooltip);4595btn.onclick = enterEditingMode;4596}4597editBadgeEl.appendChild(btn);4598} else {4599// 'editing' - show Cancel + Save separated4600editBadgeEl.innerHTML = '';4601editBadgeEl.style.gap = '8px';4602const cancel = document.createElement('button');4603cancel.textContent = 'Cancel';4604Object.assign(cancel.style, calloutStyle(MUTED, HAIRLINE));4605cancel.addEventListener('mouseenter', () => { cancel.style.color = P.text; });4606cancel.addEventListener('mouseleave', () => { cancel.style.color = P.textDim; });4607cancel.onclick = cancelEditing;4608const save = document.createElement('button');4609save.textContent = 'Save';4610Object.assign(save.style, calloutStyle(PRIMARY_TEXT, ACCENT));4611save.style.background = ACCENT;4612save.onclick = applyEditing;4613editBadgeEl.append(cancel, save);4614}4615positionEditBadge();4616}46174618// Decide which way the popover opens: away from the picked element. If the4619// bar landed below the element, popover slides DOWN from the bar's bottom.4620// If the bar landed above, popover slides UP from the bar's top.4621function popoverDirection() {4622if (!barEl || !selectedElement) return 'below';4623const br = barEl.getBoundingClientRect();4624const er = selectedElement.getBoundingClientRect();4625return br.top >= er.bottom - 4 ? 'below' : 'above';4626}46274628// The popover overlaps the bar by OVERLAP px on the bar-facing side. With4629// popover z-index below bar, that overlap sits behind bar (invisible) and4630// reinforces the "tucked behind" feel. Padding compensates so the real4631// content starts flush with bar's outer edge.4632const TUNE_OVERLAP = 6;46334634// Closed clip-path depends on direction: for 'below' clip from the far4635// (bottom) edge so the reveal grows downward from the bar; for 'above'4636// clip from the top edge so the reveal grows upward from the bar.4637function closedClipPath(direction) {4638return direction === 'below' ? 'inset(0 0 100% 0)' : 'inset(100% 0 0 0)';4639}46404641function setClipPath(value, withTransition) {4642const saved = paramsPanelEl.style.transition;4643if (!withTransition) paramsPanelEl.style.transition = 'none';4644paramsPanelEl.style.clipPath = value;4645if (!withTransition) {4646void paramsPanelEl.offsetHeight;4647paramsPanelEl.style.transition = saved;4648}4649}46504651function positionParamsPanel() {4652if (!paramsPanelEl || !barEl || barEl.style.display === 'none') return;4653const br = barEl.getBoundingClientRect();4654const direction = popoverDirection();4655const prevDirection = paramsPanelEl.dataset.tuneDirection;46564657// top/left/width are NOT in the transition list, so they snap instantly.4658paramsPanelEl.style.left = br.left + 'px';4659paramsPanelEl.style.width = br.width + 'px';46604661if (direction === 'below') {4662paramsPanelEl.style.top = (br.bottom - TUNE_OVERLAP) + 'px';4663paramsPanelEl.style.borderRadius = '0 0 10px 10px';4664paramsPanelEl.style.paddingTop = (14 + TUNE_OVERLAP) + 'px';4665paramsPanelEl.style.paddingBottom = '14px';4666} else {4667const ih = paramsPanelEl.offsetHeight || 80;4668paramsPanelEl.style.top = (br.top - ih + TUNE_OVERLAP) + 'px';4669paramsPanelEl.style.borderRadius = '10px 10px 0 0';4670paramsPanelEl.style.paddingTop = '14px';4671paramsPanelEl.style.paddingBottom = (14 + TUNE_OVERLAP) + 'px';4672}4673paramsPanelEl.dataset.tuneDirection = direction;46744675// If currently closed and direction flipped (or first-time setup),4676// snap the clip-path to the new direction's closed pose without4677// transitioning (so the clip doesn't slide across the element).4678if (!tuneOpen && (!prevDirection || prevDirection !== direction)) {4679setClipPath(closedClipPath(direction), false);4680}4681}46824683function showParamsPanel() {4684if (!paramsPanelEl) return;4685positionParamsPanel();4686paramsPanelEl.style.pointerEvents = 'auto';4687// rAF so the positioning paint commits before the transition fires.4688requestAnimationFrame(() => {4689setClipPath('inset(0 0 0 0)', true);4690});4691}46924693function hideParamsPanel() {4694if (!paramsPanelEl) return;4695paramsPanelEl.style.pointerEvents = 'none';4696const direction = paramsPanelEl.dataset.tuneDirection || 'below';4697setClipPath(closedClipPath(direction), true);4698}46994700// Build/rebuild the panel's contents for the current variant AND apply4701// its defaults to the variant wrapper (so scoped CSS responds even before4702// the user opens the popover). Visibility is governed by tuneOpen.4703function refreshParamsPanel() {4704if (state !== 'CYCLING') {4705paramsCurrentValues = {};4706tuneOpen = false;4707hideParamsPanel();4708return;4709}4710const variantEl = getVisibleVariantEl();4711const params = parseVariantParams(variantEl);4712if (!variantEl || params.length === 0) {4713paramsCurrentValues = {};4714tuneOpen = false;4715hideParamsPanel();4716return;4717}4718applyParamDefaults(variantEl, params);4719buildParamsPanel(variantEl, params);4720if (tuneOpen) {4721// If already visible (variant cycled while open), refresh in place4722// instead of re-running the clip-path animation.4723const alreadyVisible = paramsPanelEl.style.display === 'block'4724&& paramsPanelEl.style.opacity === '1';4725if (alreadyVisible) positionParamsPanel();4726else showParamsP