evidence · 46.0 KiB

shell-ux-critique.md

Committed at docs/design/shell-ux-critique.md. This is what grounds the requirements that cite it.

Open raw source

Shell UX critique: navigation model, card switcher, and shell coherence — 25 September 2026

Requested directly by the operator: “did we ever get good feedback on our design? … i don’t think we’ve really thought through the shell ux particularly well … we need to make it better.” Three examples were named explicitly and are addressed by name below: the card switcher shows one card at a time, there are no icons on the cards, and swiping up from the bottom of Settings does nothing.

0. Relationship to other reviews — read this first

This repository already carries a lot of design material, and this document does not replace any of it. It is a fourth document in a set that should be read together:

  1. docs/design/handheld-shell/ (README.md, transitions.md, motion-research.md) — the original planning artifacts for a gesture shell, written before most of it was built. Large parts of what this critique flags as “missing” are things these documents already called for.
  2. docs/design/handheld-shell/visual-gap-audit.md (24 September) — compares the installed shell against the browser prototype; mostly visual craft (spacing, empty panel space, drawer information density).
  3. docs/design/webos-polish-review.md (24 September) — a code-grounded polish pass with primary-source webOS citations; found the raw-title card header (P0-1), full-height empty panels (P0-2), Wi-Fi error placement (P0-3), uneven Settings row spacing (P0-4), and four P1s including the font-consistency and shade preview/history duplication findings. This critique does not re-derive any of those findings; it cites them by ID.
  4. openspec/changes/the-handheld-gets-a-design-and-ux-review/ — an open, in-progress, separately-owned formal review round (its own worktree, docs/research/handheld-ux/review-round-2/) with a heavier host+board process. This document is not that review and does not complete any of its tasks. A coordinator folding this in should treat it the same way webos-polish-review.md asks to be treated: raw input for that review’s references-and-gap.md and findings.md, not a replacement.
  5. openspec/changes/the-handheld-presents-a-coherent-shell/ — this is the important one. It is an open, unarchived proposal whose delta spec (specs/runtime/handheld-shell-design/spec.md) and design.md already specify a fully coherent, app-like navigation model, real card-header icons, and non-color pressed-state feedback everywhere. Its tasks.md marks the corresponding implementation tasks [ ] (open, not done). Two of this critique’s three headline findings — the Settings swipe-up dead end and the card icon — are not new UX ideas. They are the shipped code not yet doing what this already-approved-for-planning spec says it should do. Section 1 below names the exact requirement and task ID for each such finding instead of proposing new spec text for it, per this repo’s own instruction to “route existing issues to existing proposals.”

Given that, this document’s job is narrower and more useful than a green-field critique: (a) supply the missing code-level proof that turns “swipe up does nothing in Settings” from a user impression into a traced, citable defect with exact file:line evidence a task owner can act on immediately, and (b) surface the handful of findings — mainly switcher/deck legibility, shade quick controls, and vision accessibility — that no open proposal currently owns, which is what openspec/changes/the-shell-behaves-as-one-coherent-system/ (Deliverable 2, alongside this document) turns into requirements.

Evidence discipline, per AGENTS.md/.skills/k230-spec-change/SKILL.md: every code claim below is a direct git-tracked source read on origin/master (this worktree’s base revision, 80817e3d), cited by file:line, checked interactively during this review. No board or /dev/ttyACM0 access was used; no source file was edited to produce this document. Claims about on-glass feel, touch-target reach, or optical legibility are explicitly marked UNVERIFIED and are not asserted from a screenshot or code read alone.

1. The user’s three examples, traced to exact code

1.1 “Swiping up from the bottom in Settings does nothing” — confirmed, and here is exactly why

This is real, reproducible from the source, and has a precise, single-cause explanation spanning both halves of the shell.

The compositor cedes all touch to the client while any overlay route is mapped. nix/card-shell/adapter.c:2130-2131:

if (drawer_mapped())
    return false;

This check sits in input_down() before the bottom-edge entry gesture code (cs_begin_entry/cs_edge_down, lines 2201-2211) is ever reached. Drawer, Shade, and Settings are three routes painted by one and the same Wayland layer-shell surface (namespace k230-shell-drawer, matched at adapter.c:1710, mapped on Layer::Overlay — confirmed by the Rust client’s own comment at nix/rust-shell-client/src/main.rs:1051, “the settings overlay … sits on Layer::Overlay, always above every ordinary toplevel”). drawer_mapped() is true for all three routes alike, so every touch that isn’t already an active drawer/shade gesture or a blocked contact is handed straight to the client — the compositor’s own bottom-edge “shrink into the deck” recognizer never sees it, regardless of where on the screen the touch starts.

