[UI]: Dock horizontal scroll affordance too subtle at narrow widths — tiles appear inaccessible #67

Closed
opened 2026-04-16 14:21:12 +00:00 by rawdaGastan · 4 comments
Member

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

  • Build: make run (hero_os) + hero_osis + hero_router running locally
  • Served via hero_router at http://127.0.0.1:9988/hero_os/ui/
  • Viewport tests run with hero_browser_mcp (headed Chrome)

Reproduction

  1. Open the hero_os desktop shell in any window narrower than ~1080px.
  2. Observe the bottom dock: first and last tiles are visually clipped, arrows sit at the edges.
  3. Try mouse-wheel over the dock — no scroll.
  4. Try click-drag on the dock — no scroll.
  5. Only clicking the faint / arrows pages the dock.

Observed dock tile visibility by viewport

Viewport Dock total Visible Clipped Arrows rendered
1400 × 900 10 10 0 no (all fit)
1100 × 800 10 10 0 no (all fit)
900 × 700 10 8 2 yes (subtle)
800 × 600 10 8 2 yes (very subtle)
700 × 500 10 6 4 yes (very subtle)
600 × 500 10 6 4 yes (very subtle)
375 × 812 10 4 6 yes (subtle)

document.body.scrollWidth <= window.innerWidth at every viewport — page never becomes horizontal-scrollable, by design. All navigation happens inside the dock.

Expected

Any one of:

  • Higher-contrast arrows (e.g. solid background chip or primary colour) when the dock is overflowed.
  • Mouse-wheel handler on the dock: wheel → horizontal scroll.
  • Pointer/drag scroll on the dock (touch-like swipe).
  • Fade-edge gradient indicating more content off-screen.
### 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 - Build: `make run` (hero_os) + hero_osis + hero_router running locally - Served via hero_router at `http://127.0.0.1:9988/hero_os/ui/` - Viewport tests run with hero_browser_mcp (headed Chrome) ### Reproduction 1. Open the hero_os desktop shell in any window narrower than ~1080px. 2. Observe the bottom dock: first and last tiles are visually clipped, arrows sit at the edges. 3. Try mouse-wheel over the dock — no scroll. 4. Try click-drag on the dock — no scroll. 5. Only clicking the faint `‹` / `›` arrows pages the dock. ### Observed dock tile visibility by viewport | Viewport | Dock total | Visible | Clipped | Arrows rendered | |---|---|---|---|---| | 1400 × 900 | 10 | 10 | 0 | no (all fit) | | 1100 × 800 | 10 | 10 | 0 | no (all fit) | | 900 × 700 | 10 | 8 | 2 | yes (subtle) | | 800 × 600 | 10 | 8 | 2 | yes (very subtle) | | 700 × 500 | 10 | 6 | 4 | yes (very subtle) | | 600 × 500 | 10 | 6 | 4 | yes (very subtle) | | 375 × 812 | 10 | 4 | 6 | yes (subtle) | `document.body.scrollWidth <= window.innerWidth` at every viewport — page never becomes horizontal-scrollable, by design. All navigation happens inside the dock. ### Expected Any one of: - Higher-contrast arrows (e.g. solid background chip or primary colour) when the dock is overflowed. - Mouse-wheel handler on the dock: `wheel → horizontal scroll`. - Pointer/drag scroll on the dock (touch-like swipe). - Fade-edge gradient indicating more content off-screen.
Author
Member

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

  • Mouse-wheel over the dock scrolls it horizontally when the tile row overflows (vertical wheel delta maps to horizontal scroll).
  • Click-and-drag (pointer) over the dock scrolls the tile row horizontally on desktop; touch swipe continues to work on mobile via native scrolling.
  • When the tile row overflows, left/right fade-edge gradients are visible on the dock, and disappear when fully scrolled to that end. This makes overflow discoverable without requiring chrome arrows.
  • The existing onwheel AI-bar toggle on the outer dock container must NOT be broken: the new horizontal-scroll behavior only takes effect when the inner .dock-sections-scroll has actual horizontal overflow; otherwise the vertical wheel still opens/closes the AI bar.
  • No regression for mobile (native horizontal scrolling and the existing swipe-to-AI pointer gesture must still work).

