From a55c4cbfff147882a35df624f4d3bd060d158207 Mon Sep 17 00:00:00 2001 From: "Thomas (Aeshus)" Date: Sat, 25 Apr 2026 08:27:10 -0400 Subject: [PATCH 1/6] Prettify --- src/app/exercise/[id]/ExercisePageClient.tsx | 168 ++++- src/app/globals.css | 631 ++++++++++++++++-- src/app/page.tsx | 30 +- src/components/AppShell/ImagineRitSidebar.tsx | 135 +++- src/components/AppShell/Sidebar.tsx | 249 ++++--- .../panels/SourcePanel/SourcePanel.tsx | 14 +- src/components/shared/SuccessBanner.tsx | 2 +- 7 files changed, 1006 insertions(+), 223 deletions(-) diff --git a/src/app/exercise/[id]/ExercisePageClient.tsx b/src/app/exercise/[id]/ExercisePageClient.tsx index 92e66b4..d1621b5 100644 --- a/src/app/exercise/[id]/ExercisePageClient.tsx +++ b/src/app/exercise/[id]/ExercisePageClient.tsx @@ -1,8 +1,10 @@ 'use client'; -import { use, useEffect } from 'react'; +import { use, useEffect, useState } from 'react'; +import { usePathname, useRouter } from 'next/navigation'; import { useExerciseContext } from '@/state/ExerciseContext'; -import { getExercise } from '@/exercises/registry'; +import { getExercise, getAllExercises } from '@/exercises/registry'; +import { imagineRitExercises } from '@/exercises/imagine-rit'; import { StackSim } from '@/engine/simulators/StackSim'; import { HeapSim } from '@/engine/simulators/HeapSim'; import { WinHeapSim } from '@/engine/simulators/WinHeapSim'; @@ -16,6 +18,8 @@ import InputPanel from '@/components/panels/InputPanel/InputPanel'; import LogPanel from '@/components/panels/LogPanel/LogPanel'; import { ErrorBoundary } from '@/components/ErrorBoundary'; +const MOBILE_BREAKPOINT = '(max-width: 900px)'; + function retAddrInMain(symbols: Record): number { return (symbols.main || BASE_SYMBOLS.main) + 0x25; } @@ -56,9 +60,106 @@ function computeSymbols(exercise: ReturnType): Record +
directions
+
+ {currentExercise ? ( +
+ ) : ( +
Select an exercise to begin.
+ )} +
+
+ ); +} + +function MobileExercisePager() { + const router = useRouter(); + const pathname = usePathname(); + const { state } = useExerciseContext(); + const currentId = state.currentExerciseId; + const isImagineRit = pathname?.startsWith('/imagine-rit/'); + const orderedExercises = isImagineRit ? imagineRitExercises : getAllExercises(); + const currentIndex = orderedExercises.findIndex((exercise) => exercise.id === currentId); + const prevExercise = currentIndex > 0 ? orderedExercises[currentIndex - 1] : null; + const nextExercise = + currentIndex >= 0 && currentIndex < orderedExercises.length - 1 + ? orderedExercises[currentIndex + 1] + : null; + const basePath = isImagineRit ? '/imagine-rit' : '/exercise'; + + return ( +
+ + +
+ ); +} + export default function ExercisePageClient({ params }: { params: Promise<{ id: string }> }) { const { id } = use(params); - const { dispatch, stackSim, heapSim, asmEmulator } = useExerciseContext(); + const { dispatch, stackSim, heapSim, asmEmulator, currentExercise } = useExerciseContext(); + const pathname = usePathname(); + const [isMobile, setIsMobile] = useState(false); + const [activeMobileTab, setActiveMobileTab] = useState<'source' | 'viz' | 'log'>('source'); + + useEffect(() => { + if (typeof window === 'undefined') return; + + const mediaQuery = window.matchMedia(MOBILE_BREAKPOINT); + const syncIsMobile = (event?: MediaQueryListEvent) => { + setIsMobile(event?.matches ?? mediaQuery.matches); + }; + + syncIsMobile(); + if (typeof mediaQuery.addEventListener === 'function') { + mediaQuery.addEventListener('change', syncIsMobile); + return () => mediaQuery.removeEventListener('change', syncIsMobile); + } + + mediaQuery.addListener(syncIsMobile); + return () => mediaQuery.removeListener(syncIsMobile); + }, []); + + useEffect(() => { + setActiveMobileTab('source'); + }, [id]); + + useEffect(() => { + const mainElement = document.querySelector('#app-body > main'); + if (!mainElement) return; + + if (isMobile) { + mainElement.classList.add('exercise-main-mobile-shell'); + } else { + mainElement.classList.remove('exercise-main-mobile-shell'); + } + + return () => { + mainElement.classList.remove('exercise-main-mobile-shell'); + }; + }, [isMobile, pathname]); useEffect(() => { const exercise = getExercise(id); @@ -267,6 +368,67 @@ export default function ExercisePageClient({ params }: { params: Promise<{ id: s dispatch({ type: 'LOAD_EXERCISE', exerciseId: id }); }, [id]); // eslint-disable-line + const mobileVizLabel = + currentExercise?.vizMode === 'asm' || currentExercise?.vizMode === 'asm-stack' + ? 'Assembly' + : 'Visual'; + + if (isMobile) { + return ( +
+ + +
+
+ + + +
+ +
+ {activeMobileTab === 'source' && } + {activeMobileTab === 'viz' && ( + + + + )} + {activeMobileTab === 'log' && } +
+
+ +
+ + + + +
+
+ ); + } return ( <> diff --git a/src/app/globals.css b/src/app/globals.css index a4d7d69..a29209d 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,52 +1,158 @@ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } +html { + color-scheme: dark; +} + +@media (prefers-reduced-motion: no-preference) { + html { + interpolate-size: allow-keywords; + scroll-behavior: smooth; + } +} + :root { - --bg: #0c0c0c; - --panel-bg: #141414; - --panel-border: #333; - --text: #ddd; - --text-dim: #999; - --green: #4ec9b0; - --blue: #569cd6; - --amber: #ce9178; - --red: #f44747; - --yellow: #dcdcaa; - --purple: #c586c0; - --comment: #7fbf68; - --font: 'Cascadia Code', 'Fira Code', 'JetBrains Mono', 'Consolas', monospace; -} - -html, body { height: 100%; background: var(--bg); color: var(--text); font-family: var(--font); font-size: 13px; } + --page-width: 76rem; + --text-width: 68ch; + --page-padding: clamp(1rem, 2vw + 0.5rem, 2rem); + --section-padding: clamp(1.25rem, 1rem + 1vw, 2rem); + --padding-lg: 2rem; + --padding: 1rem; + --padding-sm: 0.625rem; + --padding-xs: 0.375rem; + --gap: 1rem; + --radius-xl: 2rem; + --radius-lg: 1.25rem; + --radius-md: 0.9rem; + --radius-sm: 0.625rem; + --shadow-lg: 0 30px 80px rgba(0, 0, 0, 0.32); + --shadow-md: 0 14px 38px rgba(0, 0, 0, 0.22); + --3xl: clamp(3.2rem, 5vw, 5.5rem); + --2xl: clamp(2.4rem, 3vw, 3.6rem); + --xl: clamp(1.75rem, 2vw, 2.4rem); + --lg: 1.35rem; + --md: 1.0625rem; + --regular: 1rem; + --sm: 0.9rem; + --w-bold: 700; + --w-regular: 400; + --font-display: "IBM Plex Sans", "Aptos", "Segoe UI", sans-serif; + --font-sans: "IBM Plex Sans", "Aptos", "Segoe UI", sans-serif; + --font-mono: "JetBrains Mono Variable", "IBM Plex Mono", "JetBrains Mono", "Cascadia Code", monospace; + --bg: #061018; + --bg-soft: #0d1720; + --bg-panel: rgba(11, 22, 31, 0.82); + --bg-panel-strong: rgba(16, 28, 39, 0.94); + --bg-panel-solid: #0f1a24; + --bg-header: rgba(6, 13, 20, 0.82); + --fg: #f4f6f8; + --fg-muted: rgba(244, 246, 248, 0.72); + --fg-subtle: rgba(244, 246, 248, 0.52); + --border-color: rgba(126, 157, 177, 0.22); + --border-strong: rgba(126, 157, 177, 0.38); + --accent: #f59e3b; + --accent-soft: rgba(245, 158, 59, 0.16); + --accent-strong: #ffd29a; + --accent-secondary: #6de2d5; + --fg-accent: #09131a; + --panel-bg: var(--bg-panel); + --panel-border: var(--border-color); + --text: var(--fg); + --text-dim: var(--fg-muted); + --green: var(--accent); + --blue: #8ec5ff; + --amber: #ffc07a; + --red: #ff8e70; + --yellow: var(--accent-strong); + --purple: #bba4ff; + --comment: var(--accent-secondary); + --font: var(--font-sans); + --border: 1px solid var(--border-color); +} + +html, body { + height: 100%; + background: var(--bg); + color: var(--text); + font-family: var(--font-sans); + font-size: 13px; +} + +body { + min-height: 100dvh; + display: flex; + flex-direction: column; + line-height: 1.65; + background: + radial-gradient(circle at top left, rgba(245, 158, 59, 0.16), transparent 34%), + radial-gradient(circle at top right, rgba(109, 226, 213, 0.12), transparent 28%), + radial-gradient(circle at bottom center, rgba(245, 158, 59, 0.08), transparent 30%), + linear-gradient(180deg, #08131c 0%, #061018 45%, #040a10 100%); + -webkit-font-smoothing: antialiased; + position: relative; + overflow-x: hidden; +} + +body::before { + content: ''; + position: fixed; + inset: 0; + pointer-events: none; + background: + linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px); + background-size: 64px 64px; + mask-image: linear-gradient(180deg, rgba(0, 0, 0, 0.45), transparent 80%); + opacity: 0.18; +} #app { display: flex; flex-direction: column; height: 100vh; overflow: hidden; + position: relative; + z-index: 1; } header { - display: flex; align-items: center; gap: 1rem; padding: 0.5rem 1rem; - border-bottom: 1px solid var(--panel-border); background: var(--panel-bg); + display: flex; align-items: center; gap: 1rem; padding: 0.75rem 1rem; + border-bottom: 1px solid var(--panel-border); background: var(--bg-header); flex-shrink: 0; + backdrop-filter: blur(18px); +} +header h1 { + font-size: 1rem; + color: var(--fg); + font-weight: 600; + white-space: nowrap; + font-family: var(--font-display); + letter-spacing: 0.04em; } -header h1 { font-size: 14px; color: var(--green); font-weight: normal; white-space: nowrap; } /* Sidebar */ .sidebar { - width: 260px; min-width: 260px; background: var(--panel-bg); + width: 260px; min-width: 260px; background: var(--bg-panel-strong); border-right: 1px solid var(--panel-border); display: flex; flex-direction: column; overflow: hidden; transition: width 0.15s, min-width 0.15s; + position: relative; + z-index: 20; + backdrop-filter: blur(18px); } .sidebar-collapsed { width: 40px; min-width: 40px; } +.mobile-sidebar-toggle, +.mobile-sidebar-close, +.sidebar-backdrop { + display: none; +} .sidebar-toggle { display: flex; align-items: center; justify-content: center; width: 100%; height: 28px; flex-shrink: 0; background: transparent; border: none; border-bottom: 1px solid var(--panel-border); - color: var(--text-dim); font-family: var(--font); font-size: 11px; cursor: pointer; - transition: color 0.15s; + color: var(--fg-subtle); font-family: var(--font-sans); font-size: 11px; cursor: pointer; + transition: color 0.15s, background 0.15s; } -.sidebar-toggle:hover { color: var(--text); } +.sidebar-toggle:hover { color: var(--text); background: rgba(255,255,255,0.03); } .sidebar-content { flex: 1; overflow-y: auto; padding: 0.25rem 0; } @@ -59,13 +165,13 @@ header h1 { font-size: 14px; color: var(--green); font-weight: normal; white-spa .sidebar-track-header { display: flex; align-items: center; gap: 0.35rem; width: 100%; padding: 0.45rem 0.5rem; background: transparent; border: none; - color: var(--text); font-family: var(--font); font-size: 12px; - cursor: pointer; text-align: left; transition: background 0.1s; + color: var(--fg); font-family: var(--font-sans); font-size: 12px; + cursor: pointer; text-align: left; transition: background 0.1s, color 0.1s; } .sidebar-track-header:hover { background: rgba(255,255,255,0.04); } -.sidebar-track-name { flex: 1; font-weight: bold; letter-spacing: 0.02em; } +.sidebar-track-name { flex: 1; font-weight: 600; letter-spacing: 0.02em; } .sidebar-track-count { - font-size: 10px; color: var(--text-dim); margin-left: auto; white-space: nowrap; + font-size: 10px; color: var(--fg-subtle); margin-left: auto; white-space: nowrap; } /* Unit level */ @@ -75,7 +181,7 @@ header h1 { font-size: 14px; color: var(--green); font-weight: normal; white-spa .sidebar-unit-header { display: flex; align-items: center; gap: 0.3rem; width: 100%; padding: 0.3rem 0.5rem 0.3rem 1.25rem; background: transparent; border: none; - color: var(--text-dim); font-family: var(--font); font-size: 11px; + color: var(--fg-muted); font-family: var(--font-sans); font-size: 11px; cursor: pointer; text-align: left; text-transform: uppercase; letter-spacing: 0.04em; transition: background 0.1s; } @@ -90,15 +196,15 @@ header h1 { font-size: 14px; color: var(--green); font-weight: normal; white-spa display: flex; align-items: center; gap: 0.4rem; width: 100%; padding: 0.2rem 0.5rem 0.2rem 2rem; background: transparent; border: none; border-left: 2px solid transparent; - color: var(--text-dim); font-family: var(--font); font-size: 11px; + color: var(--fg-muted); font-family: var(--font-sans); font-size: 11px; cursor: pointer; text-align: left; transition: all 0.1s; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .sidebar-exercise:hover { background: rgba(255,255,255,0.03); color: var(--text); } .sidebar-exercise.active { - border-left-color: var(--green); color: var(--green); background: rgba(78,201,176,0.06); + border-left-color: var(--accent); color: var(--accent-strong); background: var(--accent-soft); } -.sidebar-exercise.completed { color: var(--comment); } +.sidebar-exercise.completed { color: var(--accent-secondary); } .sidebar-exercise-title { flex: 1; overflow: hidden; text-overflow: ellipsis; } @@ -108,13 +214,15 @@ header h1 { font-size: 14px; color: var(--green); font-weight: normal; white-spa /* Chevron arrows for accordion */ .sidebar-chevron { - flex-shrink: 0; width: 1em; text-align: center; font-size: 10px; color: var(--text-dim); + flex-shrink: 0; width: 1em; text-align: center; font-size: 10px; color: var(--fg-subtle); } #badges { display: flex; gap: 0.5rem; margin-left: auto; } .badge { - font-size: 11px; padding: 0.15rem 0.5rem; border: 1px solid var(--yellow); - color: var(--yellow); border-radius: 2px; white-space: nowrap; + font-size: 11px; padding: 0.2rem 0.6rem; border: 1px solid var(--border-strong); + color: var(--accent-strong); border-radius: 999px; white-space: nowrap; + background: rgba(255,255,255,0.03); + backdrop-filter: blur(12px); } #app-body { @@ -125,23 +233,31 @@ main { flex: 1; display: grid; overflow: hidden; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; - gap: 1px; background: var(--panel-border); + grid-auto-flow: row; + gap: var(--padding-sm); + background: transparent; + padding: var(--padding-sm); } .panel { background: var(--panel-bg); display: flex; flex-direction: column; overflow: hidden; + border: var(--border); + border-radius: var(--radius-md); + box-shadow: var(--shadow-md); + backdrop-filter: blur(18px); } .panel-hdr { - padding: 0.35rem 0.75rem; font-size: 11px; color: var(--text-dim); + padding: 0.5rem 0.9rem; font-size: 11px; color: var(--fg-subtle); border-bottom: 1px solid var(--panel-border); flex-shrink: 0; - text-transform: uppercase; letter-spacing: 0.05em; + text-transform: uppercase; letter-spacing: 0.08em; + font-family: var(--font-sans); } -.panel-body { flex: 1; overflow-y: auto; padding: 0.75rem; } +.panel-body { flex: 1; overflow-y: auto; padding: 0.85rem; } /* Source panel */ #source-panel { grid-column: 1; grid-row: 1; } #source-code { - white-space: pre; line-height: 1.6; tab-size: 4; + white-space: pre; line-height: 1.6; tab-size: 4; font-family: var(--font-mono); } .src-line { display: block; padding: 0 0.5rem; position: relative; } .src-line .ln { color: var(--text-dim); display: inline-block; width: 2.5em; text-align: right; margin-right: 1em; user-select: none; position: relative; } @@ -159,6 +275,23 @@ main { /* Stack panel */ #stack-panel { grid-column: 2; grid-row: 1; } #stack-viz { font-size: 12px; } +#stack-viz, +#exec-log, +#register-display, +.stack-addr, +.stack-byte, +.stack-ascii, +.stack-label, +.sym-name, +.sym-addr, +.gadget-addr, +.gadget-asm, +.free-list-addr, +.func-ptr-val, +#calc-result, +#built-payload { + font-family: var(--font-mono); +} .stack-row { display: flex; align-items: center; padding: 2px 0; @@ -213,7 +346,7 @@ main { .toolkit-header { display: flex; align-items: center; gap: 0.35rem; width: 100%; background: transparent; border: none; color: var(--text-dim); cursor: pointer; - font-family: var(--font); font-size: 10px; padding: 0.35rem 0.5rem; + font-family: var(--font-sans); font-size: 10px; padding: 0.45rem 0.5rem; text-transform: uppercase; letter-spacing: 0.05em; text-align: left; } .toolkit-header:hover { color: var(--text); background: rgba(255,255,255,0.03); } @@ -224,66 +357,160 @@ main { #input-area { margin-bottom: 0.75rem; } #input-area label { display: block; font-size: 11px; color: var(--text-dim); margin-bottom: 0.35rem; } #payload-input { - width: 100%; background: #0a0a0a; border: 1px solid var(--panel-border); color: var(--text); - font-family: var(--font); font-size: 13px; padding: 0.5rem; resize: none; min-height: 2.5em; + width: 100%; background: rgba(4, 12, 18, 0.78); border: 1px solid var(--border-color); color: var(--text); + font-family: var(--font-mono); font-size: 13px; padding: 0.65rem 0.75rem; resize: none; min-height: 2.5em; + border-radius: var(--radius-sm); } -#payload-input:focus { outline: none; border-color: var(--green); } -#payload-input::placeholder { color: var(--text-dim); } +#payload-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } +#payload-input::placeholder { color: var(--fg-subtle); } .input-mode-toggle { display: flex; gap: 0.5rem; margin-bottom: 0.5rem; } .input-mode-toggle button { - background: transparent; border: 1px solid var(--panel-border); color: var(--text-dim); - font-family: var(--font); font-size: 11px; padding: 0.2rem 0.5rem; cursor: pointer; + background: rgba(255,255,255,0.02); border: 1px solid var(--panel-border); color: var(--text-dim); + font-family: var(--font-sans); font-size: 11px; padding: 0.3rem 0.65rem; cursor: pointer; + border-radius: 999px; } -.input-mode-toggle button.active { border-color: var(--green); color: var(--green); } +.input-mode-toggle button.active { border-color: var(--accent); color: var(--accent-strong); background: var(--accent-soft); } .controls { display: flex; gap: 0.5rem; flex-wrap: wrap; } + +a.link-button, +button.link-button, .controls button { - background: transparent; border: 1px solid var(--panel-border); color: var(--text); - font-family: var(--font); font-size: 12px; padding: 0.35rem 1rem; cursor: pointer; - transition: all 0.15s; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.55rem; + min-height: 2.9rem; + padding: 0.75rem 1rem; + border-radius: 999px; + border: 1px solid rgba(126, 157, 177, 0.28); + background: rgba(255, 255, 255, 0.03); + color: var(--fg); + font-family: var(--font-sans); + font-size: 0.8rem; + font-weight: 700; + letter-spacing: 0.08em; + text-decoration: none; + text-transform: uppercase; + transition: + transform 0.2s ease, + border-color 0.2s ease, + background-color 0.2s ease, + color 0.2s ease, + box-shadow 0.2s ease; + white-space: nowrap; + cursor: pointer; +} + +a.link-button:hover, +a.link-button:focus-visible, +button.link-button:hover, +button.link-button:focus-visible, +.controls button:hover:not(:disabled), +.controls button:focus-visible:not(:disabled) { + border-color: rgba(245, 158, 59, 0.65); + color: var(--fg); + transform: translateY(-1px); + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.18); +} + +.link-button.primary, +.controls button.primary { + background: linear-gradient(135deg, var(--accent), #f6b865); + color: var(--fg-accent); + border-color: transparent; + box-shadow: 0 12px 30px rgba(245, 158, 59, 0.18); +} + +.link-button.primary:hover, +.link-button.primary:focus-visible, +.controls button.primary:hover:not(:disabled), +.controls button.primary:focus-visible:not(:disabled) { + background: linear-gradient(135deg, #f8ae53, #ffd08c); + color: #071018; +} + +.link-button.secondary { + background: rgba(9, 20, 29, 0.72); + border-color: rgba(109, 226, 213, 0.24); +} + +.link-button.secondary:hover, +.link-button.secondary:focus-visible { + border-color: rgba(109, 226, 213, 0.58); +} + +.link-button.secondary-accent { + color: var(--accent-secondary); + border-color: rgba(109, 226, 213, 0.35); +} + +.link-button.secondary-accent:hover, +.link-button.secondary-accent:focus-visible { + color: var(--fg); +} + +.controls button:disabled, +.link-button:disabled { + opacity: 0.3; + cursor: not-allowed; + transform: none; + box-shadow: none; } -.controls button:hover:not(:disabled) { border-color: var(--green); color: var(--green); } -.controls button.primary { border-color: var(--green); color: var(--green); } -.controls button:disabled { opacity: 0.3; cursor: not-allowed; } /* Payload builder */ #payload-builder { margin-top: 0.75rem; padding: 0.5rem; border: 1px solid var(--panel-border); font-size: 11px; + border-radius: var(--radius-sm); + background: rgba(255,255,255,0.02); } #payload-builder h4 { color: var(--text-dim); font-weight: normal; margin-bottom: 0.5rem; text-transform: uppercase; letter-spacing: 0.05em; } .builder-row { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.35rem; } .builder-row label { color: var(--text-dim); width: 10em; } .builder-row input { - background: #0a0a0a; border: 1px solid var(--panel-border); color: var(--text); - font-family: var(--font); font-size: 11px; padding: 0.25rem 0.4rem; width: 10em; + background: rgba(4, 12, 18, 0.78); border: 1px solid var(--panel-border); color: var(--text); + font-family: var(--font-mono); font-size: 11px; padding: 0.35rem 0.5rem; width: 10em; + border-radius: var(--radius-sm); } -.builder-row input:focus { outline: none; border-color: var(--green); } +.builder-row input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } .builder-row .hint { color: var(--text-dim); font-size: 10px; } -#built-payload { color: var(--yellow); word-break: break-all; margin-top: 0.5rem; padding: 0.35rem; background: #0a0a0a; min-height: 1.5em; } +#built-payload { + color: var(--yellow); + word-break: break-all; + margin-top: 0.5rem; + padding: 0.35rem; + background: rgba(4, 12, 18, 0.78); + min-height: 1.5em; + border-radius: var(--radius-sm); +} /* Hex calculator */ #hex-calc { margin-top: 0.75rem; padding: 0.5rem; border: 1px solid var(--panel-border); font-size: 11px; + border-radius: var(--radius-sm); + background: rgba(255,255,255,0.02); } #hex-calc h4 { color: var(--text-dim); font-weight: normal; margin-bottom: 0.5rem; text-transform: uppercase; letter-spacing: 0.05em; } .calc-row { display: flex; align-items: center; gap: 0.35rem; margin-bottom: 0.35rem; flex-wrap: wrap; } .calc-row input { - background: #0a0a0a; border: 1px solid var(--panel-border); color: var(--text); - font-family: var(--font); font-size: 12px; padding: 0.25rem 0.4rem; width: 10em; + background: rgba(4, 12, 18, 0.78); border: 1px solid var(--panel-border); color: var(--text); + font-family: var(--font-mono); font-size: 12px; padding: 0.35rem 0.5rem; width: 10em; + border-radius: var(--radius-sm); } -.calc-row input:focus { outline: none; border-color: var(--green); } +.calc-row input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } .calc-op { color: var(--yellow); font-size: 14px; font-weight: bold; width: 1.5em; text-align: center; } .calc-eq { color: var(--green); font-size: 14px; font-weight: bold; width: 1.5em; text-align: center; } #calc-result { - color: var(--green); font-size: 12px; font-weight: bold; padding: 0.25rem 0.4rem; - background: #0a0a0a; border: 1px solid var(--green); min-width: 10em; display: inline-block; + color: var(--accent-strong); font-size: 12px; font-weight: 600; padding: 0.35rem 0.5rem; + background: rgba(4, 12, 18, 0.78); border: 1px solid var(--accent); min-width: 10em; display: inline-block; + border-radius: var(--radius-sm); } .calc-hint { color: var(--text-dim); font-size: 10px; margin-top: 0.25rem; } @@ -313,9 +540,9 @@ main { #exercise-desc { padding: 0.5rem 0.75rem; font-size: 12px; line-height: 1.5; border-bottom: 1px solid var(--panel-border); flex-shrink: 0; - color: var(--text); background: rgba(78, 201, 176, 0.03); + color: var(--text); background: linear-gradient(180deg, rgba(245, 158, 59, 0.08), rgba(255,255,255,0.02)); } -#exercise-desc strong { color: var(--green); font-weight: normal; } +#exercise-desc strong { color: var(--accent-strong); font-weight: 600; } /* Sandbox controls */ .sandbox-controls { @@ -329,24 +556,30 @@ main { /* Success banner */ #success-banner { display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); - background: #111; border: 2px solid var(--green); padding: 2rem 3rem; - text-align: center; z-index: 100; box-shadow: 0 0 60px rgba(78,201,176,0.2); + background: var(--bg-panel-strong); border: 1px solid var(--accent); padding: 2rem 3rem; + text-align: center; z-index: 100; box-shadow: var(--shadow-lg); + border-radius: var(--radius-lg); + backdrop-filter: blur(18px); } #success-banner.show { display: block; animation: fadeIn 0.3s ease; } -#success-banner h2 { color: var(--green); font-size: 16px; margin-bottom: 0.5rem; } +#success-banner h2 { color: var(--accent-strong); font-size: 16px; margin-bottom: 0.5rem; } #success-banner p { color: var(--text); font-size: 13px; } #success-banner button { - margin-top: 1rem; background: transparent; border: 1px solid var(--green); color: var(--green); - font-family: var(--font); font-size: 12px; padding: 0.35rem 1rem; cursor: pointer; + margin-top: 1rem; background: linear-gradient(180deg, var(--accent-strong), var(--accent)); border: 1px solid var(--accent); color: var(--fg-accent); + font-family: var(--font-sans); font-size: 12px; padding: 0.45rem 1rem; cursor: pointer; + border-radius: var(--radius-sm); } @keyframes fadeIn { from { opacity: 0; transform: translate(-50%, -50%) scale(0.95); } to { opacity: 1; transform: translate(-50%, -50%) scale(1); } } @keyframes badgeSlideIn { from { opacity: 0; transform: translateX(40px); } to { opacity: 1; transform: translateX(0); } } /* CRT scanline overlay */ .crt::after { - content: ''; position: fixed; top: 0; left: 0; width: 100%; height: 100%; - background: repeating-linear-gradient(0deg, rgba(0,0,0,0.03) 0px, rgba(0,0,0,0.03) 1px, transparent 1px, transparent 3px); - pointer-events: none; z-index: 1000; + content: ''; + position: fixed; + inset: 0; + pointer-events: none; + background: radial-gradient(circle at center, transparent 55%, rgba(0, 0, 0, 0.28) 100%); + z-index: 1000; } /* Scrollbar */ @@ -354,6 +587,254 @@ main { ::-webkit-scrollbar-track { background: var(--panel-bg); } ::-webkit-scrollbar-thumb { background: var(--panel-border); } +@media (max-width: 900px) { + #app { + height: 100dvh; + } + + header { + gap: 0.75rem; + padding-left: 4.75rem; + } + + #badges { + display: none; + } + + .mobile-sidebar-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + position: fixed; + top: 0.55rem; + left: 0.75rem; + height: 2rem; + padding: 0 0.75rem; + border: 1px solid var(--panel-border); + background: var(--bg-panel-strong); + color: var(--text); + font-family: var(--font-sans); + font-size: 11px; + cursor: pointer; + z-index: 60; + border-radius: 999px; + backdrop-filter: blur(18px); + box-shadow: var(--shadow-md); + } + +.mobile-sidebar-toggle.active { + border-color: var(--accent); + color: var(--accent-strong); + background: rgba(16, 28, 39, 0.98); + } + + .sidebar-backdrop { + display: block; + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.6); + border: none; + z-index: 40; + } + + .sidebar { + position: fixed; + top: 0; + left: 0; + height: 100dvh; + width: min(82vw, 320px); + min-width: 0; + max-width: 320px; + border-right: 1px solid var(--panel-border); + transform: translateX(-100%); + transition: transform 0.18s ease; + box-shadow: 0 0 32px rgba(0, 0, 0, 0.35); + z-index: 50; + } + + main.exercise-main-mobile-shell { + display: block; + padding: var(--padding-sm); + overflow: hidden; + } + + .mobile-exercise-shell { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + gap: var(--padding-sm); + } + + .mobile-directions-panel { + flex-shrink: 0; + } + + .mobile-directions-panel .panel-body { + max-height: clamp(7.5rem, 24vh, 12rem); + overflow: auto; + } + + .mobile-directions-content, + .mobile-directions-empty { + font-size: 12px; + line-height: 1.6; + color: var(--text); + } + + .mobile-directions-content strong { + color: var(--accent-strong); + font-weight: 600; + } + + .mobile-workspace { + min-height: 0; + flex: 1; + display: flex; + flex-direction: column; + gap: 0.5rem; + } + + .mobile-workspace-tabs { + display: flex; + gap: 0.5rem; + flex-shrink: 0; + } + + .mobile-workspace-tab { + flex: 1; + min-width: 0; + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 2.6rem; + padding: 0.65rem 0.75rem; + border-radius: 999px; + border: 1px solid rgba(126, 157, 177, 0.28); + background: rgba(255, 255, 255, 0.03); + color: var(--fg-muted); + font-family: var(--font-sans); + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + cursor: pointer; + } + + .mobile-workspace-tab.active { + background: linear-gradient(135deg, var(--accent), #f6b865); + color: var(--fg-accent); + border-color: transparent; + box-shadow: 0 12px 30px rgba(245, 158, 59, 0.18); + } + + .mobile-workspace-panel { + min-height: 0; + flex: 1; + } + + .mobile-workspace-panel > .panel { + height: 100%; + min-height: 0; + } + + .mobile-workspace-panel > .panel .panel-body { + min-height: 0; + } + + .mobile-bottom-dock { + flex-shrink: 0; + display: flex; + flex-direction: column; + gap: 0.5rem; + } + + .mobile-bottom-dock #input-panel { + max-height: min(38dvh, 23rem); + } + + .mobile-bottom-dock #input-panel .panel-body { + overflow: auto; + } + + .mobile-exercise-pager { + display: flex; + gap: 0.5rem; + } + + .mobile-exercise-pager .link-button { + flex: 1; + min-width: 0; + } + + .sidebar.sidebar-mobile-open { + transform: translateX(0); + } + + .sidebar.sidebar-collapsed { + width: min(82vw, 320px); + min-width: 0; + } + + .sidebar-toggle { + display: none; + } + + .mobile-sidebar-close { + display: flex; + align-items: center; + justify-content: flex-end; + width: 100%; + min-height: 2.5rem; + padding: 0 0.85rem; + background: transparent; + border: none; + border-bottom: 1px solid var(--panel-border); + color: var(--fg-muted); + font-family: var(--font-sans); + font-size: 11px; + cursor: pointer; + } + + #app-body { + min-height: 0; + } + + main { + grid-template-columns: minmax(0, 1fr); + grid-template-rows: none; + grid-auto-rows: minmax(18rem, auto); + overflow-y: auto; + overflow-x: hidden; + } + + #source-panel, + #stack-panel, + #input-panel, + #log-panel { + grid-column: 1; + grid-row: auto; + min-height: 18rem; + } + + .panel-body { + padding: 0.65rem; + overflow: auto; + } + + #source-code { + min-width: max-content; + } + + .stack-row { + min-width: max-content; + } + + #register-display { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + /* Gadget table */ #gadget-table { margin-top: 0.75rem; font-size: 11px; } #gadget-table h4 { color: var(--text-dim); font-weight: normal; margin-bottom: 0.35rem; text-transform: uppercase; letter-spacing: 0.05em; } @@ -373,6 +854,12 @@ main { .reg-val { color: var(--amber); } .reg-val.changed { color: var(--green); animation: flash 0.4s ease; } +@media (max-width: 900px) { + #register-display { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + /* Sigframe builder */ #sigframe-builder { margin-top: 0.75rem; padding: 0.5rem; border: 1px solid var(--panel-border); diff --git a/src/app/page.tsx b/src/app/page.tsx index 2bfd93b..015abef 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -113,16 +113,8 @@ export default function Dashboard() { {continueExercise && (
@@ -169,16 +161,8 @@ export default function Dashboard() { {completedCount === 0 && (
@@ -218,20 +202,12 @@ export default function Dashboard() { }} /> diff --git a/src/components/AppShell/ImagineRitSidebar.tsx b/src/components/AppShell/ImagineRitSidebar.tsx index a933081..7882c70 100644 --- a/src/components/AppShell/ImagineRitSidebar.tsx +++ b/src/components/AppShell/ImagineRitSidebar.tsx @@ -1,8 +1,11 @@ 'use client'; +import { useCallback, useEffect, useState } from 'react'; import { useRouter, usePathname } from 'next/navigation'; import { useExerciseContext } from '@/state/ExerciseContext'; +const MOBILE_BREAKPOINT = '(max-width: 900px)'; + const IMAGINE_RIT_EXERCISES = [ { id: 'rit-01', title: 'The Stack Frame' }, { id: 'rit-02', title: 'The Overflow' }, @@ -15,42 +18,112 @@ export default function ImagineRitSidebar() { const router = useRouter(); const pathname = usePathname(); const { state } = useExerciseContext(); + const [isMobile, setIsMobile] = useState(false); + const [mobileOpen, setMobileOpen] = useState(false); const activeId = pathname?.split('/imagine-rit/')[1] ?? ''; + const toggleMobileSidebar = useCallback(() => { + setMobileOpen((prev) => !prev); + }, []); + + const closeMobileSidebar = useCallback(() => { + setMobileOpen(false); + }, []); + + useEffect(() => { + if (typeof window === 'undefined') return; + + const mediaQuery = window.matchMedia(MOBILE_BREAKPOINT); + const syncIsMobile = (event?: MediaQueryListEvent) => { + const matches = event?.matches ?? mediaQuery.matches; + setIsMobile(matches); + if (!matches) { + setMobileOpen(false); + } + }; + + syncIsMobile(); + if (typeof mediaQuery.addEventListener === 'function') { + mediaQuery.addEventListener('change', syncIsMobile); + return () => mediaQuery.removeEventListener('change', syncIsMobile); + } + + mediaQuery.addListener(syncIsMobile); + return () => mediaQuery.removeListener(syncIsMobile); + }, []); + + useEffect(() => { + if (isMobile) { + setMobileOpen(false); + } + }, [isMobile, pathname]); + return ( - + + ); } diff --git a/src/components/AppShell/Sidebar.tsx b/src/components/AppShell/Sidebar.tsx index 7bca2e7..fc5a6ab 100644 --- a/src/components/AppShell/Sidebar.tsx +++ b/src/components/AppShell/Sidebar.tsx @@ -6,6 +6,7 @@ import { useExerciseContext } from '@/state/ExerciseContext'; import { TRACKS, UNITS, getExercise } from '@/exercises/registry'; const STORAGE_KEY = '0xvrig-sidebar-v1'; +const MOBILE_BREAKPOINT = '(max-width: 900px)'; interface SidebarState { collapsed: boolean; @@ -36,6 +37,8 @@ export default function Sidebar() { const { state } = useExerciseContext(); const [sidebarState, setSidebarState] = useState(() => loadSidebarState()); + const [isMobile, setIsMobile] = useState(false); + const [mobileOpen, setMobileOpen] = useState(false); // Persist sidebar state on change useEffect(() => { @@ -60,6 +63,36 @@ export default function Sidebar() { })); }, []); + const toggleMobileSidebar = useCallback(() => { + setMobileOpen((prev) => !prev); + }, []); + + const closeMobileSidebar = useCallback(() => { + setMobileOpen(false); + }, []); + + useEffect(() => { + if (typeof window === 'undefined') return; + + const mediaQuery = window.matchMedia(MOBILE_BREAKPOINT); + const syncIsMobile = (event?: MediaQueryListEvent) => { + const matches = event?.matches ?? mediaQuery.matches; + setIsMobile(matches); + if (!matches) { + setMobileOpen(false); + } + }; + + syncIsMobile(); + if (typeof mediaQuery.addEventListener === 'function') { + mediaQuery.addEventListener('change', syncIsMobile); + return () => mediaQuery.removeEventListener('change', syncIsMobile); + } + + mediaQuery.addListener(syncIsMobile); + return () => mediaQuery.removeListener(syncIsMobile); + }, []); + // Keyboard shortcuts useEffect(() => { function handleKeyDown(e: KeyboardEvent) { @@ -83,6 +116,7 @@ export default function Sidebar() { }, [toggleCollapse]); const { collapsed } = sidebarState; + const isDesktopCollapsed = collapsed && !isMobile; // Determine which exercise is active from the URL path const exercisePrefix = '/exercise/'; @@ -114,94 +148,143 @@ export default function Sidebar() { } }, [activeExerciseId]); + useEffect(() => { + if (isMobile) { + setMobileOpen(false); + } + }, [isMobile, pathname]); + + const sidebarClasses = [ + 'sidebar', + isDesktopCollapsed ? 'sidebar-collapsed' : '', + mobileOpen ? 'sidebar-mobile-open' : '', + ] + .filter(Boolean) + .join(' '); + return ( - + + + ); } diff --git a/src/components/panels/SourcePanel/SourcePanel.tsx b/src/components/panels/SourcePanel/SourcePanel.tsx index 6f012c7..9f0176f 100644 --- a/src/components/panels/SourcePanel/SourcePanel.tsx +++ b/src/components/panels/SourcePanel/SourcePanel.tsx @@ -77,14 +77,14 @@ function highlightAsm(text: string): string { }); } -export default function SourcePanel() { +export default function SourcePanel({ showDescription = true }: { showDescription?: boolean }) { const { currentExercise, state } = useExerciseContext(); if (!currentExercise) { return (
source.c
-
Select an exercise to begin.
+ {showDescription &&
Select an exercise to begin.
}
@@ -100,10 +100,12 @@ export default function SourcePanel() { return (
{fileName}
-
+ {showDescription && ( +
+ )}
{lines.map((line, i) => { diff --git a/src/components/shared/SuccessBanner.tsx b/src/components/shared/SuccessBanner.tsx index e947bde..5b226e2 100644 --- a/src/components/shared/SuccessBanner.tsx +++ b/src/components/shared/SuccessBanner.tsx @@ -26,7 +26,7 @@ export default function SuccessBanner() {
{currentExercise.realWorld}
)} -
From 25bcdbc19afa7b7d9ede14ab56ff682c185752de Mon Sep 17 00:00:00 2001 From: "Thomas (Aeshus)" Date: Sat, 25 Apr 2026 08:30:54 -0400 Subject: [PATCH 2/6] Redesign 2 --- src/app/exercise/[id]/ExercisePageClient.tsx | 12 ++ src/app/globals.css | 65 ++++-- src/app/imagine-rit/page.tsx | 190 +++++++++++++++--- src/components/AppShell/ImagineRitSidebar.tsx | 10 + src/components/AppShell/Sidebar.tsx | 10 + src/components/panels/InputPanel/Toolkit.tsx | 51 +++-- 6 files changed, 273 insertions(+), 65 deletions(-) diff --git a/src/app/exercise/[id]/ExercisePageClient.tsx b/src/app/exercise/[id]/ExercisePageClient.tsx index d1621b5..ad2841a 100644 --- a/src/app/exercise/[id]/ExercisePageClient.tsx +++ b/src/app/exercise/[id]/ExercisePageClient.tsx @@ -19,6 +19,7 @@ import LogPanel from '@/components/panels/LogPanel/LogPanel'; import { ErrorBoundary } from '@/components/ErrorBoundary'; const MOBILE_BREAKPOINT = '(max-width: 900px)'; +const MOBILE_SIDEBAR_TOGGLE_EVENT = '0xvrig:toggle-mobile-sidebar'; function retAddrInMain(symbols: Record): number { return (symbols.main || BASE_SYMBOLS.main) + 0x25; @@ -105,6 +106,13 @@ function MobileExercisePager() { > ← Previous + + +
+
+ +
+
+
+ What to Expect +
+
+ 5 guided exercises +
+
+ No prior exploitation experience required +
+
+ Visual stack and control-flow feedback +
+
+ Roughly 30 to 45 minutes total +
+
+ +
+
+ Progress +
+
+ {doneCount}/{EXERCISES.length} +
+
+ {'█'.repeat(doneCount)}{'░'.repeat(EXERCISES.length - doneCount)} +
+
+
+ -
- Progress: {doneCount}/{EXERCISES.length}{' '} - - {'█'.repeat(doneCount)}{'░'.repeat(EXERCISES.length - doneCount)} - -
+
+
+ Why this exists +
+
+ {[ + { + title: 'No jargon wall', + text: 'The exercises explain what the stack, return address, and payload are while you use them.', + }, + { + title: 'Short feedback loops', + text: 'You can try an input, see the visual effect immediately, and iterate without needing a separate setup.', + }, + { + title: 'Builds confidence', + text: 'Each lesson introduces one idea, then carries it into the next exercise instead of throwing everything at you at once.', + }, + ].map((item) => ( +
+
+ {item.title} +
+
+ {item.text} +
+
+ ))} +
+
-
- {EXERCISES.map((ex, i) => { +
+
+ Workshop Path +
+
+ {EXERCISES.map((ex, i) => { const done = completed.has(ex.id); const isNext = !done && EXERCISES.slice(0, i).every(e => completed.has(e.id)); return ( @@ -60,34 +178,42 @@ export default function ImagineRitPage() { key={ex.id} onClick={() => router.push(`/imagine-rit/${ex.id}`)} style={{ - padding: '0.75rem 1rem', - border: `1px solid ${isNext ? 'var(--green)' : 'var(--panel-border)'}`, + padding: '1rem 1.1rem', + border: `1px solid ${isNext ? 'var(--accent)' : 'var(--panel-border)'}`, + borderRadius: 'var(--radius-md)', + background: done ? 'rgba(109, 226, 213, 0.08)' : 'rgba(255,255,255,0.025)', cursor: 'pointer', - opacity: done ? 0.6 : 1, + opacity: 1, }} > -
- {done && } - {isNext && } +
+ Lesson {i + 1} +
+
+ {done && } + {isNext && } {ex.title}
-
{ex.desc}
+
{ex.desc}
); })} -
+
+ {doneCount === EXERCISES.length && (
-
+
Workshop Complete!
-
+
You learned how buffer overflows work, hijacked program execution, bypassed ASLR, and built a ROP chain. Nice work!
diff --git a/src/components/AppShell/ImagineRitSidebar.tsx b/src/components/AppShell/ImagineRitSidebar.tsx index 7882c70..9968e7b 100644 --- a/src/components/AppShell/ImagineRitSidebar.tsx +++ b/src/components/AppShell/ImagineRitSidebar.tsx @@ -5,6 +5,7 @@ import { useRouter, usePathname } from 'next/navigation'; import { useExerciseContext } from '@/state/ExerciseContext'; const MOBILE_BREAKPOINT = '(max-width: 900px)'; +const MOBILE_SIDEBAR_TOGGLE_EVENT = '0xvrig:toggle-mobile-sidebar'; const IMAGINE_RIT_EXERCISES = [ { id: 'rit-01', title: 'The Stack Frame' }, @@ -59,6 +60,15 @@ export default function ImagineRitSidebar() { } }, [isMobile, pathname]); + useEffect(() => { + function handleExternalToggle() { + setMobileOpen((prev) => !prev); + } + + window.addEventListener(MOBILE_SIDEBAR_TOGGLE_EVENT, handleExternalToggle); + return () => window.removeEventListener(MOBILE_SIDEBAR_TOGGLE_EVENT, handleExternalToggle); + }, []); + return ( <> - {isOpen &&
{section.component}
} -
- ); - })} +
+ {visibleSections.map((section) => ( + + ))} +
+
+ {selectedSection.component} +
); } From b662ca02fe18fd2cbe03251d9b90bf16f152b7d7 Mon Sep 17 00:00:00 2001 From: "Thomas (Aeshus)" Date: Sat, 25 Apr 2026 08:34:35 -0400 Subject: [PATCH 3/6] Style --- src/app/exercise/[id]/ExercisePageClient.tsx | 28 +++- src/app/globals.css | 133 ++++++++++++++++-- src/components/AppShell/ImagineRitSidebar.tsx | 2 +- src/components/AppShell/Sidebar.tsx | 2 +- .../panels/InputPanel/InputPanel.tsx | 4 +- src/components/panels/InputPanel/Toolkit.tsx | 17 ++- src/components/panels/LogPanel/LogPanel.tsx | 2 +- 7 files changed, 163 insertions(+), 25 deletions(-) diff --git a/src/app/exercise/[id]/ExercisePageClient.tsx b/src/app/exercise/[id]/ExercisePageClient.tsx index ad2841a..908fde3 100644 --- a/src/app/exercise/[id]/ExercisePageClient.tsx +++ b/src/app/exercise/[id]/ExercisePageClient.tsx @@ -15,6 +15,7 @@ import { BASE_SYMBOLS } from '@/exercises/shared/symbols'; import SourcePanel from '@/components/panels/SourcePanel/SourcePanel'; import VizPanel from '@/components/panels/VizPanel/VizPanel'; import InputPanel from '@/components/panels/InputPanel/InputPanel'; +import Toolkit from '@/components/panels/InputPanel/Toolkit'; import LogPanel from '@/components/panels/LogPanel/LogPanel'; import { ErrorBoundary } from '@/components/ErrorBoundary'; @@ -130,7 +131,7 @@ export default function ExercisePageClient({ params }: { params: Promise<{ id: s const { dispatch, stackSim, heapSim, asmEmulator, currentExercise } = useExerciseContext(); const pathname = usePathname(); const [isMobile, setIsMobile] = useState(false); - const [activeMobileTab, setActiveMobileTab] = useState<'source' | 'viz' | 'log'>('source'); + const [activeMobileTab, setActiveMobileTab] = useState<'source' | 'viz' | 'log' | 'misc'>('source'); useEffect(() => { if (typeof window === 'undefined') return; @@ -381,9 +382,7 @@ export default function ExercisePageClient({ params }: { params: Promise<{ id: s dispatch({ type: 'LOAD_EXERCISE', exerciseId: id }); }, [id]); // eslint-disable-line const mobileVizLabel = - currentExercise?.vizMode === 'asm' || currentExercise?.vizMode === 'asm-stack' - ? 'Assembly' - : 'Visual'; + 'Assembly'; if (isMobile) { return ( @@ -417,7 +416,16 @@ export default function ExercisePageClient({ params }: { params: Promise<{ id: s className={`mobile-workspace-tab${activeMobileTab === 'log' ? ' active' : ''}`} onClick={() => setActiveMobileTab('log')} > - Log + Console + +
@@ -429,12 +437,20 @@ export default function ExercisePageClient({ params }: { params: Promise<{ id: s )} {activeMobileTab === 'log' && } + {activeMobileTab === 'misc' && currentExercise && ( +
+
misc
+
+ +
+
+ )}
- +
diff --git a/src/app/globals.css b/src/app/globals.css index 9e9bebd..4b88fea 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -373,6 +373,31 @@ main { .toolkit-panel { padding: 0 0.5rem 0.5rem; } +.toolkit-stack { + display: flex; + flex-direction: column; + gap: 0.75rem; + margin-top: 0; + border-top: 0; +} +.toolkit-stack-section { + padding: 0.85rem 0.95rem; + border: 1px solid var(--panel-border); + border-radius: var(--radius-sm); + background: rgba(255,255,255,0.02); +} +.toolkit-stack-title { + color: var(--accent-strong); + font-family: var(--font-sans); + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + margin-bottom: 0.5rem; +} +.toolkit-stack-body > *:first-child { + margin-top: 0 !important; +} /* Input panel */ #input-panel { grid-column: 1; grid-row: 2; } @@ -661,17 +686,23 @@ button.link-button:focus-visible, .sidebar { position: fixed; - top: 0; + top: auto; left: 0; - height: 100dvh; - width: min(82vw, 320px); + right: 0; + bottom: 0; + height: min(74dvh, 42rem); + width: 100%; min-width: 0; - max-width: 320px; - border-right: 1px solid var(--panel-border); - transform: translateX(-100%); + max-width: none; + border-right: none; + border-top: 1px solid var(--panel-border); + border-top-left-radius: 1.4rem; + border-top-right-radius: 1.4rem; + transform: translateY(calc(100% + 1rem)); transition: transform 0.18s ease; - box-shadow: 0 0 32px rgba(0, 0, 0, 0.35); + box-shadow: 0 -18px 50px rgba(0, 0, 0, 0.4); z-index: 50; + overflow: hidden; } main.exercise-main-mobile-shell { @@ -773,6 +804,10 @@ button.link-button:focus-visible, min-height: 0; } + .mobile-misc-panel .panel-body { + overflow: auto; + } + .mobile-bottom-dock { flex-shrink: 0; display: flex; @@ -803,7 +838,7 @@ button.link-button:focus-visible, } .sidebar.sidebar-mobile-open { - transform: translateX(0); + transform: translateY(0); } .sidebar.sidebar-collapsed { @@ -818,17 +853,89 @@ button.link-button:focus-visible, .mobile-sidebar-close { display: flex; align-items: center; - justify-content: flex-end; + justify-content: center; width: 100%; - min-height: 2.5rem; - padding: 0 0.85rem; - background: transparent; + min-height: 3.25rem; + padding: 0.85rem 1rem 0.75rem; + background: linear-gradient(180deg, rgba(255,255,255,0.03), rgba(255,255,255,0.01)); border: none; border-bottom: 1px solid var(--panel-border); color: var(--fg-muted); font-family: var(--font-sans); - font-size: 11px; + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; cursor: pointer; + position: sticky; + top: 0; + z-index: 2; + backdrop-filter: blur(18px); + } + + .mobile-sidebar-close::before { + content: ''; + position: absolute; + top: 0.45rem; + left: 50%; + width: 3rem; + height: 0.28rem; + margin-left: -1.5rem; + border-radius: 999px; + background: rgba(244, 246, 248, 0.28); + } + + .sidebar-content { + padding: 0.4rem 0 1rem; + } + + .sidebar-track { + border-bottom: none; + padding: 0 0.65rem 0.75rem; + } + + .sidebar-track-header { + min-height: 3.25rem; + padding: 0.8rem 0.9rem; + border-radius: var(--radius-sm); + background: rgba(255,255,255,0.03); + border: 1px solid var(--panel-border); + font-size: 0.92rem; + } + + .sidebar-unit { + margin-top: 0.45rem; + } + + .sidebar-unit-header { + min-height: 2.9rem; + padding: 0.7rem 0.9rem 0.7rem 1rem; + border-radius: var(--radius-sm); + font-size: 0.8rem; + background: rgba(255,255,255,0.02); + } + + .sidebar-exercise { + min-height: 3.15rem; + padding: 0.75rem 0.95rem 0.75rem 1.25rem; + margin-top: 0.25rem; + border-left-width: 3px; + font-size: 0.92rem; + white-space: normal; + line-height: 1.45; + border-radius: 0 var(--radius-sm) var(--radius-sm) 0; + } + + .sidebar-exercise-title { + white-space: normal; + line-height: 1.4; + } + + .sidebar-track-count, + .sidebar-unit-count, + .sidebar-chevron, + .sidebar-exercise-check { + font-size: 0.78rem; } #app-body { diff --git a/src/components/AppShell/ImagineRitSidebar.tsx b/src/components/AppShell/ImagineRitSidebar.tsx index 9968e7b..944caad 100644 --- a/src/components/AppShell/ImagineRitSidebar.tsx +++ b/src/components/AppShell/ImagineRitSidebar.tsx @@ -98,7 +98,7 @@ export default function ImagineRitSidebar() { aria-label="Close Imagine RIT navigation" onClick={closeMobileSidebar} > - Close + Done
diff --git a/src/components/AppShell/Sidebar.tsx b/src/components/AppShell/Sidebar.tsx index 7b51841..3f798fd 100644 --- a/src/components/AppShell/Sidebar.tsx +++ b/src/components/AppShell/Sidebar.tsx @@ -210,7 +210,7 @@ export default function Sidebar() { aria-label="Close exercise navigation" onClick={closeMobileSidebar} > - Close + Done {(isMobile || !collapsed) && ( diff --git a/src/components/panels/InputPanel/InputPanel.tsx b/src/components/panels/InputPanel/InputPanel.tsx index a17405a..f32531a 100644 --- a/src/components/panels/InputPanel/InputPanel.tsx +++ b/src/components/panels/InputPanel/InputPanel.tsx @@ -14,7 +14,7 @@ import AsmStepInput from './inputs/AsmStepInput'; import AsmQuizInput from './inputs/AsmQuizInput'; import Toolkit from './Toolkit'; -export default function InputPanel() { +export default function InputPanel({ showToolkit = true }: { showToolkit?: boolean }) { const { currentExercise, asmEmulator } = useExerciseContext(); const ex = currentExercise; @@ -82,7 +82,7 @@ export default function InputPanel() {
{content} - {ex && } + {showToolkit && ex && }
diff --git a/src/components/panels/InputPanel/Toolkit.tsx b/src/components/panels/InputPanel/Toolkit.tsx index 8fa2af6..9e8b117 100644 --- a/src/components/panels/InputPanel/Toolkit.tsx +++ b/src/components/panels/InputPanel/Toolkit.tsx @@ -15,7 +15,9 @@ interface ToolSection { component: React.ReactNode; } -export default function Toolkit({ exercise }: { exercise: Exercise }) { +export default function Toolkit( + { exercise, variant = 'tabs' }: { exercise: Exercise; variant?: 'tabs' | 'stack' }, +) { const sections: ToolSection[] = [ { id: 'symbols', label: 'SYMBOLS', visible: !!exercise.showSymbols, component: }, { id: 'calc', label: 'HEX CALC', visible: !!exercise.showCalc, component: }, @@ -40,6 +42,19 @@ export default function Toolkit({ exercise }: { exercise: Exercise }) { if (visibleSections.length === 0) return null; + if (variant === 'stack') { + return ( +
+ {visibleSections.map((section) => ( +
+
{section.label}
+
{section.component}
+
+ ))} +
+ ); + } + const selectedSection = visibleSections.find((section) => section.id === activeSection) ?? visibleSections[0]; return ( diff --git a/src/components/panels/LogPanel/LogPanel.tsx b/src/components/panels/LogPanel/LogPanel.tsx index 31d9095..7357980 100644 --- a/src/components/panels/LogPanel/LogPanel.tsx +++ b/src/components/panels/LogPanel/LogPanel.tsx @@ -15,7 +15,7 @@ export default function LogPanel() { return (
-
execution log
+
console
{state.logMessages.map((entry, i) => ( From 5fb0d764433ba9ad1f74839b0cf1070aef44588f Mon Sep 17 00:00:00 2001 From: "Thomas (Aeshus)" Date: Sat, 25 Apr 2026 08:48:49 -0400 Subject: [PATCH 4/6] Fix stuff --- src/app/exercise/[id]/ExercisePageClient.tsx | 35 ++++--- src/app/globals.css | 97 ++++++++++++++----- src/app/imagine-rit/[id]/page.tsx | 2 +- src/app/imagine-rit/page.tsx | 5 +- src/components/AppShell/ImagineRitSidebar.tsx | 1 + .../panels/InputPanel/InputPanel.tsx | 45 +++++++++ src/exercises/imagine-rit/index.ts | 2 + .../imagine-rit/rit-00-how-to-use.ts | 61 ++++++++++++ 8 files changed, 211 insertions(+), 37 deletions(-) create mode 100644 src/exercises/imagine-rit/rit-00-how-to-use.ts diff --git a/src/app/exercise/[id]/ExercisePageClient.tsx b/src/app/exercise/[id]/ExercisePageClient.tsx index 908fde3..60ff07b 100644 --- a/src/app/exercise/[id]/ExercisePageClient.tsx +++ b/src/app/exercise/[id]/ExercisePageClient.tsx @@ -64,20 +64,33 @@ function computeSymbols(exercise: ReturnType): Record -
directions
-
- {currentExercise ? ( -
- ) : ( -
Select an exercise to begin.
- )} +
+
+ directions +
+ {!collapsed && ( +
+ {currentExercise ? ( +
+ ) : ( +
Select an exercise to begin.
+ )} +
+ )}
); } diff --git a/src/app/globals.css b/src/app/globals.css index 4b88fea..be47751 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -52,7 +52,10 @@ html { --border-strong: rgba(126, 157, 177, 0.38); --accent: #f59e3b; --accent-soft: rgba(245, 158, 59, 0.16); - --accent-strong: #ffd29a; + --accent-wash: rgba(245, 158, 59, 0.08); + --accent-border: rgba(245, 158, 59, 0.65); + --accent-glow: rgba(245, 158, 59, 0.18); + --accent-strong: var(--accent); --accent-secondary: #6de2d5; --fg-accent: #09131a; --panel-bg: var(--bg-panel); @@ -61,9 +64,9 @@ html { --text-dim: var(--fg); --green: var(--accent); --blue: #8ec5ff; - --amber: #ffc07a; + --amber: var(--accent); --red: #ff8e70; - --yellow: var(--accent-strong); + --yellow: var(--accent); --purple: #bba4ff; --comment: var(--accent-secondary); --font: var(--font-sans); @@ -84,9 +87,9 @@ body { flex-direction: column; line-height: 1.65; background: - radial-gradient(circle at top left, rgba(245, 158, 59, 0.16), transparent 34%), + radial-gradient(circle at top left, var(--accent-soft), transparent 34%), radial-gradient(circle at top right, rgba(109, 226, 213, 0.12), transparent 28%), - radial-gradient(circle at bottom center, rgba(245, 158, 59, 0.08), transparent 30%), + radial-gradient(circle at bottom center, var(--accent-wash), transparent 30%), linear-gradient(180deg, #08131c 0%, #061018 45%, #040a10 100%); -webkit-font-smoothing: antialiased; position: relative; @@ -120,7 +123,7 @@ header { } header h1 { font-size: 1rem; - color: var(--fg); + color: var(--accent); font-weight: 600; white-space: nowrap; font-family: var(--font-display); @@ -242,7 +245,7 @@ main { .panel { background: var(--panel-bg); display: flex; flex-direction: column; overflow: hidden; border: var(--border); - border-radius: var(--radius-md); + border-radius: 0.35rem; box-shadow: var(--shadow-md); backdrop-filter: blur(18px); } @@ -251,6 +254,7 @@ main { border-bottom: 1px solid var(--panel-border); flex-shrink: 0; text-transform: uppercase; letter-spacing: 0.08em; font-family: var(--font-sans); + font-weight: 700; } .panel-body { flex: 1; overflow-y: auto; padding: 0.85rem; } @@ -312,7 +316,7 @@ main { .region-buffer .stack-byte { background: rgba(78, 201, 176, 0.15); color: var(--green); } .region-canary .stack-byte { background: rgba(197, 134, 192, 0.15); color: var(--purple); } .region-ebp .stack-byte { background: rgba(86, 156, 214, 0.15); color: var(--blue); } -.region-ret .stack-byte { background: rgba(206, 145, 120, 0.15); color: var(--amber); } +.region-ret .stack-byte { background: var(--accent-soft); color: var(--amber); } .stack-byte.overflow { background: rgba(244, 71, 71, 0.3) !important; color: var(--red) !important; @@ -366,7 +370,7 @@ main { text-transform: uppercase; } .toolkit-tab.active { - background: linear-gradient(135deg, var(--accent), #f6b865); + background: var(--accent); border-color: transparent; color: var(--fg-accent); } @@ -383,7 +387,7 @@ main { .toolkit-stack-section { padding: 0.85rem 0.95rem; border: 1px solid var(--panel-border); - border-radius: var(--radius-sm); + border-radius: 0.35rem; background: rgba(255,255,255,0.02); } .toolkit-stack-title { @@ -403,6 +407,16 @@ main { #input-panel { grid-column: 1; grid-row: 2; } #input-area { margin-bottom: 0.75rem; } #input-area label { display: block; font-size: 11px; color: var(--text-dim); margin-bottom: 0.35rem; } +.input-panel-note { + margin-bottom: 0.75rem; + padding: 0.65rem 0.75rem; + border: 1px solid var(--panel-border); + background: rgba(255,255,255,0.03); + color: var(--text); + font-size: 0.82rem; + line-height: 1.55; + border-radius: var(--radius-sm); +} #payload-input { width: 100%; background: rgba(4, 12, 18, 0.78); border: 1px solid var(--border-color); color: var(--text); font-family: var(--font-mono); font-size: 13px; padding: 0.65rem 0.75rem; resize: none; min-height: 2.5em; @@ -460,7 +474,7 @@ button.link-button:hover, button.link-button:focus-visible, .controls button:hover:not(:disabled), .controls button:focus-visible:not(:disabled) { - border-color: rgba(245, 158, 59, 0.65); + border-color: var(--accent-border); color: var(--fg); transform: translateY(-1px); box-shadow: 0 12px 30px rgba(0, 0, 0, 0.18); @@ -468,18 +482,18 @@ button.link-button:focus-visible, .link-button.primary, .controls button.primary { - background: linear-gradient(135deg, var(--accent), #f6b865); + background: var(--accent); color: var(--fg-accent); border-color: transparent; - box-shadow: 0 12px 30px rgba(245, 158, 59, 0.18); + box-shadow: 0 12px 30px var(--accent-glow); } .link-button.primary:hover, .link-button.primary:focus-visible, .controls button.primary:hover:not(:disabled), .controls button.primary:focus-visible:not(:disabled) { - background: linear-gradient(135deg, #f8ae53, #ffd08c); - color: #071018; + background: var(--accent); + color: var(--fg-accent); } .link-button.secondary { @@ -587,7 +601,7 @@ button.link-button:focus-visible, #exercise-desc { padding: 0.75rem 0.9rem; font-size: 14px; line-height: 1.7; border-bottom: 1px solid var(--panel-border); flex-shrink: 0; - color: var(--text); background: linear-gradient(180deg, rgba(245, 158, 59, 0.08), rgba(255,255,255,0.02)); + color: var(--text); background: linear-gradient(180deg, var(--accent-wash), rgba(255,255,255,0.02)); } #exercise-desc strong { color: var(--accent-strong); font-weight: 600; } @@ -612,7 +626,7 @@ button.link-button:focus-visible, #success-banner h2 { color: var(--accent-strong); font-size: 16px; margin-bottom: 0.5rem; } #success-banner p { color: var(--text); font-size: 13px; } #success-banner button { - margin-top: 1rem; background: linear-gradient(180deg, var(--accent-strong), var(--accent)); border: 1px solid var(--accent); color: var(--fg-accent); + margin-top: 1rem; background: var(--accent); border: 1px solid var(--accent); color: var(--fg-accent); font-family: var(--font-sans); font-size: 12px; padding: 0.45rem 1rem; cursor: pointer; border-radius: var(--radius-sm); } @@ -641,7 +655,8 @@ button.link-button:focus-visible, header { gap: 0.75rem; - padding-left: 4.75rem; + padding-left: 1rem; + padding-right: 4.75rem; } #badges { @@ -654,7 +669,7 @@ button.link-button:focus-visible, justify-content: center; position: fixed; top: 0.55rem; - left: 0.75rem; + right: 0.75rem; height: 2rem; padding: 0 0.75rem; border: 1px solid var(--panel-border); @@ -723,11 +738,41 @@ button.link-button:focus-visible, flex-shrink: 0; } + .mobile-directions-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + } + + .mobile-directions-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 2rem; + padding: 0.35rem 0.75rem; + border-radius: 999px; + border: 1px solid rgba(126, 157, 177, 0.28); + background: rgba(255, 255, 255, 0.03); + color: var(--text); + font-family: var(--font-sans); + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + cursor: pointer; + flex-shrink: 0; + } + .mobile-directions-panel .panel-body { max-height: clamp(8.5rem, 27vh, 14rem); overflow: auto; } + .mobile-directions-panel.is-collapsed .panel-hdr { + border-bottom: none; + } + .mobile-directions-panel .panel-hdr { color: var(--accent-strong); } @@ -784,10 +829,10 @@ button.link-button:focus-visible, } .mobile-workspace-tab.active { - background: linear-gradient(135deg, var(--accent), #f6b865); + background: var(--accent); color: var(--fg-accent); border-color: transparent; - box-shadow: 0 12px 30px rgba(245, 158, 59, 0.18); + box-shadow: 0 12px 30px var(--accent-glow); } .mobile-workspace-panel { @@ -816,13 +861,19 @@ button.link-button:focus-visible, } .mobile-bottom-dock #input-panel { - max-height: min(38dvh, 23rem); + min-height: 0; + height: auto; + max-height: min(42dvh, 26rem); } .mobile-bottom-dock #input-panel .panel-body { overflow: auto; } + .mobile-bottom-dock #input-area { + margin-bottom: 0; + } + .mobile-exercise-pager { display: flex; gap: 0.5rem; @@ -1031,7 +1082,7 @@ button.link-button:focus-visible, .heap-chunk-data .byte { width: 2em; text-align: center; font-size: 11px; padding: 1px 0; border-radius: 1px; } .heap-chunk-data .byte.allocated { color: var(--green); background: rgba(78,201,176,0.1); } .heap-chunk-data .byte.freed { color: var(--text-dim); background: rgba(255,255,255,0.03); } -.heap-chunk-data .byte.pointer { color: var(--amber); background: rgba(206,145,120,0.15); } +.heap-chunk-data .byte.pointer { color: var(--amber); background: var(--accent-soft); } .heap-chunk-data .byte.corrupted { color: var(--red); background: rgba(244,71,71,0.2); } .heap-chunk-label { font-size: 10px; color: var(--text-dim); padding: 2px 4px; text-align: center; border-top: 1px solid var(--panel-border); } .free-lists { margin-top: 0.75rem; font-size: 11px; } diff --git a/src/app/imagine-rit/[id]/page.tsx b/src/app/imagine-rit/[id]/page.tsx index a7fa6db..96bc13c 100644 --- a/src/app/imagine-rit/[id]/page.tsx +++ b/src/app/imagine-rit/[id]/page.tsx @@ -1,6 +1,6 @@ import ExercisePageClient from '@/app/exercise/[id]/ExercisePageClient'; -const IMAGINE_RIT_IDS = ['rit-01', 'rit-02', 'rit-03', 'rit-04', 'rit-rop']; +const IMAGINE_RIT_IDS = ['rit-00', 'rit-01', 'rit-02', 'rit-03', 'rit-04', 'rit-rop']; export function generateStaticParams() { return IMAGINE_RIT_IDS.map((id) => ({ id })); diff --git a/src/app/imagine-rit/page.tsx b/src/app/imagine-rit/page.tsx index 4542a49..f0406db 100644 --- a/src/app/imagine-rit/page.tsx +++ b/src/app/imagine-rit/page.tsx @@ -5,6 +5,7 @@ import { useRouter } from 'next/navigation'; import { loadProgress } from '@/state/persistence'; const EXERCISES = [ + { id: 'rit-00', title: '00: How to Use 0xVRIG', desc: 'Get comfortable with the panels, controls, tools, and navigation before the exploitation lessons begin.' }, { id: 'rit-01', title: '01: The Stack Frame', desc: 'Watch how the computer organizes memory — like a stack of sticky notes.' }, { id: 'rit-02', title: '02: The Overflow', desc: 'Type too much and crash the program. Yes, it\'s that easy.' }, { id: 'rit-03', title: '03: Hijack Execution', desc: 'Make the program run a secret function it was never supposed to call.' }, @@ -94,7 +95,7 @@ export default function ImagineRitPage() { What to Expect
- 5 guided exercises + 6 guided exercises
No prior exploitation experience required @@ -103,7 +104,7 @@ export default function ImagineRitPage() { Visual stack and control-flow feedback
- Roughly 30 to 45 minutes total + Roughly 35 to 50 minutes total
diff --git a/src/components/AppShell/ImagineRitSidebar.tsx b/src/components/AppShell/ImagineRitSidebar.tsx index 944caad..3729219 100644 --- a/src/components/AppShell/ImagineRitSidebar.tsx +++ b/src/components/AppShell/ImagineRitSidebar.tsx @@ -8,6 +8,7 @@ const MOBILE_BREAKPOINT = '(max-width: 900px)'; const MOBILE_SIDEBAR_TOGGLE_EVENT = '0xvrig:toggle-mobile-sidebar'; const IMAGINE_RIT_EXERCISES = [ + { id: 'rit-00', title: 'How to Use 0xVRIG' }, { id: 'rit-01', title: 'The Stack Frame' }, { id: 'rit-02', title: 'The Overflow' }, { id: 'rit-03', title: 'Hijack Execution' }, diff --git a/src/components/panels/InputPanel/InputPanel.tsx b/src/components/panels/InputPanel/InputPanel.tsx index f32531a..cb87201 100644 --- a/src/components/panels/InputPanel/InputPanel.tsx +++ b/src/components/panels/InputPanel/InputPanel.tsx @@ -14,9 +14,49 @@ import AsmStepInput from './inputs/AsmStepInput'; import AsmQuizInput from './inputs/AsmQuizInput'; import Toolkit from './Toolkit'; +function getInputHint(mode?: string): string | null { + if (!mode) return null; + + if ( + mode === 'input-text' + || mode === 'input-hex' + || mode === 'input-fmt-read' + || mode === 'input-fmt-write' + || mode === 'input-int-overflow' + || mode === 'input-signedness' + || mode === 'heap-uaf' + || mode === 'heap-double-free' + || mode === 'heap-overflow' + || mode === 'heap-tcache-poison' + || mode === 'heap-fastbin-dup' + || mode === 'heap-unsorted-bin' + || mode === 'heap-house-force' + || mode === 'heap-house-spirit' + || mode === 'heap-house-orange' + || mode === 'heap-house-einherjar' + || mode === 'heap-house-lore' + || mode === 'final-chain' + || mode === 'final-blind' + ) { + return 'The Console updates after you submit input here. If nothing is happening yet, enter a payload and run or step the program.'; + } + + if ( + mode === 'step' + || mode === 'asm-step' + || mode === 'asm-registers' + || mode === 'asm-quiz' + ) { + return 'Use the controls here to move the exercise forward. The Console fills in as you step through the program.'; + } + + return null; +} + export default function InputPanel({ showToolkit = true }: { showToolkit?: boolean }) { const { currentExercise, asmEmulator } = useExerciseContext(); const ex = currentExercise; + const inputHint = getInputHint(ex?.mode); let content: React.ReactNode; if (!ex) { @@ -81,6 +121,11 @@ export default function InputPanel({ showToolkit = true }: { showToolkit?: boole
input
+ {inputHint && ( +
+ {inputHint} +
+ )} {content} {showToolkit && ex && }
diff --git a/src/exercises/imagine-rit/index.ts b/src/exercises/imagine-rit/index.ts index 28bcb19..f159467 100644 --- a/src/exercises/imagine-rit/index.ts +++ b/src/exercises/imagine-rit/index.ts @@ -1,4 +1,5 @@ import { Exercise } from '../types'; +import rit00HowToUse from './rit-00-how-to-use'; import rit01Stack from './rit-01-stack'; import rit02Overflow from './rit-02-overflow'; import rit03Hijack from './rit-03-hijack'; @@ -6,6 +7,7 @@ import rit04Aslr from './rit-04-aslr'; import babyRop from './baby-rop'; export const imagineRitExercises: Exercise[] = [ + rit00HowToUse, rit01Stack, rit02Overflow, rit03Hijack, diff --git a/src/exercises/imagine-rit/rit-00-how-to-use.ts b/src/exercises/imagine-rit/rit-00-how-to-use.ts new file mode 100644 index 0000000..bbe3a86 --- /dev/null +++ b/src/exercises/imagine-rit/rit-00-how-to-use.ts @@ -0,0 +1,61 @@ +import { Exercise } from '../types'; + +const rit00HowToUse: Exercise = { + id: 'rit-00', + unitId: 'imagine-rit', + title: '00: How to Use 0xVRIG', + desc: 'This short intro explains the interface before the real workshop starts. Keep the directions open when you need context, switch between Code, Assembly, Console, and Misc to inspect the program from different angles, and use the compact input area at the bottom to step or send payloads. The Contents button opens the full workshop map so you can jump between lessons.', + source: { + c: [ + { text: '#include ', cls: '' }, + { text: '', cls: '' }, + { text: 'void demo(void) {', cls: '', fn: true }, + { text: ' char buf[16]; // memory shown in the Assembly view', cls: 'highlight' }, + { text: ' puts("Explore each panel as you go.");', cls: '' }, + { text: ' (void)buf;', cls: '' }, + { text: '}', cls: '' }, + { text: '', cls: '' }, + { text: 'int main(void) {', cls: '' }, + { text: ' demo();', cls: 'highlight' }, + { text: ' return 0;', cls: '' }, + { text: '}', cls: '' }, + ], + }, + mode: 'step', + vizMode: 'stack', + bufSize: 16, + showSymbols: true, + showBuilder: true, + showCalc: true, + showGadgetTable: true, + gadgets: { + 0x08048300: 'pop eax; pop ebx; mov [ebx], eax; ret', + }, + steps: [ + { + region: 'none', + log: ['info', 'Start here: directions explain the goal of the current lesson and stay separate from the code so you can read them while working.'], + }, + { + region: 'ret', + log: ['action', 'Use the Code tab to inspect the source and the Assembly tab to watch the stack frame and control-flow state change underneath it.'], + }, + { + region: 'buffer', + log: ['action', 'Open Misc for helper tools like symbols, the hex calculator, payload builder, and gadget listings when an exercise enables them.'], + }, + { + region: 'ebp', + log: ['action', 'The bottom input dock is where you type payloads, step execution, or send trial inputs. Some lessons only need controls, so that area stays compact.'], + }, + { + region: 'all', + log: ['info', 'Use Previous, Contents, and Next at the bottom to move through the workshop. Once this layout feels familiar, continue into the first real stack lesson.'], + }, + ], + check() { return false; }, + winTitle: 'Orientation Complete', + winMsg: 'You now know where to read directions, inspect code and memory, use helper tools, and move between exercises.', +}; + +export default rit00HowToUse; From 3484e06c38e3742bc325f0e23333e2b44f88fc27 Mon Sep 17 00:00:00 2001 From: "Thomas (Aeshus)" Date: Sat, 25 Apr 2026 09:05:53 -0400 Subject: [PATCH 5/6] yay --- src/app/exercise/layout.tsx | 4 + src/app/globals.css | 199 +++++++++++++++--- src/app/imagine-rit/layout.tsx | 7 +- .../panels/InputPanel/InputPanel.tsx | 74 +++---- .../panels/InputPanel/inputs/AsmQuizInput.tsx | 2 + .../panels/InputPanel/inputs/AsmStepInput.tsx | 16 +- .../InputPanel/inputs/FinalBlindInput.tsx | 15 +- .../InputPanel/inputs/FinalChainInput.tsx | 15 +- .../panels/InputPanel/inputs/FmtReadInput.tsx | 8 +- .../InputPanel/inputs/FmtWriteInput.tsx | 8 +- .../InputPanel/inputs/HeapStepInput.tsx | 39 +++- .../panels/InputPanel/inputs/StepControls.tsx | 21 +- .../panels/InputPanel/inputs/TextHexInput.tsx | 34 ++- .../InputPanel/inputs/WalkthroughButton.tsx | 17 ++ src/components/shared/SolutionGuideModal.tsx | 195 +++++++++++++++++ src/components/shared/ToastMessage.tsx | 22 ++ src/state/reducer.ts | 21 ++ src/state/types.ts | 8 + 18 files changed, 591 insertions(+), 114 deletions(-) create mode 100644 src/components/panels/InputPanel/inputs/WalkthroughButton.tsx create mode 100644 src/components/shared/SolutionGuideModal.tsx create mode 100644 src/components/shared/ToastMessage.tsx diff --git a/src/app/exercise/layout.tsx b/src/app/exercise/layout.tsx index 97e64e4..1bc19b4 100644 --- a/src/app/exercise/layout.tsx +++ b/src/app/exercise/layout.tsx @@ -5,6 +5,8 @@ import { ExerciseContextProvider } from '@/state/ExerciseContext'; import Sidebar from '@/components/AppShell/Sidebar'; import SuccessBanner from '@/components/shared/SuccessBanner'; import BadgePopup from '@/components/shared/BadgePopup'; +import ToastMessage from '@/components/shared/ToastMessage'; +import SolutionGuideModal from '@/components/shared/SolutionGuideModal'; export default function ExerciseLayout({ children }: { children: React.ReactNode }) { return ( @@ -21,6 +23,8 @@ export default function ExerciseLayout({ children }: { children: React.ReactNode
+ +
diff --git a/src/app/globals.css b/src/app/globals.css index be47751..c280bae 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -119,15 +119,21 @@ header { display: flex; align-items: center; gap: 1rem; padding: 0.75rem 1rem; border-bottom: 1px solid var(--panel-border); background: var(--bg-header); flex-shrink: 0; + justify-content: center; + position: relative; + min-height: 3.25rem; backdrop-filter: blur(18px); } header h1 { font-size: 1rem; color: var(--accent); - font-weight: 600; + font-weight: 800; white-space: nowrap; font-family: var(--font-display); letter-spacing: 0.04em; + position: absolute; + left: 50%; + transform: translateX(-50%); } /* Sidebar */ @@ -220,10 +226,15 @@ header h1 { flex-shrink: 0; width: 1em; text-align: center; font-size: 10px; color: var(--fg-subtle); } -#badges { display: flex; gap: 0.5rem; margin-left: auto; } +#badges { + display: flex; + gap: 0.5rem; + position: absolute; + right: 1rem; +} .badge { font-size: 11px; padding: 0.2rem 0.6rem; border: 1px solid var(--border-strong); - color: var(--accent-strong); border-radius: 999px; white-space: nowrap; + color: var(--accent-strong); border-radius: 0.35rem; white-space: nowrap; background: rgba(255,255,255,0.03); backdrop-filter: blur(12px); } @@ -359,9 +370,9 @@ main { min-height: 2.15rem; padding: 0.45rem 0.75rem; background: rgba(255,255,255,0.03); - border: 1px solid var(--panel-border); - border-radius: 999px; - color: var(--text); + border: 1px solid var(--accent-secondary); + border-radius: 0.35rem; + color: var(--accent-secondary); cursor: pointer; font-family: var(--font-sans); font-size: 10px; @@ -407,15 +418,53 @@ main { #input-panel { grid-column: 1; grid-row: 2; } #input-area { margin-bottom: 0.75rem; } #input-area label { display: block; font-size: 11px; color: var(--text-dim); margin-bottom: 0.35rem; } -.input-panel-note { - margin-bottom: 0.75rem; - padding: 0.65rem 0.75rem; - border: 1px solid var(--panel-border); +.input-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; +} +.input-panel-header-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 0.5rem; + flex-wrap: wrap; +} +.input-panel-progress { + color: var(--accent-secondary); + font-size: 0.76rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} +.input-panel-action { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 2rem; + padding: 0.35rem 0.75rem; + border: 1px solid rgba(126, 157, 177, 0.28); + border-radius: 0.35rem; background: rgba(255,255,255,0.03); color: var(--text); - font-size: 0.82rem; - line-height: 1.55; - border-radius: var(--radius-sm); + font-family: var(--font-sans); + font-size: 0.68rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + text-align: center; + white-space: normal; + line-height: 1.3; + cursor: pointer; +} +.input-panel-action:hover, +.input-panel-action:focus-visible { + border-color: var(--accent); + color: var(--text); +} +.input-panel-shell.is-collapsed .panel-hdr { + border-bottom: none; } #payload-input { width: 100%; background: rgba(4, 12, 18, 0.78); border: 1px solid var(--border-color); color: var(--text); @@ -429,15 +478,18 @@ main { display: flex; gap: 0.5rem; margin-bottom: 0.5rem; } .input-mode-toggle button { - background: rgba(255,255,255,0.02); border: 1px solid var(--panel-border); color: var(--text-dim); + background: rgba(255,255,255,0.02); border: 1px solid var(--accent-secondary); color: var(--accent-secondary); font-family: var(--font-sans); font-size: 11px; padding: 0.3rem 0.65rem; cursor: pointer; - border-radius: 999px; + border-radius: 0.35rem; } .input-mode-toggle button.active { border-color: var(--accent); color: var(--accent-strong); background: var(--accent-soft); } .controls { display: flex; gap: 0.5rem; flex-wrap: wrap; } +.controls .walkthrough-button { + margin-left: auto; +} a.link-button, button.link-button, @@ -448,7 +500,7 @@ button.link-button, gap: 0.55rem; min-height: 2.9rem; padding: 0.75rem 1rem; - border-radius: 999px; + border-radius: 0.35rem; border: 1px solid rgba(126, 157, 177, 0.28); background: rgba(255, 255, 255, 0.03); color: var(--fg); @@ -498,7 +550,7 @@ button.link-button:focus-visible, .link-button.secondary { background: rgba(9, 20, 29, 0.72); - border-color: rgba(109, 226, 213, 0.24); + border-color: rgba(126, 157, 177, 0.28); } .link-button.secondary:hover, @@ -508,7 +560,7 @@ button.link-button:focus-visible, .link-button.secondary-accent { color: var(--accent-secondary); - border-color: rgba(109, 226, 213, 0.35); + border-color: rgba(126, 157, 177, 0.28); } .link-button.secondary-accent:hover, @@ -630,7 +682,88 @@ button.link-button:focus-visible, font-family: var(--font-sans); font-size: 12px; padding: 0.45rem 1rem; cursor: pointer; border-radius: var(--radius-sm); } +.app-toast { + position: fixed; + left: 50%; + bottom: 1.25rem; + transform: translateX(-50%); + z-index: 240; + min-width: min(32rem, calc(100vw - 2rem)); + max-width: calc(100vw - 2rem); + padding: 0.9rem 1rem; + border: 1px solid var(--accent-secondary); + border-radius: 0.35rem; + background: rgba(8, 19, 28, 0.96); + color: var(--text); + box-shadow: var(--shadow-md); + text-align: center; + font-size: 0.9rem; + line-height: 1.5; + animation: toastIn 0.18s ease; +} +.modal-backdrop { + position: fixed; + inset: 0; + z-index: 220; + display: flex; + align-items: center; + justify-content: center; + padding: 1rem; + background: rgba(0, 0, 0, 0.7); +} +.solution-guide-modal { + width: min(46rem, 100%); + max-height: min(85dvh, 52rem); + overflow: auto; + padding: 1.25rem; + border: 1px solid var(--panel-border); + border-radius: 0.35rem; + background: var(--bg-panel-strong); + box-shadow: var(--shadow-lg); +} +.solution-guide-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1rem; +} +.solution-guide-kicker { + color: var(--accent-secondary); + font-size: 0.76rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + margin-bottom: 0.35rem; +} +.solution-guide-header h2 { + color: var(--text); + font-family: var(--font-display); + font-size: 1.5rem; + line-height: 1.1; +} +.solution-guide-section + .solution-guide-section { + margin-top: 1rem; +} +.solution-guide-label { + color: var(--accent); + font-size: 0.76rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + margin-bottom: 0.35rem; +} +.solution-guide-section p, +.solution-guide-steps { + color: var(--text); + font-size: 0.95rem; + line-height: 1.7; +} +.solution-guide-steps { + padding-left: 1.25rem; +} @keyframes fadeIn { from { opacity: 0; transform: translate(-50%, -50%) scale(0.95); } to { opacity: 1; transform: translate(-50%, -50%) scale(1); } } +@keyframes toastIn { from { opacity: 0; transform: translateX(-50%) translateY(0.35rem); } to { opacity: 1; transform: translateX(-50%) translateY(0); } } @keyframes badgeSlideIn { from { opacity: 0; transform: translateX(40px); } to { opacity: 1; transform: translateX(0); } } /* CRT scanline overlay */ @@ -672,14 +805,14 @@ button.link-button:focus-visible, right: 0.75rem; height: 2rem; padding: 0 0.75rem; - border: 1px solid var(--panel-border); + border: 1px solid rgba(126, 157, 177, 0.28); background: var(--bg-panel-strong); color: var(--text); font-family: var(--font-sans); font-size: 11px; cursor: pointer; z-index: 60; - border-radius: 999px; + border-radius: 0.35rem; backdrop-filter: blur(18px); box-shadow: var(--shadow-md); } @@ -751,7 +884,7 @@ button.link-button:focus-visible, justify-content: center; min-height: 2rem; padding: 0.35rem 0.75rem; - border-radius: 999px; + border-radius: 0.35rem; border: 1px solid rgba(126, 157, 177, 0.28); background: rgba(255, 255, 255, 0.03); color: var(--text); @@ -769,6 +902,24 @@ button.link-button:focus-visible, overflow: auto; } + .input-panel-header { + align-items: flex-start; + } + + .input-panel-header-actions { + gap: 0.35rem; + } + + .input-panel-progress { + width: 100%; + text-align: right; + } + + .input-panel-action { + font-size: 0.62rem; + padding: 0.3rem 0.55rem; + } + .mobile-directions-panel.is-collapsed .panel-hdr { border-bottom: none; } @@ -816,10 +967,10 @@ button.link-button:focus-visible, justify-content: center; min-height: 2.6rem; padding: 0.65rem 0.75rem; - border-radius: 999px; - border: 1px solid rgba(126, 157, 177, 0.28); + border-radius: 0.35rem; + border: 1px solid var(--accent-secondary); background: rgba(255, 255, 255, 0.03); - color: var(--fg-muted); + color: var(--accent-secondary); font-family: var(--font-sans); font-size: 0.75rem; font-weight: 700; diff --git a/src/app/imagine-rit/layout.tsx b/src/app/imagine-rit/layout.tsx index 816f5da..9b09b8d 100644 --- a/src/app/imagine-rit/layout.tsx +++ b/src/app/imagine-rit/layout.tsx @@ -1,15 +1,18 @@ 'use client'; +import Link from 'next/link'; import { ExerciseContextProvider } from '@/state/ExerciseContext'; import ImagineRitSidebar from '@/components/AppShell/ImagineRitSidebar'; import SuccessBanner from '@/components/shared/SuccessBanner'; +import ToastMessage from '@/components/shared/ToastMessage'; +import SolutionGuideModal from '@/components/shared/SolutionGuideModal'; export default function ImagineRitLayout({ children }: { children: React.ReactNode }) { return (
-

0xVRIG

+

0xVRIG

@@ -19,6 +22,8 @@ export default function ImagineRitLayout({ children }: { children: React.ReactNo
+ +
); diff --git a/src/components/panels/InputPanel/InputPanel.tsx b/src/components/panels/InputPanel/InputPanel.tsx index cb87201..4165f5c 100644 --- a/src/components/panels/InputPanel/InputPanel.tsx +++ b/src/components/panels/InputPanel/InputPanel.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useState } from 'react'; import { useExerciseContext } from '@/state/ExerciseContext'; import StepControls from './inputs/StepControls'; import TextHexInput from './inputs/TextHexInput'; @@ -14,49 +15,10 @@ import AsmStepInput from './inputs/AsmStepInput'; import AsmQuizInput from './inputs/AsmQuizInput'; import Toolkit from './Toolkit'; -function getInputHint(mode?: string): string | null { - if (!mode) return null; - - if ( - mode === 'input-text' - || mode === 'input-hex' - || mode === 'input-fmt-read' - || mode === 'input-fmt-write' - || mode === 'input-int-overflow' - || mode === 'input-signedness' - || mode === 'heap-uaf' - || mode === 'heap-double-free' - || mode === 'heap-overflow' - || mode === 'heap-tcache-poison' - || mode === 'heap-fastbin-dup' - || mode === 'heap-unsorted-bin' - || mode === 'heap-house-force' - || mode === 'heap-house-spirit' - || mode === 'heap-house-orange' - || mode === 'heap-house-einherjar' - || mode === 'heap-house-lore' - || mode === 'final-chain' - || mode === 'final-blind' - ) { - return 'The Console updates after you submit input here. If nothing is happening yet, enter a payload and run or step the program.'; - } - - if ( - mode === 'step' - || mode === 'asm-step' - || mode === 'asm-registers' - || mode === 'asm-quiz' - ) { - return 'Use the controls here to move the exercise forward. The Console fills in as you step through the program.'; - } - - return null; -} - export default function InputPanel({ showToolkit = true }: { showToolkit?: boolean }) { - const { currentExercise, asmEmulator } = useExerciseContext(); + const { currentExercise, asmEmulator, state, dispatch } = useExerciseContext(); const ex = currentExercise; - const inputHint = getInputHint(ex?.mode); + const [collapsed, setCollapsed] = useState(false); let content: React.ReactNode; if (!ex) { @@ -117,19 +79,31 @@ export default function InputPanel({ showToolkit = true }: { showToolkit?: boole } return ( -
-
input
-
-
- {inputHint && ( -
- {inputHint} -
+
+
+ input +
+ {state.inputProgress && ( + {state.inputProgress} )} + +
+
+ {!collapsed && ( +
+
{content} {showToolkit && ex && }
-
+
+ )}
); } diff --git a/src/components/panels/InputPanel/inputs/AsmQuizInput.tsx b/src/components/panels/InputPanel/inputs/AsmQuizInput.tsx index 7caad4b..19cf930 100644 --- a/src/components/panels/InputPanel/inputs/AsmQuizInput.tsx +++ b/src/components/panels/InputPanel/inputs/AsmQuizInput.tsx @@ -4,6 +4,7 @@ import { useState, useCallback } from 'react'; import { useExerciseContext } from '@/state/ExerciseContext'; import { Emulator } from '@/engine/emulator-interface'; import { AsmQuizQuestion } from '@/exercises/types'; +import WalkthroughButton from './WalkthroughButton'; interface AsmQuizInputProps { emulator: Emulator | null; @@ -115,6 +116,7 @@ export default function AsmQuizInput({ emulator, questions }: AsmQuizInputProps) +
diff --git a/src/components/panels/InputPanel/inputs/AsmStepInput.tsx b/src/components/panels/InputPanel/inputs/AsmStepInput.tsx index c954a72..ae5c663 100644 --- a/src/components/panels/InputPanel/inputs/AsmStepInput.tsx +++ b/src/components/panels/InputPanel/inputs/AsmStepInput.tsx @@ -1,9 +1,10 @@ 'use client'; -import { useState, useCallback, useRef } from 'react'; +import { useState, useCallback, useRef, useEffect } from 'react'; import { useExerciseContext } from '@/state/ExerciseContext'; import { Emulator } from '@/engine/emulator-interface'; import { StepResult } from '@/engine/x86/types'; +import WalkthroughButton from './WalkthroughButton'; interface AsmStepInputProps { emulator: Emulator | null; @@ -15,6 +16,13 @@ export default function AsmStepInput({ emulator, onStepResult }: AsmStepInputPro const [stepCount, setStepCount] = useState(0); const changedRegsRef = useRef>(new Set()); + useEffect(() => { + dispatch({ type: 'SET_INPUT_PROGRESS', progress: `Steps ${stepCount}` }); + return () => { + dispatch({ type: 'SET_INPUT_PROGRESS', progress: null }); + }; + }, [stepCount, dispatch]); + const triggerWin = useCallback(() => { if (!currentExercise) return; const ex = currentExercise; @@ -91,18 +99,18 @@ export default function AsmStepInput({ emulator, onStepResult }: AsmStepInputPro +
- Steps: {stepCount} {currentInstr && !halted && ( - + Next: {currentInstr.mnemonic}{' '} {currentInstr.operands} )} {halted && ( - + [HALTED] )} diff --git a/src/components/panels/InputPanel/inputs/FinalBlindInput.tsx b/src/components/panels/InputPanel/inputs/FinalBlindInput.tsx index 20a599d..a77e1e0 100644 --- a/src/components/panels/InputPanel/inputs/FinalBlindInput.tsx +++ b/src/components/panels/InputPanel/inputs/FinalBlindInput.tsx @@ -3,6 +3,7 @@ import { useState, useCallback } from 'react'; import { useExerciseContext } from '@/state/ExerciseContext'; import { hexStrToBytes, hex8 } from '@/engine/helpers'; +import WalkthroughButton from './WalkthroughButton'; export default function FinalBlindInput() { const { state, dispatch, heapSim, currentExercise } = useExerciseContext(); @@ -16,7 +17,7 @@ export default function FinalBlindInput() { const doUafWrite = useCallback(() => { if (!heap || !ex) return; if (!payload1.trim()) { - dispatch({ type: 'LOG', cls: 'info', msg: 'Enter a hex payload.' }); + dispatch({ type: 'SHOW_TOAST', message: 'Enter a hex payload before submitting phase 1.' }); return; } @@ -49,7 +50,7 @@ export default function FinalBlindInput() { const doFinalWrite = useCallback(() => { if (!heap || !ex) return; if (!payload2.trim()) { - dispatch({ type: 'LOG', cls: 'info', msg: 'Enter the address to write.' }); + dispatch({ type: 'SHOW_TOAST', message: 'Enter the value to write before submitting phase 2.' }); return; } @@ -100,7 +101,10 @@ export default function FinalBlindInput() { resize: 'vertical', }} /> -
+
+ + +
); } @@ -126,7 +130,10 @@ export default function FinalBlindInput() { resize: 'vertical', }} /> -
+
+ + +
); } diff --git a/src/components/panels/InputPanel/inputs/FinalChainInput.tsx b/src/components/panels/InputPanel/inputs/FinalChainInput.tsx index d58ab67..5ae5c92 100644 --- a/src/components/panels/InputPanel/inputs/FinalChainInput.tsx +++ b/src/components/panels/InputPanel/inputs/FinalChainInput.tsx @@ -3,6 +3,7 @@ import { useState, useCallback } from 'react'; import { useExerciseContext } from '@/state/ExerciseContext'; import { hexStrToBytes, hex8 } from '@/engine/helpers'; +import WalkthroughButton from './WalkthroughButton'; export default function FinalChainInput() { const { state, dispatch, heapSim, currentExercise } = useExerciseContext(); @@ -17,7 +18,7 @@ export default function FinalChainInput() { if (!heap) return; const val = parseInt(count); if (isNaN(val)) { - dispatch({ type: 'LOG', cls: 'info', msg: 'Enter a number.' }); + dispatch({ type: 'SHOW_TOAST', message: 'Enter a valid count before submitting.' }); return; } @@ -50,7 +51,7 @@ export default function FinalChainInput() { const doSubmit = useCallback(() => { if (!heap || !ex) return; if (!payload.trim()) { - dispatch({ type: 'LOG', cls: 'info', msg: 'Enter a hex payload.' }); + dispatch({ type: 'SHOW_TOAST', message: 'Enter a hex payload before submitting.' }); return; } @@ -105,7 +106,10 @@ export default function FinalChainInput() { fontSize: '12px', }} /> -
+
+ + +
); } @@ -133,7 +137,10 @@ export default function FinalChainInput() { }} />
-
+
+ + +
); } diff --git a/src/components/panels/InputPanel/inputs/FmtReadInput.tsx b/src/components/panels/InputPanel/inputs/FmtReadInput.tsx index bdf62ec..f708bac 100644 --- a/src/components/panels/InputPanel/inputs/FmtReadInput.tsx +++ b/src/components/panels/InputPanel/inputs/FmtReadInput.tsx @@ -3,6 +3,7 @@ import { useState, useCallback } from 'react'; import { useExerciseContext } from '@/state/ExerciseContext'; import { simulateFmtRead } from '@/engine/execution/FmtEngine'; +import WalkthroughButton from './WalkthroughButton'; export default function FmtReadInput() { const { state, dispatch, stackSim, currentExercise } = useExerciseContext(); @@ -14,7 +15,7 @@ export default function FmtReadInput() { const doRun = useCallback(() => { if (!ex || !sim) return; if (!payload.trim()) { - dispatch({ type: 'LOG', cls: 'info', msg: 'Enter a format string first.' }); + dispatch({ type: 'SHOW_TOAST', message: 'Enter a format string before running printf().' }); return; } @@ -53,7 +54,10 @@ export default function FmtReadInput() { }} />
-
+
+ + +
); } diff --git a/src/components/panels/InputPanel/inputs/FmtWriteInput.tsx b/src/components/panels/InputPanel/inputs/FmtWriteInput.tsx index 4ecf52f..754beee 100644 --- a/src/components/panels/InputPanel/inputs/FmtWriteInput.tsx +++ b/src/components/panels/InputPanel/inputs/FmtWriteInput.tsx @@ -3,6 +3,7 @@ import { useState, useCallback } from 'react'; import { useExerciseContext } from '@/state/ExerciseContext'; import { simulateFmtWrite } from '@/engine/execution/FmtEngine'; +import WalkthroughButton from './WalkthroughButton'; export default function FmtWriteInput() { const { state, dispatch, stackSim, currentExercise } = useExerciseContext(); @@ -14,7 +15,7 @@ export default function FmtWriteInput() { const doRun = useCallback(() => { if (!ex || !sim) return; if (!payload.trim()) { - dispatch({ type: 'LOG', cls: 'info', msg: 'Enter a format string first.' }); + dispatch({ type: 'SHOW_TOAST', message: 'Enter a format string before running printf().' }); return; } @@ -53,7 +54,10 @@ export default function FmtWriteInput() { }} /> -
+
+ + +
); } diff --git a/src/components/panels/InputPanel/inputs/HeapStepInput.tsx b/src/components/panels/InputPanel/inputs/HeapStepInput.tsx index 5bea4d6..09fdf63 100644 --- a/src/components/panels/InputPanel/inputs/HeapStepInput.tsx +++ b/src/components/panels/InputPanel/inputs/HeapStepInput.tsx @@ -1,8 +1,9 @@ 'use client'; -import { useState, useCallback } from 'react'; +import { useState, useCallback, useEffect } from 'react'; import { useExerciseContext } from '@/state/ExerciseContext'; import { hexStrToBytes } from '@/engine/helpers'; +import WalkthroughButton from './WalkthroughButton'; import { UAF_STEPS, DF_STEPS, TCP_STEPS, FBD_STEPS, USB_STEPS, HOF_STEPS, HOS_STEPS, HOO_STEPS, HOE_STEPS, HOL_STEPS, @@ -34,6 +35,23 @@ export default function HeapStepInput() { const ex = currentExercise; const heap = heapSim.current; const phase = state.heapPhase; + const steps = getStepsForMode(ex?.mode ?? ''); + const hasSteps = steps.length > 0; + const isInputPhase = !hasSteps || phase === 'input' || phase === 'overwrite-top' || phase === 'fake-headers' || phase === 'free-fake' || phase === 'final-write' || phase === 'null-byte' || phase === 'corrupt-top' || phase === 'bk-write'; + + useEffect(() => { + if (!hasSteps || isInputPhase) { + dispatch({ type: 'SET_INPUT_PROGRESS', progress: null }); + return; + } + + const current = Math.min(state.heapStep + 1, steps.length); + dispatch({ type: 'SET_INPUT_PROGRESS', progress: `Step ${current}/${steps.length}` }); + + return () => { + dispatch({ type: 'SET_INPUT_PROGRESS', progress: null }); + }; + }, [dispatch, hasSteps, isInputPhase, state.heapStep, steps.length]); const doStep = useCallback(() => { if (!ex || !heap) return; @@ -61,13 +79,13 @@ export default function HeapStepInput() { const doSubmit = useCallback(() => { if (!ex || !heap) return; if (!payload.trim()) { - dispatch({ type: 'LOG', cls: 'info', msg: 'Enter a hex payload first.' }); + dispatch({ type: 'SHOW_TOAST', message: 'Enter a hex payload before submitting.' }); return; } const bytes = hexStrToBytes(payload); if (bytes.length === 0) { - dispatch({ type: 'LOG', cls: 'info', msg: 'Invalid hex input.' }); + dispatch({ type: 'SHOW_TOAST', message: 'That hex payload is invalid. Enter valid hex bytes first.' }); return; } @@ -109,16 +127,12 @@ export default function HeapStepInput() { } }, [ex, heap, payload, state.heapNames, state.symbols, dispatch]); - const steps = getStepsForMode(ex?.mode ?? ''); - const hasSteps = steps.length > 0; - const isInputPhase = !hasSteps || phase === 'input' || phase === 'overwrite-top' || phase === 'fake-headers' || phase === 'free-fake' || phase === 'final-write' || phase === 'null-byte' || phase === 'corrupt-top' || phase === 'bk-write'; - if (!isInputPhase) { return (
-
-
- Step {state.heapStep + 1}/{steps.length} +
+ +
); @@ -147,7 +161,10 @@ export default function HeapStepInput() { }} />
-
+
+ + +
); } diff --git a/src/components/panels/InputPanel/inputs/StepControls.tsx b/src/components/panels/InputPanel/inputs/StepControls.tsx index 7026efc..60f849c 100644 --- a/src/components/panels/InputPanel/inputs/StepControls.tsx +++ b/src/components/panels/InputPanel/inputs/StepControls.tsx @@ -1,9 +1,11 @@ 'use client'; +import { useEffect } from 'react'; import { useExerciseContext } from '@/state/ExerciseContext'; import { hex8 } from '@/engine/helpers'; import { StackSim } from '@/engine/simulators/StackSim'; import { BASE_SYMBOLS } from '@/exercises/shared/symbols'; +import WalkthroughButton from './WalkthroughButton'; function retAddrInMain(symbols: Record): number { return (symbols.main || BASE_SYMBOLS.main) + 0x25; @@ -11,10 +13,24 @@ function retAddrInMain(symbols: Record): number { export default function StepControls() { const { state, dispatch, stackSim, heapSim, auxViz, currentExercise } = useExerciseContext(); + const totalSteps = currentExercise?.steps?.length ?? 0; + const allDone = totalSteps > 0 && state.stepIndex >= totalSteps; - if (!currentExercise || !currentExercise.steps) return null; + useEffect(() => { + if (!totalSteps) { + dispatch({ type: 'SET_INPUT_PROGRESS', progress: null }); + return; + } - const allDone = state.stepIndex >= currentExercise.steps.length; + const current = Math.min(state.stepIndex + 1, totalSteps); + dispatch({ type: 'SET_INPUT_PROGRESS', progress: `Step ${current}/${totalSteps}` }); + + return () => { + dispatch({ type: 'SET_INPUT_PROGRESS', progress: null }); + }; + }, [totalSteps, state.stepIndex, dispatch]); + + if (!currentExercise || !currentExercise.steps) return null; function handleStep() { if (!currentExercise || !currentExercise.steps) return; @@ -126,6 +142,7 @@ export default function StepControls() { + {allDone && ( All steps complete {'\u2713'} diff --git a/src/components/panels/InputPanel/inputs/TextHexInput.tsx b/src/components/panels/InputPanel/inputs/TextHexInput.tsx index e225081..e73480a 100644 --- a/src/components/panels/InputPanel/inputs/TextHexInput.tsx +++ b/src/components/panels/InputPanel/inputs/TextHexInput.tsx @@ -1,9 +1,10 @@ 'use client'; -import { useState, useCallback } from 'react'; +import { useState, useCallback, useEffect } from 'react'; import { useExerciseContext } from '@/state/ExerciseContext'; import { hexStrToBytes, strToBytes } from '@/engine/helpers'; import { generateExecSteps, execCurrentStep, ExecStep } from '@/engine/execution/StepEngine'; +import WalkthroughButton from './WalkthroughButton'; export default function TextHexInput() { const { state, dispatch, stackSim, currentExercise } = useExerciseContext(); @@ -16,18 +17,32 @@ export default function TextHexInput() { const ex = currentExercise; const sim = stackSim.current; + useEffect(() => { + if (!execSteps) { + dispatch({ type: 'SET_INPUT_PROGRESS', progress: null }); + return; + } + + const completed = Math.min(execIndex, execSteps.length); + dispatch({ type: 'SET_INPUT_PROGRESS', progress: `Step ${completed}/${execSteps.length}` }); + + return () => { + dispatch({ type: 'SET_INPUT_PROGRESS', progress: null }); + }; + }, [execSteps, execIndex, dispatch]); + const doStep = useCallback(() => { if (!ex || !sim) return; // Generate steps if not started if (!execSteps) { if (!payload.trim()) { - dispatch({ type: 'LOG', cls: 'info', msg: 'Enter a payload first.' }); + dispatch({ type: 'SHOW_TOAST', message: 'Enter a payload first before stepping the program.' }); return; } const bytes = mode === 'hex' ? hexStrToBytes(payload) : strToBytes(payload); if (!bytes.length) { - dispatch({ type: 'LOG', cls: 'info', msg: 'Empty payload.' }); + dispatch({ type: 'SHOW_TOAST', message: 'That input is empty. Enter a payload before running.' }); return; } @@ -96,11 +111,14 @@ export default function TextHexInput() { if (!steps) { if (!payload.trim()) { - dispatch({ type: 'LOG', cls: 'info', msg: 'Enter a payload first.' }); + dispatch({ type: 'SHOW_TOAST', message: 'Enter a payload first before running the program.' }); return; } const bytes = mode === 'hex' ? hexStrToBytes(payload) : strToBytes(payload); - if (!bytes.length) return; + if (!bytes.length) { + dispatch({ type: 'SHOW_TOAST', message: 'That input is empty. Enter a payload before running.' }); + return; + } sim.resetForInput(); sim.clearBlank(); steps = generateExecSteps(ex, bytes, sim, state.symbols); @@ -186,12 +204,8 @@ export default function TextHexInput() { + - {execSteps && ( -
- Step {Math.min(execIndex + 1, execSteps.length)}/{execSteps.length} -
- )} ); } diff --git a/src/components/panels/InputPanel/inputs/WalkthroughButton.tsx b/src/components/panels/InputPanel/inputs/WalkthroughButton.tsx new file mode 100644 index 0000000..a7e2921 --- /dev/null +++ b/src/components/panels/InputPanel/inputs/WalkthroughButton.tsx @@ -0,0 +1,17 @@ +'use client'; + +import { useExerciseContext } from '@/state/ExerciseContext'; + +export default function WalkthroughButton() { + const { dispatch } = useExerciseContext(); + + return ( + + ); +} diff --git a/src/components/shared/SolutionGuideModal.tsx b/src/components/shared/SolutionGuideModal.tsx new file mode 100644 index 0000000..d3f6709 --- /dev/null +++ b/src/components/shared/SolutionGuideModal.tsx @@ -0,0 +1,195 @@ +'use client'; + +import { hex8 } from '@/engine/helpers'; +import { Exercise } from '@/exercises/types'; +import { useExerciseContext } from '@/state/ExerciseContext'; + +interface SolutionGuide { + answer: string; + why: string; + steps: string[]; +} + +function getSolutionGuide(exercise: Exercise, symbols: Record): SolutionGuide { + switch (exercise.id) { + case 'rit-00': + return { + answer: 'Use Step to move through the guided interface tour, and inspect each workspace tab once as you go.', + why: 'This lesson exists to teach the interface itself, so the correct answer is understanding where the simulator views, tools, console, and navigation live.', + steps: [ + 'Keep the directions open and read them first.', + 'Use Code and Assembly together so you can connect the source to the stack view.', + 'Open Misc to see where the symbols table, calculator, payload builder, and gadgets appear.', + 'Use the bottom pager and Contents button so the workshop navigation feels familiar before you start exploiting anything.', + ], + }; + case 'rit-01': + return { + answer: 'Press Step until all guided stack-frame steps are complete.', + why: 'This lesson is only about observing how a function call lays out the stack: return address first, saved base pointer next, and buffer below them.', + steps: [ + 'Step once to save the return address.', + 'Step again to place the saved base pointer.', + 'Step again to allocate the 16-byte buffer.', + 'Finish the sequence and compare the buffer location to the return address above it.', + ], + }; + case 'rit-02': + return { + answer: 'Send more than 20 bytes so your input reaches the return address. A simple junk payload with at least 24 bytes is enough to crash it.', + why: 'The 16-byte buffer is followed by a 4-byte saved base pointer and then the 4-byte return address. Once you pass byte 20, you begin corrupting control flow.', + steps: [ + 'Fill the first 16 bytes to cover the buffer.', + 'Add 4 more bytes to write through the saved base pointer.', + 'Add at least 4 extra bytes of junk so the saved return address becomes garbage.', + 'Run the payload and watch the return address become invalid in the visualization.', + ], + }; + case 'rit-03': + return { + answer: `Use 20 bytes of padding followed by win() in little-endian form. In this run, win() is ${hex8(symbols.win)}.`, + why: 'The first 20 bytes walk up to the saved return address. Replacing that final address with win() makes the function return into the secret function instead of back to main().', + steps: [ + 'Open Misc and find win() in the symbols table.', + 'Add 20 bytes of padding: 16 for the buffer and 4 for the saved base pointer.', + `Append win() as ${hex8(symbols.win)} in little-endian byte order.`, + 'Submit the payload and confirm that the return address now points to win().', + ], + }; + case 'rit-04': + return { + answer: `Take the leaked main() address, add 0x150, then send 20 bytes of padding plus ${hex8(symbols.win)} in little-endian form.`, + why: 'ASLR changes the absolute addresses, but the offset between main() and win() does not change in this lesson. A single leak is enough to reconstruct the winning target.', + steps: [ + `Read the leaked main() address from the console. In this run, main() is ${hex8(symbols.main)}.`, + `Compute win() as main() + 0x150, which is ${hex8(symbols.win)} here.`, + 'Build the same overwrite as before: 20 bytes of padding to reach the saved return address.', + `Append ${hex8(symbols.win)} in little-endian form and submit the payload.`, + ], + }; + case 'rit-rop': { + const gadget = exercise.gadgets ? Number(Object.keys(exercise.gadgets)[0]) : 0; + return { + answer: `Use 20 bytes of padding, then ${hex8(gadget)}, then 0xdeadbeef, then ${hex8(exercise.flagAddr ?? 0)}, then ${hex8(symbols.win)}.`, + why: 'That gadget pops a value and a destination address from the stack, writes the value into memory, and returns. You use it to set flag_check to 0xdeadbeef before jumping into win().', + steps: [ + 'Add 20 bytes of padding to reach the saved return address.', + `Set the first return target to the gadget at ${hex8(gadget)}.`, + 'Put 0xdeadbeef next so the gadget loads it as the value to write.', + `Put ${hex8(exercise.flagAddr ?? 0)} after that as the write target, then finish with ${hex8(symbols.win)} so execution lands in win().`, + ], + }; + } + default: + if (exercise.mode === 'step') { + return { + answer: 'Use the Step control until the guided sequence completes.', + why: 'Step-based lessons are teaching state changes in order, so the correct approach is to advance one guided action at a time and read the console alongside the visualization.', + steps: [ + 'Press Step to advance one guided action.', + 'Compare the resulting console message to the visual change.', + 'Keep stepping until the sequence completes.', + ], + }; + } + + if (exercise.mode === 'input-hex' || exercise.mode === 'input-text') { + return { + answer: 'Use enough padding to reach the control point, then overwrite that target with the value this lesson is asking for.', + why: 'These lessons are about turning user-controlled bytes into a meaningful memory or control-flow change once your input reaches the vulnerable target.', + steps: [ + 'Use the source and assembly views to find the vulnerable buffer and the target beyond it.', + 'Count through the buffer and any saved metadata that sits between the buffer and the target.', + 'Build the overwrite with the payload builder if available, then submit it and confirm the target changed as intended.', + ], + }; + } + + if (exercise.mode.startsWith('heap-') || exercise.mode === 'final-chain' || exercise.mode === 'final-blind') { + return { + answer: 'Advance through the setup until you control an allocator write, then use the final input to overwrite the target pointer or value.', + why: 'Heap lessons teach how corrupted allocator metadata can redirect a later allocation or write into a sensitive target.', + steps: [ + 'Use Step when the lesson is still preparing allocator state.', + 'Switch to the input phase when it appears and submit the bytes that poison metadata or overwrite the target.', + 'Use the heap view to confirm where the final write lands before triggering the win condition.', + ], + }; + } + + if (exercise.mode.startsWith('asm-')) { + return { + answer: 'Step through the instructions, read the resulting register and memory state, and use those observed values as your answer.', + why: 'Assembly lessons are about understanding what the machine actually did, not guessing from the source or mnemonic names.', + steps: [ + 'Use Step to trace the state changes instruction by instruction.', + 'Use the console and register display together so you can verify each effect.', + 'Answer based on the final state the emulator shows, then reset and replay if needed.', + ], + }; + } + + return { + answer: 'Use the directions and helper tools to identify the winning memory or control-flow target, then submit the input that changes the program into that state.', + why: 'Every exercise in 0xVRIG is modeling the path from user-controlled input to a security-relevant state change.', + steps: [ + 'Identify the target the lesson wants you to reach.', + 'Use the available tabs and tools to find the right offset, address, or value.', + 'Submit the input and compare the observed state to the expected target.', + ], + }; + } +} + +export default function SolutionGuideModal() { + const { state, dispatch, currentExercise } = useExerciseContext(); + + if (!state.showSolutionGuide || !currentExercise) return null; + + const guide = getSolutionGuide(currentExercise, state.symbols); + + return ( +
dispatch({ type: 'DISMISS_SOLUTION_GUIDE' })}> +
event.stopPropagation()} + > +
+
+
Walkthrough
+

Show & Explain Correct Answer

+
+ +
+ +
+
Correct Answer
+

{guide.answer}

+
+ +
+
Why It Works
+

{guide.why}

+
+ +
+
Step By Step
+
    + {guide.steps.map((step) => ( +
  1. {step}
  2. + ))} +
+
+
+
+ ); +} diff --git a/src/components/shared/ToastMessage.tsx b/src/components/shared/ToastMessage.tsx new file mode 100644 index 0000000..18b1c37 --- /dev/null +++ b/src/components/shared/ToastMessage.tsx @@ -0,0 +1,22 @@ +'use client'; + +import { useEffect } from 'react'; +import { useExerciseContext } from '@/state/ExerciseContext'; + +export default function ToastMessage() { + const { state, dispatch } = useExerciseContext(); + + useEffect(() => { + if (!state.toast) return undefined; + const timer = setTimeout(() => dispatch({ type: 'DISMISS_TOAST' }), 2600); + return () => clearTimeout(timer); + }, [state.toast, dispatch]); + + if (!state.toast) return null; + + return ( +
+ {state.toast.message} +
+ ); +} diff --git a/src/state/reducer.ts b/src/state/reducer.ts index ffbcf3c..e192de3 100644 --- a/src/state/reducer.ts +++ b/src/state/reducer.ts @@ -8,6 +8,7 @@ export function createInitialState(): AppState { completed: loadProgress(), logMessages: [], inputMode: 'text', + inputProgress: null, stepIndex: 0, symbols: { ...BASE_SYMBOLS }, aslrBase: 0, @@ -26,6 +27,8 @@ export function createInitialState(): AppState { execLine: -1, vizRenderKey: 0, showSuccess: null, + toast: null, + showSolutionGuide: false, }; } @@ -37,6 +40,7 @@ export function reducer(state: AppState, action: Action): AppState { currentExerciseId: action.exerciseId, logMessages: [], stepIndex: 0, + inputProgress: null, running: false, execStepIndex: 0, execStepsTotal: 0, @@ -52,6 +56,8 @@ export function reducer(state: AppState, action: Action): AppState { execLine: -1, vizRenderKey: state.vizRenderKey + 1, showSuccess: null, + toast: null, + showSolutionGuide: false, }; case 'LOG': @@ -72,6 +78,9 @@ export function reducer(state: AppState, action: Action): AppState { case 'SET_INPUT_MODE': return { ...state, inputMode: action.mode }; + case 'SET_INPUT_PROGRESS': + return { ...state, inputProgress: action.progress }; + case 'SET_STEP_INDEX': return { ...state, stepIndex: action.index }; @@ -141,6 +150,18 @@ export function reducer(state: AppState, action: Action): AppState { case 'DISMISS_SUCCESS': return { ...state, showSuccess: null }; + case 'SHOW_TOAST': + return { ...state, toast: { message: action.message } }; + + case 'DISMISS_TOAST': + return { ...state, toast: null }; + + case 'SHOW_SOLUTION_GUIDE': + return { ...state, showSolutionGuide: true }; + + case 'DISMISS_SOLUTION_GUIDE': + return { ...state, showSolutionGuide: false }; + case 'RESET': return { ...createInitialState(), diff --git a/src/state/types.ts b/src/state/types.ts index 59a10c7..cef49a7 100644 --- a/src/state/types.ts +++ b/src/state/types.ts @@ -3,6 +3,7 @@ export interface AppState { completed: Set; logMessages: Array<{ cls: string; msg: string }>; inputMode: 'text' | 'hex'; + inputProgress: string | null; stepIndex: number; symbols: Record; aslrBase: number; @@ -21,6 +22,8 @@ export interface AppState { execLine: number; vizRenderKey: number; showSuccess: { title: string; msg: string } | null; + toast: { message: string } | null; + showSolutionGuide: boolean; } export type Action = @@ -29,6 +32,7 @@ export type Action = | { type: 'LOG_BATCH'; messages: Array<{ cls: string; msg: string }> } | { type: 'CLEAR_LOG' } | { type: 'SET_INPUT_MODE'; mode: 'text' | 'hex' } + | { type: 'SET_INPUT_PROGRESS'; progress: string | null } | { type: 'SET_STEP_INDEX'; index: number } | { type: 'INCREMENT_STEP' } | { type: 'SET_RUNNING'; running: boolean } @@ -44,4 +48,8 @@ export type Action = | { type: 'SET_EXEC_LINE'; line: number } | { type: 'SHOW_SUCCESS'; title: string; msg: string } | { type: 'DISMISS_SUCCESS' } + | { type: 'SHOW_TOAST'; message: string } + | { type: 'DISMISS_TOAST' } + | { type: 'SHOW_SOLUTION_GUIDE' } + | { type: 'DISMISS_SOLUTION_GUIDE' } | { type: 'RESET' }; From 3e9b5b3e6ad453a7cf6b653755c71aef75fc86cb Mon Sep 17 00:00:00 2001 From: "Thomas (Aeshus)" Date: Sat, 25 Apr 2026 09:21:26 -0400 Subject: [PATCH 6/6] Finish --- src/app/exercise/[id]/ExercisePageClient.tsx | 14 ++- src/app/globals.css | 5 +- src/app/imagine-rit/congratulations/page.tsx | 101 ++++++++++++++++++ src/app/imagine-rit/page.tsx | 63 ++++++----- .../panels/InputPanel/InputPanel.tsx | 2 +- src/components/shared/SuccessBanner.tsx | 19 +++- 6 files changed, 172 insertions(+), 32 deletions(-) create mode 100644 src/app/imagine-rit/congratulations/page.tsx diff --git a/src/app/exercise/[id]/ExercisePageClient.tsx b/src/app/exercise/[id]/ExercisePageClient.tsx index 60ff07b..9f64018 100644 --- a/src/app/exercise/[id]/ExercisePageClient.tsx +++ b/src/app/exercise/[id]/ExercisePageClient.tsx @@ -109,6 +109,11 @@ function MobileExercisePager() { ? orderedExercises[currentIndex + 1] : null; const basePath = isImagineRit ? '/imagine-rit' : '/exercise'; + const nextHref = nextExercise + ? `${basePath}/${nextExercise.id}` + : isImagineRit && currentId === 'rit-rop' + ? '/imagine-rit/congratulations' + : null; return (
@@ -130,10 +135,10 @@ function MobileExercisePager() {
); @@ -393,6 +398,9 @@ export default function ExercisePageClient({ params }: { params: Promise<{ id: s } dispatch({ type: 'LOAD_EXERCISE', exerciseId: id }); + if (id === 'rit-00') { + dispatch({ type: 'EXERCISE_COMPLETED', exerciseId: id }); + } }, [id]); // eslint-disable-line const mobileVizLabel = 'Assembly'; diff --git a/src/app/globals.css b/src/app/globals.css index c280bae..fe705b8 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -370,7 +370,7 @@ main { min-height: 2.15rem; padding: 0.45rem 0.75rem; background: rgba(255,255,255,0.03); - border: 1px solid var(--accent-secondary); + border: 1px solid rgba(126, 157, 177, 0.28); border-radius: 0.35rem; color: var(--accent-secondary); cursor: pointer; @@ -766,6 +766,7 @@ button.link-button:focus-visible, @keyframes toastIn { from { opacity: 0; transform: translateX(-50%) translateY(0.35rem); } to { opacity: 1; transform: translateX(-50%) translateY(0); } } @keyframes badgeSlideIn { from { opacity: 0; transform: translateX(40px); } to { opacity: 1; transform: translateX(0); } } + /* CRT scanline overlay */ .crt::after { content: ''; @@ -968,7 +969,7 @@ button.link-button:focus-visible, min-height: 2.6rem; padding: 0.65rem 0.75rem; border-radius: 0.35rem; - border: 1px solid var(--accent-secondary); + border: 1px solid rgba(126, 157, 177, 0.28); background: rgba(255, 255, 255, 0.03); color: var(--accent-secondary); font-family: var(--font-sans); diff --git a/src/app/imagine-rit/congratulations/page.tsx b/src/app/imagine-rit/congratulations/page.tsx new file mode 100644 index 0000000..b9f2d74 --- /dev/null +++ b/src/app/imagine-rit/congratulations/page.tsx @@ -0,0 +1,101 @@ +'use client'; + +import { useRouter } from 'next/navigation'; + +export default function ImagineRitCongratulationsPage() { + const router = useRouter(); + + return ( +
+
+
+ Workshop Complete +
+

+ Congratulations, and thank you for trying Imagine RIT. +

+

+ You just worked through the full beginner workshop: stack layout, overflow basics, return-address control, + ASLR bypassing, and a first ROP chain. That is a real tour of how memory corruption exploits are built. +

+

+ Thanks for spending the time with 0xVRIG. The goal of this track is to make low-level exploitation feel + understandable instead of mysterious, and you made it to the end. +

+
+ + +
+
+ +
+ {[ + { + title: 'What you learned', + text: 'How buffers sit near saved control data, why overflows matter, and how attackers turn memory corruption into control-flow changes.', + }, + { + title: 'What to try next', + text: 'Use the full 0xVRIG catalog to dig into heap attacks, format strings, sandbox escapes, and more advanced exploit patterns.', + }, + { + title: 'When to revisit', + text: 'Come back after a break and replay the workshop with the walkthrough turned off. If you can rebuild each payload yourself, the ideas have stuck.', + }, + ].map((item) => ( +
+
+ {item.title} +
+
+ {item.text} +
+
+ ))} +
+
+ ); +} diff --git a/src/app/imagine-rit/page.tsx b/src/app/imagine-rit/page.tsx index f0406db..7896b9e 100644 --- a/src/app/imagine-rit/page.tsx +++ b/src/app/imagine-rit/page.tsx @@ -30,9 +30,13 @@ export default function ImagineRitPage() { return (
@@ -40,10 +44,10 @@ export default function ImagineRitPage() { display: 'grid', gap: '1rem', gridTemplateColumns: 'repeat(auto-fit, minmax(18rem, 1fr))', - marginBottom: '1.75rem', + marginBottom: '0.75rem', }}>
- Beginner Workshop + Presented At Imagine RIT

- Imagine RIT teaches binary exploitation without assuming you already know systems. + A First Look at Binary Exploitation

- This track is for people who are curious about memory corruption, overflows, and exploit chains but have never touched a debugger or written an exploit before. Each exercise is short, guided, and visual. + This track is meant for people arriving at the Imagine RIT event, RITSEC workshops, or simple curiosity who want a first pass through stack overflows and exploit thinking without needing a full reverse-engineering setup first.

- You will see how programs store data, what goes wrong when inputs are too large, how control flow gets hijacked, and how attackers work around modern defenses. + VRIG, the Vulnerability Research Interest Group, is part of RITSEC and is presenting this workshop using the interactive visual environment here. It lets you see the code, stack state, console output, and helper tools in one place while the lesson walks you through what is happening. If you want the broader context, you can find VRIG at{' '} + vri.group + {' '}and RITSEC at{' '} + ritsec.club.

-
+
- Why this exists + RITSEC + VRIG
{[ { - title: 'No jargon wall', - text: 'The exercises explain what the stack, return address, and payload are while you use them.', + title: 'RITSEC context', + text: 'RITSEC is the broader student security organization behind the workshop. It runs student-focused security events, CTFs, and workshops, and you can find more at https://ritsec.club.', }, { - title: 'Short feedback loops', - text: 'You can try an input, see the visual effect immediately, and iterate without needing a separate setup.', + title: 'What VRIG does', + text: 'VRIG stands for Vulnerability Research Interest Group. It is part of RITSEC, and this is the group presenting the workshop at Imagine RIT. The broader VRIG project lives at https://vri.group/.', }, { - title: 'Builds confidence', - text: 'Each lesson introduces one idea, then carries it into the next exercise instead of throwing everything at you at once.', + title: 'Why this format works', + text: 'Instead of asking you to memorize jargon first, the exercises introduce one concept at a time and let you see the effect immediately.', }, ].map((item) => (
router.push(`/imagine-rit/${ex.id}`)} style={{ - padding: '1rem 1.1rem', + padding: '0.85rem', border: `1px solid ${isNext ? 'var(--accent)' : 'var(--panel-border)'}`, borderRadius: 'var(--radius-md)', background: done ? 'rgba(109, 226, 213, 0.08)' : 'rgba(255,255,255,0.025)', @@ -204,8 +211,8 @@ export default function ImagineRitPage() { {doneCount === EXERCISES.length && (
You learned how buffer overflows work, hijacked program execution, bypassed ASLR, and built a ROP chain. Nice work!
+
+ +
)}
diff --git a/src/components/panels/InputPanel/InputPanel.tsx b/src/components/panels/InputPanel/InputPanel.tsx index 4165f5c..ad2a10a 100644 --- a/src/components/panels/InputPanel/InputPanel.tsx +++ b/src/components/panels/InputPanel/InputPanel.tsx @@ -16,7 +16,7 @@ import AsmQuizInput from './inputs/AsmQuizInput'; import Toolkit from './Toolkit'; export default function InputPanel({ showToolkit = true }: { showToolkit?: boolean }) { - const { currentExercise, asmEmulator, state, dispatch } = useExerciseContext(); + const { currentExercise, asmEmulator, state } = useExerciseContext(); const ex = currentExercise; const [collapsed, setCollapsed] = useState(false); diff --git a/src/components/shared/SuccessBanner.tsx b/src/components/shared/SuccessBanner.tsx index 5b226e2..6663ac0 100644 --- a/src/components/shared/SuccessBanner.tsx +++ b/src/components/shared/SuccessBanner.tsx @@ -1,12 +1,27 @@ 'use client'; +import { usePathname, useRouter } from 'next/navigation'; import { useExerciseContext } from '@/state/ExerciseContext'; export default function SuccessBanner() { const { state, dispatch, currentExercise } = useExerciseContext(); + const router = useRouter(); + const pathname = usePathname(); if (!state.showSuccess) return null; + const isImagineRitFinalExercise = + pathname?.startsWith('/imagine-rit/') + && currentExercise?.id === 'rit-rop'; + + function handleContinue() { + dispatch({ type: 'DISMISS_SUCCESS' }); + + if (isImagineRitFinalExercise) { + router.push('/imagine-rit/congratulations'); + } + } + return (

{state.showSuccess.title}

@@ -26,8 +41,8 @@ export default function SuccessBanner() {
{currentExercise.realWorld}
)} -
);