The Rust client, which now owns the touch, has no bottom-edge-swipe-up case for Settings. nix/rust-shell-client/src/service_ui.rs:321-386 (panel_intent, the Route::Settings arm) recognizes exactly four things: a downward swipe starting near the top (start.1 < 130.0 && dy > 90.0, line 322 — note this closes Settings on a downward drag, not upward) a tap on the top-right corner (line 328), taps inside a power-confirmation card (lines 331-343), and taps on the four capability rows/Wi-Fi/brightness/ keyboard/power controls (lines 345-385). There is no case for an upward drag starting near the bottom of the screen. A swipe there falls through every guard and returns None (self.route == Route::Settings branch in main.rs:2832-2950, particularly the dx.abs() > 18.0 || dy.abs() > 18.0 early return at service_ui.rs:325-326, which rejects it as a “drag,” and no subsequent branch matches it either). The gesture is swallowed with no visible response. This is not a timing bug or a hit-box-too-small bug; the code path for it does not exist.

webOS/M3 comparison. In webOS, the gesture area and its wave/center-button affordance worked identically from any screen — Launcher, Settings-menu overlay, or a full-screen app — because there was exactly one navigation mechanism, not a per-surface one (PCWorld). In Android with gesture navigation, the system back/home gesture is intercepted at the window-manager level before any app or system surface — a Quick Settings panel, a system dialog — gets a chance to consume it differently; Android’s own gesture-compatibility guide exists specifically to police apps that try to special-case edge gestures rather than let every surface honor the same one (Ensure compatibility with gesture navigation). Material 3 Expressive’s predictive-back and recents gestures are, likewise, one system-level mechanism regardless of which surface is showing (9to5Google, Material 3 Expressive redesign). This shell instead has an overlay layer that opts the entire touch surface out of the compositor’s shell gesture the moment it is mapped.

This is already specified, just not implemented. the-handheld-presents-a -coherent-shell’s runtime/handheld-shell-design spec already requires exactly the fix: “An inward edge Back gesture SHALL dismiss the topmost shell context in order… The bottom Home gesture SHALL remain the shell escape while a context is open” (Requirement “Shade and contextual Back preserve the task,” specs/runtime/handheld-shell-design/spec.md:57-71). The matching tasks — 2.2 (“Implement qualified side-edge contextual Back for shell sheet/Settings/shade/drawer/keyboard”) and 4.4 (“Define touch ownership for bottom Home/drawer, top shade, side Back, keyboard, deck and app content”) — are both still [ ] open in tasks.md. Priority: P0. Owner: that change’s tasks 2.2/4.4, not this document. This section exists so whoever picks up those tasks has the exact bypass (adapter.c:2130-2131) and the exact missing case (service_ui.rs’s Route::Settings arm) instead of having to rediscover them.

1.2 “There are no icons on the cards” — confirmed; the “icon” is a letter badge derived from the same broken title

The card switcher does paint something icon-shaped next to each card’s label, but it is not the app’s icon. nix/card-shell/adapter.c:1187:

char letter = show_icon ? card_badge_letter(display_title) : 0;