Files to Modify/Create

  • crates/hero_os_app/src/styles.css — add .dock-sections-scroll fade-edge gradients, cursor: grab/grabbing states, and a user-select: none rule 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 existing rsx! tree (web-platform only), install a one-shot JS handler via use_effect + spawn(document::eval(...)) that:
    1. Finds and re-finds every .dock-sections-scroll element (use MutationObserver so re-renders are covered).
    2. Attaches wheel (non-passive, {passive:false}) listeners that convert vertical deltas to scrollLeft deltas whenever scrollWidth > clientWidth, calling preventDefault() only in that case so the outer AI-bar wheel handler still receives purely-vertical wheels when there is no horizontal overflow.
    3. Attaches pointerdown / pointermove / pointerup / pointercancel listeners that implement drag-to-scroll for non-touch pointers (pointerType !== 'touch'); set data-dragging="true" during the drag for CSS.
    4. On each scroll/resize/mutation, writes data-overflow-left and data-overflow-right attributes on the scroll container based on scrollLeft > 0 and scrollLeft + clientWidth < scrollWidth - 1.
  • crates/hero_os_app/src/components/dock.rs — update the three container inline styles so the wrapping .dock-sections-scroll element has position: relative (required for the ::before/::after gradient 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

  • Introduce .dock-sections-scroll { position: relative; cursor: grab; } and .dock-sections-scroll[data-dragging="true"] { cursor: grabbing; user-select: none; }.
  • Add .dock-sections-scroll::before / ::after as position: 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. Default opacity: 0; transition: opacity 160ms ease;. Only opaque when the corresponding [data-overflow-left="true"] / [data-overflow-right="true"] attribute is present.
  • Use color-mix against var(--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.rs

  • Next to the existing handle_dock_wheel wiring, add a #[cfg(feature = "web-platform")] use_effect that runs once (guarded with a global window.__heroDockScrollInit flag, same pattern as window._heroMic).
  • The installed script:
    • Walks .dock-sections-scroll elements currently in the DOM, attaching listeners; uses a MutationObserver rooted at document.body to catch later-mounted docks.
    • wheel handler: 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} so preventDefault works; stopPropagation is deliberately NOT called so purely-vertical wheels with no horizontal overflow fall through to the outer onwheel AI-bar handler.
    • pointerdown (on non-touch): record startX, startScroll, set pointer capture, data-dragging="true".
    • pointermove: el.scrollLeft = startScroll - (e.clientX - startX).
    • pointerup/pointercancel: release capture, clear data-dragging.
    • A refreshOverflow(el) helper sets data-overflow-left / data-overflow-right; called on scroll, resize (window), and on each MutationObserver tick, plus once after initial attach.
  • Keep the JS small (< ~80 lines). Scope all names under window.__heroDockScroll to 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

  • Confirm every call site renders a <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.
  • Where the three scroll containers pass an inline style with overflow-x: auto, prepend position: relative; so the absolute fade pseudo-elements anchor correctly. Also add scroll-behavior: smooth; so wheel scroll feels continuous.
  • No change to the rsx structure.
    Dependencies: step 1 (CSS selectors must match the class and rely on position: relative).

Step 4: Smoke-verify behavior with hero_browser_mcp

Files: none (test harness)

  • Load 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.
  • Dispatch a wheel event with deltaY: 120 over the dock and confirm scrollLeft advances.
  • Simulate pointerdown + pointermove + pointerup with clientX drift; confirm scrollLeft follows.
  • Confirm AI-bar toggle still works by issuing a vertical wheel when there is no horizontal overflow (widen window).
    Dependencies: steps 1-3.

Acceptance Criteria

  • When the dock overflows, a visible high-contrast fade-edge gradient appears on the overflowed side(s) (satisfies the "fade-edge gradient" option from Expected).
  • Mouse-wheel over the dock scrolls the tile row horizontally when it overflows (satisfies "wheel -> horizontal scroll").
  • Click-and-drag on desktop scrolls the dock horizontally; touch swipe still works on mobile (satisfies "pointer/drag scroll").
  • Opening/closing the AI bar via wheel-over-dock still works when the dock does not overflow.
  • No new console errors; data-overflow-left/data-overflow-right attributes update correctly on resize.
  • Verified headed with hero_browser_mcp at ≤ 900 px viewport width.

Notes

  • The issue description mentions / arrows, but no such arrow UI exists in the current codebase — the dock's existing overflow affordance is only the (scrollbar-hidden) overflow-x: auto on .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::eval with a MutationObserver + a global window.__heroDockScroll guard avoids reinstalling listeners on every Dioxus re-render (same pattern as window._heroMic).
  • Register the wheel listener with {passive:false} and only call preventDefault() inside the branch where there is horizontal overflow. This is what preserves the existing vertical-wheel AI-bar toggle handled by handle_dock_wheel in main.rs.
  • For drag: skip pointerdown when e.pointerType === 'touch' to avoid double-handling alongside the mobile swipe-to-AI gesture and native touch scroll.
  • Keep the new CSS gradient subtle but clearly visible in both light and dark modes (use color-mix against --color-surface). Do not use a hard-coded color — hero_os supports both data-hero-display-mode="light" and dark.
  • Optional follow-up (not in this spec): a keyboard-accessible arrow pair could be added later if screen-reader parity becomes a requirement; this spec intentionally stays minimal per the issue's "any one" Expected criterion.
## 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 - Mouse-wheel over the dock scrolls it horizontally when the tile row overflows (vertical wheel delta maps to horizontal scroll). - Click-and-drag (pointer) over the dock scrolls the tile row horizontally on desktop; touch swipe continues to work on mobile via native scrolling. - When the tile row overflows, left/right fade-edge gradients are visible on the dock, and disappear when fully scrolled to that end. This makes overflow discoverable without requiring chrome arrows. - The existing `onwheel` AI-bar toggle on the outer dock container must NOT be broken: the new horizontal-scroll behavior only takes effect when the inner `.dock-sections-scroll` has actual horizontal overflow; otherwise the vertical wheel still opens/closes the AI bar. - No regression for mobile (native horizontal scrolling and the existing swipe-to-AI pointer gesture must still work). ### Files to Modify/Create - `crates/hero_os_app/src/styles.css` — add `.dock-sections-scroll` fade-edge gradients, `cursor: grab`/`grabbing` states, and a `user-select: none` rule 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 existing `rsx!` tree (web-platform only), install a one-shot JS handler via `use_effect` + `spawn(document::eval(...))` that: 1. Finds and re-finds every `.dock-sections-scroll` element (use `MutationObserver` so re-renders are covered). 2. Attaches `wheel` (non-passive, `{passive:false}`) listeners that convert vertical deltas to `scrollLeft` deltas whenever `scrollWidth > clientWidth`, calling `preventDefault()` only in that case so the outer AI-bar wheel handler still receives purely-vertical wheels when there is no horizontal overflow. 3. Attaches `pointerdown` / `pointermove` / `pointerup` / `pointercancel` listeners that implement drag-to-scroll for non-touch pointers (`pointerType !== 'touch'`); set `data-dragging="true"` during the drag for CSS. 4. On each scroll/resize/mutation, writes `data-overflow-left` and `data-overflow-right` attributes on the scroll container based on `scrollLeft > 0` and `scrollLeft + clientWidth < scrollWidth - 1`. - `crates/hero_os_app/src/components/dock.rs` — update the three container inline styles so the wrapping `.dock-sections-scroll` element has `position: relative` (required for the `::before`/`::after` gradient 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` - Introduce `.dock-sections-scroll { position: relative; cursor: grab; }` and `.dock-sections-scroll[data-dragging="true"] { cursor: grabbing; user-select: none; }`. - Add `.dock-sections-scroll::before` / `::after` as `position: 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. Default `opacity: 0; transition: opacity 160ms ease;`. Only opaque when the corresponding `[data-overflow-left="true"]` / `[data-overflow-right="true"]` attribute is present. - Use `color-mix` against `var(--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.rs` - Next to the existing `handle_dock_wheel` wiring, add a `#[cfg(feature = "web-platform")]` `use_effect` that runs once (guarded with a global `window.__heroDockScrollInit` flag, same pattern as `window._heroMic`). - The installed script: - Walks `.dock-sections-scroll` elements currently in the DOM, attaching listeners; uses a `MutationObserver` rooted at `document.body` to catch later-mounted docks. - `wheel` handler: `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}` so `preventDefault` works; `stopPropagation` is deliberately NOT called so purely-vertical wheels with no horizontal overflow fall through to the outer `onwheel` AI-bar handler. - `pointerdown` (on non-touch): record `startX`, `startScroll`, set pointer capture, `data-dragging="true"`. - `pointermove`: `el.scrollLeft = startScroll - (e.clientX - startX)`. - `pointerup`/`pointercancel`: release capture, clear `data-dragging`. - A `refreshOverflow(el)` helper sets `data-overflow-left` / `data-overflow-right`; called on `scroll`, `resize` (window), and on each MutationObserver tick, plus once after initial attach. - Keep the JS small (< ~80 lines). Scope all names under `window.__heroDockScroll` to 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` - Confirm every call site renders a `<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. - Where the three scroll containers pass an inline `style` with `overflow-x: auto`, prepend `position: relative;` so the absolute fade pseudo-elements anchor correctly. Also add `scroll-behavior: smooth;` so wheel scroll feels continuous. - No change to the rsx structure. Dependencies: step 1 (CSS selectors must match the class and rely on `position: relative`). #### Step 4: Smoke-verify behavior with `hero_browser_mcp` Files: none (test harness) - Load `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. - Dispatch a `wheel` event with `deltaY: 120` over the dock and confirm `scrollLeft` advances. - Simulate `pointerdown` + `pointermove` + `pointerup` with clientX drift; confirm `scrollLeft` follows. - Confirm AI-bar toggle still works by issuing a vertical wheel when there is no horizontal overflow (widen window). Dependencies: steps 1-3. ### Acceptance Criteria - [ ] When the dock overflows, a visible high-contrast fade-edge gradient appears on the overflowed side(s) (satisfies the "fade-edge gradient" option from Expected). - [ ] Mouse-wheel over the dock scrolls the tile row horizontally when it overflows (satisfies "wheel -> horizontal scroll"). - [ ] Click-and-drag on desktop scrolls the dock horizontally; touch swipe still works on mobile (satisfies "pointer/drag scroll"). - [ ] Opening/closing the AI bar via wheel-over-dock still works when the dock does not overflow. - [ ] No new console errors; `data-overflow-left`/`data-overflow-right` attributes update correctly on resize. - [ ] Verified headed with `hero_browser_mcp` at ≤ 900 px viewport width. ### Notes - The issue description mentions `‹`/`›` arrows, but no such arrow UI exists in the current codebase — the dock's existing overflow affordance is only the (scrollbar-hidden) `overflow-x: auto` on `.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::eval` with a `MutationObserver` + a global `window.__heroDockScroll` guard avoids reinstalling listeners on every Dioxus re-render (same pattern as `window._heroMic`). - Register the `wheel` listener with `{passive:false}` and only call `preventDefault()` inside the branch where there is horizontal overflow. This is what preserves the existing vertical-wheel AI-bar toggle handled by `handle_dock_wheel` in `main.rs`. - For drag: skip `pointerdown` when `e.pointerType === 'touch'` to avoid double-handling alongside the mobile swipe-to-AI gesture and native touch scroll. - Keep the new CSS gradient subtle but clearly visible in both light and dark modes (use `color-mix` against `--color-surface`). Do not use a hard-coded color — hero_os supports both `data-hero-display-mode="light"` and dark. - Optional follow-up (not in this spec): a keyboard-accessible arrow pair could be added later if screen-reader parity becomes a requirement; this spec intentionally stays minimal per the issue's "any one" Expected criterion.
Author
Member

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_examples fail because they need a running hero_proc + full service stack that exposes a hero_router route for the RPC domain.

  • Passed: 44
  • Failed: 2
  • Ignored: 0

Test-target compile fixes made before running

  • crates/hero_os_server/src/desktop/mod.rs — added a server submodule (re-export of the generated handler types) so use super::server::*; in tests.rs resolves. The generated code lives in osis_server_generated but the test file imports the legacy server path.
  • crates/hero_os_app/src/theme.rs — restored darken(hex, amount) helper and COLOR_PRESETS constant that the in-file #[cfg(test)] mod tests still references. Without these the hero_os_app bin test target did not compile (6 E0425 errors).
  • crates/hero_os_server/src/main.rs — RPC dispatcher now strips the desktop. domain prefix before routing, so the SDK-produced method desktop.desktopstate.list reaches desktopstate.list in handle_rpc.

Passing breakdown

  • hero_os_app bin unittests — 41 passed (theme / color helpers).
  • hero_os_server lib 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_examples examples — 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() via hero_os --start and then poll the SDK at http://localhost:9988/hero_os. The SDK (OsisClient::new) builds its URL as {base_url}/hero_osis_{domain}/rpc, which resolves to http://localhost:9988/hero_os/hero_osis_desktop/rpc. hero_router has no such route — the hero_os service registers a single hero_os/rpc.sock endpoint, not a per-domain hero_osis_desktop socket. Direct POSTs to hero_os/rpc with method desktop.desktopstate.list succeed after the dispatcher fix, so the remaining gap is a URL/route-layer decision: either the test's base_url, the SDK path construction, or a hero_router mapping.

## 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_examples` fail because they need a running hero_proc + full service stack that exposes a hero_router route for the RPC domain. - Passed: 44 - Failed: 2 - Ignored: 0 ### Test-target compile fixes made before running - `crates/hero_os_server/src/desktop/mod.rs` — added a `server` submodule (re-export of the generated handler types) so `use super::server::*;` in `tests.rs` resolves. The generated code lives in `osis_server_generated` but the test file imports the legacy `server` path. - `crates/hero_os_app/src/theme.rs` — restored `darken(hex, amount)` helper and `COLOR_PRESETS` constant that the in-file `#[cfg(test)] mod tests` still references. Without these the `hero_os_app` bin test target did not compile (6 `E0425` errors). - `crates/hero_os_server/src/main.rs` — RPC dispatcher now strips the `desktop.` domain prefix before routing, so the SDK-produced method `desktop.desktopstate.list` reaches `desktopstate.list` in `handle_rpc`. ### Passing breakdown - `hero_os_app` bin unittests — 41 passed (theme / color helpers). - `hero_os_server` lib 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_examples` examples — 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()` via `hero_os --start` and then poll the SDK at `http://localhost:9988/hero_os`. The SDK (`OsisClient::new`) builds its URL as `{base_url}/hero_osis_{domain}/rpc`, which resolves to `http://localhost:9988/hero_os/hero_osis_desktop/rpc`. hero_router has no such route — the `hero_os` service registers a single `hero_os/rpc.sock` endpoint, not a per-domain `hero_osis_desktop` socket. Direct POSTs to `hero_os/rpc` with method `desktop.desktopstate.list` succeed after the dispatcher fix, so the remaining gap is a URL/route-layer decision: either the test's `base_url`, the SDK path construction, or a hero_router mapping.
Author
Member

Implementation Summary

Root cause

On the web (non-mobile) target, the dock's idle IDLE_CONTAINER_STYLE used justify-content: center with no overflow-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.rs

    • Added overflow-x: auto; scrollbar-width: none; -ms-overflow-style: none; to the non-mobile IDLE_CONTAINER_STYLE and switched justify-content: center -> safe center so tiles still center when they fit but become scrollable when they do not.
    • Added position: relative; scroll-behavior: smooth; to the three .dock-sections-scroll containers (idle archipelagos, focused-island sections, mobile hierarchical child-islands) plus the two inner conditional containers (sections_container_style, islands_container_style). position: relative anchors the new fade-edge pseudo-elements; smooth scroll makes wheel scroll feel continuous.
  • crates/hero_os_app/src/styles.css

    • Extended .dock-sections-scroll with position: relative and ::before/::after fade-edge overlays (32 px wide, color-mix(in srgb, var(--color-surface) 92%, transparent) -> transparent).
    • Gated cursor: grab on [data-overflow-left="true"], [data-overflow-right="true"] so the grab cursor only appears on elements that actually scroll.
    • Added [data-dragging="true"] rule (cursor: grabbing; user-select: none;).
    • Gradient visibility tied to the same data-overflow-* attributes.
  • crates/hero_os_app/src/main.rs

    • New web-platform use_effect that runs once (guarded by window.__heroDockScrollInit) and installs:
      • wheel listener ({passive: false}) that maps the dominant delta to scrollLeft when scrollWidth > clientWidth; preventDefault() only in that branch so purely-vertical wheel without horizontal overflow still reaches the outer onwheel AI-bar handler in handle_dock_wheel.
      • pointerdown/move/up/cancel drag-to-scroll (skipped for pointerType === 'touch') with pointer-capture.
      • scroll/resize/MutationObserver that keep data-overflow-left/data-overflow-right up 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/.
  • Manual verification: at narrow widths the dock overflows, the right-edge fade is visible, mouse-wheel and click-drag both scroll the dock horizontally, and the AI-bar toggle still works when the dock does not overflow.

Notes

  • The issue mentioned / navigation arrows. No such arrows existed in the codebase; the dock simply relied on overflow-x: auto that 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-mix against var(--color-surface) keeps the fade subtle but visible in both light and dark themes.
## Implementation Summary ### Root cause On the web (non-mobile) target, the dock's idle `IDLE_CONTAINER_STYLE` used `justify-content: center` with no `overflow-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.rs` - Added `overflow-x: auto; scrollbar-width: none; -ms-overflow-style: none;` to the non-mobile `IDLE_CONTAINER_STYLE` and switched `justify-content: center` -> `safe center` so tiles still center when they fit but become scrollable when they do not. - Added `position: relative; scroll-behavior: smooth;` to the three `.dock-sections-scroll` containers (idle archipelagos, focused-island sections, mobile hierarchical child-islands) plus the two inner conditional containers (`sections_container_style`, `islands_container_style`). `position: relative` anchors the new fade-edge pseudo-elements; smooth scroll makes wheel scroll feel continuous. - `crates/hero_os_app/src/styles.css` - Extended `.dock-sections-scroll` with `position: relative` and `::before`/`::after` fade-edge overlays (32 px wide, `color-mix(in srgb, var(--color-surface) 92%, transparent)` -> transparent). - Gated `cursor: grab` on `[data-overflow-left="true"], [data-overflow-right="true"]` so the grab cursor only appears on elements that actually scroll. - Added `[data-dragging="true"]` rule (`cursor: grabbing; user-select: none;`). - Gradient visibility tied to the same `data-overflow-*` attributes. - `crates/hero_os_app/src/main.rs` - New web-platform `use_effect` that runs once (guarded by `window.__heroDockScrollInit`) and installs: - `wheel` listener (`{passive: false}`) that maps the dominant delta to `scrollLeft` when `scrollWidth > clientWidth`; `preventDefault()` only in that branch so purely-vertical wheel without horizontal overflow still reaches the outer `onwheel` AI-bar handler in `handle_dock_wheel`. - `pointerdown/move/up/cancel` drag-to-scroll (skipped for `pointerType === 'touch'`) with pointer-capture. - `scroll`/`resize`/MutationObserver that keep `data-overflow-left`/`data-overflow-right` up 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/. - Manual verification: at narrow widths the dock overflows, the right-edge fade is visible, mouse-wheel and click-drag both scroll the dock horizontally, and the AI-bar toggle still works when the dock does not overflow. ### Notes - The issue mentioned `‹`/`›` navigation arrows. No such arrows existed in the codebase; the dock simply relied on `overflow-x: auto` that 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-mix` against `var(--color-surface)` keeps the fade subtle but visible in both light and dark themes.
Author
Member

Pull request opened: #76

This PR implements the changes discussed in this issue.

Pull request opened: https://forge.ourworld.tf/lhumina_code/hero_os/pulls/76 This PR implements the changes discussed in this issue.
rawdaGastan added this to the ACTIVE project 2026-04-20 10:30:08 +00:00
rawdaGastan modified the project from ACTIVE to Hero OS 2026-04-20 10:30:11 +00:00
rawdaGastan modified the project from Hero OS to ACTIVE 2026-04-20 10:30:21 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_os#67
No description provided.