// app.jsx — мини-апп «мера.» // IOSDevice + home/test/result flow for Beck, PSS-10, Clarity const { useState, useEffect, useRef, useCallback } = React; const { IOSDevice } = window; const { BECK, PSS, PSS_OPTS, PSS_REVERSE, interpBeck, interpPss, interpClarity } = window.MERA_DATA; const EMBED = /[?&]embed=1/.test(location.search) || (window.self !== window.top); // ─── HOME ───────────────────────────────────────────────── function Home({ onPick, results }){ const tests = [ { id:'beck', code:'A', title:<>шкала бека, info:'21 пункт · ~7 мин', n:21, done:!!results.beck }, { id:'pss', code:'B', title:<>стресс · pss-10, info:'10 вопросов · ~3 мин', n:10, done:!!results.pss }, { id:'clarity', code:'C', title:<>ясность · vas, info:'1 шкала · ~1 мин', n:1, done:!!results.clarity }, ]; const doneCount = Object.values(results).filter(Boolean).length; const last = results.last; return (
сегодня · {new Date().toLocaleDateString('ru-RU',{day:'numeric',month:'long'})}

тихая проверка
самочувствия.

Три шкалы — депрессия, стресс, ясность мышления. Без советов, без баллов в красном. Просто точка отсчёта.

{doneCount}/3сегодня
14dстрик
v0.1версия
шкалы3 · готовые
{tests.map(t => ( ))}
{last && ( <>
последний{last.label}
{last.kicker}{last.when}

{last.title}

{last.bars.map((b,i) => ( ))}
)}
); } // ─── TEST COVER (intro for a single test) ───────────────── function Cover({ test, onStart, onBack }){ const meta = { beck: { code:'A', title:<>шкала бека, n:'21', kind:'bdi', desc:'В каждой группе — четыре утверждения (0–3). Выберите то, что точнее всего описывает Вас на этой неделе и сегодня.', m:'~7 мин', score:'0–63', q:'21 группа', opt:'4 пункта' }, pss: { code:'B', title:<>воспринимаемый стресс, n:'10', kind:'pss-10', desc:'Десять вопросов о прошедшем месяце. Пятибалльная шкала: никогда — почти никогда — иногда — довольно часто — часто.', m:'~3 мин', score:'0–40', q:'10 вопр.', opt:'5 опций' }, clarity: { code:'C', title:<>ясность мышления, n:'1', kind:'vas', desc:'Визуальная аналоговая шкала. Передвиньте бегунок: от полного тумана (0) до исключительной ясности (10).', m:'~1 мин', score:'0–10', q:'1 шкала', opt:'0–10' }, }[test]; return (
{meta.code} · {meta.kind} {meta.m}
{meta.code} · шкала · {meta.kind}

{meta.title}

{meta.desc}

время
{meta.m.replace('~','')}
диапазон
{meta.score}
вопросов
{meta.q}
опций
{meta.opt}
// прежде чем начать:
Это self-report. Это не диагноз и не замена консультации специалиста — это точка отсчёта, которую полезно сравнить через неделю.
); } // ─── BECK & PSS multiple-choice test ────────────────────── function ChoiceTest({ test, items, opts, onDone, onBack }){ const [idx, setIdx] = useState(0); const [answers, setAnswers] = useState({}); // {id: opt index} const cur = items[idx]; const sel = answers[cur.id]; const isLast = idx === items.length - 1; function pick(o){ setAnswers(a => ({...a, [cur.id]: o})); } function next(){ if (isLast){ // compute score let score = 0; if (test === 'beck'){ for (const it of items){ const v = answers[it.id] ?? 0; score += v; } } else { // PSS — reverse positive items for (const it of items){ const raw = answers[it.id] ?? 0; score += PSS_REVERSE.has(it.id) ? (4 - raw) : raw; } } onDone(score, answers); } else { setIdx(idx + 1); } } function prev(){ if (idx > 0) setIdx(idx - 1); } const code = test === 'beck' ? 'A · бек' : 'B · pss-10'; const total = items.length; return (
{code} {idx+1}/{total}
прогресс {idx+1} / {total}
{test === 'beck' ? `группа ${String(cur.id).padStart(2,'0')} · ${cur.title}` : `вопрос ${String(cur.id).padStart(2,'0')}`}

{test === 'beck' ? <>выберите одно утверждение, которое лучше всего описывает эту неделю и сегодня : cur.text}