display_title is the same raw, unresolved client window title already documented as wrong in webos-polish-review.md P0-1 (card_display_title, adapter.c:364-376, a 3-entry allowlist that falls through to the client’s live title string for everything else — e.g. a terminal card’s badge letter changes as the shell prompt changes, because the “icon” is a function of whatever text happens to be in the title bar that frame). card_icon_badge() (nix/card-shell/render.c:143-175) draws a rounded-square Cairo path filled with a flat color and one Pango-rendered capital letter — there is no icon asset, SVG, or .desktop Icon= lookup anywhere in nix/card-shell/*.c (confirmed: no icon hits besides this badge function and its declaration). Compare this with the drawer, one file over in the Rust client, which does correctly resolve real vector icons through the installed icon theme (nix/rust-shell-client/src/icon.rs, confirmed rendering sharp/unpixelated in docs/evidence/coherent-shell/rust-icons-host.png, per webos-polish-review.md P2-2) — the capability to show a real icon already exists in this codebase, just not on the compositor side that draws cards.

webOS/M3 comparison. webOS cards always paired the app’s manifest icon with its name in the card header — never the window’s own live content — because a card’s identity has to survive whatever transient state the content is in (source needed for §3 of webos-polish-review.md, already cited there). Android’s recents carousel shows each task’s launcher icon in a small badge on every card, independent of a screenshot’s content (9to5Google, Material 3 Expressive redesign describes the updated recents carousel; the badge-plus-thumbnail pattern predates M3 Expressive and is unchanged by it).

Already specified, not implemented. runtime/handheld-shell-design’s “Installed application icons retain identity and privacy” requirement (spec.md:109-120) already requires the drawer, “eligible card headers,” and trusted notifications to resolve the real desktop-entry icon with a bounded cache and a neutral fallback — cards are explicitly in scope, already. Task 1.4 (“Resolve desktop-entry Icon through installed icon themes with fixed drawer/card/notification sizes…”) is still [ ] open. Priority: P0. Owner: task 1.4, not this document. The concrete gap this section adds: task 1.4’s fix needs a resolver reachable from the C compositor (which owns card_icon_badge), not just the Rust drawer; the two are separate processes today and neither shares an icon cache with the other, so task 1.4 is not “extend icon.rs,” it is “build (or share) an icon resolver on both sides of the Wayland boundary.” That architectural note is not currently written down anywhere in the-handheld-presents-a-coherent-shell’s design.md decisions and is worth adding there when task 1.4 is picked up.

1.3 “The card switcher shows one at a time” — mostly confirmed; the actual number is “one full card plus an 8.7%-wide sliver of each neighbor,” which is not the same claim and matters for the fix

This is the one of the three where precision changes the fix. The switcher (CS_DECK mode) is not literally a single full-bleed card with no peripheral vision — the layout math already produces a thin peek of the left/right neighbor — but that peek is far too narrow to serve its purpose.

nix/card-shell-policy/card-shell-policy.c:29-38 (cs_default_config, called with the real panel dimensions 568×1232):

.card_width=.84*(width-48),          // = .84 * 520 = 436.8 px
.gap=16,

and the deck layout (card-shell-policy.c:192-210, abbreviated):

double pitch=p->config.card_width+p->config.gap;   // = 452.8 px
...
.x=(p->config.width-p->config.card_width)/2+offset*pitch+translation,

For the selected card (offset=0): x = 65.6, spanning to 502.4 — already inset from both edges by 65.6px. For the right neighbor (offset=1): x = 518.4, spanning to 955.2 — only 568 - 518.4 = 49.6 of its 436.8px width is on-screen: 8.7% of one neighboring card, mirrored on the left. That sliver cannot show an app name, an icon, or recognizable live content — it is a color swatch at best, not “seeing what’s open.” A person genuinely cannot tell what else is running without paging through one card at a time, which is the user’s complaint restated precisely: not “zero peek,” but “peek too thin to be useful.”

webOS/M3 comparison. webOS’s own card view showed multiple live, recognizable cards simultaneously with visible gutters between them, not a one-card-plus-sliver layout — that multi-card legibility is the entire premise of “see everything open at a glance” that both reviewers (Gruber, Daring Fireball) and this project’s own docs/design/handheld-shell/README.md single out as the point of the metaphor (Daring Fireball, “webOS gestures … proved ahead of its time”). Android’s Material 3 Expressive recents carousel is explicitly a carousel of partially-visible cards you can see several of at once, each carrying an icon badge, with continuous horizontal scroll rather than a mostly-full-screen single card (9to5Google, Material 3 Expressive redesign; Android Authority, Material 3 Expressive deep dive).

Not already specified — this is genuinely new scope. The open the-handheld-presents-a-coherent-shell requirement covering related ground, “Bottom app switching follows both touch axes” (spec.md:31-55), governs the lateral quick-switch drag gesture (“a full or near-full app carousel… does not first complete entry into the overview”) — that is a deliberate decision (design.md decision 11) about how switching feels while dragging, not a claim about how legible the static, released deck overview is. No open change specifies a minimum neighbor-peek fraction for the idle deck. This is exactly the kind of P0 this critique’s companion proposal, openspec/changes/the-shell-behaves-as-one-coherent-system/, turns into a requirement (see that proposal’s “deck legibility” requirement). Recommended concrete direction, not yet a commitment: shrink card_width (or grow the peek independently of card size) enough that a neighbor’s icon badge is legible — a rough target of showing 20-25% of each neighbor is enough to read a square icon badge at this panel’s DPI, but the exact fraction needs a host-rendered comparison, not a guess; see the proposal’s tasks.

2. Navigation model consistency (beyond Settings)

The Settings dead end in §1.1 is not an isolated bug; it is one symptom of a deeper structural fact: this shell has three independent navigation state machines that do not share a gesture vocabulary.

  1. The C compositor’s card policy (card-shell-policy.c): CS_NORMAL / CS_DECK / CS_ENTERING / CS_EXPANDING / CS_CLOSING / CS_DRAGGING, with its own bottom-edge-only gesture recognizer (cs_edge_*, cs_entry_*).
  2. The Rust overlay client’s route dispatch (Route::Drawer / Shade / Settings / Hide, nix/rust-shell-client/src/lib.rs:27-32), which receives raw wl_touch events once drawer_mapped() cedes them, and implements its own, route-specific gesture logic per surface.
  3. The Rust client’s separate Home surface (Layer::Bottom, always mapped, main.rs:910-912), with its own home.down/motion/up input path (main.rs:2634-2751), entirely disjoint from both of the above.

Each of these three recognizes a different gesture for “leave this surface”:

SurfaceOwning state machineDismiss gesture found in codeDirection
App card (deck)C compositorBottom-edge upward drag (cs_edge_down/cs_begin_entry, adapter.c:2201-2211)up
DrawerRust DrawerNavigationDownward drag from the top of the (unscrolled) list, dy > 110.0 (navigation.rs:165-168)down
ShadeRust panel_intentUpward drag near the top, dy < -90.0, start.1 < 180.0 (service_ui.rs:279-282)up
SettingsRust panel_intentDownward drag near the top, dy > 90.0, start.1 < 130.0 (service_ui.rs:321-324)down
Settings (alternate)Rust panel_intentTap the top-right corner (service_ui.rs:328-330)tap

Four surfaces, three different directions, one of them (Settings) reachable two different ways and one of the four (bottom-edge-up, the one an app itself uses) reachable from none of the three overlay routes. This is also not what the open coherent-shell spec asks for: its “Back is contextual shell navigation” decision (design.md:21) specifies a single side-edge inward swipe as the one dismiss gesture for “long-press sheet, Settings, shade, drawer, then keyboard” — none of the three implemented gestures above is a side-edge swipe; they are three different top-anchored vertical swipes, each hand-tuned per surface. This is further, sharper evidence for the same open tasks (2.2/4.4) cited in §1.1 — not a new requirement, but worth recording here because it shows the inconsistency is systemic (three separately-invented gestures) rather than a single oversight in Settings.

Priority: P0 (compounds directly with §1.1). Fix: implement the already-specified single side-edge Back (task 2.2) and route every overlay’s dismiss through it, retiring the three bespoke top-anchored swipe predicates. Code: navigation.rs:165-168, service_ui.rs:279-282, service_ui.rs:321-324, adapter.c:2130-2216.

Dead ends

From three levels deep (Shade → Settings → Wi-Fi → password Entry), there is no verified route back to Home except tapping back through each level in turn — wifi_view.page unwinds one page per tap (main.rs:2832-2844), and Settings itself only closes via the two gestures in the table above. There is no shortcut equivalent to webOS’s persistent gesture-area center button, which offered card view from any depth (PCWorld). Priority: P1. This is the same root cause as §1.1/§2’s missing bottom-Home escape (once that lands, a bottom swipe from Wi-Fi Entry would resolve it), so no separate fix is proposed — it is listed to make the blast radius of task 2.2/4.4 explicit: fixing “Settings” alone is not enough if Wi-Fi’s own sub-pages don’t inherit the same escape.

3. The card switcher (additional findings beyond §1.2/§1.3)

Ordering. The deck’s card order comes from whatever order the Sway-side caller populates p->cards[] in (card-shell-policy.c:125-143 manages selected index adjustments on insert/remove, but the array’s insertion order itself is supplied by the caller in adapter.c, which this review did not fully trace to a recency-of-use or z-order guarantee). This is UNVERIFIED — flagging it as an open question rather than a finding: webOS kept cards in a stable, predictable left-right order (not MRU-reordering, which is itself a notable, deliberate webOS choice — cards don’t jump around when you switch to one), and this project’s own design.md decision 11 says “Both routes SHALL use the deck’s stable left/right app order, unchanged by focus-only switching,” so stability is at least intended. Recommend a task owner confirm this with a host trace rather than treat it as settled from this reading.

Close affordance. An upward throw past throw_distance/throw_speed (cs_default_config, card-shell-policy.c:38) requests a close; this matches webOS’s flick-to-close and is not a P0/P1 finding on its own. The gap is discoverability of the gesture (see §8) and legibility once the deck peek is fixed (§1.3) — a person needs to be able to select a card before they can flick-close it, and today that means paging through full-card swipes first.

Empty state. Confirmed present and reasonable: CS_MESSAGE_EMPTY renders “No running apps” (adapter.c:1239, positioned via status_y at adapter.c:1283) with the deck’s upward drawer cue still available. No finding here.

Getting to Home/drawer from the switcher. Per design.md decision 1, Home (the pinned-icon screen, since superseded by the-shell-presents-a-pinned-home-screen) is reached by leaving the deck entirely (a separate Layer::Bottom surface, not part of CS_DECK), and the drawer is a continued upward pull from the deck (transitions.md). This matches the user’s mental model reasonably well and is not re-flagged here, though the drawer-from-deck gesture inherits the same “swipe up” vocabulary that Settings/Shade don’t honor (§2).

Home is a pinned-icon grid on its own always-mapped Layer::Bottom surface (home_grid.rs, home_pager.rs, home_state.rs, wired in main.rs), shipped per the-shell-presents-a-pinned-home-screen. This is a substantial, already-implemented improvement over the “no home screen at all” state that proposal itself describes, and this critique has no P0/P1 finding against it from a source read; its own UNVERIFIED markers (real-finger board acceptance) stand as written in that change.

Drawer layout. Grid, not list: COLUMNS = 3, ROW_HEIGHT = 160.0, TILE_HEIGHT = 148.0 (nix/rust-shell-client/src/navigation.rs:4-6), with a tested minimum tile size of 56×56 logical px (navigation.rs:243, three_columns_hit_only_painted_tiles). Scrolling is continuous (drag/flick/coast via DrawerNavigation::motion/tick, navigation.rs:122-205), not paginated. This is a reasonable, already-decent implementation; visual-gap-audit.md’s P1 finding about the installed drawer being seven full-width rows describes an older generation, not this 3-column grid (see webos-polish-review.md §0’s chrome-generation correction for the same kind of generation confusion — worth the same caution here: confirm which drawer generation any future screenshot shows before citing it).

Search: there is none, and that is a recorded decision, not an oversight. A targeted search of home_grid.rs, home_pager.rs, home_screen.rs, and catalog.rs for “search,” “filter,” or any text-input concept returns nothing. This matches the-handheld-presents-a-coherent-shell/design.md decision 2 verbatim: “Search remains deferred.” This is not a gap this critique treats as new scope — it is a considered, explicit deferral, and OpenSpec culture in this repo (see AGENTS.md’s “record what was rejected and why… expensive to find and cheap to re-enter”) means it should not be silently reversed by a drive-by proposal. Recommendation, not a requirement: given the user’s broader dissatisfaction and that this device already has a functioning on-screen keyboard (making a type-to-filter box cheap to add once the drawer’s layout is otherwise settled), the coordinator may want to revisit that specific deferral explicitly — webOS’s TouchPad generalized exactly this into “Just Type” (PCWorld TouchPad piece) and Android’s app drawer has offered type-to-filter search since its earliest versions. This document flags it; it does not decide it.

Recent apps. No separate “recents” list distinct from the live card deck was found; the deck itself is the recency-ish surface. Not flagged as a defect — this mirrors webOS’s single-multitasking-metaphor philosophy deliberately (design.md’s own framing), and Android’s separate recents screen is not a fit for a device this size without also solving §1.3’s legibility problem first.

5. Notifications and shade

No quick-toggle tiles in the shade — genuinely new scope. The shade (Route::Shade’s panel_intent, service_ui.rs:279-320) recognizes exactly: scroll, a per-notification action tap, OpenSettings, NotificationDismissAll, and the swipe-to-close from §2. Brightness (ServiceRequest::Brightness) and keyboard toggle (ServiceRequest::KeyboardToggle) exist only inside Route::Settings (service_ui.rs:353-370) — one full navigation hop away from the shade, with no shortcut. Neither Android’s Quick Settings grid nor webOS’s tap-the-status-bar semitransparent Wi-Fi/Bluetooth/Airplane menu requires leaving the notification-adjacent surface for the handful of highest-frequency toggles (GSMArena, Palm Pre review; Android Quick Settings is a standard part of the gesture-navigation shade, Material 3 Expressive redesign coverage). Priority: P1. Not owned by any open change (confirmed: no hit for “quick toggle”/“Quick Settings” anywhere in openspec/changes/*/design.md or proposal.md). This is one of the three requirements in the companion proposal.

