電波 design tokens, carrier meter, and the state faceplate #10
Labels
No labels
needs-info
needs-triage
ready-for-agent
ready-for-human
v2
wayfinder:grilling
wayfinder:map
wayfinder:prototype
wayfinder:research
wayfinder:task
wayfinder:ticket
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
aiko/denpa#10
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What to build
The foundation every other slice draws on, delivered end-to-end by replacing the current purple
idlepill with the real state indicator.Tokens — express the colour, type, spacing, radius and shadow scales from the handoff as Chakra v3 theme/semantic tokens. Do not port inline styles from the design HTML; they are inline only because the design tool required it.
Fonts — Archivo (400/500/600/800/900) and DotGothic16 (400), self-hosted so the Tauri build works offline. DotGothic16 is a bitmap gothic; a generic mono is not a substitute.
Animations —
carrier,sweep,blink,inbound,halateas named keyframes. Underprefers-reduced-motion, dropsweepandhalatebut keepcarrierat reduced amplitude — it is data, not decoration.Carrier meter + state faceplate — one shared component driven by
AiStateContext, covering everyAiStateEnumvalue per the 1d table (IDLE floor, LISTENING bound to real mic level, THINKING_SPEAKING halate band, INTERRUPTED rules, LOADING tuning static, WAITING dimmed floor). Mount it where the state pill is today.Design reference:
docs/design/denpa-receiver/README.md(card ids1a,4a… index intoDenpa Receiver.dc.html, openable in a browser). — 1d is the contract table.Acceptance criteria
AiStateEnumvalue renders its faceplate treatment; LISTENING bar heights track real mic levelprefers-reduced-motiondrops sweep/halate, keeps carrier at reduced amplitudeBlocked by
None - can start immediately
Landed on
issue/denpa-10-design-tokens.Shape —
src/renderer/src/theme/holdsdenpa-tokens.ts(the raw scales, the only place hexes live),system.ts(denpaSystem, mounted by bothChakraProvidercall sites) anddenpa.css(self-hosted@font-faceplus the five named keyframes).src/renderer/src/components/receiver/holdsfaceplate-spec.ts(the 1d table as data: JP + EN label, meter treatment, key treatment, partial-transmission rule, plusmotionPlanandtruncatePartial),carrier-bars.ts(bar geometry — count, step, colour, level),carrier-meter.tsxandstate-faceplate.tsx. The footer's purple#7C5CFFpill andai-state-indicator.tsxare gone.Tests — 28 new, 56 total (
npm test: 8 files, 56 passed; baseline was 28). They cover: everyAiStateEnumvalue has a treatment and an unknown state throws rather than defaulting to a working IDLE; the six JP/EN labels exactly, with a character-set guard so nobody reuses thezhlocale; WAITING holds the IDLE meter floor and differs only in the key; LOADING stays distinct from the idle floor and from listening-at-silence on a non-colour property; LISTENING heights monotonic in mic level with every bar moving, and clamped against the 0-100 vs 0-1 unit mismatch that exists invad-context.tsx; listening-at-level-0 is not the idle floor; station/pal/faceplate geometry derives from one step; reduced motion drops sweep and halate and keeps carrier at reduced amplitude; the faceplate renders exactly onerole="status"region carrying both labels with the barsaria-hidden, and mic-level changes do not re-announce; the Chakra system actually resolves every role token; no remote font URL anywhere, everyurl()resolving to a committed file; no raw hex undercomponents/receiver/.npm run build:webwas run as a check that the tokens compile and the fonts ship: the woff2 files land indist/web/fonts/with theurl()s rewritten relative, so the packaged build resolves them offline.Left out — 自発 / UNPROMPTED is in the 1d table but is not an
AiStateEnumvalue and its band is deferred, so it is absent rather than stubbed. No global body-font override and no dock/station-bar chrome; this issue only remounts the state indicator where the pill was.Assumed (all six written to the questions ledger) — the JP labels are spec strings, not i18n copy; the mic level is
VADContext.previousTriggeredProbability, a peak-hold that never falls mid-utterance, since it is the only level the client has; the px spacing scale is deliberately not registered as Chakra tokens because it would shadow the rem steps this fork already uses; fonts are subset woff2 committed topublic/fonts/;faceplateForthrows on an unknown state.Finding, not from this branch —
npm run lintandnpm run typecheckare already red onmain. Lint dies before linting (eslint-config-airbnbreferenced by.eslintrc.js, not installed). Typecheck reports 585 errors onmainand 585 on this branch, almost all in the vendoredWebSDK/andMotionSync/trees; this diff adds zero.Review round on
issue/denpa-10-design-tokens— pushed. This supersedes the test counts in my comment above; everything else there still holds.What was built —
src/renderer/src/theme/holdsdenpa-tokens.ts(raw scales; the only place hexes live),system.ts(denpaSystem, mounted by bothChakraProvidercall sites) anddenpa.css(self-hosted@font-faceplus the five named keyframes).src/renderer/src/components/receiver/holdsfaceplate-spec.ts(the 1d table as data: JP + EN label, meter treatment, key treatment, partial-transmission rule, plusmotionPlanandtruncatePartial),carrier-bars.ts(bar geometry),carrier-meter.tsxandstate-faceplate.tsx. The footer's purple#7C5CFFpill andai-state-indicator.tsxare deleted.What the review caught, and what changed
state-faceplate.tsxnow has aSeveredRulesmeter: two 2px unprompted-token rules with 切断 between, inside the 52x26 band footprint, never reachingCarrierMeter.TuningStaticmeter paintingrepeating-linear-gradient(115deg, rgba(255,45,149,.28) 0 3px, transparent 3px 9px)with a 10pxrgba(53,240,232,.5)bar on thesweepkeyframe at 1600ms linear.HalateBandnow backgrounds atrgba(255,45,149,.14)withlinear-gradient(90deg, transparent, rgba(255,45,149,.75), transparent)on an absolutely-positioned child carrying the animation. Rendered markup verified to contain no#ff2d95.animation-name: sweep; halate band + gradient overlay), each also asserting the carrier bars are absent. All three were confirmed failing against the pre-fix component for the right reason before the fix. Plus afaceplate-spectest pinning both gradient strings to the 1d recipes.Two supporting changes fell out of that. A
denpaColors.metergroup (band/halate/static/sweep) intheme/denpa-tokens.tskeeps the three translucent fills where raw values live — the no-raw-hex test still passes. And the sharedsweepkeyframe now reads its end position from--sweep-travel(default900%); its fixed-140% → 900%travel was authored for a thin bar in a tall meter and moved a 10px bar 104px through a 26px band, visible only about 35% of the cycle. The tuning band sets--sweep-travel: 26px. No other consumer of that keyframe exists.One thing worth knowing about the test style: the rendering tests use a
meterMarkup()slice helper rather than asserting on the whole document. Chakra emits every registered token as a CSS var in an inline<style>block, so a document-widetoContainon a colour or gradient string passes on the token declaration alone — a false green, and it is the assertion style the review suggested. The assertions are scoped to the meter element viadata-denpa-metermarkers.Tests —
npm test: 8 files, 60 passed, 2.64s. Baseline was 28. Nothing from the findings list was skipped.What did not pass
Two checks are red, both red at HEAD and unrelated to this diff.
npm run lintnever lints: ESLint 8.57.1 aborts withcouldn't find the config "airbnb"becauseeslint-config-airbnbis not installed.npm run typecheckreports 585 error lines — 582 in the vendoredsrc/renderer/WebSDK/, 3 pre-existing unused-import errors in files this branch does not touch. I stashed the diff and re-ran both: counts identical with and without it, so this round adds zero type errors. Not fixed, per the scope rule; recorded in the questions ledger.Also: the
cavecrew-reviewerpass the global instructions call for could not run — this agent has no agent-spawn tool — so review was a manual diff pass. That process step is unmet.Deliberately left out
自発 / UNPROMPTED is in the 1d table but is not an
AiStateEnumvalue and its break-in band waits on the adapter carrying a pushed turn, so it is absent rather than stubbed. No global body-font override, no dock key row, no station/pal bar chrome; the faceplate mounts only where the pill was, and the station/pal geometry is reachable viacarrierBars({variant:'station'})for the branches that build those bands. The px spacing scale is exported asdenpaSpacevalues but not registered as Chakra tokens — numeric keys would shadow the rem steps this fork uses everywhere. LOADING keepscarrierin its motion list even though the faceplate no longer draws bars for it, because the parked 2b tab shows a carrier for LOADING off the same spec row. Under reduced motion the tuning static holds still (sweep dropped bymotionPlan, as its existing test requires) and stays distinguishable from the IDLE floor by pattern rather than movement. The design's#7c6288(~4.0:1) was avoided for the faceplate's EN label in favour oftext.mute, since that label carries state rather than being redundant with the meter.Assumptions
All are in the questions ledger.
@testing-librarywere added, contrary to the spec plan. The a11y contract is asserted against real markup viareact-dom/server'srenderToStaticMarkupinside aChakraProvider, which runs in the existingenvironment: 'node'. Zero new dependencies; the original 28 tests and the global vitest config are untouched.denpaSystem.token("colors.carrier") === "#ff2d95"can never pass. The wiring test instead asserts every role token is registered (tokens.getByNamedefined,token()truthy —undefinedis exactly what wrong config nesting yields) and that the raw scale undercolors.denpa.*resolves to the hex.@font-faceblocks.previousTriggeredProbabilityis reachable fromVADContextand does trigger re-renders (setPreviousTriggeredProbabilitycallsforceUpdate, and it resets to 0 on speech end /stopMic). It is a peak-hold that never falls mid-utterance, which is the level LISTENING is bound to.carrierBarstakes a plain number, so a decaying level later touches onlyVADContext.StateFaceplatetakes the mic level as a prop rather than readingVADContext: importingvad-contextpulls the Live2D/Cubism chain (@frameworkalias) into every surface showing state and makes it unrenderable in tests. State still comes fromAiStateContextviauseContext, so no provider is needed in tests. The footer passesuseVAD().previousTriggeredProbability.prefers-reduced-motionis handled in JS (framer-motion'suseReducedMotionfeeding the puremotionPlan), not a CSS media block, so there is one mechanism and it is the one under test.denpa.cssholds only@font-faceand the five keyframes; the carrier keyframe reads--bar-scaleand--carrier-ampso amplitude reduction composes with the data-driven bar height.0.45— the design says only "reduced". The test pins strictly-between-zero-and-full, not the number.[.34,.30,.34,.32], above IDLE's[.2,.28,.2,.24], so mic-open-and-silent never draws as mic-closed. Level maps asfloor + (1-floor) * level * weightwith weights[1,.88,1,.82], so every bar moves and none exceeds 1.carrierunder reduced motion (onlysweepis dropped) so tuning-with-no-data-yet never reads as dead. Silence, not-yet-loaded and idle are pairwise distinct on kind and scale profile, not on hue.faceplateForthrows on an unknown state rather than falling back.AiStateis a closed union so this is unreachable in practice, but there is no error boundary above the footer if it ever is reached.faceplate-spec.tsas product markings rather than i18n keys, so the visible label and the screen-reader announcement cannot drift. The now-unusedaiState.*keys inlocales/enandlocales/zhwere left alone as out of scope.No PR opened, nothing closed, nothing merged,
mainuntouched.Shipped. Merged to
mainat875a261(phase B1 — client foundation).Closed as part of reconciling the board with
main: the autonomous run's policy was not to auto-close, so shipped work stayed open and the tracker read as though nothing had landed. That policy is now reversed — issues close when they land.