Skip to content

active-ruler: ticks light up at integer crossings - #13

Merged
jackhenderson12 merged 2 commits into
mainfrom
active-ruler
May 9, 2026
Merged

active-ruler: ticks light up at integer crossings#13
jackhenderson12 merged 2 commits into
mainfrom
active-ruler

Conversation

@jackhenderson12

Copy link
Copy Markdown
Contributor

Summary

  • Makes the 0–10 ruler on every funnel an active vote indicator. Each tick has one of three states driven purely by Math.floor(|votes|): unfilled (neutral grey, default), filled (sign-coloured, slightly muted), current (sign-coloured, fully opaque, 1.5× thicker stroke).
  • Tick 0 is always passive — it's the baseline reference, not a milestone the user reaches.
  • Sign drives colour (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.
  • Pure layout / styling change inside the existing aria-hidden ruler <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)/5 pattern is what users feel as "votes get expensive at the top."

Files changed

  • New src/lib/rulerState.ts — pure tickState(tick, votes): 'unfilled' | 'filled' | 'current'.
  • New 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.
  • Modified src/components/Funnel.tsx — minor and major tick lines plus major labels now read inline style (stroke/opacity/width or fill/opacity/weight) from per-tick state, with a 160 ms CSS transition that collapses to none under prefers-reduced-motion.

What didn't change

  • Funnel geometry, water rendering, surface line, outline, rim.
  • Pour stream, pour control, credit pool.
  • Math layer, reducer, hold-to-pour state machine.
  • ARIA contract — the active ruler is decorative; screen readers still read the under-funnel readout (PR #14).
  • Intro copy, on-load explainer, footer, default ballot.

Test plan

  • All 49 tests pass (npm test) — 37 prior + 12 new tick-state tests.
  • npm run lint clean.
  • npm run typecheck clean.
  • npm run build:all succeeds across SPA, library, and Web Component targets.
  • Visually verified in npm run dev:
    • Empty state: every tick on every funnel renders var(--lqv-fg) neutral grey at the existing 0.32 / 0.55 opacity. No current tick anywhere.
    • +5 votes: minor ticks 1, 3 filled green; minor tick 5 current (green, opacity 1, stroke 2 px); major ticks 2, 4 filled green with green labels; major ticks 6, 8, 10 unfilled.
    • −3 votes: minor ticks 1 filled red; minor tick 3 current (red, opacity 1, stroke 2 px); major tick 2 filled red with red label.
    • −4 votes: major tick 4 current (red, opacity 1, stroke 3 px, label font-weight: 600); confirms the current treatment shows on a major tick + its label.
    • Cross-zero: drained Harris from +5 through 0 to −4. Ticks unfilled in descending order on the way down (5 → 4 → 3 → 2 → 1 → 0 all clear at 0), then re-filled in red as magnitude built negative.
    • Mixed grid: each ruler reflects only its own funnel's state; no cross-talk.
  • DOM stroke / opacity / width values match the documented states (see docs/round-15/README.md for the verified-DOM tables).

See docs/round-15/README.md for the full per-state walkthrough and the mid-hold cadence table.

Do not merge — for review only.

🤖 Generated with Claude Code

Jack Henderson and others added 2 commits May 9, 2026 14:07
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>
@netlify

netlify Bot commented May 9, 2026

Copy link
Copy Markdown

Deploy Preview for liquid-qv ready!

Name Link
🔨 Latest commit e03be9a
🔍 Latest deploy log https://app.netlify.com/projects/liquid-qv/deploys/69ff78227658600008575030
😎 Deploy Preview https://deploy-preview-13--liquid-qv.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@jackhenderson12
jackhenderson12 merged commit 4e7d4e9 into main May 9, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant