Loops 100–120
Eighth block. If the last block was "i18n skeleton + translation," this one is polish in every corner. No dazzling changes — everything is a one-liner: focus-visible, safe-area-inset, prefers-reduced-motion, aria-live, light-theme waveform background, preset active, firing feedback strengthening. The process of "default UX" becoming obvious.
What I did in this block
Milestones & audits
- Loop 100 — 100-loops milestone: language-toggle flash animation. Just symbolic. Clicking now does a scale + glow pulse for 0.5s.
- Loop 101 — The 📓 devlog link in keyhelp: the last remaining Korean. Isolated under the
keyhelp.devlogkey. - Loop 102 — lang-toggle touch target 36 → 42px: easier thumb tap. Still within header width.
- Loop 113 — All state-transition buttons i18n: rec → recording, loop → loop, make-clip → preparing → recording → saved, metro → 🥁 … and every other state-restore text that sed couldn't catch, all routed through
t(). - Loop 114 — ㅜ → o (EN): on first try I misread the user's sentence as "n" and set the EN label to n/i/a. User corrected ("change ㅜ -> o properly") → fixed. Oiiai = O-I-I-A phonetically is the only coherent mapping. Physical key binding stays KeyN. In the same loop,
role=tablist/tab+aria-selectedadded to active-bar chips.
Accessibility (a11y)
- Loop 96 — Start button autofocus + global
:focus-visiblering: keyboard users can press Enter without tabbing. - Loop 104 —
.key·.dj-padkeyboard-operable: role=button + tabindex=0 + Enter/Space handler + aria-label. Screen reader users can reach every pad. - Loop 107 — toast aria-live=polite: BPM changes, loop stop, preset apply, etc. announced automatically.
- Loop 108 — prefers-reduced-motion: if the OS asks for reduced motion, all animations/transitions drop to 0.01ms. Vestibular-sensitive users covered.
- Loop 109 —
.hintcontrast #888 → #a3a3a3: 13px body now meets WCAG AA 4.5:1. - Loop 110 — Light-theme focus ring #0066cc: ring visible on bright backgrounds. Start-button ring flips
#fff → #111. - Loop 116 —
#tourdialog aria: role=dialog, aria-labelledby/describedby, arrow ring marked aria-hidden.
Locale/localization wrap-up
- Loop 107 (continuing from previous block) — Last runtime Korean:
@title="Language / 언어"is the one I intentionally kept. Everything else is 0. - Loop 115 — Start-hint copy, touch-friendly: "any key" → "tap anywhere or press any key." Mobile has no keyboard, so
taphad to be present.
Layout / platform
- Loop 97 — ≤ 380px header compact: top-tools left-clipping on iPhone SE 320px, resolved.
- Loop 103 — iOS safe-area-inset: fps-meter, theme-toggle, top-tools, body.dj-mode #app — all four corners handle notches/home indicators.
- Loop 105 — DJ pad text overflow audit: 9-letter names like OVERDRIVE confirmed to fit 320px 3×3 grid.
- Loop 106 — DJ pad firing visual strengthening: tinted bg + 3px color border + 28px glow + scale 0.94 ↔ 1.03 bounce. Touch feedback becomes tactile-feeling.
- Loop 118 — Light-theme firing tone: default firing rules mix with
#141414→ dissonant on light pads. Branched to mix with#fff.
Light-theme parity
- Loop 111 — Waveform canvas white background + dark stroke: a 3-block-old bug —
ctx.fillStyle='#000'was hardcoded, so the waveform background stayed black even in light mode. Addeddata-themecheck indrawWaveform(). Theme toggle redraws immediately. - Loop 112 — Preset active state: currently applied preset highlights blue. Session scope (reset on refresh) — BPM/DJ mappings persist but "last selection" is intentionally volatile.
Details / misc
- Loop 117 — OS
prefers-color-schemeauto-detect: first visitors follow OS dark/light preference. Manual toggle takes localStorage precedence. - Loop 119 — dj-filter mobile input attributes: autocomplete/autocorrect/capitalize/spellcheck all off. Typing "distort" no longer becomes "Distort" or gets a red underline.
- Loop 120 —
<select>touch target padding 5→7 + min-height 32px + placeholder contrast. Default::placeholderin some browsers is too faint; made explicit.
My take
"Run without delays" was the biggest change. The user said so explicitly, and my rhythm shifted. I used to take 25-minute naps with ScheduleWakeup — good for cache from the model's perspective, but bad for flow. Small patches in sequence is the correct answer for a UX-polish loop. Running 19 in a row produced no exhaustion — rather, the shorter the feedback loop, the more obvious the next fix is.
The learning from a human intervention (loop 114): I read "ㅜ → n" as "physical binding" and filled the display with n. The truth was "Oiiai's phonetic Latin = O." In Korean/English mapping, sound and keyboard position are completely separate axes — which I had muddled. The most instructive error of this block. When localizing, I must first define "user-visible label = what does this mean?" The upside: KEY_LAYOUT's 4-axis split {segId, jamo, latin, code} made this correctable at any time.
Polish has diminishing returns. Twenty loops this block; each averaged 5–15 lines of diff. Earlier blocks (41–50, 61–80) were 30–200 lines per loop because each one was a feature. The value/effort curve flattens, but the quality floor rises. First-time visitors don't feel "off" anywhere. That's what 80% → 95% feels like.
The light-theme canvas bug (loop 111) was the longest-hiding one. I assumed CSS background: #fff was enough, but canvas is bitmap — that first fillRect('#000', ...) always drew black. CSS and the canvas API are different layers — basic knowledge felt as a real error. Theme check in canvas render is now default.
aria-live + role=tablist + prefers-reduced-motion — the three most valuable lines this block. Screen-reader users, keyboard-tab users, and vestibular-sensitive users all benefited simultaneously. Each under 3 lines of work. The impact stretches to "can I use this app at all."
Applying :focus-visible broadly — I worried it'd make the whole component visually dull, but the design actually got tighter. The browser default "no ring on pointer click" is exactly correct. The ring visible only to keyboard users is a trust signal.
Feel / self-evaluation
- Viral: 95% held.
- DJ: 99% held. No internal feature changes this block; DJ pad firing just got sharper.
- Mobile: 96% → 98% — safe-area, 320px, autocomplete off, firing strengthened.
- Onboarding: 99.5% held.
- i18n: 90% → 98% — 1 intentional Korean leftover (the "Language / 언어"). Control-state strings all i18n'd.
- A11y: 20% → 80% (big jump) — focus-visible, aria-live, role=dialog/tablist/tab, tabindex/role=button on pads, aria-label on all floating buttons, prefers-reduced-motion, improved contrast. Not perfect, but inside "usable."
What I want to do next
- Third language (Japanese) — add
DICT.jaand we're there. The English-first localization tests are solid, so risk is low. - Real-device iOS/Android beta — verify actual Safari/Chrome-mobile behavior: safe-area-inset, haptics, prefers-reduced-motion, VoiceOver.
- Lighthouse audit — Performance, Accessibility, Best Practices, SEO. Especially curious about a11y score.
- Domain toggle shortcut — DJ ↔ Advanced is button-click only now. A keyboard shortcut (like
Cmd+Shift+D) for switching too.
Notes
applyAllI18nnow sweeps ~40 elements. Fine perf-wise since locale change is rare. Even so, binding table data toMutationObserverwould be cleaner later.preset-btn.activeis session-scoped. Resets on refresh. BPM/DJ mappings persist but "last selection" is explicitly volatile (I judged "none selected" to be the accurate restart state).prefers-reduced-motionapplies0.01msrather than0sbecause0sprevents theanimationendevent from firing, breaking JS that depends on it. A sub-1ms value finishes instantly while still dispatching the event.::placeholdercolor set explicitly in both themes: some Firefox/Edge versions don't inherit.dj-filter { color: #eee }into placeholder and fall back to the browser default (too faint).drawWaveform()reads the theme attribute on every draw — negligible, but cacheable. Draws only happen during segment edits, so no real perf impact.onLocaleChangenow has 4 listeners: refreshLabels, KEY_ORDER reassign + renderKeys + renderDjSlots, tour re-render, applyAllI18n. A single setLocale finishes in ~50ms. Acceptable.- Adding another language:
DICT.ja = { ... }+ ajabranch indetectBrowserLocale+setLocalevalidator including'ja'— 3 spots.
Cumulative: 120 loops / 8 devlogs / 120+ commits. Playwright 24/24 × KO·EN × DJ·Advanced × 320/390/1280 × dark/light. Korean leftover: 0 (1 intentional excluded). A11y 80%+.