# Ports audit — `rider`, `koboyo`, `insurance` vs. makepad-ui-craft Reviewed statically against `SKILL.md` (Emil Kowalski + Jakub Krehel doctrine, Makepad-translated), walking every state the code defines: hover, down, open/close, toast, drag, settle. No live replay was run (headless sandbox) — everything below is from the source at the cited lines; anything not inspectable is marked **Not verified**. **Headline:** all three crates contain **zero `Animator` tracks** (`grep -c Animator */src/app.rs` → 0, 0, 0). Every state change in ~3,900 lines of UI is a hard snap via `set_visible` / `refresh_chrome`, and every plain-View "button" commits its action on `finger_down`. The model layers (drag physics, thresholds, velocity) are genuinely good — the *presentation* layer is where the slop allegations land. --- ## What's already right (don't "fix" these) | Location | What | Principle | |---|---|---| | `koboyo/src/canvas_view.rs` pan/draw/zoom-at-cursor | 1:1, unsmoothed direct manipulation | Apple §2 — touch and content move together; 100+×/day → no animation. Correct. | | `insurance/src/model.rs:SheetDrag` | 1:1 drag with grab-offset, velocity thresholds (−0.2 up / +0.28 down px·ms⁻¹), peek clamp, 8px tap tolerance | Apple §1–2, gesture hysteresis. The math is the right foundation for §4 below. | | `insurance/src/app.rs` (FingerDown capture via `hits_with_capture_overload`) | pointer capture on the grab handle | Apple §2 pointer capture rule | | `rider` PageFlip with no page transition | navigation in a prototype, tens of times/day → restraint is defensible | Emil frequency table (a subtle 0.2s crossfade would also be fine; heavy slides would not) | | Rider/insurance toast timers stopped & regenerated on re-show (`toast_gen`) | no stale-timer flicker | Sonner "handle edge cases invisibly" | --- ## insurance (`insurance/src/app.rs`, `model.rs`) | Severity | Location | Before | After | Why | | --- | --- | --- | --- | --- | | HIGH | `app.rs:1427–1531` (all nav tiles, quick actions, `dl_btn`, `fab_btn`, chips) | `finger_down(actions).is_some()` → action fires on press | Feedback on `FingerDown` (press animator), commit on `FingerUp` only if `is_over`; RECIPES §1 | Apple tap contract: highlight on down, commit on up, cancel by dragging away. Commit-on-down makes every mis-touch irreversible. | | HIGH | whole crate (0 animators) | No pressable has any down/hover state | `down: instance` + 0.96 SDF scale, `snap(1.0)` in / `Forward {duration: 0.15}` `OutQuad` out | Emil: buttons must feel like the UI is listening; feedback is the foundation everything else sits on. | | MEDIUM | `app.rs:1313–1321`, `1252–1258` | Toast = `set_visible(true)` … timer … `set_visible(false)` | Enter 0.3s rise+fade `Bezier(0.23,1,0.32,1)`, exit 0.2s same path down, `OutQuad`; RECIPES §3 | Toasts are occasional → standard animation; pop-in/pop-out reads as broken; exit softer than enter, same direction (spatial consistency). | | MEDIUM | `app.rs:1580–1585` + `model.rs::finish_drag` | Release → `finish_drag` decides mode → `refresh_chrome` snaps the sheet to it | Project momentum (`v/1000·0.998/0.002`), pick nearest snap, settle from current pos carrying release velocity (RECIPES §4); interruptible by a new FingerDown | Apple §4–6: the seam between drag and animation is where "fluid" lives; a snap discards the finger's velocity the model already measured. | | MEDIUM | `app.rs:262–265` (Health tile: outer `radius: 20`, `padding top 14`, inner 38px view `radius: 13`) and sibling tiles at `273+` | Nested radii unrelated to padding (concentric would be outer = 13 + 14 = 27, or inner = 6) | Recompute per `outer = inner + padding`; audit every `Card`(20)/`IconTile`(15)/inner(13,19) nesting the same way | Jakub: mismatched nested radii are the most common "feels off". Side padding not visible at the cited line — flagged from top-padding math; **verify each tile**. | | LOW | `app.rs` throughout (e.g. `:31`, `:108`, `:122`, `:170` — `#7b5cf6`, `#8a74ff`, `#f7a982`…) | Brand/status hex literals repeated per-widget | Hoist to shared tokens in the `script_mod!` `mod` object / theme; components reference roles | Jakub better-colors: primitives never applied directly; unthemable and drift-prone. | | LOW | `app.rs:139`, `:264` etc. (`🔔`, `❤`, `⚡`-style glyph labels) | Emoji as icons | SDF-drawn or asset icons, one asset recolored per state, outline default / fill active | Emoji ignore palette + stroke weight and render differently per platform; can't take hover/disabled states. | **Verdict: Block** (2 HIGH). The model layer is approve-quality; the chrome needs the press contract and the sheet settle before this reads as crafted. ## rider (`rider/src/app.rs`) | Severity | Location | Before | After | Why | | --- | --- | --- | --- | --- | | HIGH | `app.rs:1110–1214` (nav tiles, pills, `back_found`, `wtc_pill`, suggestion rows) | Commit on `finger_down`; real `Button`s at `:1200`, `:1215` correctly use `clicked()` but have no press state | Same fix as insurance: press animator + commit on up-if-over | Same tap contract; inconsistent commit semantics between Views and Buttons in one screen also violates "same look = same behavior". | | MEDIUM | `app.rs:1004`, `1279`, `882` | Toast pop-in/out via `set_visible` | RECIPES §3 toast track | Same as insurance. | | MEDIUM | whole crate | No hover/down states on any pressable | RECIPES §1 | Emil responsiveness contract. | | LOW | chat `PortalList` (new incoming message) | Messages appear with no entry | 0.2s fade + 8px rise, `OutQuad`, only for *newly appended* rows — never on scroll-back | Prevent jarring appearance; chat arrivals are occasional. **Not verified** whether the list distinguishes new vs. re-bound rows. | | LOW | driver-found / arriving screens (state transitions via `refresh`) | Full-snap screen state swaps | Keep instant (frequency defensible), or a single 0.2s crossfade | Restraint is acceptable here; flag only for consistency with whatever insurance ships. | **Verdict: Block** (1 HIGH — commit-on-down), rest is polish. ## koboyo (`koboyo/src/app.rs`, `canvas_view.rs`, `model.rs`) | Severity | Location | Before | After | Why | | --- | --- | --- | --- | --- | | HIGH | `app.rs:824–883` (rail buttons, `t_lock`, `t_shapes`, `t_connect`, `t_all_tools`, `drawer_close`, `zoom_minus`…) | Commit on `finger_down`, no press state | Press animator + commit on up-if-over. Exception: tools that *arm a drag* (pen, lasso) may act on down — that's direct manipulation, document it | Tap contract; tool rail is high-frequency so keep the feedback minimal (opacity/color ≤0.1s), but it must exist. | | MEDIUM | `app.rs:608–618` (`set_drawer`), `:268` | 112-tool drawer appears/disappears via `set_visible` | Slide+fade, 0.25s in `Bezier(0.32,0.72,0,1)`, 0.18s out `OutQuad`, same edge both ways | Drawers are occasional → standard animation; also keyboard focus is set before the surface exists visually — focus after the enter starts. | | MEDIUM | `app.rs:601–605` (`fly_shapes`, `fly_connect`, `menu_panel`, `exp_panel`, `view_panel`) | Popover panels pop via `set_visible` | 0.15–0.2s scale 0.97→1 + fade, **origin at the triggering rail button** (SDF anchor, not center) | Emil origin-aware popovers: grow from the trigger, never from nothing, never from center. | | MEDIUM | `model.rs:407` (`reset_zoom`), `:419`/`:722` (`fit_to_bbox`/`zoom_to_fit`) | Button-triggered camera jumps snap instantly | Animate camera (pan+zoom together) over ~0.25s `InOutCubic` / `ExpDecay {d1: 0.82, d2: 0.97}`, interruptible by any new gesture | Occasional, spatial context-preserving move — the one camera motion that *should* animate (wheel/pinch zoom stays instant). | | MEDIUM | `app.rs:558`, `:589` | Toast via `set_visible` | RECIPES §3 | Same as the other crates. | | LOW | tool rail / drawer rows | Emoji-or-text glyphs as tool icons, no active-state fill convention | One drawn asset per icon, recolor per state, outline default / fill active | Jakub icons rule. **Not verified** per-row — inspect `drawer_rows`. | **Verdict: Block** (1 HIGH), with the note that koboyo's canvas *gesture* layer is the most craft-correct code in the repo — the gap is entirely in the chrome. --- ## Fix order (highest leverage first) 1. **One shared `Pressable` base widget** (RECIPES §1) adopted by all three crates — clears every HIGH at once and gives the repo a house press-feel (`0.96`, snap-in/0.15s-out, commit-on-up). 2. **One shared toast widget** with the enter/exit track — clears three MEDIUMs. 3. **Insurance sheet settle** (RECIPES §4) — the marquee interaction; the model already measures everything the settle needs. 4. Koboyo drawer/popover enters (origin-aware), camera-fit animation. 5. Token pass (hex → theme roles), radius concentricity pass, icon pass.