Dismiss-all exists, per-item dismiss exists (side-swipe, notification_swipe_release, service_ui.rs), grouping does not. The notification list is a flat chronological sequence (ServiceView.notifications.events, service_data.rs) with no per-source grouping. Android groups notifications by app in a collapsible stack; this is a real gap but P2, not P0/P1 — this device’s expected notification volume (a personal handheld with a bounded set of installed apps, no third-party app store) makes grouping lower-leverage than the quick-toggle gap above.

The singular/plural bug is still present, unresolved. Confirmed by direct read, not re-derived: render.rs:1844, format!("{count} notifications") — literally “1 notifications” for a single event. Already flagged as part of webos-polish-review.md P1-4; cited here only to confirm it survives in the current tree and to note it is a one-line fix independent of that finding’s larger preview/history-redundancy point.

6. Motion

Cross-reference webos-polish-review.md’s P1-1 (font inconsistency) and visual-gap-audit.md’s P2 motion note (real-finger report of “abrupt app zoom-out and mechanical release”) rather than re-deriving them. One additional, source-grounded finding neither document makes:

Card-entry/expand motion is a fixed-duration linear ramp, not spring physics, contradicting both the webOS precedent and this project’s own stated non-goal against it. card-shell-policy.c:499-508 (cs_tick, CS_EXPANDING mode):

