makepad-ports/skills/makepad-ui-craft/PORTS-AUDIT.md
andodeki e09f6bbb51 Add makepad-ui-craft skill pack: Emil Kowalski + Jakub Krehel doctrine translated to Makepad 2.0
- 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
2026-09-01 18:36:57 +00:00

8.7 KiB
Raw Permalink Blame History

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 §12, 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:14271531 (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:13131321, 12521258 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:15801585 + 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 §46: the seam between drag and animation is where "fluid" lives; a snap discards the finger's velocity the model already measured.
MEDIUM app.rs:262265 (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:11101214 (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:824883 (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:608618 (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:601605 (fly_shapes, fly_connect, menu_panel, exp_panel, view_panel) Popover panels pop via set_visible 0.150.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.