{test === 'beck' &&

группа {cur.id} — «{cur.short}»

}
{(test === 'beck' ? cur.opts : opts).map((o, i) => { const txt = test === 'beck' ? o.replace(/^\d+ · /,'') : o.replace(/^\d+ · /,''); return ( ); })}
{idx > 0 && }
); } // ─── CLARITY VAS ────────────────────────────────────────── function ClarityTest({ onDone, onBack }){ const [val, setVal] = useState(5.0); const trackRef = useRef(null); const onPointer = useCallback((e) => { const track = trackRef.current; if (!track) return; const rect = track.getBoundingClientRect(); const x = (e.clientX ?? e.touches?.[0]?.clientX) - rect.left; const pct = Math.max(0, Math.min(1, x / rect.width)); setVal(Math.round(pct * 100) / 10); // 0.0 – 10.0 }, []); function onDown(e){ onPointer(e); const move = (ev) => onPointer(ev); const up = () => { window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up); }; window.addEventListener('pointermove', move); window.addEventListener('pointerup', up); } return (
C · ясность · vas 1 / 1

насколько ясно вы
мыслите прямо сейчас?

{val.toFixed(1)}из 10
{[0,1,2,3,4,5,6,7,8,9,10].map(n => {n})}
0 · туман, спутанность чёткость · 10
); } // ─── RESULT ─────────────────────────────────────────────── function Result({ test, score, answers, onBack }){ if (test === 'beck'){ const i = interpBeck(score); // Compute subscale breakdown (cognitive vs somatic): simplified const cognitive = [1,2,3,5,7,8,9,12,13,14].reduce((s,id) => s + (answers[id] ?? 0), 0); const somatic = [10,11,15,16,17,18,19,20,21].reduce((s,id) => s + (answers[id] ?? 0), 0); return (
A · бек · результат 21/21
результат · шкала бека

тон — {i.lab}

{score} / 63 {i.lab}уровень
симптомов

{i.desc}

когнитивная
{cognitive}/30
соматическая
{somatic}/27
self-report{new Date().toLocaleDateString('ru-RU')}
); } if (test === 'pss'){ const i = interpPss(score); return (
B · pss-10 · результат 10/10
результат · воспринимаемый стресс

уровень — {i.lab}

{score} / 40 {i.lab}стресс
за месяц

{i.desc}

self-report{new Date().toLocaleDateString('ru-RU')}
); } // clarity const i = interpClarity(score); return (
C · ясность · результат 1/1
результат · ясность мышления

сейчас — {i.lab}

{score.toFixed(1)} / 10 {i.lab}чёткость
мыслей

{i.desc}