double duration=p->config.reduced_motion ? 60 : 160;
...
p->expand_progress=fmin(1,elapsed/duration);

This is elapsed / duration clamped to 1 — a straight linear interpolation over a fixed 160ms (or 60ms reduced-motion) window, with no easing curve, spring, or velocity carry-in from the gesture that triggered it. This specific transition (card expand/collapse) is distinct from the entry-drag tracking (cs_entry_motion) and release-settle (cs_entry_up_at/entry_release_velocity) paths, which do carry release velocity into a spring-like decel (card-shell-policy.c:589-609, 699-739) — so the codebase already has two different motion models side by side: physically-continuous drag/release for the two-axis quick-switch, and a flat linear timer for plain expand/collapse. Material 3 Expressive’s defining motion change is explicitly a physics-based “spatial springs” system replacing fixed-duration easing curves precisely because linear/eased timers “can feel mechanical” (9to5Google, Material 3 Expressive redesign). webOS’s own documented card behavior — “a card follows a held drag and can be thrown upward to close” — is continuous-motion-out even without a named physics model. Priority: P1. Fix: carry the entry/release velocity model already implemented for cs_entry_* into CS_EXPANDING’s tick function instead of the separate fixed-duration ramp, so tap-to-expand feels like the same physical system as the drag gestures rather than a distinct, timer-driven animation. Code: card-shell-policy.c:493-522 (linear), compare card-shell-policy.c:670-740 (velocity-aware) for the pattern to reuse.

