From 44a3a7182635b3afcba3b75c8a20f80082038454 Mon Sep 17 00:00:00 2001 From: NeoVand Date: Tue, 21 Jul 2026 23:32:42 -0500 Subject: [PATCH] cheatsheet: read beside it, and let it focus on the exercise at hand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes to make the cheat sheet a working companion rather than an overlay: - It now pushes the content aside like the Agent and Playground panels (desktop reading mode, at its own 21rem width), so a learner can keep an inline exercise and the reference on screen together. - When the learner is at a playground or challenge, the sheet narrows itself to the commands that exercise reaches for — playgrounds via their suggested-command walkthrough, challenges via the whole salted kit (the sheet explains, it never solves). A ListFilter toggle in the toolbar turns the focus off and on, and only appears when there is an exercise to focus on; a tinted strip names the exercise so the short list reads as a feature, not missing content. The mapping is pure and tested (exercise-commands.ts): scroll-spy anchors ARE scenario/challenge ids, and challenge-parsing's existing splitSegments/commandWordOf resolve command words per pipeline segment, sudo unwrapped. The panel title drops to "Cheat Sheet" to make room for the new toolbar control; aria-labels stay pinned for e2e. Co-Authored-By: Claude Fable 5 --- src/lib/components/layout/CheatSheet.svelte | 105 +++++++++++++++++-- src/lib/playground/exercise-commands.test.ts | 61 +++++++++++ src/lib/playground/exercise-commands.ts | 76 ++++++++++++++ src/routes/+page.svelte | 20 ++-- src/routes/layout.css | 14 ++- 5 files changed, 257 insertions(+), 19 deletions(-) create mode 100644 src/lib/playground/exercise-commands.test.ts create mode 100644 src/lib/playground/exercise-commands.ts diff --git a/src/lib/components/layout/CheatSheet.svelte b/src/lib/components/layout/CheatSheet.svelte index 89e97d6..032c2ea 100644 --- a/src/lib/components/layout/CheatSheet.svelte +++ b/src/lib/components/layout/CheatSheet.svelte @@ -14,10 +14,13 @@ Eye, FileCode, FolderPlus, + Gamepad2, Globe, LifeBuoy, + ListFilter, Lock, Package, + Puzzle, Route, Scissors, Workflow, @@ -34,6 +37,8 @@ type CheatSheetCommand } from '$lib/data/cheat-sheet'; import { tokenizeShellCommand } from '$lib/data/bash-syntax'; + import { readingContext } from '$lib/ai/reading-context.svelte'; + import { exerciseFocusOf, rowUsesWords } from '$lib/playground/exercise-commands'; let { open = false, onToggle }: { open: boolean; onToggle: () => void } = $props(); @@ -73,12 +78,41 @@ 'life-buoy': LifeBuoy }; + /* ── exercise focus ────────────────────────────────────────────────── + When the learner is AT a playground or challenge — the scroll-spy + anchor resolves to one, or the panel opened on one — the sheet can + narrow itself to the commands that exercise actually reaches for. + The lightbulb-free ListFilter toggle in the toolbar turns it off and + on; it only appears while there is an exercise to focus on. */ + let focusEnabled = $state(true); + + const exercise = $derived(exerciseFocusOf(readingContext.scenarioId ?? readingContext.sectionId)); + + /** The sheet narrowed to the exercise's commands — null when that would + * leave nothing to show (an exercise whose commands the sheet lacks). */ + const focusedCategories = $derived.by(() => { + if (!exercise) return null; + const result: CheatSheetCategory[] = []; + for (const category of cheatSheet) { + const commands = category.commands.filter((cmd) => rowUsesWords(cmd.command, exercise.words)); + if (commands.length > 0) result.push({ ...category, commands }); + } + return result.length > 0 ? result : null; + }); + + const focusActive = $derived(focusEnabled && focusedCategories !== null); + const focusAccent = $derived( + exercise?.kind === 'challenge' ? 'var(--color-challenge)' : 'var(--color-important)' + ); + const FocusIcon = $derived(exercise?.kind === 'challenge' ? Puzzle : Gamepad2); + let filteredCategories = $derived.by(() => { + const base = focusActive && focusedCategories ? focusedCategories : cheatSheet; const query = searchQuery.toLowerCase().trim(); - if (!query) return cheatSheet; + if (!query) return base; const result: CheatSheetCategory[] = []; - for (const category of cheatSheet) { + for (const category of base) { const matchingCommands = category.commands.filter( (cmd) => cmd.command.toLowerCase().includes(query) || @@ -116,10 +150,57 @@ for (const category of filteredCategories) expandedCategories.add(category.label); } }); + + // A focused sheet is short; a collapsed category inside it hides half of + // an already-small kit. Expand what the focus surfaces (never collapse). + $effect(() => { + if (focusActive && focusedCategories) { + for (const category of focusedCategories) expandedCategories.add(category.label); + } + }); +{#snippet focusToggle()} + + {#if exercise && focusedCategories} + + {/if} +{/snippet} + +{#snippet focusStrip()} + + {#if focusActive && exercise} +
+ +

+ Commands for {exercise.title} +

+
+ {/if} +{/snippet} + {#snippet legend()} @@ -212,7 +293,7 @@