PageSourceSearch

https://repression.net/_import/components/onboarding.f0d96210.js

js repression.net collected 2026-09-26 02:16:53 UTC 8,489 bytes, 237 lines download raw bytes

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.