7. Visual hierarchy, typography, colour, shape, iconography, touch targets

Fully covered by webos-polish-review.md (P0-2 dead panel space, P0-4 uneven Settings spacing, P1-1 font inconsistency, P1-3 preview fallback treatment, P2-1 radius drift, P2-2 icon rendering confirmed fine) and visual-gap-audit.md (drawer density, shade hierarchy). Not re-derived here. One addition:

Touch targets are asserted at 56 logical px by this project’s own design doc, but that number has never been checked against Material’s 48dp guideline at this panel’s actual density. runtime/handheld-shell-design’s “Shared portrait shell language” requirement already sets “at least 56 such [logical] pixels tall” as the shell’s own bar (spec.md:9) — comfortably above Material’s minimum recommendation of 48×48dp, “a physical size of about 9mm regardless of screen size” (Material Design 3, Accessibility designing). But “56 logical pixels” and “48dp” are only the same physical size if the panel’s actual pixels-per-inch is known and the shell’s logical-to-physical scale is 1:1, which runtime/shell’s CPU-rendering requirement confirms it is (“the current output scale of 1, one logical pixel equals one panel pixel,” restated at handheld-shell-design/spec.md:9) — but no committed document in this repo states the RM69A10’s actual PPI or does the 9mm-physical-size arithmetic to confirm 56px clears the bar rather than merely asserting a round number. Priority: P2 (documentation gap, not a code defect) — cited here so a task owner closing 1.5/5.x physical acceptance tasks computes and records the actual mm figure once, rather than trusting the 56px constant by inspection.

8. Feedback: pressed states, loading, errors, no haptics

Pressed-state feedback exists — inconsistently. The drawer (DrawerNavigation::pressed, navigation.rs:211-221, wired to renderer.set_drawer_pressed in main.rs:2643-2651), Home grid (home.pressed(...), main.rs:2429), and both theme/background carousels (sync_theme_pressed/sync_background_pressed, throughout main.rs) all give an immediate visual highlight on touch-down, held only while the finger stays on the same tile — a reasonable state-layer equivalent to Material’s ripple/state-layer convention (Material 3 ripple support notes). Settings’ own capability rows and Power cards have no equivalent. A targeted search of the Route::Settings rendering path (render.rs:1519-1660, settings_row_y/settings_layout/settings_confirm_layout) finds no pressed state anywhere in that block, and panel_intent’s Route::Settings arm (service_ui.rs:321-386) only ever fires on release — a tap on Wi-Fi, Brightness, Keyboard, Reboot, or Power off gives the user zero visual acknowledgement until the resulting screen change actually lands. Combined with §11’s unmeasured redraw cost on this “one slow core” board, a tap with no immediate feedback and an uncertain-latency screen change is the worst combination available — precisely the situation instant press feedback exists to prevent. runtime/handheld-shell-design’s “Surfaces explain their state” requirement already requires “Focus and pressed states SHALL have a non-color cue” (spec.md:121-123) as a cross-surface requirement — this is a further, concrete data point that the current Settings implementation does not meet it, filed against that existing requirement rather than as new scope. Priority: P1. Code: render.rs:1519-1660 lacks what navigation.rs:211-221 already demonstrates as the pattern to copy.

