1const KEY = "oi-onboarding-v1" 2 3const FUNNEL = `<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 16 16" style="height:0.75em;display:inline-block;vertical-align:middle;flex-shrink:0"><path d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5v-2z"/></svg>` 4const CHART_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M4 11H2v3h2v-3zm5-4H7v7h2V7zm5-5v12h-2V2h2zm-2-1a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1h-2zM6 7a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H7a1 1 0 0 1-1-1V7zm-5 4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v3a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1v-3z"/></svg>` 5const PERSON_ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M8 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm2-3a2 2 0 1 1-4 0 2 2 0 0 1 4 0zm4 8c0 1-1 1-1 1H3s-1 0-1-1 1-4 6-4 6 3 6 4zm-1-.004c-.001-.246-.154-.986-.832-1.664C11.516 10.68 10.289 10 8 10c-2.29 0-3.516.68-4.168 1.332-.678.678-.83 1.418-.832 1.664h10z"/></svg>` 6 7const CSS = ` 8@keyframes _oi_pulse { 9 0% { box-shadow: 0 0 0 0 rgba(234,88,12,0.7); } 10 70% { box-shadow: 0 0 0 7px rgba(37,99,235,0); } 11 100% { box-shadow: 0 0 0 0 rgba(37,99,235,0); } 12} 13._oi_dot { 14 position: absolute; 15 top: -5px; right: -5px; 16 width: 9px; height: 9px; 17 border-radius: 50%; 18 background: #ea580c; 19 pointer-events: none; 20 animation: _oi_pulse 1.5s ease-out infinite; 21 z-index: 10; 22} 23._oi_card_head { 24 display: flex; 25 align-items: center; 26 justify-content: space-between; 27 margin-bottom: 14px; 28} 29._oi_card_title { 30 font-size: 0.78em; 31 text-transform: uppercase; 32 letter-spacing: 0.07em; 33 opacity: 0.5; 34 font-weight: 600; 35} 36._oi_close { 37 background: none; border: none; cursor: pointer; 38 color: inherit; opacity: 0.35; font-size: 1.1rem; 39 line-height: 1; padding: 2px 5px; border-radius: 4px; 40 transition: opacity 0.15s; flex-shrink: 0; 41} 42._oi_close:hover { opacity: 0.7; } 43._oi_grid { 44 display: grid; 45 grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); 46 gap: 14px; 47} 48._oi_item { display: flex; gap: 10px; align-items: flex-start; } 49._oi_icon { 50 flex-shrink: 0; width: 30px; height: 30px; 51 border-radius: 6px; background: rgba(128,128,128,0.1); 52 display: flex; align-items: center; justify-content: center; opacity: 0.65; 53} 54._oi_label { font-weight: 600; font-size: 0.88em; margin-bottom: 3px; } 55._oi_desc { font-size: 0.8em; opacity: 0.65; line-height: 1.45; } 56._oi_demo_chip { 57 display: inline-flex; align-items: baseline; gap: 2px; 58 background: rgba(128,128,128,0.12); border-radius: 3px; 59 padding: 0 4px 1px; font-size: inherit; white-space: nowrap; 60} 61._oi_help_btn { 62 position: fixed; 63 bottom: 24px; right: 24px; 64 width: 36px; height: 36px; 65 border-radius: 50%; 66 border: 1.5px solid var(--theme-foreground-faint, rgba(128,128,128,0.3)); 67 background: var(--theme-background, #fff); 68 color: inherit; 69 cursor: pointer; 70 font-size: 0.9rem; 71 font-weight: 600; 72 opacity: 0.55; 73 transition: opacity 0.15s, box-shadow 0.15s; 74 z-index: 1000; 75 display: flex; align-items: center; justify-content: center; 76 box-shadow: 0 1px 4px rgba(0,0,0,0.08); 77} 78._oi_help_btn:hover { opacity: 1; box-shadow: 0 2px 8px rgba(0,0,0,0.15); } 79._oi_panel { 80 position: fixed; 81 bottom: 24px; 82 right: 68px; 83 left: 24px; 84 background: var(--theme-background, #fff); 85 border: 1px solid var(--theme-foreground-faint, rgba(128,128,128,0.2)); 86 border-radius: 10px; 87 padding: 16px 20px 18px; 88 box-shadow: 0 4px 20px rgba(0,0,0,0.12); 89 z-index: 999; 90 box-sizing: border-box; 91} 92` 93 94function injectCSS() { 95 if (document.getElementById("_oi_css")) return 96 const s = document.createElement("style") 97 s.id = "_oi_css" 98 s.textContent = CSS 99 document.head.appendChild(s) 100} 101 102const STRINGS = { 103 ru: { 104 panelTitle: 'Ðак ÑабоÑаÑÑ Ñо ÑÑÑаниÑей', 105 helpTitle: 'Ðак ÑабоÑаÑÑ Ñо ÑÑÑаниÑей', 106 items: [ 107 { label: 'СÑÑлки-ÑилÑÑÑÑ', desc: `ÐажимайÑе на <span class="_oi_demo_chip">Ñакие ÑÑÑлки ${FUNNEL}</span> в ÑекÑÑе, ÑÑÐ¾Ð±Ñ ÑоÑмиÑоваÑÑ Ð²ÑбоÑÐºÑ Ð´Ð°Ð½Ð½ÑÑ ` }, 108 { label: 'ÐÑаÑики', desc: 'ÐликайÑе на ÑÑолбÑÑ, ÑÑейки и дÑÑгие ÑлеменÑÑ Ð³ÑаÑиков, ÑÑÐ¾Ð±Ñ ÑилÑÑÑоваÑÑ Ð´Ð°Ð½Ð½Ñе' }, 109 { label: 'ÐаÑÑоÑки и ÑаблиÑÑ', desc: 'ÐажмиÑе на каÑÑоÑÐºÑ Ñеловека или ÑÑÑÐ¾ÐºÑ ÑаблиÑÑ, ÑÑÐ¾Ð±Ñ Ð¾ÑкÑÑÑÑ ÐµÐ³Ð¾ иÑÑоÑиÑ' }, 110 ], 111 }, 112 en: { 113 panelTitle: 'How to use this page', 114 helpTitle: 'How to use this page', 115 items: [ 116 { label: 'Filter links', desc: `Click on <span class="_oi_demo_chip">links like this ${FUNNEL}</span> in the text to filter the data` }, 117 { label: 'Charts', desc: 'Click on bars, cells, and other chart elements to filter the data' }, 118 { label: 'Cards and tables', desc: 'Click on a person\'s card or a table row to view their story' }, 119 ], 120 }, 121} 122 123const _icons = [FUNNEL, CHART_ICON, PERSON_ICON] 124 125function _buildGridHtml(lang) { 126 const { items } = STRINGS[lang] || STRINGS.ru
127 return `<div class="_oi_grid">${items.map((item, i) => ` 128 <div class="_oi_item"> 129 <div class="_oi_icon">${_icons[i]}</div> 130 <div> 131 <div class="_oi_label">${item.label}</div> 132 <div class="_oi_desc">${item.desc}</div> 133 </div> 134 </div>`).join('')}</div>` 135} 136 137let _panel = null 138 139function getOrCreatePanel(lang = 'ru') { 140 if (_panel) return _panel 141 const s = STRINGS[lang] || STRINGS.ru 142 _panel = document.createElement("div") 143 _panel.className = "_oi_panel" 144 _panel.style.display = "none" 145 _panel.innerHTML = ` 146 <div class="_oi_card_head"> 147 <span class="_oi_card_title">${s.panelTitle}</span> 148 <button class="_oi_close">â</button> 149 </div> 150 ${_buildGridHtml(lang)}` 151 _panel.querySelector("._oi_close").addEventListener("click", () => { 152 localStorage.setItem(KEY, "1") 153 hidePanel() 154 }) 155 document.body.appendChild(_panel) 156 return _panel 157} 158 159function showPanel() { 160 const p = getOrCreatePanel() 161 p.style.display = "" 162} 163 164function hidePanel() { 165 if (_panel) _panel.style.display = "none" 166} 167 168function togglePanel() { 169 const p = getOrCreatePanel() 170 if (p.style.display === "none") showPanel() 171 else hidePanel() 172} 173 174function createHelpButton(lang = 'ru') { 175 const s = STRINGS[lang] || STRINGS.ru 176 const btn = document.createElement("button") 177 btn.className = "_oi_help_btn" 178 btn.title = s.helpTitle 179 btn.textContent = "?" 180 btn.addEventListener("click", togglePanel) 181 document.body.appendChild(btn) 182} 183 184function addPulse(el) { 185 if (el.querySelector("._oi_dot")) return 186 const dot = document.createElement("span") 187 dot.className = "_oi_dot" 188 el.appendChild(dot) 189 el.addEventListener("click", () => dot.remove(), { once: true }) 190} 191 192// Watches for first .oi-filter-chip using MutationObserver + periodic fallback 193function watchForFirstChip() { 194 const selector = ".oi-filter-chip:not([data-oi])" 195 196 const tryMark = () => { 197 const el = document.querySelector(selector) 198 if (!el) return false 199 el.setAttribute("data-oi", "1") 200 addPulse(el) 201 return true 202 } 203 204 if (tryMark()) return 205 206 // MutationObserver for dynamically added chips 207 const obs = new MutationObserver(() => { if (tryMark()) obs.disconnect() }) 208 obs.observe(document.body, { childList: true, subtree: true }) 209 210 // Fallback: retry a few times in case Observable Framework batches DOM updates 211 let attempts = 0 212 const poll = setInterval(() => { 213 if (tryMark() || ++attempts > 20) { clearInterval(poll); obs.disconnect() } 214 }, 300) 215} 216 217/** 218 * Initializes onboarding. 219 * Always adds the "?" button (fixed, bottom-right). 220 * On first visit â shows the panel above the button and a pulsing dot on the first filter chip. 221 * On subsequent visits â panel is hidden, opens on click. 222 * @param {string} [lang='ru'] - Language: 'ru' or 'en' 223 */ 224export function initOnboarding(lang = 'ru') { 225 if (typeof window === "undefined") return document.createElement("div") 226 227 injectCSS() 228 createHelpButton(lang) 229 getOrCreatePanel(lang) // create eagerly so parent:scroll can apply translateY before first click 230 231 if (!localStorage.getItem(KEY)) { 232 watchForFirstChip() 233 showPanel() 234 } 235 236 return document.createElement("div") 237}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.