Files
mcn-short-video/.workbuddy/skills/impeccable/scripts/live-browser.js
T

11652 lines
449 KiB
JavaScript
Raw Normal View History

/**
* Impeccable Live Variant Mode - Browser Script
*
* Injected into the user's page via <script src="http://localhost:PORT/live.js">.
* The server prepends window.__IMPECCABLE_TOKEN__ and window.__IMPECCABLE_PORT__
* before this code.
*
* UI: a single floating bar that morphs between three states -
* configure (pick action + go), generating (progressive dots), and cycling
* (prev/next + accept/discard). Feels like Spotlight, not a modal.
*/
(function () {
'use strict';
if (typeof window === 'undefined') return;
// Guard against double-init. Bun's HTML loader may process the <script> tag
// and create a bundled copy alongside the external load, or HMR may re-execute.
// Check BEFORE reading token/port to catch all cases.
if (window.__IMPECCABLE_LIVE_INIT__) return;
window.__IMPECCABLE_LIVE_INIT__ = true;
const TOKEN = window.__IMPECCABLE_TOKEN__;
const PORT = window.__IMPECCABLE_PORT__;
if (!TOKEN || !PORT) {
window.__IMPECCABLE_LIVE_INIT__ = false; // reset so the real load can init
return;
}
//
// Design tokens
//
// Brand kinpaku (gold) is pinned to the site's neo-kinpaku tokens
// (see site/styles/kinpaku-tokens.css) so Accept / knobs / cycle-dots /
// the selection outline / the comment tag all match the site's accent,
// not a washed theme-adjusted one. These mirror the kit's picker
// colors in site/styles/kinpaku-kit.css; keep them in sync by hand.
const C = {
brand: 'oklch(84% 0.19 80.46)', // kinpaku gold
brandHov: 'oklch(86% 0.07 84)', // kinpaku-pale (hover lift)
brandSoft: 'oklch(84% 0.19 80.46 / 0.18)', // kinpaku-dim
ink: 'oklch(4% 0.004 95)', // lacquer-deep
ash: 'oklch(55% 0.018 82)', // warm muted text
paper: 'oklch(98% 0.005 95 / 0.92)', // light overlay on user pages
paperSolid:'oklch(98% 0.005 95)',
mist: 'oklch(90% 0.008 82 / 0.6)', // light hairline
white: 'oklch(99% 0 0)',
};
// Picker bar chrome - mirrors .live-demo-gbar / .live-demo-ctx in kinpaku-kit.css.
// Quiet neutral elevation: no gold halo ring (gold is reserved for the brand
// mark and the active control, not the container outline).
const PICKER_SHADOW =
'0 16px 36px -12px oklch(0% 0 0 / 0.6)';
const FONT = 'system-ui, -apple-system, sans-serif';
const MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';
// z-index: detect overlays use 99999, so our UI must be above them
const Z = { highlight: 100001, bar: 100005, picker: 100007, toast: 100010 };
const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'; // ease-out-quint
const PREFIX = 'impeccable-live';
const IMPECCABLE_COMMAND = (window.__IMPECCABLE_COMMAND_PREFIX__ || '/') + 'impeccable';
const PICK_CURSOR_STYLE_ID = PREFIX + '-pick-cursor-style';
const MANUAL_APPLY_STATE_TTL_MS = 15 * 60 * 1000;
const sessionState = window.__IMPECCABLE_LIVE_SESSION__?.createLiveBrowserSessionState({
prefix: PREFIX,
storage: localStorage,
idFactory: () => crypto.randomUUID().replace(/-/g, '').slice(0, 8),
});
if (!sessionState) {
console.error('[impeccable] live-browser-session.js was not loaded. Live mode cannot start safely.');
window.__IMPECCABLE_LIVE_INIT__ = false;
return;
}
const HIGHLIGHT_TRANSITION =
'top 140ms ' + EASE +
', left 140ms ' + EASE +
', width 140ms ' + EASE +
', height 140ms ' + EASE +
', opacity 150ms ease';
const TOOLTIP_TRANSITION =
'top 140ms ' + EASE + ', left 140ms ' + EASE + ', opacity 150ms ease';
const SKIP_TAGS = new Set([
'html', 'head', 'body', 'script', 'style', 'link', 'meta', 'noscript', 'br', 'wbr',
]);
// Command vocabulary (values + labels + icons) comes from the canonical source,
// skill/scripts/live/vocabulary.mjs, which live-server.mjs serializes into
// window.__IMPECCABLE_VOCAB__ when it serves /live.js (same injection path as
// the token/port above, so it is always present here). The icons stack above
// each chip label and recolor to C.brand when selected (strokes use
// currentColor). ACTIONS drives the picker grid; ICONS maps value -> svg.
const VOCAB = Array.isArray(window.__IMPECCABLE_VOCAB__) ? window.__IMPECCABLE_VOCAB__ : [];
const ICONS = {};
const ACTIONS = VOCAB.map((c) => {
ICONS[c.value] = c.icon;
return { value: c.value, label: c.label };
});
const LIVE_CHROME_MOUNT_CONTRACT = ['root', 'transport', 'state', 'actions'];
const LIVE_UI_SURFACES = [
{ 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'] },
{ key: 'pending-copy-edit-dock', ids: [PREFIX + '-pending-dock'] },
{ key: 'element-selection-chrome', ids: [PREFIX + '-highlight', PREFIX + '-tooltip', PREFIX + '-bar', PREFIX + '-selection-pill', PREFIX + '-input', PREFIX + '-configure-voice', PREFIX + '-configure-bar-tooltip'] },
{ key: 'action-picker', ids: [PREFIX + '-picker'] },
{ key: 'edit-chrome', ids: [PREFIX + '-edit-badge'] },
{ key: 'generating-row', ids: [PREFIX + '-bar', PREFIX + '-shader'] },
{ key: 'variant-cycling-row', ids: [PREFIX + '-bar', PREFIX + '-params-panel'] },
{ key: 'variant-params-panel', ids: [PREFIX + '-params-panel'] },
{ key: 'saving-confirmed-rows', ids: [PREFIX + '-bar'] },
{ 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'] },
{ key: 'annotation-chrome', ids: [PREFIX + '-annot', PREFIX + '-annot-svg', PREFIX + '-annot-pins', PREFIX + '-annot-clear'] },
{ key: 'design-system-panel', ids: [PREFIX + '-design-host'] },
{ key: 'toasts-and-errors', ids: [PREFIX + '-toast'] },
{ key: 'css-isolation-boundary', ids: [PREFIX + '-root'] },
];
const LIVE_UI_COMPONENT_IDS = [...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids))];
//
// State
//
let state = 'IDLE';
let hoveredElement = null;
let selectedElement = null;
let currentSessionId = null;
let expectedVariants = 0;
let arrivedVariants = 0;
let visibleVariant = 0;
let generationPhase = null;
// Ascending order of the agent-generation lifecycle. The visible progress bar
// must never regress: a `browser_resumed`/behind checkpoint re-broadcasts an
// earlier phase (the server regresses the snapshot phase to `generating` on a
// behind checkpoint), and without this the bar jumps backward mid-generation.
// Unranked phases (params sidecar flow, unknown values) always pass so we
// never block a phase we do not model.
const PHASE_RANK = {
queued: 0,
picked_up: 1,
scaffolding: 2,
scaffold_fallback: 3,
source_ready: 4,
generation_ready: 5,
generating: 5,
variants_progress: 5,
first_variant_generating: 6,
first_variant_validating: 7,
first_reviewable: 8,
remaining_variants_generating: 9,
remaining_variants_validating: 10,
second_reviewable: 11,
all_variants_ready: 12,
variants_ready: 12,
variant_parameters_generating: 13,
variant_parameters_validating: 14,
parameters_ready: 15,
};
function shouldAdvancePhase(current, next) {
if (!next || next === current) return false;
const nextRank = PHASE_RANK[next];
const currentRank = PHASE_RANK[current];
// Only block a known-lower phase from overwriting a known-higher one.
if (nextRank === undefined || currentRank === undefined) return true;
return nextRank >= currentRank;
}
let parameterGenerationState = 'idle';
let parameterReadyAnnouncedSession = null;
let svelteComponentSession = null;
let svelteRuntimePromise = null;
let pendingSvelteComponentRetryObserver = null;
let currentSourceFile = null;
let currentPreviewFile = null;
let currentPreviewMode = null;
let recoveryWaitingForAnchor = false;
let pickedAnchorSnapshot = null;
let pickedAnchorViewportTop = null;
let pendingVariantAnchorRetryObserver = null;
let pendingAcceptedSession = null;
let variantObserver = null;
let variantSelectionInFlight = false;
let variantSelectionPromise = null;
let recoveringEmptyCycling = false;
let hasProjectContext = false;
let selectedAction = 'impeccable';
let selectedCount = 3;
const browserOwner = sessionState.owner;
let checkpointTimer = null;
// Scroll lock - holds window.scrollY at a fixed value while the session is
// active, so HMR DOM patches and variant swaps can't drift the page. See
// startScrollLock / stopScrollLock below.
let scrollLockObserver = null;
let scrollLockTargetY = null;
let scrollLockAnchorTop = null;
let scrollLockRaf = null;
let scrollLockAbort = null;
const SCROLL_ANCHOR_LOCK_ID = 'impeccable-scroll-anchor-lock';
const VARIANT_STATE_STYLE_ID = 'impeccable-variant-state';
const DISCARD_STATE_STYLE_ID = 'impeccable-discard-state';
// Dedicated key for scroll position - SEPARATE from LS_KEY so that
// saveSession's state updates don't clobber a carefully-captured scrollY.
// (Previously: saveSession wrote scrollY alongside state, so every call
// during resume overwrote the pre-reload value with whatever the browser
// had landed on, typically 0.)
function writeScrollY(y) { sessionState.writeScrollY(y); }
function readScrollY() { return sessionState.readScrollY(); }
function clearScrollY() { sessionState.clearScrollY(); }
// Pre-empt the browser: apply manual scroll restoration and jump to the
// saved scrollY at script-parse time. Retries on fonts.ready and load
// are essential: scrollTo(y) clamps to the current document.scrollHeight,
// which is often hundreds of pixels short of the final value until
// async-loaded fonts swap in and reflow.
try {
history.scrollRestoration = 'manual';
const savedY = readScrollY();
if (savedY != null) {
const apply = () => {
if (Math.abs(window.scrollY - savedY) > 0.5) {
window.scrollTo(0, savedY);
}
};
apply();
if (document.fonts?.ready) document.fonts.ready.then(apply).catch(() => {});
window.addEventListener('load', apply, { once: true });
}
} catch {}
// UI refs
let highlightEl = null;
let tooltipEl = null;
let barEl = null;
let barHideSeq = 0;
let pickerEl = null;
let toastEl = null;
let scrollRaf = null;
let editBadgeEl = null;
let editBadgeProxyRoot = null;
let editBadgeProxyByTarget = new Map();
//
// Helpers
//
const domHelpers = window.__IMPECCABLE_LIVE_DOM__?.createLiveBrowserDomHelpers({
prefix: PREFIX,
skipTags: SKIP_TAGS,
document,
});
if (!domHelpers) {
console.error('[impeccable] live-browser-dom.js was not loaded. Live mode cannot start safely.');
window.__IMPECCABLE_LIVE_INIT__ = false;
return;
}
const {
own,
pickable,
desc,
rectIsUsableAnchor,
makeFrozenAnchor,
id8,
cssId,
liveUiRoot,
uiAppend,
uiAppendStyle,
uiGetById,
activeElementDeep,
defangOutsideHandlers,
} = domHelpers;
window.__IMPECCABLE_LIVE_CHROME_CORE__ = {
version: 1,
adapter: window.__IMPECCABLE_LIVE_ADAPTER__ || 'dom',
mountContract: LIVE_CHROME_MOUNT_CONTRACT,
surfaces: LIVE_UI_SURFACES,
componentIds: LIVE_UI_COMPONENT_IDS,
root: liveUiRoot,
append: uiAppend,
appendStyle: uiAppendStyle,
getById: uiGetById,
activeElementDeep,
debugState: () => ({
state,
currentSessionId,
expectedVariants,
arrivedVariants,
visibleVariant,
savedSession: loadSession(),
sourceFile: currentSourceFile,
previewFile: currentPreviewFile,
previewMode: currentPreviewMode,
barText: barEl?.textContent || null,
barConnected: !!barEl?.isConnected,
hasSvelteComponentSession: !!svelteComponentSession,
mountedSvelteVariant: svelteComponentSession?.mountedVariant || 0,
pickActive,
pendingApplyInFlight,
hoveredElement: hoveredElement ? {
tag: hoveredElement.tagName,
classes: hoveredElement.className,
pickable: pickable(hoveredElement),
} : null,
pendingSvelteComponentRetry: !!pendingSvelteComponentRetryObserver,
recoveryWaitingForAnchor,
evtSourceReadyState: evtSource ? evtSource.readyState : null,
}),
};
//
// Highlight overlay
//
function initHighlight() {
highlightEl = document.createElement('div');
highlightEl.id = PREFIX + '-highlight';
Object.assign(highlightEl.style, {
position: 'fixed', top: '0', left: '0', width: '0', height: '0',
border: '2px solid ' + C.brand, borderRadius: '3px',
pointerEvents: 'none', zIndex: Z.highlight, boxSizing: 'border-box',
transition: HIGHLIGHT_TRANSITION,
display: 'none', opacity: '0',
});
uiAppend(highlightEl);
tooltipEl = document.createElement('div');
tooltipEl.id = PREFIX + '-tooltip';
Object.assign(tooltipEl.style, {
position: 'fixed',
background: C.ink, color: C.white,
fontFamily: MONO, fontSize: '10px', fontWeight: '500',
padding: '2px 6px', borderRadius: '3px',
zIndex: Z.highlight + 1, pointerEvents: 'none',
whiteSpace: 'nowrap', display: 'none',
letterSpacing: '0.02em',
transition: TOOLTIP_TRANSITION,
});
uiAppend(tooltipEl);
}
function shouldShowHighlightTagTooltip() {
// Configure/edit carry the tag in the bar selection pill, so keep only the outline.
return state !== 'CONFIGURING' && state !== 'EDITING';
}
function hideHighlightTagTooltip() {
if (!tooltipEl) return;
tooltipEl.style.opacity = '0';
tooltipEl.style.display = 'none';
}
function showHighlight(el) {
if (!el || !highlightEl) return;
if (el.hasAttribute?.('data-impeccable-insert-placeholder')) return;
const r = el.getBoundingClientRect();
const top = (r.top - 2) + 'px', left = (r.left - 2) + 'px';
const width = (r.width + 4) + 'px', height = (r.height + 4) + 'px';
const showTagTooltip = shouldShowHighlightTagTooltip();
const hiWasHidden = highlightEl.style.display === 'none' || highlightEl.style.opacity === '0';
if (hiWasHidden) {
// Snap to first target without animating from (0,0), then fade in.
highlightEl.style.transition = 'none';
Object.assign(highlightEl.style, { top, left, width, height, display: 'block' });
void highlightEl.offsetWidth;
highlightEl.style.transition = HIGHLIGHT_TRANSITION;
highlightEl.style.opacity = '1';
} else {
Object.assign(highlightEl.style, { top, left, width, height, display: 'block', opacity: '1' });
}
if (!showTagTooltip) {
hideHighlightTagTooltip();
return;
}
const tipTop = r.top - 20;
const tipY = (tipTop < 4 ? r.bottom + 4 : tipTop) + 'px';
const tipX = Math.max(4, r.left) + 'px';
tooltipEl.textContent = desc(el);
if (hiWasHidden) {
tooltipEl.style.transition = 'none';
Object.assign(tooltipEl.style, { top: tipY, left: tipX, display: 'block' });
void tooltipEl.offsetWidth;
tooltipEl.style.transition = TOOLTIP_TRANSITION;
tooltipEl.style.opacity = '1';
} else {
Object.assign(tooltipEl.style, { top: tipY, left: tipX, display: 'block', opacity: '1' });
}
}
function hideHighlight() {
if (highlightEl) { highlightEl.style.opacity = '0'; highlightEl.style.display = 'none'; }
if (tooltipEl) { tooltipEl.style.opacity = '0'; tooltipEl.style.display = 'none'; }
}
//
// Annotation overlay (comment pins + kinpaku strokes)
//
// Active while state === 'CONFIGURING'. The overlay is a fixed-positioned
// sibling of <body> mirroring selectedElement's bounding rect. Click (no
// drag) drops a comment pin; drag paints a kinpaku SVG stroke. All coords
// are stored in element-local CSS px so they survive scroll / resize and
// correlate directly with the captured PNG.
//
const DRAG_THRESHOLD = 5; // px - below this, treat pointerup as a click
const PIN_DBL_CLICK_MS = 300; // two clicks on the same pin within this delete it
let annotOverlayEl = null;
let annotSvgEl = null;
let annotPinsEl = null;
let annotClearChipEl = null;
let annotState = { comments: [], strokes: [] };
let annotActive = false;
// `annotPointer` is either:
// { kind: 'new', x0, y0, moved, strokeEl, strokePoints } creating a stroke/pin
// { kind: 'pin', idx, startPointer, startPin, moved } dragging an existing pin
let annotPointer = null;
let annotEditing = null; // { idx, input, wrapEl }
let annotLastPinClick = { idx: -1, time: 0 }; // for click-click-to-delete
let placeholderResizeLayerEl = null;
let placeholderResizeDrag = null;
function initAnnotOverlay() {
annotOverlayEl = document.createElement('div');
annotOverlayEl.id = PREFIX + '-annot';
Object.assign(annotOverlayEl.style, {
position: 'fixed', top: '0', left: '0', width: '0', height: '0',
pointerEvents: 'auto', zIndex: Z.highlight + 2,
display: 'none', overflow: 'visible',
cursor: 'crosshair', touchAction: 'none',
});
annotSvgEl = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
annotSvgEl.id = PREFIX + '-annot-svg';
Object.assign(annotSvgEl.style, {
position: 'absolute', top: '0', left: '0',
width: '100%', height: '100%',
// The SVG itself doesn't absorb clicks; individual hit-paths opt-in via
// pointer-events=stroke so gaps still fall through to the overlay.
pointerEvents: 'none', overflow: 'visible',
});
annotOverlayEl.appendChild(annotSvgEl);
annotPinsEl = document.createElement('div');
annotPinsEl.id = PREFIX + '-annot-pins';
Object.assign(annotPinsEl.style, {
position: 'absolute', inset: '0',
pointerEvents: 'none',
});
annotOverlayEl.appendChild(annotPinsEl);
annotClearChipEl = document.createElement('div');
annotClearChipEl.id = PREFIX + '-annot-clear';
annotClearChipEl.dataset.annotClear = 'true';
annotClearChipEl.textContent = 'Clear';
Object.assign(annotClearChipEl.style, {
position: 'absolute', top: '8px', right: '8px',
background: C.ink, color: C.white,
fontFamily: FONT, fontSize: '10px', fontWeight: '500',
letterSpacing: '0.08em', textTransform: 'uppercase',
padding: '5px 12px', borderRadius: '999px',
cursor: 'pointer', pointerEvents: 'auto',
display: 'none', userSelect: 'none',
boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
});
annotOverlayEl.appendChild(annotClearChipEl);
placeholderResizeLayerEl = document.createElement('div');
placeholderResizeLayerEl.id = PREFIX + '-placeholder-resize';
Object.assign(placeholderResizeLayerEl.style, {
position: 'absolute',
inset: '0',
pointerEvents: 'none',
display: 'none',
zIndex: '2',
});