No haptics — by hardware, confirmed, not revisited here. docs/design/handheld-shell/motion-research.md already documents a schematic-text search of the vendor board finding no motor/vibration/haptic identifier, and states plainly “no vibration work is planned for the current board.” Given that, and given §8’s finding above, visual feedback is not optional polish on this device — it is the only feedback channel a person gets, which raises the priority of fixing Settings’ missing pressed state specifically (already reflected as P1 above) beyond what it would be on a device with a fallback haptic buzz.

9. Discoverability

The gesture hints that do exist are permanent on-screen text on every visit (webos-polish-review.md P1-2 documents three inconsistent styles for this across the drawer footer, shade subheading, and deck footer) — this contradicts the user’s own prior, already-recorded rejection of permanent chrome (webos-polish-review.md §0’s history of the rejected persistent Apps/Windows/Back/Home bar) in spirit, even though these are hint strings rather than buttons. This project’s own motion-research.md already proposed the fix — “consider showing each hint only until its gesture has been performed once (a small persisted flag)” — but a targeted search finds no persisted first-use flag anywhere in nix/rust-shell-client/src/ or nix/card-shell/. This is the same finding as P1-2, re-filed as “the project’s own proposed fix for P1-2 is still unimplemented,” not a new finding — no new priority assigned beyond P1-2’s existing P1.

10. Keyboard interplay, text entry, accessibility

Keyboard/gesture conflict arbitration exists and looks reasonably considered. adapter.c:2106-2111: keyboard_gestures_enabled() intercepts touch before drawer/shade dispatch, and kg_down is passed launcher_mapped() || drawer_mapped() || popup_at(x, y) as an explicit conflict signal. This is a real, code-level attempt at exactly the arbitration the-keyboard-follows-touch-gestures change specifies. Whether it feels right (the escape-while-typing case) remains an explicit UNVERIFIED real-finger gate in that change’s own tasks — not re-litigated here.

