Files
church-translate/docs/PLAN-language-columns.md
T

78 lines
4.2 KiB
Markdown
Raw Permalink Normal View History

# Plan — side-by-side language columns with a real move animation
## The ask
"Make this portion side by side with one side being the active languages and the
other side being the disabled languages. There needs to be a nice animation of
the languages moving sides, so that they don't seem like they are teleporting
instantly to the other side."
## What exists now
One scrolling list of 105 rows, each a `<label class="langrow">` with a
checkbox, English name, native name and a voice/captions tag. Toggling calls
`toggleFeatured()` which mutates `opConfig.featured`, re-renders the whole list
with `renderOpLangList()`, and debounces a save. The row does not move — only
its checkbox state changes. Filter and "with a voice only" narrow the list;
"Turn all off" empties it.
## Design
┌─ ON (3) ───────────────┐ ┌─ OFF (102) ─────────────┐
│ Español (América Latina)│ │ Afrikaans │
│ Português (Brasil) │ │ Albanian Shqip │
│ Português (Portugal) │ │ Amharic አማርኛ │
└─────────────────────────┘ └─────────────────────────┘
- Two columns, each independently scrollable, each with a count in its header.
- One filter box and one "with a voice only" toggle drive BOTH columns.
- Clicking a row moves it to the other column. The checkbox goes away: in a
two-column layout the column IS the state, and a checkbox next to it would
say the same thing twice.
- Empty column shows a short instruction rather than blank space.
## The animation — FLIP, because it is the only technique that is not a lie
Naively re-rendering makes a row vanish from one column and appear in the other:
the "teleporting" the ask names. FLIP animates the real transition:
1. **First** — before mutating state, record `getBoundingClientRect()` for every
currently visible row, keyed by language code.
2. **Last** — mutate and re-render. Rows are now in their final positions.
3. **Invert** — for each row that both existed before and exists now, apply
`transform: translate(dx, dy)` putting it visually back where it was.
4. **Play** — on the next frame, clear the transform with a transition. The row
travels from its old position to its new one.
Constraints taken from the UX guidance:
- **transform/opacity ONLY.** Never animate width/height/top/left — they force
layout on every frame.
- **Batch reads then writes.** 105 rows × 2 measurements will thrash layout if
interleaved. All reads first, then all writes.
- **Only animate rows that are actually on screen.** A row scrolled far out of
view should not fly hundreds of pixels; it just appears in place.
- **Respect `prefers-reduced-motion`** — skip the animation entirely, keep the
move. Rated High severity.
- **Interruptible.** Clicking three languages quickly must not queue three
animations or leave a row stuck mid-flight.
- **~260ms, ease-out**, matching the app's existing `paneIn` feel.
- The row in flight gets a raised z-index so it passes over its neighbours
rather than under them.
## What must not regress
- `toggleFeatured()`'s optimistic update + 500ms debounced save, and its revert
on failure. Ticking twenty languages must stay one write.
- The filter, the "with a voice only" toggle, "Turn all off", and the count line.
- `opConfig.featured` stays the single source of truth; the columns are a view.
- Alphabetical order inside each column, and the voice/captions tag per row.
- No horizontal overflow — the card is ~470px wide inside the settings column.
## Open questions for the audit
1. FLIP across two DIFFERENT scroll containers — what breaks?
2. What happens when the destination column is scrolled such that the row's
final position is outside the visible area?
3. Rapid clicking: how do in-flight transforms interact with a re-render?
4. Is 105 rows × 2 rect reads per toggle acceptable on this machine?
5. Does removing the checkbox lose anything — keyboard operability, screen
reader state, the "I can see what's off" affordance?
6. Two independently scrolling columns inside a page that also scrolls — does
that create a scroll trap?