docs(07): import design UX team review (designer-delivery-4) — conditional sign-off
Design UX team responded to v4 UX bundle 2026-05-16: ✅ CONDITIONAL sign-off for v0.7.0 ship + Phase 7 close. Phase 8 (Windows) can start as soon as the 3 v0.7.0 blockers land — other v0.7.0 items don't gate Phase 8 kickoff. === TL;DR per Ask === Ask 1 (tab/title split): ✅ bless + KILL "Home" title (1 piece of copy that doesn't sound like Prowler; dial is its own headline) Ask 2 (first-launch): ✅ bless + cut splash hold 1.6s→800ms; 1 open question on no-config path (could become blocker) Ask 3 (voice/copy): ✅ bless 90% + 5 line rewrites (drop "idle" meta, em-dash placeholders, "Endpoints"→"servers") Ask 4a (a11y): ⏸ defer full pass + 3 must-fix (dial content-desc, icon labels, Reset touch targets) Ask 4b (Servers density): ⚠ reshape in v0.7.x (collapsed default + 1 Reset/group + optional-field marking + paste affordance) === 3 firm v0.7.0 ship blockers === B-1 · Horizon-sliver halo on Home eats 30% of viewport (visual scale bug) B-2 · Connected meta row reads literally "exit · ms" with no values (suppress empty fields) B-3 · Bogus-IP / endpoint failure silently returns dial to idle. Surface dialError + errConnectFailed ErrorBanner. (Same as my bug-shaped #1 — UX team confirms it as a real UX gap, not intentional.) Wiring already exists in strings.dart + ErrorBanner; missing the connect-loop-drain → state transition. === Bug-shaped observations dispositions === #1 silent-fail → BLOCKS v0.7.0 (= B-3 above) #2 8s drop-detect → v0.7.x (annoying not deceptive; investigate xray-core keepalive interval + add UI tunnel-health ping) === UNTRIGGERED states dispositions === 02c connecting → accept absence 02d dial-error → does NOT exist; it's bug #1 in disguise — fix bug = state captured 08-err-network → accept code-review 08-err-connect-failed→ same as 02d / bug #1 08-err-server-unreach→ accept code-review 08-err-switch → accept code-review No real-device follow-up bundle requested. === v0.7.0 work list === - B-1, B-2, B-3 (3 firm blockers) - §1 drop "Home" title - §2 splash 1.6s→800ms - §3 5 copy rewrites - §4a A-1/A-2/A-3 a11y must-fixes - §2 ¶4 verify no-config first-launch path (might add 1 blocker) === v0.7.x followup batch === - Full a11y pass (Accessibility Scanner + TalkBack) - Servers screen reshape (4 changes) - Bug #2 tunnel-drop detection - Russian translation brief Phase 7 close path: this is the "rewrites" branch from .continue-here.md decision matrix. Plan needed to batch the v0.7.0 work before Phase 7 marks complete. /gsd-plan-phase 7 --gaps OR multi-task /gsd-quick chain. Source review document: designer-delivery-4-ux-team-review/UX-REVIEW.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
# UX team sign-off · Prowler v0.6.6 (phase-07 bundle v4)
|
||||
|
||||
**From:** Design UX team
|
||||
**To:** Phase-7 owner / Flutter team
|
||||
**Date:** 2026-05-16
|
||||
**Build under review:** `prowler-debug-v0.6.6-multiabi.apk` (sha256 `af3667bc…`)
|
||||
**Bundle:** `prowler-phase07-ux-team-v4` — 20 stills + 9 cold-launch frames + `UNTRIGGERED.md`
|
||||
**Verdict:** ✅ **Conditional sign-off.** Ships at v0.7.0 after the 3 blockers below land. Voice + first-launch flow blessed. Servers density flagged as a v0.7.x reshape, not a v0.7.0 blocker.
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
| | Verdict |
|
||||
|---|---|
|
||||
| **Ask 1** · Connect tab / Home title split | ✅ Bless. Rename `Home` → drop it entirely. See §1. |
|
||||
| **Ask 2** · First-launch zero-onboarding | ✅ Bless with one tweak. The dial IS the onboarding. See §2. |
|
||||
| **Ask 3** · Voice / copy bets | ✅ Bless 90%. Five specific rewrites in §3. |
|
||||
| **Ask 4a** · A11y | ⏸ Defer full pass to v0.7.x, with 3 must-fix items at v0.7.0. See §4a. |
|
||||
| **Ask 4b** · Servers density | ⚠️ Reshape in v0.7.x. Not a v0.7.0 blocker — it's a power-user screen. See §4b. |
|
||||
| **Bug-shaped #1** · Bogus-IP silent-fail | 🚫 **Blocks v0.7.0.** Real UX gap. See §5. |
|
||||
| **Bug-shaped #2** · 8s tunnel-drop detection | ⏸ v0.7.x — annoying not blocking. See §5. |
|
||||
| **Untriggered states** | Accept code-review evidence for 5/6. The 6th (02d dial-error) is bug #1 in disguise — fix the bug, capture the state. |
|
||||
|
||||
**Three blockers for v0.7.0 ship:**
|
||||
- B-1 · Horizon-sliver halo on Home eats 30% of viewport. Scale it down per design-system spec. (Visual bug, not UX gap, but ships in this build.)
|
||||
- B-2 · Connected-state Home meta row reads literally `exit · ms` with no values. Suppress empty fields.
|
||||
- B-3 · Bogus-IP / endpoint failure silently returns dial to idle. Surface `dialError` ("Tap to retry") + an ErrorBanner.
|
||||
|
||||
Everything else is fine ship.
|
||||
|
||||
---
|
||||
|
||||
## §1 · Ask 1 — Tab label vs screen title
|
||||
|
||||
You asked: bless or kill the `Connect` / `Home` split.
|
||||
|
||||
**Kill the `Home` title. Keep `Connect` as the tab label.**
|
||||
|
||||
Two reasons:
|
||||
1. The dial is its own headline. A 288dp glowing cat-in-a-ring doesn't need the word "Home" hovering over it — that's belt-and-suspenders for a one-screen surface.
|
||||
2. "Home" is the single piece of copy in this whole app that doesn't sound like Prowler. It reads like a generic React Native starter. The rest of the voice is lowercase-confident; "Home" is title-case civilian.
|
||||
|
||||
**Specific change:** delete the `Home` line. Keep the `PROWLER` eyebrow. The screen becomes:
|
||||
```
|
||||
PROWLER (i)
|
||||
|
||||
[ horizon-sliver ]
|
||||
[ DIAL ]
|
||||
idle ← or Tap to prowl, see §3
|
||||
[ LAYER ] [ EXIT ]
|
||||
```
|
||||
|
||||
If you want to keep a title for nav-stack reasons, make it `Tap to prowl` and remove the duplicate caption under the dial. Don't say the same thing twice.
|
||||
|
||||
The tab label stays `Connect`. It's the right verb for the action and the right anchor for nav-bar muscle memory — every other VPN client labels its primary tab this way, and that's a feature not a bug.
|
||||
|
||||
---
|
||||
|
||||
## §2 · Ask 2 — First-launch zero-onboarding
|
||||
|
||||
You asked: does "Tap to prowl" + a giant unlabeled circle teach the flow?
|
||||
|
||||
**Yes, with one caveat that's not actually about onboarding.**
|
||||
|
||||
The cold-launch frames (`frames/01-cold-launch/frame_{00..08}.png`) tell a clean story: splash → Home → dial visible. The dial is *visually unambiguous* — it's the biggest thing on the screen, it pulses, and it says "Tap to prowl" with text. A first-time user does the right thing on muscle memory alone.
|
||||
|
||||
**What I'd change:** the splash hold (~1.6s before Home renders, per frame_02 timing). That's slow for an app whose value prop is "tap → tunnel." Cut to 800ms — long enough for the chromatic-aberration wordmark to register, short enough not to feel like a stalled launch.
|
||||
|
||||
**What I'd NOT change:** there's no need for a wizard, a "what's a layer?" intro card, or a permissions pre-screen. The dial → OS consent dialog → handshake is the right teach-by-doing. The bundle's `08-err-consent.png` shows the recovery path when the user taps Cancel; that's a sufficient safety net.
|
||||
|
||||
**One real risk** that's masked by the bundle: what happens on first launch when there's *no config loaded yet?* `00-launcher-home.png` → `02-home-screen.png` assumes a baked-in config. If a tester installs the APK and there's no `vless://` configured, tapping the dial should NOT just fail silently → it should bounce to Settings → Paste from clipboard with a one-line nudge. Confirm this is implemented, or this becomes a blocker.
|
||||
|
||||
---
|
||||
|
||||
## §3 · Ask 3 — Voice & copy
|
||||
|
||||
You asked: bless the set or mark specific lines for rewrite.
|
||||
|
||||
**Bless 90% of it.** The voice is the strongest thing about this build. `Pure vibe, zero function.` is a love letter to the user. `=^..^= thanks for being a friend.` is hand-on-heart correct. The L3 line — "Moscow → Amsterdam. Slower but stubborn." — does in 5 words what most VPN apps do in a paragraph and a diagram.
|
||||
|
||||
**5 specific lines to rewrite:**
|
||||
|
||||
| Where | Current | Proposed | Why |
|
||||
|---|---|---|---|
|
||||
| Dial meta, idle | `idle` | *(remove — the caption "Tap to prowl" already covers it)* | "idle" is dev-shorthand bleeding through. The user doesn't need a redundant state label under a button that already tells them what to do. |
|
||||
| LAYER chip when idle | `— · idle` | `not connected` or hide chip values entirely until connected | Same problem — "idle" twice on one screen, plus the em-dash placeholder reads like missing data. |
|
||||
| EXIT chip when idle | `—` | `not connected` or hide | Worse than the layer chip because the dash sits alone. |
|
||||
| EXIT chip when on L1 (no remote exit) | `—` + label `exit · ms` | `direct` (in the value) and drop the `ms` unit when there's no value | Currently `02e-home-dial-connected.png` reads literally `exit · ms` with empty numerics underneath. Embarrassing in screenshots. **This is blocker B-2.** |
|
||||
| Servers subtitle | `Endpoints. One per layer.` | `One server per layer.` | "Endpoints" is the only piece of network jargon that escapes containment. The tab is called Servers; the subtitle should match the tab's vocabulary, not the engineer's. |
|
||||
|
||||
**Don't rewrite:**
|
||||
- `Tap to prowl` — perfect.
|
||||
- `Connecting…` — perfect.
|
||||
- `Tunnel up` — perfect (and the past-tense brevity matches the cat-walking visual).
|
||||
- `Tap to retry` — perfect.
|
||||
- `handshake ▓▒░` — top-tier brand voice, ship it.
|
||||
- `Tunnel dropped` — perfect.
|
||||
- `Local TCP tricks. No server needed.` — "tricks" lands; it's playful without being cute. Keep it.
|
||||
- `Pick a layer.` + `Tap a layer to switch. Switching tears down and reconnects.` — this is two stacked imperatives but they're doing different jobs (title + helptext) and both are short. Fine.
|
||||
- All the error-banner copy. The "Tap Allow on the next prompt." flow is exemplary.
|
||||
|
||||
**Russian translation note:** when `.arb` work starts, the only line that needs translator hand-holding is `=^..^= thanks for being a friend.` — that's a cultural reference (Golden Girls theme) that won't survive a literal translation. Brief the translator to write a Russian sign-off in the same register: warm, slightly retro, slightly inside-joke. Don't try to translate ASCII-cat emoticons either; let them stand.
|
||||
|
||||
---
|
||||
|
||||
## §4 · Ask 4 — A11y & Servers density
|
||||
|
||||
### 4a · A11y
|
||||
|
||||
**Defer the full WCAG 2.1 AA pass to v0.7.x.** Three items must-fix at v0.7.0:
|
||||
|
||||
| | Item | Why it's must-fix |
|
||||
|---|---|---|
|
||||
| A-1 | `content-description` on the dial. Currently a screen reader sees an unlabeled `GestureDetector`. Add: `"Connect to VPN. Currently {state}."` where `{state}` interpolates the dial's text label. | A 288dp control with no semantic label is unusable, not just inconvenient. |
|
||||
| A-2 | `content-description` on the info icon top-right of Home + the copy icon top-right of Logs. Both currently auto-derive nothing because they're SVG-mask buttons. | Same root as A-1, smaller blast radius. |
|
||||
| A-3 | Touch-target audit on the Reset arrows in Servers (`04b-servers-expanded.png`). They look ≤32dp on capture — below Android Material 48dp minimum. | Density problem AND a11y problem. Solve them together — see §4b. |
|
||||
|
||||
**Acceptance criteria for the v0.7.x full pass:** Android Accessibility Scanner clean + manual TalkBack run-through of the cold-launch → connect → switch-layer → disconnect happy path. Skip WCAG 2.1 AA formal audit unless a public release is planned (private distribution model from brief means we're not subject to it). Reduced-motion is already passing per `09`/`10`/`11`.
|
||||
|
||||
### 4b · Servers density
|
||||
|
||||
**Reshape in v0.7.x. Not a v0.7.0 blocker** — this is a power-user / tester surface, not a daily-use one.
|
||||
|
||||
Concerns from `04b-servers-expanded.png` (DIRECT group expanded, 8 advanced fields visible):
|
||||
- 11+ field cards in a single scrollable column
|
||||
- Each field has its own Reset arrow (top-right) — visually noisy, touch-target-undersized
|
||||
- "Fingerprint" is empty and there's no signal that it's optional vs required
|
||||
- The same pattern repeats for BYPASS and RELAY below the fold
|
||||
|
||||
**For v0.7.0:** ship as-is. Most users will paste a `vless://` URI from Settings and never see this screen.
|
||||
|
||||
**For v0.7.x reshape:** four changes, in priority order:
|
||||
1. **First-run state of this screen = all groups collapsed.** A power user can expand. A new tester sees three named pills, taps the one they're configuring, expands. Defaults to friction-light.
|
||||
2. **One Reset button per group**, bottom-right of each expanded panel. Drop the per-field Resets. (Fixes A-3 and the visual noise at once.)
|
||||
3. **Mark optional fields explicitly.** `Fingerprint (optional)` in the label, or move them under a second `Show optional fields` toggle below `Show advanced`.
|
||||
4. **Add a contextual "Paste vless://" affordance** at the top of this screen, not just in Settings. The user-paths-to-config-input shouldn't all funnel through Settings → scroll → paste card.
|
||||
|
||||
Density verdict for one-handed mobile: **the collapsed view (`04-servers-screen.png`) is fine.** The expanded view is what hurts, and the fix is to make expansion an explicit power-user choice with a single Reset button per group.
|
||||
|
||||
---
|
||||
|
||||
## §5 · Bug-shaped observations
|
||||
|
||||
You flagged these tentatively. Both are real.
|
||||
|
||||
### Bug #1 · Bogus-IP / endpoint failure silently returns dial to idle 🚫 BLOCKS v0.7.0
|
||||
|
||||
This is the same root cause as the untriggered `02d-home-dial-error` state. When the user pastes a bad config or their endpoint goes dark, the dial cycles through retries and then *silently* returns to `Tap to prowl`. The user has no signal anything went wrong.
|
||||
|
||||
**Required fix:**
|
||||
- After N failed connect attempts (3? configurable?), surface the `dialError` state — red-tinted glow ring, "Tap to retry" caption, AND
|
||||
- Below the dial, surface an ErrorBanner: `Connection failed.` / `We tried 3 times. Switch layer or check your network.` — exactly the `errConnectFailed` strings already in `strings.dart`.
|
||||
|
||||
The strings exist. The error banner component exists (we saw it in `08-err-consent.png`). The dial-error visual state exists in the design system. What's missing is the *wiring* from "connect attempt loop drained" → "show these things." That's not a design ask; it's a Flutter task. But this *blocks ship* because shipping a VPN client where failure modes are silent is bad UX, full stop.
|
||||
|
||||
### Bug #2 · Tunnel-drop detection takes >8s ⏸ v0.7.x
|
||||
|
||||
`Tunnel up` persists for 8+ seconds after airplane mode cuts underlying network on AVD. May be xray-core keepalive tuning, not Prowler logic. Annoying — a user looks at the dial, sees "Tunnel up," and their browser is timing out. But not deceptive in the way bug #1 is.
|
||||
|
||||
**v0.7.x:** investigate xray-core's keepalive interval + add a UI-side tunnel-health ping every 3-5s. If two pings fail, drop to `dialError`. Don't block v0.7.0 on this; document as known issue.
|
||||
|
||||
---
|
||||
|
||||
## §6 · Untriggered states (UNTRIGGERED.md disposition)
|
||||
|
||||
You asked: (a) bless strings as sufficient, (b) demand real-device captures, or (c) accept code-review.
|
||||
|
||||
**(a) + (c).** Specifically:
|
||||
|
||||
- 02c connecting (<100ms on AVD) → **accept absence.** The strings.dart inventory + the existing `Connecting…` design in the system are sufficient. If you want belt-and-suspenders, run one real-device capture on a 4G connection where handshake spans 200-500ms; that's not a blocker.
|
||||
- 02d dial-error → **does NOT exist as an untriggered state.** It's bug #1. Fix the bug and the state becomes triggerable.
|
||||
- 08-err-network → **accept code-review.** Strings + ErrorBanner component exist. Future real-device capture nice-to-have.
|
||||
- 08-err-connect-failed → **same as 02d** — part of bug #1.
|
||||
- 08-err-server-unreachable → **accept code-review.** Same as err-network.
|
||||
- 08-err-switch → **accept code-review.** Lowest-priority error state (only triggers during mid-handshake layer-switch, narrow window).
|
||||
|
||||
**No real-device follow-up bundle requested.** Bug #1 is the only thing here that needs to ship-block v0.7.0; everything else is text-evidence-sufficient.
|
||||
|
||||
---
|
||||
|
||||
## §7 · What ships at v0.7.0 vs v0.7.x
|
||||
|
||||
**v0.7.0 (Phase 7 close):**
|
||||
- B-1 · Scale horizon-sliver halo on Home to design-system spec (currently ~3× too dominant)
|
||||
- B-2 · Suppress empty values + units in Home meta row (`exit · ms` bug)
|
||||
- B-3 · Wire `dialError` + `errConnectFailed` to connect-loop drain (bug #1)
|
||||
- §1 · Drop the `Home` screen title
|
||||
- §2 · Cut splash hold from ~1.6s to ~800ms
|
||||
- §3 · 5 copy rewrites (idle, em-dash placeholders, "endpoints" → "servers")
|
||||
- §4a · A-1, A-2, A-3 (a11y must-fixes)
|
||||
|
||||
**v0.7.x (followup polish batch):**
|
||||
- §4a · Full A11y pass against Android Accessibility Scanner + manual TalkBack
|
||||
- §4b · Servers screen reshape (all 4 changes)
|
||||
- Bug #2 · Tunnel-drop detection ping
|
||||
- Voice nice-to-have: Russian translator brief for `.arb` work
|
||||
|
||||
Sign-off conditional on the v0.7.0 list above. Phase 8 (Windows) can start as soon as B-1, B-2, B-3 land — the other v0.7.0 items don't gate it.
|
||||
|
||||
---
|
||||
|
||||
## Closing
|
||||
|
||||
This is a build I'd be happy to put on my own phone after the v0.7.0 fixes land. The voice carries the brand single-handedly; the dial-as-onboarding is the right call; the layer model reads correctly on the first try. The cracks are all on the edges — error-state plumbing, one over-aggressive halo, one over-dense form. Nothing structural.
|
||||
|
||||
Good work. Forward to engineering.
|
||||
|
||||
— UX team
|
||||
Reference in New Issue
Block a user