Phase 16 — UI polish: from "functional" to "amazing" #2
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 16 — UI polish: from "functional" to "amazing"
Context
Phase 15-release shipped
v0.2.0-level functionality but the UI is bare-minimum. The hand-rolled ~6 KiB CSS subset (crates/hero_assistance_ui_wasm/assets/app_overrides.css) was a fix-now to unblock the v0.2.0 walkthrough — it makes the components readable, but the result is cramped, lacks polish, and doesn't take advantage of thedioxus-bootstrap-csstyped-component library that's already in ourCargo.toml.This issue tracks the focused UI polish session that brings the embed up to "amazing" quality.
Pre-Phase-16 baseline screenshots: see
tests/baselines/desktop-{empty,acme,detail,globex,globex-error}.pngfrom session 23.Pre-session investigation (~15 min)
Before writing any UI code, settle the foundation. Multiple attempts in session 23 failed:
dioxus_bootstrap_css::BootstrapHead(usesasset!()) silently failed under plaincargo build— works only withdx build<link rel="stylesheet">with jsDelivr CDN URL broke webkit2gtk render entirely<style>{include_str!()}</style>of full Bootstrap broke Dioxus rsx text-content rendering (likely size limit or curly-brace parser quirk)Need to evaluate and pick one:
dx buildasset pipeline — handlesasset!()properly via themanganisruntime. Restructure_app's build to usedx bundleinstead ofcargo build. May affect packaging/CI.wry'sregister_uri_scheme_protocol— register a custom URL scheme likeassist://that resolves to embedded resources viainclude_bytes!(). Bypasses asset pipelines entirely. Probably the cleanest path; one-time wiring in_app's launcher.<style>chunks — split bootstrap.min.css into ~50 KiB chunks. Quick hack if size was the issue._appextracts bundled CSS to~/.cache/hero_assistance_app/assets/bootstrap.min.csson first run, references via<link href="file://...">.Pick whichever the investigation says is least risky.
wry::WebViewBuilder::with_custom_protocolis the leading candidate.Implementation
1. Foundation: ship full Bootstrap 5.3.3 reliably to both shells
app_overrides.cssto ~30 lines of_app-specific palette tweaks (the dark-mode color overrides matching_app/src/app.css's left-panel palette).cargo build -p hero_assistance_app --releaseANDcargo check -p hero_assistance_ui_wasm --target wasm32-unknown-unknownstay green._ui --dist) still loads styles correctly — currently it pulls Bootstrap via askama static paths from_ui_wasm/index.htmlhead; this should be unified.2. Layout overhaul (the cramped feel)
.container(max-width 720px) with.container-fluidso content uses available width minus reasonable side padding.<Row><Col xl={8}>thread + composer</Col><Col xl={4}>sidebars</Col></Row>. Stacks on small screens, side-by-side on large.ScrollIntoView.gap-*,mb-*,mt-*utilities consistently.3. Migrate components to
dioxus-bootstrap-csstyped RSXThe crate (
~/.cargo/git/checkouts/dioxus-bootstrap-*/.../crates/dioxus-bootstrap/src/) shipsCard,Button,Container,Row,Col,Modal,Toast,Alert,Badge,ListGroup,Form,InputGroup,Nav,Tabs,Spinner,Placeholder,Pagination,Breadcrumb, etc. — all already on disk via the existing dependency.crates/hero_assistance_ui_wasm/src/components/tickets/list.rs— replace rawclass="card"/class="list-group-item"withCard { body: ... }andListGroup/ListGroupItem.crates/hero_assistance_ui_wasm/src/components/tickets/detail.rs— same migration; the sidebars becomeCards with properheader/bodyslots.crates/hero_assistance_ui_wasm/src/components/tickets/composer.rs— replace raw<form>/<textarea>/<button>withForm/InputGroup/Button.crates/hero_assistance_ui_wasm/src/components/enrollment.rs—Cardfor the sign-in card;Alert { color: Color::Danger }for the validation/InvalidOrExpired error states (replaces the inlineis-invalidtext);Spinnerfor the busy state.<form>inlist.rs. Migrate toModal { title: "Create ticket", show: signal, ... }._app/src/project_config.rs. Migrate toModal { size: ModalSize::Lg, scrollable: true, ... }._app/src/app.csscustom (.tab-strip/.tab-pane). Migrate toNav { variant: NavVariant::Tabs }+TabContent.4. Visual polish
→✕inapp_overrides.css). Reference via the icon classes (bi bi-send-fill,bi bi-x-lg,bi bi-arrow-left,bi bi-gear-fill,bi bi-plus-lg,bi bi-bell, etc.).user_id. Render in comment author area, presence sidebars.presence.updateevents.@keyframes, ~150ms) on new comment arrivals; smoothtransformon tab switches; scale-in on modal open..fs-3or.h3), comment author bold + timestamp muted small + content body. Avoid the current "everything looks the same weight" feel.bg-success-subtle(open) /bg-secondary-subtle(archived); comment row borders useborder-subtle.5. Empty / loading / error states
Placeholderfrom dioxus-bootstrap-css. Show on firstcomment.list/ticket.listwhile resolving.ticket.listreturns empty. "Be the first to comment" in empty thread.Toast { color: Color::Success }"Welcome, Alice!" auto-dismissing after 3s. Replaces the current "✅ Signed in" box.Toast { color: Color::Danger }.Spinnerwhile server resolves.6.
_app-specific polishdioxus::desktop::Config::new().with_window(WindowBuilder::new().with_title("Hero Assistance")).WindowBuilder::with_window_icon._appshell uses Bootstrap for the left panel too — currently_app/src/app.cssis hand-rolled. Migrate#left-panelstyles to Bootstrap utilities (bg-body-tertiary,border-end, etc.) so the left panel matches the right pane's style.tracing::info!calls at: window-open, per-tab transport-resolved, per-tab driver-spawned, project-config-overlay-open. Helps debuggability.7. Re-capture all baselines + visual diff
tests/baselines/desktop-{empty,acme,detail,globex,globex-error}.pngafter polish lands.desktop-before-after.mddoc) so reviewers can see the improvement.<1%policy) — though desktop pixel-diffing is hard without a virtual display harness (per L-07-style limitation).Out of scope for Phase 16
_app(different problem; stays on roadmap).UNIQUE(email)+display_name) — touches schema; separate session.askama_templates) — separate session.Success criteria
_appdesktop window looks coherent, spacious, professional. Indistinguishable from a "this team has UX designers" product._ui_wasm::Appcodebase.cargo buildtime stays under ~30s post-cold-cache.Why this matters
D-09 §"Argument" pt 2 makes embed-as-Support-tab the primary deployment of
hero_assistance— meaning the UI quality has to hold up not just inside_app, but as a tab inside other Hero project UIs (freezone admin, TFGrid console, Hero OS shell). Functional-but-lame is fine for a v0.1 backend-complete release; it's not fine for the customer-facing surface that defines the product.References
runs/phase15_findings.md(recorded both fix-now items + 10 deferred to Phase 16+)tests/baselines/desktop-*.pngdecisions/D-09-dioxus-ui-adoption.md_appis dioxus desktop):decisions/D-13-app-shell-dioxus-desktop.mddecisions/D-17-config-persistence-layer.md~/.cargo/git/checkouts/dioxus-bootstrap-af2c6a1d42b4a423/.../crates/dioxus-bootstrap/Session 27 (2026-05-01) — Phase 16b-impl complete
Commits on
development:c9c1cac—feat(_ui_wasm,_app)(session-27): Phase 16b-impl visual polish surface(15 files, +477/-389)ae5ca9a—chore(sessions): record session-27 manifest + CLAUDE.md/prompt.md handoff(3 files, +152/-34)What shipped (8 of 12 §3 work items)
docs/dev/e2e_checklist.md.Replace→ Delete orphanapp_overrides.csswith typed componentscrates/hero_assistance_ui_wasm/assets/app_overrides.css(287 LoC) was orphan dead code: never<link>-ed, neverinclude_bytes!-ed, never served viaassist://, no.hassist-app-rootwrapper rendered in any DOM. Full Bootstrap CSS atassist://bootstrap.min.css(228 KiB) handles all rendering already. Lossless cleanup, zero pixel change.StatusBadgemigrated to typedBadge; orphan.badge-{pending,ok,err}rules dropped fromapp.css;EmptyTabsgainsbi-collection display-4icon;event-listrow getstext-overflow: ellipsisfor long project names.Collapsewraps the project-config Modal's "Add subscription" form (chevron-right ↔ chevron-down toggle; auto-expand when subs is empty). 150msopacityease-in-out fade on.app-tab-paneswaps.crates/hero_assistance_ui_wasm/src/components/toast.rs(~140 LoC + 1 unit test).RpcErrorToast+DEFAULT_AUTO_DISMISS_MS = 5_000;gloo_timers::callback::Timeouton wasm32; pinnable. Wired attickets/composer.rs(send-side errors) +enrollment.rsEmail/Token banner-class errors. InlineAlertkept for input-tied validation.TicketListSkeleton(tickets/list.rs) +CommentListSkeleton(tickets/detail.rs) using typedPlaceholder+PlaceholderParagraphglow animations;aria-busy=true.CommentListgained aloading: boolprop to distinguish "loading" from "real-empty".crates/hero_assistance_ui_wasm/assets/icon.png(377-byte 64×64 RGBA speech-bubble glyph; PIL-generated from theindex.htmlSVG).WINDOW_ICON_PNGbyte-const._app/src/main.rs::load_window_icondecodes viapngcrate (new dep, 0.17, pure-Rust).WindowBuilder::with_window_iconapplied.assist://icon.pngslot for embedded webview link tags.Breadcrumb { Home / Tickets / <ticket-name> }ontickets/detail.rsreplaces the prior ad-hoc back-arrow link. Single-affordance navigation./admin /, /editorpages are inherited fromhero_collaband untouched in 16b; recapture would reproduce identical PNGs. Desktop captures are part of Phase 16b-release.prompt.md§3 (297 → 321 passing).M1-H-15(window-icon row) flipped from[skip: M2]toSchema? ✅. Section H summary updated: 15 M1, no more M2 deferrals.sessions/27.yml+ CLAUDE.md AUTOGEN block + prompt.md §1/§3 rewrites).Test posture
_ui_wasmlib passing (+1 from W5 unit testphase16b_default_auto_dismiss_ms_is_five_seconds) / 3 L-03 unchanged / 1 phase10 transient flake / 8 ignored.Decisions and limitations
AppPropsprop contract per D-09; no new feature gates; noassist://shape change.assist://protocol-shape changes.What's next
v0.2.0 tag is the Rule-6 user-driven gate — Phase 16b-release, the agent-half / user-half split that worked for Phase 13a/13b and Phase 15-release. The user clicks through the 4-port demo against the new desktop chrome (collapse animation in project-config Modal; tab-content fade; window icon in title bar; status badge typed migration; empty-tabs icon polish) + SPA polish (
RpcErrorToaston send failures; Placeholder skeletons during ticket-list and comment-list initial fetches; Breadcrumb on ticket detail), captures 5 fresh desktop PNGs pertests/baselines/desktop-README.md, stamps Section H + F + M Human? cells indocs/dev/e2e_checklist.md, thengit tag v0.2.0ondevelopmentand pushes. Estimated ~1 session.Full next-session entry:
prompt.md§3 — Candidate A (Phase 16b-release; this issue's closure path) OR Candidate B (Phase 18c Playwright suite, #3; ~2–3 sessions; B.5 runs).Session 30 (2026-05-01) — Phase 16b-fix complete; v0.2.0 deferred
Commits on
development(pushed):9a1fed1—feat(_app,_ui_wasm)(session-30): Phase 16b-fix — × truncate + disable-on-N=1 guard + Link→button (4 sites) + Breadcrumb hand-roll378b291—chore(sessions,docs)(session-30): record session-30 manifest + CLAUDE.md/prompt.md handoff + 4 fresh desktop PNGs + findings docWhat was tested (Rule-6 walkthrough)
User-driven walkthrough of
_appdesktop binary against the seeded 2-project demo (scripts/phase13_demo.sh). Six §3 walkthrough items were exercised:_NET_WM_ICON)WM_CLASS+ needs.desktopfile installCUSTOM_HEAD)Add subscriptionform).app-tab-pane150ms opacity)connecting…→connected (2))bi-collectioniconBugs found — 7 desktop findings
Full record in
runs/phase16b_findings.md.ModalSize::Lg)_app/src/project_config.rs(text-truncate+max-width: 14rem+title=tooltip)disabled-on-N=1; spawn-deferral alone insufficient. Real fix deferred. Root cause: tokio drop ordering ofArc<Mutex<UnboundedReceiver>>(NativeEventsFeed) when lastProjectTabdrops simultaneously with multipleuse_futurecancellations_appto system browser (Brave opensfile:///)Link { to: Route::TicketDetail }→button { onclick: nav.push(...) }+use_navigator(). Sites:tickets/list.rs::TicketCard+AnonymousCta+tickets/detail.rs::TicketDetailAnonymous Alert +enrollment.rs::DoneStep. Root cause:<a href="/tickets/N">resolves tofile:///under thedioxus://base; webkit2gtk navigation_handler doesn't trap it;webbrowser::open()fallback opens system browser_ui_wasm/src/components/tickets/detail.rs— replaced typedBreadcrumb+BreadcrumbItem { href }with hand-rollednav[aria-label="breadcrumb"] > ol.breadcrumb > li.breadcrumb-item > button. L5 Playwright spec (breadcrumb.spec.ts) still passes (asserts on text +.breadcrumb-itemcount, not<a>shape)EmptyTabs)pkill hero_assistance_app; rm -f ~/.local/share/hero_assistance/subscriptions.toml; HERO_ASSISTANCE_SUBSCRIPTIONS= ./target/release/hero_assistance_app_app/src/config.rs::resolve_NET_WM_ICON; dock requires.desktopfile install (post-v1 packaging concern);_appcode path verified correctTest posture
cargo test --no-fail-fast→ 73 integration passing + 32_ui_wasmlib + everywhere else; 3 L-03 inherited failures + 1 phase10 transient flake unchanged from s29 — zero regressions from today's source fixes. Kickstart's verify-block extended +16 (374 → 390); all assertions pass.4 fresh desktop PNGs captured this session (
desktop-{acme,detail,globex,globex-error}.png) reflecting today's source state including the 4 navigation fixes.desktop-empty.pngretained from s23 pending B5 fix. Section M-11..M-14 dates bumped to 2026-05-01; M-10 keeps 2026-04-30 stamp. Section H Human? stamps: H-1/H-8/H-13 walked s23 + H-5/H-10/H-12 walked s30 = 6 ✅; H-15 =[skip: WM-ENV-NO-ICON-DISPLAY]per environmental finding.v0.2.0 tag — DEFERRED
The polish surface materially shipped today (4 navigation bugs fixed, walkthrough Items 1-5 verified ✅, Item 6 component code verified by code grep). However, B5 (empty-launch blank) and B2 (real fix) are unresolved, so the user opted to defer the v0.2.0 tag until both close in a clean walkthrough.
Architectural lesson recorded: the SPA path (
_ui --distover socat → port 8083-8086, browser-driven via Playwright/MCP) covers ~70% of UI surface — sign-in / ticket list / detail / breadcrumb / events / RPC — already automated by un-skipped L5 specs (s29). The_app-only outer shell (window chrome / multi-project tab strip /Manage subscriptionsModal / aggregator left panel /assist://custom protocol / D-17 TOML persistence / theme on first paint) is irreducibly desktop and is the only surface that requires Rule-6 walkthrough. 5 of today's 7 bugs were_app-only webview quirks — ticket Link escape, Breadcrumb escape, N=1→0 deadlock, empty-launch blank, window icon environmental. Future Rule-6 sessions on_appreserved for the minimum desktop-chrome surface; everything else stays in the SPA / Playwright lane.What's next — Phase 16b-release-fix → tag v0.2.0
Tracked here. Two
_app-irreducible items remain, both deferred from this session:tracing::info!counters in_app/src/main.rs::with_custom_protocolto confirm assist:// CSS loads on empty-boot (rule out CSS-load); inspectdisplay: grid; grid-template-columns: 360px 1frunder minimal right-pane content; check Dioxus 0.7 hook-stack edge cases under zero-yieldforloop. ~30 min investigation + fix.disabled: subscriptions().len() <= 1guard. Refactor_app/src/app.rs::ProjectTab's per-tabuse_futuredriver task to use an explicit shutdown signal (tokio::sync::Notifyorwatch::channel<bool>) instead of relying on Sender drop semantics + Mutex lock release during simultaneous component drop. ~1-2 hours.Re-walk Items 1-6 in
_appwith both fixes landed (all six should be ✅), re-capturedesktop-empty.png, thengit tag -a v0.2.0ondevelopment. ~1 session, B.5 skip.Independent alternative if user prefers backend work first: L-04 + L-03 closures (
userstableUNIQUE(email)+display_namemigration + 3 inherited test failures closed). B.5 RUNS (schema migration). ~1 session.See
prompt.md§3 Candidate A vs B for full work-item lists.Closed by v0.2.0 (
12601f4; session 32, Phase 16b-fix-redux).Root cause
The s30 spawn-yield + s31 watch-channel + first half of s32 driver-out-of-
ProjectTabattempts all targeted async lifecycle in the per-tab driver task. The actual B2 wedge was a synchronous render-storm in theselected_tabauto-pickuse_effect, masked since 15d (session 22) by the s30/s31 disable-on-N=1 UI guard.Diagnosis came from
/proc/<pid>/statusshowing stateR (Running), notS/Dfutex_wait — telling us this was a CPU-bound spin, not a deadlock. The auto-pick effect readselected_tab()(subscribes effect to signal) AND wroteselected_tab.set(...); on N=1→0 with current=Some(stale)/next=None, the set-and-self-notify cycle ran at ~60fps until the WM tripped its "not responding" detector. Dioxus 0.7Signal::setnotifies unconditionally (no PartialEq short-circuit).Fix (2 lines):
.peek()for the read + skip-no-op-write equality check.What landed
ProjectTabrefactor stays —_app::Appowns per-tab driver lifecycle viaDriversmap (Rc<RefCell<HashMap<ProjectId, DriverEntry>>>) +feeds/tab_statusSignal maps + diffinguse_effect(synchronous-only, off-UI cancellation viatokio_util::sync::CancellationToken).ProjectTabis purely presentational.spawn-yieldworkaround + s31disabled: is_onlyUI guard fromproject_config.rs.Walkthrough verdicts (all 5 acceptance scenarios green)
Test posture
L1: 209 native passing (+5 s32 unit tests for diff set-arithmetic + ReadOnlyRpc Arc-pointer-identity equality) / 32
_ui_wasmlib unchanged. L2/L3/L4/L5/L6/L7: unchanged from s31. 3 L-03 inherited + 1 phase10 transient flake unchanged. Build (cargo build --release) + wasm32 (cargo check) green. Kickstart 431 verify assertions all pass (423 → 431).Lessons recorded (
runs/phase16b_findings.md)/proc/<pid>/statusfirst. StateR= spin (CPU-bound loop); stateS/Dwith futex_wait on the main thread = deadlock. Right diagnostic narrows the search by an order of magnitude.Signal::setnotifies subscribers unconditionally. Effects that read AND write the same Signal must use.peek()for the read OR guard the write with an equality check (or both).