Accessibility beyond motion reduction: effectively absent, and this is different from the already-planned “accessibility aid.” A search across nix/rust-shell-client/src/*.rs for “accessib,” “a11y,” “contrast,” “large_text,” or “screen reader” returns only reduced_motion — a single boolean env var (K230_SETTINGS_REDUCED_MOTION, main.rs:3344-3345) that shortens animation durations. the-handheld-presents-a-coherent-shell already plans an “accessibility aid” (design.md decision 2, task 1.5) — but that aid is specifically “large labeled route controls” for gesture discoverability (a big-button alternative to swipes), not a vision accommodation like adjustable text size or a high-contrast palette variant. Those are a different axis of accessibility entirely and nothing in any open change currently covers them. Priority: P1. Genuinely new scope — the second of the three requirements in the companion proposal.

11. Performance-perceived UX on this slow CPU

What is actually measured and committed: docs/evidence/shell-performance.txt records Sway/Pixman output-commit latency for a terminal-scrolling-plus- visible-keyboard workload only — median 14.0ms, p95 15.6ms, p99 22.7ms over 254 commits, explicitly caveated “these durations do not imply an FPS, physical refresh, or touch-latency result.” docs/evidence/card-composition-board/ card-board-first-pass-telemetry.txt records CPU-time and RSS samples for an opt-in card-composition trial, not frame latency.

Nothing is committed for card-switcher drag latency, drawer flick-scroll cost, or Settings panel transition cost — the exact interactions this critique is about. This matters more here than it would elsewhere: Material 3 Expressive’s spring-based motion model assumes near-60fps responsiveness to feel “alive” rather than laggy (9to5Google, Material 3 Expressive redesign), and this device has one slow CPU core doing CPU rasterization with no GPU path (runtime/shell’s own “renders on the CPU” requirement, openspec/specs/runtime/shell/spec.md:83-121). Recommending motion polish (§6’s spring-physics fix, or the deck-density change in §1.3, which repaints more of the screen every frame) without first knowing whether this board can sustain it at interactive rates risks prescribing a fix that makes the felt experience worse, not better. the-handheld-presents-a-coherent-shell’s task 4.5 (“touch→scene damage/commit→frame-done→output-presented trace IDs… declared board p95/p99 budgets”) already exists to close exactly this gap for the coordinated compositor scene — still open. Priority: P1 process finding, not a new requirement — recommend task 4.5 be run (or a narrow subset of it) before committing to any motion-heavy fix from this document, including the ones in §1.3 and §6.

12. Top findings, ranked

#FindingScreenPriorityOwner
1Bottom-edge swipe-up does nothing on Settings/Drawer/Shade (compositor cedes touch via drawer_mapped(), and the Rust client has no matching case)Settings (user’s example)P0the-handheld-presents-a-coherent-shell tasks 2.2/4.4 (open)
2Card switcher “icon” is a single-letter badge derived from the same broken raw window title, not a real app iconCard switcher (user’s example)P0the-handheld-presents-a-coherent-shell task 1.4 (open); needs a compositor-side resolver, currently undesigned
3Deck neighbor peek is ~8.7% of a card width — present, but too thin to identify what else is openCard switcher (user’s example)P0New — the-shell-behaves-as-one-coherent-system
4Three different, direction-inconsistent overlay-dismiss gestures (Drawer down, Shade up, Settings down) instead of one shell BackDrawer/Shade/SettingsP0the-handheld-presents-a-coherent-shell tasks 2.2/4.4 (open), same root cause as #1
5Card header shows the raw, live-changing window title instead of the app’s nameCard switcherP0 (already filed as webos-polish-review.md P0-1)that document
6Secondary panels fill the full screen regardless of content, large dead voidsSettings/Themes/Wi-Fi/ShadeP0 (already filed as webos-polish-review.md P0-2)that document
7Settings capability rows have no pressed/touch feedback at all, unlike every other tappable surface in the shellSettingsP1New evidence against handheld-shell-design’s existing “Surfaces explain their state” requirement
8No quick-toggle tiles in the shade; brightness/keyboard toggle require a full hop into SettingsNotification shadeP1New — the-shell-behaves-as-one-coherent-system
9No vision-accessibility option (text size / high contrast) beyond a binary reduced-motion flagSettings/globalP1New — the-shell-behaves-as-one-coherent-system
10Card expand/collapse motion is a fixed-duration linear ramp; drag/release elsewhere already has velocity-aware spring-like settleCard expand/collapseP1New — recommend as a task, not a new requirement
11No committed frame-latency measurement for card-switcher/drawer/Settings transitions, only for terminal scrollingAll gesture surfacesP1the-handheld-presents-a-coherent-shell task 4.5 (open)
12Gesture hint text is permanent chrome on every visit; the project’s own proposed “show once” fix is unimplementedDrawer/Shade/Deck footersP1 (already filed as webos-polish-review.md P1-2)that document
13Two renderers (C deck, Rust overlay) with two independent, unpinned font declarationsDeck vs. Drawer/Shade/SettingsP1 (already filed as webos-polish-review.md P1-1)that document
14Dead ends three levels deep (Wi-Fi password entry) with no shortcut back to HomeSettings → Wi-Fi → EntryP1Same root cause as #1/#4
15Notification count string is ungrammatical for the singular case (“1 notifications”)ShadeP2 (already filed as webos-polish-review.md P1-4)that document

13. Reference material used

Primary sources for webOS and Material 3 Expressive, beyond what webos-polish-review.md and this project’s own docs/design/handheld-shell/ already cite (not repeated where identical):

No screenshot or image from any of these sources is reproduced in this repository; only qualitative behavior and directly quoted/cited text are used, consistent with webos-polish-review.md’s own sourcing discipline.

14. Mockups

Three original, schematic SVGs illustrate the switcher and Settings findings side by side with a proposed direction. They are illustrative only — not board evidence, not exact wireframes of the spec text in openspec/changes/the-shell-behaves-as-one-coherent-system/, and each says so in its own caption:

15. What still needs the board

Every finding above is a direct source or committed-evidence read; none requires the board to confirm the code says what this document says it says. What remains genuinely unverified and needs real glass, listed so a future capture doesn’t have to re-derive the mapping:

FindingDecidable from source aloneNeeds board/glass
§1.1 Settings swipe-up dead endyes — the missing code path is provable by readingconfirming the fix (once implemented) is discoverable without a hint the first time
§1.2 Card icon is a letter badgeyeslegibility of a real icon at this size once implemented
§1.3 Deck peek too thinyes — the arithmetic is exactchoosing the actual target peek fraction needs a host-rendered comparison at minimum, ideally an on-glass legibility check
§2 Three inconsistent dismiss gesturesyesnone beyond normal visual/gesture review once unified
§5 No shade quick togglesyes (absence is a grep)none — this is a net-new feature, no regression to check
§6 Linear vs. spring expand motionyes — direct code contrastwhether the reused velocity-aware model actually feels better requires a real-finger pass, per the-handheld-presents-a-coherent-shell task 5.2
§8 Settings rows have no pressed stateyesnone beyond normal visual review once added
§10 Accessibility gapyes (absence is a grep)choosing target text scale/contrast values benefits from a low-vision reviewer, not assumed from this document
§11 No switcher/drawer/Settings frame-timing evidenceyes — the evidence directory contents are directly inspectablethe measurement itself is on-glass by definition; this is exactly what task 4.5 exists to produce

None of the above claims a physical/optical/real-finger pass. Per AGENTS.md, any implementation that follows from this document needs its own named evidence class recorded against the task that implements it.