diff options
Diffstat (limited to 'course.html')
| -rw-r--r-- | course.html | 86 |
1 files changed, 63 insertions, 23 deletions
diff --git a/course.html b/course.html index c14979c..03ada60 100644 --- a/course.html +++ b/course.html @@ -20,7 +20,15 @@ header h1{font-size:1.05rem;letter-spacing:.3px} .btn.primary{background:linear-gradient(135deg,var(--accent),var(--accent2));border:0;color:#0b0b18;font-weight:700} .grow{flex:1} .layout{display:grid;grid-template-columns:280px minmax(0,1fr) 300px;gap:18px;padding:18px;max-width:1500px;margin:0 auto} -@media(max-width:1050px){.layout{grid-template-columns:1fr}.rail{display:none}} +.rail-toggle{display:none} +@media(max-width:1050px){.layout{grid-template-columns:1fr} + .rail{display:block;position:fixed;left:0;right:0;bottom:0;max-height:82vh;overflow:auto;z-index:40;border-radius:16px 16px 0 0;transform:translateY(105%);transition:transform .25s;padding-bottom:28px} + .rail.open{transform:none}.rail-toggle{display:inline-block}} +.backdrop{display:none;position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:35}.backdrop.show{display:block} +:focus-visible{outline:2px solid var(--cyan);outline-offset:2px} +button.chapter,button.lesson{width:100%;background:none;border:0;text-align:left;font-family:inherit;color:inherit} +.lesson.active{color:var(--text);background:var(--bg3);border-left-color:var(--accent)} +#reader{scroll-margin-top:70px} .panel{background:rgba(20,20,40,.72);border:1px solid var(--border);border-radius:var(--radius);padding:14px} .side h3,.rail h3{font-size:.75rem;text-transform:uppercase;letter-spacing:.12em;color:var(--text2);margin-bottom:10px} .chapter{display:flex;justify-content:space-between;gap:8px;padding:8px 10px;border-radius:9px;cursor:pointer;font-size:.85rem} @@ -55,6 +63,8 @@ main h2{font-size:1.35rem;margin-bottom:6px} <span class="chip" id="ver">loading…</span> <span class="chip" id="xp">0 XP</span> <div class="grow"></div> + <button class="btn rail-toggle" onclick="toggleRail('atlas')">Atlas</button> + <button class="btn rail-toggle" onclick="toggleRail('tutor')">Tutor</button> <button class="btn" onclick="showProjects()">Projects</button> <button class="btn" onclick="showFeed()">Daily</button> <a class="btn" href="main.html">⌨ Terminal</a> @@ -74,16 +84,18 @@ main h2{font-size:1.35rem;margin-bottom:6px} </div> </aside> <main class="panel" id="reader"><p class="small">Loading course…</p></main> - <aside class="panel rail"> - <h3>Command atlas</h3> - <input class="search" id="atlas-q" placeholder="filter 344 commands…" autocomplete="off"> + <aside class="panel rail" id="rail"> + <button class="btn" style="float:right" onclick="closeRail()">Close</button> + <h3 id="atlas-head">Command atlas</h3> + <input class="search" id="atlas-q" placeholder="filter commands…" autocomplete="off"> <div id="atlas" style="margin-top:8px;max-height:400px;overflow:auto"></div> - <h3 style="margin-top:16px">Ask the tutor</h3> + <h3 id="tutor-head" style="margin-top:16px">Ask the tutor</h3> <div id="tutor-log"></div> <input class="search" id="tutor-q" placeholder="ask about this lesson…" autocomplete="off"> <button class="btn primary" style="width:100%;margin-top:8px" id="tutor-send">Ask AI tutor</button> </aside> </div> +<div class="backdrop" id="backdrop" onclick="closeRail()"></div> <script> const BASE = '/spaces/newos4/course/'; const KEY = 'webx11_course_progress_v1'; @@ -103,6 +115,7 @@ async function boot(){ MANIFEST = m; ATLAS = a; $('ver').textContent = 'v' + m.version; $('telemetry').checked = localStorage.getItem('webx11_course_telemetry') !== 'off'; + $('atlas-q').placeholder = 'filter ' + Object.keys((a&&a.commands)||{}).length + ' commands…'; renderChapters(); renderAtlas(); renderProgress(); }catch(e){ $('reader').innerHTML = '<p style="color:var(--rose)">Course content unreachable: '+esc(e.message)+'</p>'; } } @@ -114,36 +127,45 @@ function renderProgress(){ function renderAtlas(q){ if(!ATLAS){ $('atlas').innerHTML='<p class="small" style="color:var(--rose)">atlas.json missing</p>'; return; } const items = Object.entries(ATLAS.commands).filter(([c,e])=> !q || (c+' '+e.desc+' '+e.module+' '+e.category).toLowerCase().includes(q.toLowerCase())); - $('atlas').innerHTML = '<p class="small" style="margin-bottom:6px">'+items.length+' / '+Object.keys(ATLAS.commands).length+' commands</p>' + const total = Object.keys(ATLAS.commands).length; + $('atlas').innerHTML = '<p class="small" style="margin-bottom:6px">Showing '+Math.min(items.length,60)+' of '+total+' commands'+(items.length>60?' — refine to narrow':'')+'</p>' + items.slice(0,60).map(([c,e])=>'<div class="atlas-item"><code>'+esc(c)+'</code><span>'+esc(e.desc||e.module)+' · '+esc(e.tier)+'</span></div>').join(''); } -async function renderChapters(){ - const p = prog(), wrap = $('chapters'); wrap.innerHTML = ''; +async function renderChapters(autoOpen = true){ + const p = prog(), wrap = $('chapters'); wrap.innerHTML = ''; const boxes = {}, chBtns = {}; for(const ch of MANIFEST.chapters||[]){ - const d = document.createElement('div'); d.className='chapter'; + const d = document.createElement('button'); d.type='button'; d.className='chapter'; d.setAttribute('aria-expanded','false'); d.innerHTML = '<span>'+esc(ch.icon||'📘')+' '+esc(ch.title)+'</span><small>'+ (ch.lessons||[]).filter(l=>p.completed[l.id]).length +'/'+(ch.lessons||[]).length+'</small>'; - wrap.appendChild(d); - const box = document.createElement('div'); wrap.appendChild(box); - d.onclick = () => { d.classList.toggle('active'); box.style.display = box.style.display==='none'?'block':'none'; }; + const box = document.createElement('div'); box.style.display='none'; + wrap.appendChild(d); wrap.appendChild(box); boxes[ch.id] = box; chBtns[ch.id] = d; + d.onclick = () => { const open = box.style.display==='none'; box.style.display = open?'block':'none'; d.classList.toggle('active', open); d.setAttribute('aria-expanded', String(open)); }; for(const l of ch.lessons||[]){ - const e = document.createElement('div'); e.className='lesson'+(p.completed[l.id]?' done':''); e.textContent = l.id+' · '+l.title; + const e = document.createElement('button'); e.type='button'; e.className='lesson'+(p.completed[l.id]?' done':''); e.dataset.lesson = l.id; + e.setAttribute('aria-current','false'); e.textContent = l.id+' · '+l.title; e.onclick = () => openLesson(ch.id, l.id); box.appendChild(e); } } - const p2 = prog(); const all = (MANIFEST.chapters||[]).flatMap(c=>(c.lessons||[]).map(l=>({c,l}))); - const first = all.find(x=>!p2.completed[x.l.id]) || all[0]; - if(first) openLesson(first.c.id, first.l.id, false); + if(autoOpen){ + const p2 = prog(); const all = (MANIFEST.chapters||[]).flatMap(c=>(c.lessons||[]).map(l=>({c,l}))); + const first = all.find(x=>!p2.completed[x.l.id]) || all[0]; + if(first){ if(boxes[first.c.id]){ boxes[first.c.id].style.display='block'; chBtns[first.c.id].classList.add('active'); chBtns[first.c.id].setAttribute('aria-expanded','true'); } openLesson(first.c.id, first.l.id, false); } + } + if(ACTIVE && ACTIVE.ch && boxes[ACTIVE.ch.id]){ boxes[ACTIVE.ch.id].style.display='block'; chBtns[ACTIVE.ch.id].classList.add('active'); chBtns[ACTIVE.ch.id].setAttribute('aria-expanded','true'); } + if(ACTIVE && ACTIVE.l){ const a = document.querySelector('.lesson[data-lesson="'+ACTIVE.l.id+'"]'); if(a){ a.classList.add('active'); a.setAttribute('aria-current','true'); } } } +let OPEN_SEQ = 0; async function openLesson(chId, lessonId, doTrack = true){ + const seq = ++OPEN_SEQ; $('reader').innerHTML = '<p class="small">Loading lesson…</p>'; try{ const ch = await fetch(BASE+'chapters/'+chId+'.json?v='+V).then(r=>r.json()); const l = (ch.lessons||[]).find(x=>x.id===lessonId); if(!l) throw new Error('lesson not found'); + if(seq !== OPEN_SEQ) return; ACTIVE = {ch, l}; const p = prog(); - let html = '<h2>'+esc(l.title)+'</h2><div class="meta">'+esc(l.objective||'')+' · ⏱ '+(l.minutes||5)+' min · 📘 '+esc(ch.title)+'</div>'; + let html = '<h2>'+esc(l.title)+'</h2><div class="meta">'+esc(l.objective||'')+' · ⏱ '+esc(l.minutes||5)+' min · 📘 '+esc(ch.title)+'</div>'; for(const b of l.blocks||[]){ - if(b.type==='cmd') html += '<div class="block cmd"><span>$ '+esc(b.text)+'</span><button class="btn" data-copy="'+esc(b.text)+'">Copy</button></div>'; + if(b.type==='cmd') html += '<div class="block cmd"><span>$ '+esc(b.text)+'</span><button class="btn" aria-label="Copy command" data-copy="'+esc(b.text)+'">Copy</button></div>'; else if(b.type==='code') html += '<pre class="block cmd">'+esc(b.text)+'</pre>'; else if(b.type==='tip') html += '<div class="block tip">💡 '+esc(b.text)+'</div>'; else if(b.type==='warn') html += '<div class="block warn">⚠️ '+esc(b.text)+'</div>'; @@ -159,24 +181,34 @@ async function openLesson(chId, lessonId, doTrack = true){ if(l.quiz){ html += '<div class="quiz"><strong>❓ '+esc(l.quiz.q)+'</strong>'; (l.quiz.options||[]).forEach((o,i)=>{ html += '<button class="opt" data-opt="'+i+'">'+(i+1)+') '+esc(o)+'</button>'; }); - html += '<p id="quiz-msg" style="margin-top:6px"></p></div>'; + html += '<p id="quiz-msg" role="status" aria-live="polite" style="margin-top:6px"></p></div>'; } $('reader').innerHTML = html; + document.querySelectorAll('.lesson').forEach(x=>{ x.classList.remove('active'); x.setAttribute('aria-current','false'); }); + const cur = document.querySelector('.lesson[data-lesson="'+lessonId+'"]'); + if(cur){ cur.classList.add('active'); cur.setAttribute('aria-current','true'); } + if(doTrack){ + $('reader').scrollIntoView({block:'start'}); + const h = $('reader').querySelector('h2'); if(h){ h.tabIndex = -1; h.focus({preventScroll:true}); } + } $('reader').querySelectorAll('[data-copy]').forEach(b=>b.onclick=()=>{ navigator.clipboard&&navigator.clipboard.writeText(b.dataset.copy); b.textContent='Copied'; }); - if($('hint-btn')) $('hint-btn').onclick=()=>{ $('hint').style.display='block'; }; + if($('hint-btn')){ $('hint-btn').setAttribute('aria-expanded','false'); $('hint-btn').setAttribute('aria-controls','hint'); $('hint-btn').onclick=()=>{ const h=$('hint'), open=h.style.display!=='block'; h.style.display=open?'block':'none'; $('hint-btn').setAttribute('aria-expanded',String(open)); }; } if($('done-btn')) $('done-btn').onclick=()=>{ completeLesson(l.id); }; $('reader').querySelectorAll('[data-opt]').forEach(b=>b.onclick=()=>{ const ok = parseInt(b.dataset.opt,10) === l.quiz.answer; $('quiz-msg').textContent = ok ? '✅ Correct (+5 XP)' : '❌ Not quite — re-read the lesson.'; $('quiz-msg').style.color = ok ? 'var(--green)' : 'var(--rose)'; - if(ok) completeLesson(l.id, 5); + if(ok){ const before = prog().completed[l.id]; completeLesson(l.id, 5); $('quiz-msg').textContent = before ? '✅ Correct' : '✅ Correct (+5 XP)'; } }); if (doTrack) track('lesson_open', {lesson:l.id}); }catch(e){ $('reader').innerHTML = '<p style="color:var(--rose)">Failed: '+esc(e.message)+'</p>'; } } function completeLesson(id, xp){ - const p = prog(); if(p.completed[id]) return; - p.completed[id]=new Date().toISOString(); p.xp=(p.xp||0)+(xp||10); save(p); renderProgress(); renderChapters(); + const p = prog(); + if(p.completed[id]){ if($('done-btn')) $('done-btn').textContent='✓ Done'; return; } + const award = xp || 10; + p.completed[id]=new Date().toISOString(); p.xp=(p.xp||0)+award; save(p); renderProgress(); renderChapters(false); + if($('done-btn')){ $('done-btn').textContent='✓ Done (+'+award+' XP)'; $('done-btn').disabled=true; } track('lesson_complete',{lesson:id}); } function track(type, detail){ @@ -246,6 +278,14 @@ async function showFeed(){ $('atlas-q').addEventListener('input', e => renderAtlas(e.target.value.trim())); $('tutor-send').onclick = askTutor; $('tutor-q').addEventListener('keydown', e => { if(e.key==='Enter') askTutor(); }); +function toggleRail(which){ + const r = $('rail'), b = $('backdrop'); + if(r.classList.contains('open') && r.dataset.section === which){ closeRail(); return; } + r.dataset.section = which; r.classList.add('open'); b.classList.add('show'); + const t = $(which === 'atlas' ? 'atlas-head' : 'tutor-head'); + if(t) setTimeout(()=>t.scrollIntoView({block:'start'}), 60); +} +function closeRail(){ $('rail').classList.remove('open'); $('backdrop').classList.remove('show'); } boot(); </script> </body> |