| 1 | /** |
| 2 | * Framework-neutral Impeccable live chrome contract. |
| 3 | * |
| 4 | * The production browser bundle is intentionally plain DOM so Svelte, React, |
| 5 | * Vue, and static adapters can all mount the same chrome. This module is the |
| 6 | * testable contract/inventory for that bundle; live-browser.js mirrors these |
| 7 | * values at runtime because it is served as a standalone script. |
| 8 | */ |
| 9 | |
| 10 | export const LIVE_CHROME_MOUNT_CONTRACT = Object.freeze([ |
| 11 | 'root', |
| 12 | 'transport', |
| 13 | 'state', |
| 14 | 'actions', |
| 15 | ]); |
| 16 | |
| 17 | export const LIVE_UI_SURFACES = Object.freeze([ |
| 18 | { |
| 19 | key: 'global-bottom-bar', |
| 20 | ids: [ |
| 21 | 'impeccable-live-global-bar', |
| 22 | 'impeccable-live-global-bar-brand', |
| 23 | 'impeccable-live-pick-toggle', |
| 24 | 'impeccable-live-insert-toggle', |
| 25 | 'impeccable-live-detect-toggle', |
| 26 | 'impeccable-live-detect-badge', |
| 27 | 'impeccable-live-design-toggle', |
| 28 | 'impeccable-live-page-chat', |
| 29 | 'impeccable-live-page-chat-input', |
| 30 | 'impeccable-live-page-chat-voice', |
| 31 | ], |
| 32 | states: ['rest', 'hover', 'focus-visible', 'pressed', 'active', 'tooltip'], |
| 33 | }, |
| 34 | { |
| 35 | key: 'pending-copy-edit-dock', |
| 36 | ids: ['impeccable-live-pending-dock'], |
| 37 | states: ['closed', 'open', 'hover', 'pressed', 'loading', 'rollback', 'keep-fixing'], |
| 38 | }, |
| 39 | { |
| 40 | key: 'element-selection-chrome', |
| 41 | ids: [ |
| 42 | 'impeccable-live-highlight', |
| 43 | 'impeccable-live-tooltip', |
| 44 | 'impeccable-live-bar', |
| 45 | 'impeccable-live-configure-input-wrap', |
| 46 | 'impeccable-live-input', |
| 47 | 'impeccable-live-configure-voice', |
| 48 | ], |
| 49 | states: ['rest', 'hover', 'focus-visible', 'pressed', 'disabled'], |
| 50 | }, |
| 51 | { |
| 52 | key: 'action-picker', |
| 53 | ids: ['impeccable-live-picker'], |
| 54 | states: ['closed', 'open', 'option-hover', 'option-focus'], |
| 55 | }, |
| 56 | { |
| 57 | key: 'edit-chrome', |
| 58 | ids: ['impeccable-live-edit-badge'], |
| 59 | states: ['enabled', 'disabled', 'editing', 'cancel', 'save', 'edited-content'], |
| 60 | }, |
| 61 | { |
| 62 | key: 'generating-row', |
| 63 | ids: ['impeccable-live-bar', 'impeccable-live-shader'], |
| 64 | states: ['action-label', 'animated-dots', 'generating', 'done'], |
| 65 | }, |
| 66 | { |
| 67 | key: 'variant-cycling-row', |
| 68 | ids: ['impeccable-live-bar', 'impeccable-live-params-panel'], |
| 69 | states: ['variant-1', 'variant-2', 'variant-3', 'left-disabled', 'right-disabled', 'dot-click', 'accept', 'discard'], |
| 70 | }, |
| 71 | { |
| 72 | key: 'variant-params-panel', |
| 73 | ids: ['impeccable-live-params-panel'], |
| 74 | states: ['closed', 'open-above', 'open-below', 'range', 'steps', 'toggle'], |
| 75 | }, |
| 76 | { |
| 77 | key: 'saving-confirmed-rows', |
| 78 | ids: ['impeccable-live-bar'], |
| 79 | states: ['saving', 'applying-variant', 'confirmed'], |
| 80 | }, |
| 81 | { |
| 82 | key: 'insert-mode-chrome', |
| 83 | ids: [ |
| 84 | 'impeccable-live-insert-line', |
| 85 | 'impeccable-live-insert-placeholder', |
| 86 | 'impeccable-live-placeholder-resize', |
| 87 | 'impeccable-live-insert-input', |
| 88 | 'impeccable-live-insert-voice', |
| 89 | 'impeccable-live-insert-create', |
| 90 | 'impeccable-live-insert-create-tooltip', |
| 91 | ], |
| 92 | states: ['toggle-active', 'line', 'placeholder', 'resize', 'enabled', 'disabled', 'tooltip'], |
| 93 | }, |
| 94 | { |
| 95 | key: 'annotation-chrome', |
| 96 | ids: [ |
| 97 | 'impeccable-live-annot', |
| 98 | 'impeccable-live-annot-svg', |
| 99 | 'impeccable-live-annot-pins', |
| 100 | 'impeccable-live-annot-clear', |
| 101 | ], |
| 102 | states: ['overlay', 'drawing', 'pin', 'pin-edit', 'clear'], |
| 103 | }, |
| 104 | { |
| 105 | key: 'design-system-panel', |
| 106 | ids: ['impeccable-live-design-host'], |
| 107 | states: ['closed', 'open', 'tabs', 'token-tiles', 'copy'], |
| 108 | }, |
| 109 | { |
| 110 | key: 'toasts-and-errors', |
| 111 | ids: ['impeccable-live-toast'], |
| 112 | states: ['normal', 'error', 'no-variants-mounted'], |
| 113 | }, |
| 114 | { |
| 115 | key: 'css-isolation-boundary', |
| 116 | ids: ['impeccable-live-root'], |
| 117 | states: ['shadow-root', 'style-tags', 'hostile-css'], |
| 118 | }, |
| 119 | ]); |
| 120 | |
| 121 | export const LIVE_UI_COMPONENT_IDS = Object.freeze([ |
| 122 | ...new Set(LIVE_UI_SURFACES.flatMap((surface) => surface.ids)), |
| 123 | ]); |
| 124 | |
| 125 | export function resolveLiveUiRoot(env = globalThis) { |
| 126 | const doc = env?.document; |
| 127 | const explicit = env?.__IMPECCABLE_LIVE_UI_ROOT__ |
| 128 | || env?.window?.__IMPECCABLE_LIVE_UI_ROOT__; |
| 129 | if (explicit && typeof explicit.appendChild === 'function') return explicit; |
| 130 | return doc?.body || null; |
| 131 | } |
| 132 | |
| 133 | export function getLiveUiElementById(id, env = globalThis) { |
| 134 | const doc = env?.document; |
| 135 | const root = resolveLiveUiRoot(env); |
| 136 | if (!id) return null; |
| 137 | if (root?.getElementById) { |
| 138 | const found = root.getElementById(id); |
| 139 | if (found) return found; |
| 140 | } |
| 141 | if (root?.querySelector) { |
| 142 | const found = root.querySelector('#' + escapeCssIdent(id)); |
| 143 | if (found) return found; |
| 144 | } |
| 145 | return doc?.getElementById?.(id) || null; |
| 146 | } |
| 147 | |
| 148 | export function appendToLiveUiRoot(el, env = globalThis) { |
| 149 | const root = resolveLiveUiRoot(env); |
| 150 | if (!root) throw new Error('Impeccable live UI root is not available'); |
| 151 | root.appendChild(el); |
| 152 | return el; |
| 153 | } |
| 154 | |
| 155 | export function appendStyleToLiveUiRoot(styleEl, env = globalThis) { |
| 156 | const doc = env?.document; |
| 157 | const root = resolveLiveUiRoot(env); |
| 158 | if (root && root !== doc?.body) { |
| 159 | root.appendChild(styleEl); |
| 160 | } else { |
| 161 | (doc?.head || doc?.body || root).appendChild(styleEl); |
| 162 | } |
| 163 | return styleEl; |
| 164 | } |
| 165 | |
| 166 | export function activeElementDeep(doc = globalThis.document) { |
| 167 | let active = doc?.activeElement || null; |
| 168 | while (active?.shadowRoot?.activeElement) { |
| 169 | active = active.shadowRoot.activeElement; |
| 170 | } |
| 171 | return active; |
| 172 | } |
| 173 | |
| 174 | function escapeCssIdent(value) { |
| 175 | if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') { |
| 176 | return CSS.escape(String(value)); |
| 177 | } |
| 178 | return String(value).replace(/([ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '\\$1'); |
| 179 | } |
| 180 |