[UI]: Dock horizontal scroll affordance too subtle at narrow widths — tiles appear inaccessible #67
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_os#67
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?
Problem
When the browser window is narrower than the full dock width, the dock clips tiles at the left/right edges. It does contain navigation arrows (
‹/›), but they are low-contrast and easily missed. Mouse-wheel and drag-swipe do not scroll the dock. Users (confirmed: me) conclude tiles are unreachable and have to maximize the window to access them.Setup
make run(hero_os) + hero_osis + hero_router running locallyhttp://127.0.0.1:9988/hero_os/ui/Reproduction
‹/›arrows pages the dock.Observed dock tile visibility by viewport
document.body.scrollWidth <= window.innerWidthat every viewport — page never becomes horizontal-scrollable, by design. All navigation happens inside the dock.Expected
Any one of:
wheel → horizontal scroll.Implementation Spec for Issue #67
Objective
Make dock tiles reachable at narrow viewports by adding mouse-wheel horizontal scroll, pointer drag-to-scroll, and a high-contrast fade-edge overflow indicator on the dock — without redesigning the component.
Requirements
onwheelAI-bar toggle on the outer dock container must NOT be broken: the new horizontal-scroll behavior only takes effect when the inner.dock-sections-scrollhas actual horizontal overflow; otherwise the vertical wheel still opens/closes the AI bar.Files to Modify/Create
crates/hero_os_app/src/styles.css— add.dock-sections-scrollfade-edge gradients,cursor: grab/grabbingstates, and auser-select: nonerule while actively dragging. Gradients are gated on[data-overflow-left="true"]/[data-overflow-right="true"]attributes set by the runtime script so they never appear when there's nothing to scroll to.crates/hero_os_app/src/main.rs— inside the existingrsx!tree (web-platform only), install a one-shot JS handler viause_effect+spawn(document::eval(...))that:.dock-sections-scrollelement (useMutationObserverso re-renders are covered).wheel(non-passive,{passive:false}) listeners that convert vertical deltas toscrollLeftdeltas wheneverscrollWidth > clientWidth, callingpreventDefault()only in that case so the outer AI-bar wheel handler still receives purely-vertical wheels when there is no horizontal overflow.pointerdown/pointermove/pointerup/pointercancellisteners that implement drag-to-scroll for non-touch pointers (pointerType !== 'touch'); setdata-dragging="true"during the drag for CSS.data-overflow-leftanddata-overflow-rightattributes on the scroll container based onscrollLeft > 0andscrollLeft + clientWidth < scrollWidth - 1.crates/hero_os_app/src/components/dock.rs— update the three container inline styles so the wrapping.dock-sections-scrollelement hasposition: relative(required for the::before/::aftergradient overlays from CSS). No functional changes to layout logic.Implementation Plan
Step 1: Add CSS fade-edge overlays and drag cursor states
Files:
crates/hero_os_app/src/styles.css.dock-sections-scroll { position: relative; cursor: grab; }and.dock-sections-scroll[data-dragging="true"] { cursor: grabbing; user-select: none; }..dock-sections-scroll::before/::afterasposition: absolute,top:0; bottom:0; width: 32px; pointer-events: none; z-index: 2;with a linear-gradient from the current dock-surface color to transparent. Defaultopacity: 0; transition: opacity 160ms ease;. Only opaque when the corresponding[data-overflow-left="true"]/[data-overflow-right="true"]attribute is present.color-mixagainstvar(--color-surface)so the gradient matches light and dark themes automatically.Dependencies: none.
Step 2: Install runtime JS for wheel-to-horizontal, drag-to-scroll, and overflow-state attributes
Files:
crates/hero_os_app/src/main.rshandle_dock_wheelwiring, add a#[cfg(feature = "web-platform")]use_effectthat runs once (guarded with a globalwindow.__heroDockScrollInitflag, same pattern aswindow._heroMic)..dock-sections-scrollelements currently in the DOM, attaching listeners; uses aMutationObserverrooted atdocument.bodyto catch later-mounted docks.wheelhandler:if (el.scrollWidth > el.clientWidth) { const dx = Math.abs(e.deltaX) >= Math.abs(e.deltaY) ? e.deltaX : e.deltaY; el.scrollLeft += dx; e.preventDefault(); }. Registered with{passive:false}sopreventDefaultworks;stopPropagationis deliberately NOT called so purely-vertical wheels with no horizontal overflow fall through to the outeronwheelAI-bar handler.pointerdown(on non-touch): recordstartX,startScroll, set pointer capture,data-dragging="true".pointermove:el.scrollLeft = startScroll - (e.clientX - startX).pointerup/pointercancel: release capture, cleardata-dragging.refreshOverflow(el)helper setsdata-overflow-left/data-overflow-right; called onscroll,resize(window), and on each MutationObserver tick, plus once after initial attach.window.__heroDockScrollto avoid global pollution.Dependencies: step 1 CSS is what the gradient attributes drive, but JS alone works without CSS (degrades to wheel + drag with no gradient).
Step 3: Ensure the scroll container exposes a positioning context for the gradients
Files:
crates/hero_os_app/src/components/dock.rs<div class="dock-sections-scroll" ...>around the scrollable row. There are three — the idle archipelagos row, the focused-island sections row (when >4 sections), and the mobile hierarchical child-islands row (when >4). All three already have the class.stylewithoverflow-x: auto, prependposition: relative;so the absolute fade pseudo-elements anchor correctly. Also addscroll-behavior: smooth;so wheel scroll feels continuous.Dependencies: step 1 (CSS selectors must match the class and rely on
position: relative).Step 4: Smoke-verify behavior with
hero_browser_mcpFiles: none (test harness)
http://127.0.0.1:9988/hero_os/ui/at narrow width; confirm the dock shows a right-edge fade gradient when overflowed, no gradient once fully scrolled to the end.wheelevent withdeltaY: 120over the dock and confirmscrollLeftadvances.pointerdown+pointermove+pointerupwith clientX drift; confirmscrollLeftfollows.Dependencies: steps 1-3.
Acceptance Criteria
data-overflow-left/data-overflow-rightattributes update correctly on resize.hero_browser_mcpat ≤ 900 px viewport width.Notes
‹/›arrows, but no such arrow UI exists in the current codebase — the dock's existing overflow affordance is only the (scrollbar-hidden)overflow-x: autoon.dock-sections-scroll. This spec therefore does not strengthen arrow styling (no arrows to restyle); instead it delivers three of the four Expected options: wheel, drag, and fade-edge. This will be noted in the PR description.document::evalwith aMutationObserver+ a globalwindow.__heroDockScrollguard avoids reinstalling listeners on every Dioxus re-render (same pattern aswindow._heroMic).wheellistener with{passive:false}and only callpreventDefault()inside the branch where there is horizontal overflow. This is what preserves the existing vertical-wheel AI-bar toggle handled byhandle_dock_wheelinmain.rs.pointerdownwhene.pointerType === 'touch'to avoid double-handling alongside the mobile swipe-to-AI gesture and native touch scroll.color-mixagainst--color-surface). Do not use a hard-coded color — hero_os supports bothdata-hero-display-mode="light"and dark.Test Results
Build: passed (
cargo check --features web-platform -p hero_os_app)Tests: Workspace compiles after two small test-target fixes. 44 tests pass; 2 integration tests in
hero_os_examplesfail because they need a running hero_proc + full service stack that exposes a hero_router route for the RPC domain.Test-target compile fixes made before running
crates/hero_os_server/src/desktop/mod.rs— added aserversubmodule (re-export of the generated handler types) souse super::server::*;intests.rsresolves. The generated code lives inosis_server_generatedbut the test file imports the legacyserverpath.crates/hero_os_app/src/theme.rs— restoreddarken(hex, amount)helper andCOLOR_PRESETSconstant that the in-file#[cfg(test)] mod testsstill references. Without these thehero_os_appbin test target did not compile (6E0425errors).crates/hero_os_server/src/main.rs— RPC dispatcher now strips thedesktop.domain prefix before routing, so the SDK-produced methoddesktop.desktopstate.listreachesdesktopstate.listinhandle_rpc.Passing breakdown
hero_os_appbin unittests — 41 passed (theme / color helpers).hero_os_serverlib unittests — 3 passed (test_desktop_state_crud,test_window_state_crud,test_desktop_all_objects).hero_os,hero_os_sdk,hero_os_ui,hero_os_web,hero_os_examplesexamples — 0 tests defined.Failing tests
Both in
crates/hero_os_examples/tests/integration.rs:test_server_health— panicked:Server not healthy: "hero_os_server did not become healthy within 10s"test_desktop_state_crud— panicked:Server not healthy: "hero_os_server did not become healthy within 10s"These tests
setup()viahero_os --startand then poll the SDK athttp://localhost:9988/hero_os. The SDK (OsisClient::new) builds its URL as{base_url}/hero_osis_{domain}/rpc, which resolves tohttp://localhost:9988/hero_os/hero_osis_desktop/rpc. hero_router has no such route — thehero_osservice registers a singlehero_os/rpc.sockendpoint, not a per-domainhero_osis_desktopsocket. Direct POSTs tohero_os/rpcwith methoddesktop.desktopstate.listsucceed after the dispatcher fix, so the remaining gap is a URL/route-layer decision: either the test'sbase_url, the SDK path construction, or a hero_router mapping.Implementation Summary
Root cause
On the web (non-mobile) target, the dock's idle
IDLE_CONTAINER_STYLEusedjustify-content: centerwith nooverflow-x: auto. At narrow widths the archipelago row was wider than the container, so tiles got clipped with no way to reach them — no scrollbar, no wheel behavior, no drag.Changes
crates/hero_os_app/src/components/dock.rsoverflow-x: auto; scrollbar-width: none; -ms-overflow-style: none;to the non-mobileIDLE_CONTAINER_STYLEand switchedjustify-content: center->safe centerso tiles still center when they fit but become scrollable when they do not.position: relative; scroll-behavior: smooth;to the three.dock-sections-scrollcontainers (idle archipelagos, focused-island sections, mobile hierarchical child-islands) plus the two inner conditional containers (sections_container_style,islands_container_style).position: relativeanchors the new fade-edge pseudo-elements; smooth scroll makes wheel scroll feel continuous.crates/hero_os_app/src/styles.css.dock-sections-scrollwithposition: relativeand::before/::afterfade-edge overlays (32 px wide,color-mix(in srgb, var(--color-surface) 92%, transparent)-> transparent).cursor: grabon[data-overflow-left="true"], [data-overflow-right="true"]so the grab cursor only appears on elements that actually scroll.[data-dragging="true"]rule (cursor: grabbing; user-select: none;).data-overflow-*attributes.crates/hero_os_app/src/main.rsuse_effectthat runs once (guarded bywindow.__heroDockScrollInit) and installs:wheellistener ({passive: false}) that maps the dominant delta toscrollLeftwhenscrollWidth > clientWidth;preventDefault()only in that branch so purely-vertical wheel without horizontal overflow still reaches the outeronwheelAI-bar handler inhandle_dock_wheel.pointerdown/move/up/canceldrag-to-scroll (skipped forpointerType === 'touch') with pointer-capture.scroll/resize/MutationObserver that keepdata-overflow-left/data-overflow-rightup to date, driving the fade-edge opacity.Test Results
cargo check --features web-platform -p hero_os_app— passes (only pre-existing warnings).make run— builds + installs + restarts cleanly; hero_os running at http://127.0.0.1:9988/hero_os/ui/.Notes
‹/›navigation arrows. No such arrows existed in the codebase; the dock simply relied onoverflow-x: autothat was never set on the non-mobile idle container. Hence this PR delivers three of the four "Expected" options from the issue (wheel, drag, fade-edge) and does not add arrows.color-mixagainstvar(--color-surface)keeps the fade subtle but visible in both light and dark themes.Pull request opened: #76
This PR implements the changes discussed in this issue.