- skills/README.md: provenance + index - SKILL.md: merged design-engineering doctrine, Makepad-native (animator, script_mod!) - EASING.md: CSS cubic-bezier -> Ease.Bezier translation verified against dev@b41e740 - RECIPES.md: press scale 0.96, toast enter/exit, sheet settle w/ velocity handoff, stagger, reduced motion - PORTS-AUDIT.md: Before/After audit of rider/koboyo/insurance with file:line refs
8.7 KiB
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 Buttons 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)
- One shared
Pressablebase 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). - One shared toast widget with the enter/exit track — clears three MEDIUMs.
- Insurance sheet settle (RECIPES §4) — the marquee interaction; the model already measures everything the settle needs.
- Koboyo drawer/popover enters (origin-aware), camera-fit animation.
- Token pass (hex → theme roles), radius concentricity pass, icon pass.