active-ruler: ticks light up at integer crossings - #13
Merged
Conversation
The ruler on the right edge of every funnel was passive: a static
0–10 reference scale unaffected by the user's pour. Make it active.
Each tick now has one of three states driven by Math.floor(|votes|):
unfilled tick > floor(|votes|), or tick = 0 (neutral)
filled 1 ≤ tick < floor(|votes|) (sign-coloured)
current tick = floor(|votes|), tick ≥ 1 (sign-coloured,
fully opaque,
1.5× thicker)
Tick 0 is always passive — it's the baseline reference, not a
milestone the user reaches. Sign drives colour (via voteColor), not
state: tickState(t, +m) === tickState(t, −m) for every t and m.
Cross-zero is unambiguous: as |votes| drains through 0 every tick
unfills, then re-fills in the new sign's colour as magnitude builds
on the other side.
The cadence of tick fills during a hold externalizes the cost ramp.
Tick 1 fires at 0.2 s, tick 2 at 0.8 s, tick 5 at 5.0 s, tick 10 at
20.0 s — the (2n − 1) / 5 pattern is what users *feel* as "votes get
expensive at the top."
Implementation:
- New src/lib/rulerState.ts: pure tickState(tick, votes) helper.
- New src/lib/rulerState.test.ts: 12 unit tests covering integer
crossings, drains, sign symmetry, saturation, and non-finite votes.
- src/components/Funnel.tsx: minor and major tick lines and major
labels now read their inline style (stroke/opacity/width or
fill/opacity/weight) from per-tick state. 160 ms CSS transition;
collapses to 'none' under prefers-reduced-motion.
Funnel geometry, water rendering, ARIA contract, math, reducer,
state machine — all unchanged. The active ruler lives entirely
inside the existing aria-hidden ruler <g>; screen readers continue
to consume the under-funnel readout as the source of truth.
49/49 tests pass (37 prior + 12 new). Lint, typecheck, all 3 build
targets clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-state walkthrough (empty / +5 / −3 / cross-zero / mixed grid) with verified-DOM tick stroke/opacity/width snapshots, the (2n − 1)/5 mid-hold cadence table, and notes on the prefers-reduced-motion snap-vs-fade path. Same docs pattern as rounds 13 and 14. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
✅ Deploy Preview for liquid-qv ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Math.floor(|votes|):unfilled(neutral grey, default),filled(sign-coloured, slightly muted),current(sign-coloured, fully opaque, 1.5× thicker stroke).voteColor), not state.tickState(t, +m) === tickState(t, −m)for every t and m. Cross-zero unfills every tick at 0 and re-fills in the new sign's colour as magnitude builds on the other side.aria-hiddenruler<g>. Screen readers continue to consume the under-funnel readout as the source of truth.The point of the round is that the cadence of tick fills during a hold externalizes the cost ramp. Tick 1 fires at 0.2 s, tick 5 at 5.0 s, tick 10 at 20.0 s — the
(2n − 1)/5pattern is what users feel as "votes get expensive at the top."Files changed
src/lib/rulerState.ts— puretickState(tick, votes): 'unfilled' | 'filled' | 'current'.src/lib/rulerState.test.ts— 12 unit tests covering integer crossings on the way up and on drains, sign symmetry, saturation at the ±10 cap, and defensive handling of NaN / ±Infinity.src/components/Funnel.tsx— minor and major tick lines plus major labels now read inline style (stroke/opacity/widthorfill/opacity/weight) from per-tick state, with a 160 ms CSS transition that collapses tononeunderprefers-reduced-motion.What didn't change
Test plan
npm test) — 37 prior + 12 new tick-state tests.npm run lintclean.npm run typecheckclean.npm run build:allsucceeds across SPA, library, and Web Component targets.npm run dev:var(--lqv-fg)neutral grey at the existing 0.32 / 0.55 opacity. No current tick anywhere.current(green, opacity 1, stroke 2 px); major ticks 2, 4 filled green with green labels; major ticks 6, 8, 10 unfilled.current(red, opacity 1, stroke 2 px); major tick 2 filled red with red label.current(red, opacity 1, stroke 3 px, labelfont-weight: 600); confirms the current treatment shows on a major tick + its label.docs/round-15/README.mdfor the verified-DOM tables).See
docs/round-15/README.mdfor the full per-state walkthrough and the mid-hold cadence table.Do not merge — for review only.
🤖 Generated with Claude Code