diff options
| author | Agando <roduccomm@gmail.com> | 2026-10-18 15:00:25 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-10-18 15:00:25 +0200 |
| commit | bc199e350ea4ab000df03c9e732bf3478c9c43b6 (patch) | |
| tree | dd75d59ec1acfeac2eae40de8f860ffcb34220f5 | |
| parent | 3966a870b13ab0896e6cfa7fb3bca2003846b7e5 (diff) | |
course: fix P0 content errors + completion/mobile/keyboard UX (review A)
| -rw-r--r-- | course.html | 86 | ||||
| -rw-r--r-- | course/chapters/02-architecture.json | 32 | ||||
| -rw-r--r-- | course/chapters/03-commands.json | 28 | ||||
| -rw-r--r-- | course/chapters/05-modules.json | 12 | ||||
| -rw-r--r-- | course/chapters/06-files-cloud.json | 4 | ||||
| -rw-r--r-- | course/chapters/07-automation.json | 9 | ||||
| -rw-r--r-- | course/chapters/08-build.json | 8 |
7 files changed, 115 insertions, 64 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> diff --git a/course/chapters/02-architecture.json b/course/chapters/02-architecture.json index 7dee513..8e8826b 100644 --- a/course/chapters/02-architecture.json +++ b/course/chapters/02-architecture.json @@ -7,12 +7,12 @@ { "id": "2.1", "title": "Terminal, modules, commands", - "objective": "See how the terminal, module manager and command registry fit together.", + "objective": "See what is loaded, why a command can be listed but broken, and what the system is doing.", "minutes": 9, "commands": [ "modules", - "status", - "help" + "doctor", + "status" ], "blocks": [ { @@ -37,23 +37,24 @@ }, { "type": "cmd", - "text": "check" + "text": "doctor" }, { "type": "tip", - "text": "A command that exists in `help` but fails at runtime usually has an unmet module requirement that `check` reports." + "text": "A command that exists in `help` but fails at runtime usually has an unmet module requirement that `doctor` reports." } ], "exercise": { - "instruction": "List the loaded modules.", + "instruction": "Run `doctor` and see how it ranks module problems.", "accept": [ + "doctor", "modules", - "status" + "module-info *" ], "check": { "type": "command" }, - "hint": "Type: modules" + "hint": "Type: doctor" }, "quiz": { "q": "Who executes a command after you press Enter?", @@ -76,9 +77,11 @@ "objective": "Understand where files live and why a module edit sometimes needs a rebuild and sometimes does not.", "minutes": 10, "commands": [ - "status", - "help", - "load-local" + "pwd", + "cd", + "tree", + "health-check", + "mem" ], "blocks": [ { @@ -103,14 +106,15 @@ } ], "exercise": { - "instruction": "Run `status` and find the VFS size line.", + "instruction": "Run `health-check` and find the VFS size line.", "accept": [ - "status" + "health-check", + "mem" ], "check": { "type": "command" }, - "hint": "Type: status" + "hint": "Type: health-check" }, "quiz": { "q": "Which tier needs `rebuild-bundles.py` after an edit?", diff --git a/course/chapters/03-commands.json b/course/chapters/03-commands.json index 10807cb..40fa2ad 100644 --- a/course/chapters/03-commands.json +++ b/course/chapters/03-commands.json @@ -7,16 +7,17 @@ { "id": "3.1", "title": "Find any command fast", - "objective": "Use help, commands and check to discover what exists before guessing.", + "objective": "Use help, doctor and status to discover what exists before guessing.", "minutes": 7, "commands": [ "help", + "doctor", "status" ], "blocks": [ { "type": "text", - "text": "`help` groups all 344 commands by the module that owns them." + "text": "`help` groups every registered command by the module that owns them." }, { "type": "cmd", @@ -28,18 +29,20 @@ }, { "type": "tip", - "text": "`check` shows module requirements — the fastest way to explain a command that is listed but not working." + "text": "`doctor` shows module requirements — the fastest way to explain a command that is listed but not working." } ], "exercise": { - "instruction": "Open the help for `webdav-push`.", + "instruction": "Run `doctor` and read its severity ranking.", "accept": [ - "help webdav-push" + "doctor", + "help", + "status" ], "check": { "type": "command" }, - "hint": "Type: help webdav-push" + "hint": "Type: doctor" }, "quiz": { "q": "Which command shows usage for one command?", @@ -74,11 +77,11 @@ }, { "type": "cmd", - "text": "ls | grep txt" + "text": "grep hello hello.txt" }, { "type": "warn", - "text": "`sed s/x/y/` substitutes the first match per LINE, not per file. `sed -i` is flagged destructive and needs confirmation." + "text": "`sed s/x/y/` substitutes the first match per LINE, not per file. `sed -i` overwrites the file immediately — checkpoint first (`checkpoint before-sed`). Pipes are partial in this build: `ls | grep x` may print the unfiltered listing; use `grep pattern file`." }, { "type": "text", @@ -90,15 +93,16 @@ } ], "exercise": { - "instruction": "Search the current listing for `txt`.", + "instruction": "Search a file for a word (create `hello.txt` with `edit` if needed).", "accept": [ - "ls | grep txt", - "grep txt" + "grep hello hello.txt", + "grep * hello.txt", + "grep hello *" ], "check": { "type": "command" }, - "hint": "Type: ls | grep txt" + "hint": "Type: grep hello hello.txt" }, "quiz": { "q": "`sed s/a/b/` replaces…", diff --git a/course/chapters/05-modules.json b/course/chapters/05-modules.json index 4b68f42..7829553 100644 --- a/course/chapters/05-modules.json +++ b/course/chapters/05-modules.json @@ -17,7 +17,7 @@ "blocks": [ { "type": "text", - "text": "`wpm` is the package manager for terminal modules: list the index, install a module from `packages.json` or a URL." + "text": "`wpm` is the package manager for terminal modules: list the index, install a package by name, inspect it with `wpm info`." }, { "type": "cmd", @@ -29,22 +29,22 @@ }, { "type": "text", - "text": "Every ext module in the default build auto-loads. There is no separate “needs loadmod” tier." + "text": "Most ext modules auto-load via `module-loader.js`. Manual modules such as `test-suite` need `load-local`." }, { "type": "cmd", - "text": "load-local modules/ext/calc.js" + "text": "load-local modules/test/test-suite.js" } ], "exercise": { - "instruction": "List available packages.", + "instruction": "Inspect the calc package metadata.", "accept": [ - "wpm list" + "wpm info calc" ], "check": { "type": "command" }, - "hint": "Type: wpm list" + "hint": "Type: wpm info calc" }, "quiz": { "q": "What does wpm install?", diff --git a/course/chapters/06-files-cloud.json b/course/chapters/06-files-cloud.json index 697a5f5..e997b9c 100644 --- a/course/chapters/06-files-cloud.json +++ b/course/chapters/06-files-cloud.json @@ -18,7 +18,7 @@ "blocks": [ { "type": "text", - "text": "`edit` opens the VFS editor. `diff` compares two files. `find` searches names; `filemgr` opens the visual browser." + "text": "`edit` opens the VFS editor. `diff` compares two files. `find` searches names; `files` opens the visual browser." }, { "type": "cmd", @@ -55,7 +55,7 @@ "quiz": { "q": "Which command opens the visual file manager?", "options": [ - "filemgr", + "files", "ls", "cat" ], diff --git a/course/chapters/07-automation.json b/course/chapters/07-automation.json index 3345fc2..b7eadb4 100644 --- a/course/chapters/07-automation.json +++ b/course/chapters/07-automation.json @@ -102,19 +102,20 @@ }, { "type": "warn", - "text": "`rollback` replaces current VFS state — checkpoint before experiments." + "text": "`rollback` replaces current VFS state — checkpoint before experiments. Only the 3 most recent checkpoints are kept; `checkpoint-list` shows what survived." } ], "exercise": { - "instruction": "Create a checkpoint.", + "instruction": "Create a checkpoint, then confirm it is listed with `checkpoint-list`.", "accept": [ "checkpoint *", - "checkpoint" + "checkpoint", + "checkpoint-list" ], "check": { "type": "command" }, - "hint": "Type: checkpoint my-test" + "hint": "Type: checkpoint my-test, then checkpoint-list" }, "quiz": { "q": "What does checkpoint do?", diff --git a/course/chapters/08-build.json b/course/chapters/08-build.json index 48a60bd..c09d89d 100644 --- a/course/chapters/08-build.json +++ b/course/chapters/08-build.json @@ -33,14 +33,16 @@ } ], "exercise": { - "instruction": "Run the built-in test suite.", + "instruction": "Load the test suite, then run it: `load-local modules/test/test-suite.js` then `test auto`.", "accept": [ - "status" + "test auto", + "test *", + "load-local *test-suite*" ], "check": { "type": "command" }, - "hint": "Type: test auto" + "hint": "Type: load-local modules/test/test-suite.js, then test auto" }, "quiz": { "q": "Which two places carry the version?", |