visual-analogсейчас
); } // ─── ROOT ───────────────────────────────────────────────── function App({ initialTest }){ // screen: 'home' | 'cover' | 'test' | 'result' const [screen, setScreen] = useState(initialTest && initialTest !== 'home' ? 'cover' : 'home'); const [curTest, setCurTest] = useState(initialTest && initialTest !== 'home' ? initialTest : null); const [results, setResults] = useState({}); const [lastScore, setLastScore] = useState(null); const [lastAnswers, setLastAnswers] = useState(null); // react to external tab change useEffect(() => { if (!initialTest) return; if (initialTest === 'home') { setScreen('home'); setCurTest(null); } else { setCurTest(initialTest); setScreen('cover'); } }, [initialTest]); const goHome = useCallback(() => { setScreen('home'); setCurTest(null); }, []); const startTest = useCallback(() => setScreen('test'), []); const pickTest = useCallback((id) => { setCurTest(id); setScreen('cover'); }, []); function finishTest(score, answers){ setLastScore(score); setLastAnswers(answers); // save into results const now = new Date().toLocaleDateString('ru-RU',{day:'numeric',month:'short'}); const codeMap = { beck:'бек', pss:'pss', clarity:'ясность' }; let bars, label, title; if (curTest === 'beck'){ const items = BECK.map(b => ({h: ((answers[b.id] ?? 0)/3) || 0.05})); bars = items.slice(0, 12).map((x,i) => ({...x, cls: i===6?'acc':''})); label = interpBeck(score).lab; title = `бек · ${score}/63`; } else if (curTest === 'pss'){ const items = PSS.map(p => { const raw = answers[p.id] ?? 0; const v = PSS_REVERSE.has(p.id) ? (4 - raw) : raw; return {h: v/4 || 0.05, cls: PSS_REVERSE.has(p.id) ? 'm' : (v>=3?'acc':'')}; }); bars = items; label = interpPss(score).lab; title = `pss-10 · ${score}/40`; } else { bars = Array.from({length:10}, (_,i) => ({h: i < Math.round(score) ? 0.9 : 0.15, cls: i < Math.round(score) ? 'acc' : ''})); label = interpClarity(score).lab; title = `ясность · ${score.toFixed(1)}/10`; } setResults(r => ({ ...r, [curTest]: { score, when: now }, last: { kicker:`${codeMap[curTest]} · ${now}`, when: now, title, label, bars } })); setScreen('result'); } let content; if (screen === 'home') content = ; else if (screen === 'cover') content = ; else if (screen === 'test'){ if (curTest === 'beck') content = ; else if (curTest === 'pss') content = ; else content = ; } else content = ; return ( {content} ); } // ─── MOUNT + tab wiring ─────────────────────────────────── const mount = document.getElementById('ios-mount'); const root = ReactDOM.createRoot(mount); let curTab = 'home'; function render(){ root.render(); } render(); // Tab clicks document.querySelectorAll('#tabs .pg-tab').forEach(b => { b.addEventListener('click', () => { document.querySelectorAll('#tabs .pg-tab').forEach(x => x.classList.remove('active')); b.classList.add('active'); curTab = b.dataset.test; updateNotes(curTab); render(); }); }); // Notes panel const NOTES = { home: [ { tag:'A · home', title:<>главный экран, p:<>Три шкалы карточками. Точка входа, статус-карта, последний замер для контекста.
Без советов до результата — только название, время, число пунктов. }, { tag:'паттерн', title:<>тихая подача, p:<>Большой заголовок в Instrument Serif italic, мелкие мета-метки в JetBrains Mono. Все цифры — antomic display (Anton). }, ], beck: [ { tag:'A · бдi', title:<>21 группа, p:<>В каждой группе — четыре утверждения (0–3). Выбирается одно, лучше всего описывающее прошедшую неделю и сегодня. }, { tag:'подсчёт', title:<>сумма баллов, p:<>Сумма по всем группам. Диапазон 0–63: ≤9 — минимально, 10–15 — лёгкая, 16–19 — умеренная, 20–29 — выраженная, 30+ — тяжёлая. }, { tag:'примечание', p:<>В девятой группе — суицидальные мысли. Если ответ ≥1, в продакшене показываем горячую линию. В прототипе это пропущено., title:<>9-я группа } ], pss: [ { tag:'B · pss-10', title:<>10 вопросов, p:<>Шкала воспринимаемого стресса. Спрашиваем о последнем месяце: пятибалльная сетка никогда → часто (0–4). }, { tag:'reverse', title:<>обратные пункты, p:<>Вопросы 4, 5, 7, 8 — о чувстве контроля. Считаются в обратном направлении: 4 − ответ. }, { tag:'подсчёт', title:<>сумма 0–40, p:<>≤13 — низкий стресс, 14–26 — умеренный, 27+ — высокий. }, ], clarity: [ { tag:'C · vas', title:<>визуальная шкала, p:<>Аналоговая шкала 0–10. От «полный туман» до «исключительная ясность». Делается ежедневно — чтобы поймать тренд. }, { tag:'паттерн', title:<>один жест, p:<>Драг или клик по треку — значение обновляется с шагом 0.1. Гигантская цифра в Anton как обратная связь. }, ], }; const NOTES_SCORING = { beck: [ {b:'≤9', t:'минимально'}, {b:'10–15', t:'лёгкая'}, {b:'16–19', t:'умеренная', acc:true}, {b:'20+', t:'выраж/тяж'}, ], pss: [ {b:'0–13', t:'низкий'}, {b:'14–20', t:'умерен'}, {b:'21–26', t:'умерен+', acc:true}, {b:'27+', t:'высокий'}, ], clarity: [ {b:'0–2', t:'туман'}, {b:'3–4', t:'муть'}, {b:'5–7', t:'средне', acc:true}, {b:'8–10', t:'ясно'}, ], }; function updateNotes(tab){ const body = document.getElementById('notesBody'); const tag = document.getElementById('notesTag'); tag.textContent = tab; const items = NOTES[tab] || []; body.innerHTML = ''; const tmp = document.createElement('div'); tmp.style.cssText = 'display:flex;flex-direction:column;gap:14px'; // Render via React mini for JSX content const cont = document.createElement('div'); cont.style.cssText = 'display:flex;flex-direction:column;gap:14px'; body.appendChild(cont); const r = ReactDOM.createRoot(cont); function NotesView({tab}){ const items = NOTES[tab] || []; const sc = NOTES_SCORING[tab]; return ( <> {items.map((it,i) => (
{it.tag}#{String(i+1).padStart(2,'0')}

{it.title}

{it.p}

))} {sc && ( <>
порогиscoring
{sc.map((c,i) => (
{c.b}{c.t}
))}
)} {tab === 'home' && (
стекtype

три шрифта

Instrument Serif italic — заголовки, тёплый «бумажный» голос. Anton — большие цифры. JetBrains Mono — мета-метки, теги, пороги.

)} ); } r.render(); } setTimeout(() => updateNotes('home'), 0);