ESP32-S3 Bench Guide
ESP32‑S3 N16R8 벤치 가이드: 실시간 핀아웃, 강의, 시뮬레이터 및 벤치 로그
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기A free, bilingual, phone-first PWA that teaches complete beginners how to use a computer — installable, works offline, no app-store account needed.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A phone-first learning app, growing into a small catalog of short courses.
Short lessons, hands-on practice, and no way to fail.
Built as an installable PWA: you hand someone a link, they add it to their home
screen, and it works from then on with or without a connection.README 전문 보기 ↗A phone-first learning app, growing into a small catalog of short courses. Short lessons, hands-on practice, and no way to fail.
Built as an installable PWA: you hand someone a link, they add it to their home screen, and it works from then on with or without a connection.
Courses today:
A Hub screen — the first thing you see after picking a language — lists every course with its own progress, and stays reachable any time from a course's own lesson-path screen. Progress, streak, and settings all live in one place either way; see How it is put together.
Below the courses sit three unlimited practice drills — clicking, dragging, and typing. Unlike lessons these never finish: they exist to be repeated, and keep only a personal best. See Practice modes.
The app follows your device's light or dark setting, or you can pin it either way in Settings. All of its own iconography is drawn — see Icons and theme.
Most tutorials for first-time computer users are either written for people who already know the vocabulary, or are a video you can't practice along with. The gap this fills: someone who has a phone but has never really used a laptop — who doesn't know what a "browser" is, or that a single click and a double click do different things, and who gets discouraged fast when something makes them feel dumb.
So the design choices follow from that, not from what would look impressive:
Fifteen modules, 79 lessons, built around eight interactive simulations rather than static screenshots — a working mock desktop, window manager, file explorer with real drag-and-drop, a browser with tabs and search results (ads and a scam included, on purpose), a keyboard where modifier keys latch so a touchscreen can practice real shortcuts, and a mock AI chat.
The foundation — using the machine at all:
Alt+Tab / Cmd+TabThe job-skills pathway — being useful in an entry-level office job. Ordered to prevent the usual failure, where someone can produce a document but then can't find it, send it, or tell which copy is current — so filing and email come before the apps that make the files:
SUM and basic
formulas, formatting, sorting/filtering without wrecking the table,
printingEvery lesson is tailored to the learner's actual computer (Windows or Mac, chosen once at setup) and is fully bilingual — English and Spanish, including every screen, prompt, and piece of feedback, not just the menus.
Three drills reached from the bottom of the Hub, deliberately outside the course system — no lessons, no completion, no unlocking, just a skill to repeat for as long as you like. They exist because the finite Mouse Practice module teaches the motions once; muscle memory needs volume.
Tone matches the rest of the app: no countdown, no lose condition, no "wrong". Rounds are a fixed number of targets rather than a ticking clock, so a slow careful learner and a fast one both finish.
One robustness note worth knowing: typing results above 250 WPM are shown but not saved as a personal best. That is past the world record, so it did not come from typing — a pasted answer would otherwise leave behind a score the learner could never beat.
The job-skills modules are honest about their own limits. A phone teaches vocabulary, judgment, and sequencing well — which subject line is clearer, attachment or link, which formula answers this question. It cannot teach the feel of a Save As dialog, a fill handle, or a print preview.
So those lessons pair phone-teachable concept steps with action steps: a
real task done on a real computer, with an ungraded "I did this". A practical
minimum is one computer session after every lesson or two, rather than
banking all the hands-on work until the end — otherwise recognition on a
phone gets mistaken for the ability to do the task at work.
The practice modes say the same thing out loud: opening one on a touch-first
device (matchMedia('(pointer: coarse)')) shows a warm, dismissible notice
that a real mouse or keyboard will help more. It never blocks use, and it does
not appear on a laptop — including a laptop with a touchscreen, which reports
a fine pointer because it has one.
For people who already use a computer fine and are new to AI — this one moves faster and skips re-explaining basic computer literacy. Four modules, 15 lessons:
Its five hands-on projects are real, guided actions rather than in-app
simulations — installing and using the real thing, since Claude's actual
interface changes over time and can't be faithfully mocked the way a
decades-old desktop metaphor can. See the new action step type below.
npm install
npm run dev # http://localhost:5173
| Script | What it does |
|---|---|
npm run dev | Dev server |
npm run build | Checks the curriculum, then builds to dist/ |
npm run check | Checks the curriculum only (see below) |
npm run lint | oxlint |
npm run icons | Regenerates the app icons in public/icons/ |
Installing to a home screen and working offline both need a secure origin.
localhost counts; http://192.168.x.x does not — over plain http the service
worker never registers, so you'd be testing the app without the two things that
make it an app.
So there's a self-signed cert for LAN testing. Generate it once:
mkdir -p .certs && openssl req -x509 -nodes -days 825 -newkey rsa:2048 \
-keyout .certs/key.pem -out .certs/cert.pem -config .certs/openssl.cnf
.certs/openssl.cnf lists the addresses the cert is valid for — add your
machine's LAN IP there if it changes. Then:
npm run build
HTTPS=1 npx vite preview --port 4173 # the real build, service worker and all
HTTPS=1 npm run dev # or the dev server, for iterating
Both print a Network: URL to open on the phone. The phone will warn that the
certificate isn't trusted — that's expected for a self-signed cert on your own
network. Tap through it (Chrome: Advanced → Proceed; Safari: Show Details →
visit this website) and the service worker registers normally.
.certs/ is gitignored. The certificate is only ever used by the dev and
preview servers; it has nothing to do with the production build.
dist/ is a static folder — Netlify, Vercel, Cloudflare Pages, GitHub Pages,
anything. It must be served over HTTPS (or localhost) or the service worker
won't register and the app won't install or work offline.
Serving from a subpath (GitHub Pages, say) needs the base path at build time:
BASE_PATH=/computer-basics/ npm run build
src/
courses/
index.js the catalog: COURSES array + getCourse(id)
makeCourse.js turns a module list into {MODULES, LESSON_ORDER, getLesson, …}
computer-basics/ one file per module, plain data (module1…7, moduleMouse, moduleJob1…7)
claude-001/ same shape, different course
claude-code/ stub — one placeholder lesson, status: 'coming-soon'
components/
sims/ the mock computer: desktop, windows, files, browser, keyboard
practice/ the unlimited drills (click, drag, type) + typing word lists
Art.jsx hand-drawn diagrams (phone vs. computer, the cheat sheet, …)
icons/ the drawn icon set — inline SVG, referenced by name
StepView.jsx renders one step of a lesson
ui.jsx buttons, cards, sheets, the rich-text renderer, CopyButton
screens/ Onboarding, Hub (courses + practice), Path (a course's lesson list), Lesson, Practice, Settings
state/store.jsx all progress and settings, scoped to whichever course is active
lib/
storage.js the only file that touches persistence
supabase.js the client, or null when no project is configured
merge.js combining two copies of a learner's progress (pure)
sync.js pulling and pushing that progress
gestures.js tap → click, double tap → double-click, hold → right-click
i18n/ UI strings, plus the content resolver
A course is whatever makeCourse(meta, modules) returns — the registry in
src/courses/index.js is just an ordered list of those. Adding a fifth course
later is: a new folder shaped like claude-001/, one line added to the
COURSES array. Nothing else in the app needs to know it exists.
Module files do not carry their own number. makeCourse assigns "Module 3"
from array position, so inserting a module never means hand-renumbering every
one after it.
Progress is stored nested by course —
completed: { [courseId]: { [lessonId]: {...} } } — and settings.currentCourseId
says which one is active; useApp() derives everything (unlock state, next
lesson, per-module progress) against that course only. The streak is the one
thing shared globally across every course, on purpose — a daily practice habit
doesn't care which course kept it going. src/lib/storage.js's migrate()
detects an old, pre-multi-course save structurally (a lesson record has
completedAt directly on it; a course bucket doesn't) and wraps it under
'computer-basics' — no old progress is lost when this shape changed.
A lesson is a list of steps. Any text can be a plain string, a { en, es }
pair, or dev(windows, mac) to differ by platform — and these nest.
{
id: 'm3-l4',
icon: 'wave',
minutes: 4,
title: { en: 'Moving a file', es: 'Mover un archivo' },
steps: [
{ type: 'teach', title: …, body: [ … ], visual: { art: 'file-vs-folder' } },
{ type: 'sim', sim: 'files', prompt: …, config: { goal: 'move', … } },
{ type: 'choice', prompt: …, options: [{ id, label, correct }, …] },
{ type: 'sort', prompt: …, buckets: [ … ], items: [ … ] },
{ type: 'action', title: …, body: [ … ], copyText: …, linkUrl: … },
{ type: 'recap', points: [ … ] },
],
}
Inside any string: **bold**, __a term being defined__, [[Ctrl]] for a key cap.
action is for a real task done outside the app — install something, run a
command, open Excel and build a tracker — rather than a graded question. It
never calls onMistake; there's no wrong answer to a real task, only done or
not yet. The body is the substance; copyText and linkUrl are optional
extras, since plenty of real tasks have nothing to copy and nowhere to link.
Any copyText is always rendered as visible text too (never clipboard-only —
the Copy button is a convenience on top of it, since permission can be
denied), and linkUrl opens a real target="_blank" link — the only place in
the app that leaves it. Both can be dev(windows, mac) wrapped like any other
content, for platform-specific commands.
npm run check runs the merge rules above as well, then validates every lesson across every course — missing
translations, a simulation pointing at something that isn't on screen, a
question with no right answer, a wrong answer with no explanation, a lesson
with no recap, an action step with nothing to actually do, an icon: naming
something that isn't drawn. Lesson ids only
need to be unique within their own course, so two courses can both have an
m1-l1 without colliding. It runs as part of npm run build, so those can't
ship. Coming-soon stub courses are checked only lightly (well-formed enough to
render in the Hub), not against the full content rules.
Signing in is an extra, never a gate. With no .env the app has no accounts at
all and behaves exactly as it did before they existed — src/lib/supabase.js
returns null and every caller checks for it. Copy .env.example to .env to
turn them on.
The app stays local-first. It still boots synchronously from localStorage
and never waits on a network to show a lesson; an account only adds a copy on
the server so progress can follow someone to another device. Sign out and the
progress stays on the device — nothing is deleted.
Two tables, profiles and progress, both with row-level security allowing a
user to touch only their own row. That is what makes it safe to ship the
publishable key inside the bundle, which is how Supabase is designed to work.
npm run check:rls proves it, by signing in as one user and trying to read,
overwrite and delete another's data through the real API. The secret /
service_role key bypasses those policies entirely and must never appear in
the app, in .env, or in git.
profiles also carries streak_count, lessons_done, display_name and
avatar_icon, denormalised from the progress blob on each sync. Nothing reads
them yet — they exist so that comparing streaks between people later is one
indexed query rather than a scan over everyone's JSON. The name and picture the
app shows always come from the device's own copy, so they are still right with
no network.
Signup needs a confirmation email, and this is the part that is easy to get wrong in a way nothing reports.
Without custom SMTP, Supabase refuses to deliver to anyone who is not a member
of the project's team. Not rate-limited — refused. So it works perfectly for
whoever owns the project and silently fails for every real user, which is the
most misleading possible combination. The symptom is not an error: signUp
succeeds with no session, and a screen that only checks for an error sees
success. src/state/auth.jsx therefore returns needsConfirmation alongside
error, and Account.jsx says "check your email" rather than quietly closing.
Mail goes out through Resend, from [email protected]. SMTP settings
live in the Supabase dashboard under Authentication → SMTP Settings:
smtp.resend.com, port 465, username the literal string resend, password a
Resend API key. Two things to know afterwards:
auth.jsx passes window.location.origin as emailRedirectTo, and Supabase
rejects any redirect target not on that list — so a domain change that forgets
this leaves every confirmation link doing nothing at all.The two email templates are checked into supabase/email-templates/, for the
same reason the schema is: otherwise they exist only inside a dashboard. They
are Spanish first with English underneath.
Two devices can both have progress the other has not seen, so lib/merge.js
combines them rather than picking a winner. Completed lessons are unioned;
practice bests take the higher number; settings take the more recent choice.
Two rules matter more than they look:
max, never a sum. Sync runs on a timer, on every sign-in and
on every foreground, so a rule that accumulates would drift further from the
truth on every pass. merge(merge(a,b),b) has to equal merge(a,b), and
npm run check asserts exactly that.The service worker precaches the whole build so lessons work with no connection. The cost is a page that, once loaded, would otherwise run the same JavaScript forever — a new version installs and claims the tab, but nothing tells the running app to pick it up, so someone can open the app for days and still be on the build they first loaded.
src/lib/updates.js fixes that: it owns the registration (hence
injectRegister: null in vite.config.js), asks for a new version whenever the
app becomes visible again — an installed PWA resumed from the app switcher never
re-fires load — and reports when one has taken over. App.jsx decides when to
apply it, reloading silently everywhere except inside a lesson, a practice run,
or onboarding, which are the only screens holding state that isn't yet in
localStorage. There is no update banner, on purpose: "tap here to update"
means nothing to the person this app is for.
Settings → About shows which build is running (the commit SHA in production), so "the new thing isn't there" can be answered rather than guessed at.
Every icon in the app's own furniture — course cards, module headers, lesson
nodes, callouts, settings rows — is a drawn SVG in src/components/icons/,
named by string the same way sims and art are. Content refers to them as
icon: 'folder', and names.js keeps a plain-JS list so npm run check can
catch a typo without a JSX runtime. A name that doesn't exist renders a neutral
dot rather than throwing, so a bad name is visible but never breaks a lesson
someone is part-way through.
Emoji are not used for iconography. They render differently on every platform, can't take the theme's colour, and read as a shortcut. They remain only inside the simulations, where they stand in for real desktop icons.
The set is small on purpose — around eighty glyphs reused across ~120 slots. A tight vocabulary reads as deliberate; a unique drawing per lesson reads as noise, and most lessons are variations on a handful of ideas anyway.
Because every icon is currentColor and every surface draws from the tokens in
src/index.css, dark mode is a token swap rather than a second design.
settings.theme is 'system' | 'light' | 'dark'; 'system' leaves <html>
alone and lets the prefers-color-scheme media query decide, while an explicit
choice sets data-theme and wins in both directions. The mock Windows and macOS
interfaces keep their own hardcoded colours and deliberately do not follow the
theme — a simulated file explorer should look like a real one whatever the app
around it is wearing.
Each one takes the same props — config, onSolved, onMistake(message),
showHint, solved — so a lesson names one by string. They chrome themselves
as Windows or Mac based on the learner's setup.
Touch gestures stand in for mouse actions (src/lib/gestures.js): one tap is a
click, two quick taps a double-click, press-and-hold a right-click. That mapping
is what makes it possible to practise mouse skills on a phone.
These are deliberate; changing them changes the app's character.
No payments and no analytics. Accounts exist now but are optional — see
Accounts; with no .env the app is still entirely
device-local, and nothing is ever sent anywhere.