Skip to content

Commit 87e108b

Browse files
committed
fix(FR-2770): polish docs sider version selector and mobile topbar layout (#7143)
Resolves #7142 ([FR-2770](https://lablup.atlassian.net/browse/FR-2770)) ## Summary Tightens visual polish on the docs static site after the FR-2733 / FR-2758 phases. Touches `packages/backend.ai-docs-toolkit/src/styles-web.ts` and `packages/backend.ai-docs-toolkit/src/website-builder.ts`. ### Sider version selector - Switched the sider to a flex column with `overflow: hidden`, and moved scrolling to a new `.doc-sidebar__scroll` wrapper (intro + nav groups) so the version block sits **outside** the scroll area. The version selector is now structurally pinned to the top of the sider — no `position: sticky` / z-index tricks needed, no opaque-background hack required for the bottom border. - Version block now renders horizontally: uppercase label on the left, `<select>` on the right via `justify-content: space-between`, with an **edge-to-edge** soft bottom rule. Padding is `14px 24px 10px` so the label aligns with the indentation of the nav category headers below (which combine `.doc-sidebar__scroll` 8px + `.doc-sidebar-group` 6px margin + summary 10px padding = 24px from the sider edge). - Added `min-width: 132px` on the native `<select>` so the control no longer auto-shrinks when the only option is short (e.g., `next`). ### Mobile drawer scroll chaining - iOS/Android touch handling was routing the gesture to `<body>` once the sider hit a scroll boundary, causing "menu sometimes scrolls, sometimes the article behind scrolls instead." Fixed by: - Locking the underlying page (`body.bai-drawer-open { overflow: hidden; touch-action: none; }`). - Containing the drawer's overscroll (`.doc-sidebar { overscroll-behavior: contain; touch-action: pan-y; }` in drawer mode). - Defense-in-depth: same `overscroll-behavior: contain` on the desktop `.doc-sidebar__scroll` so wheel/trackpad scrolling in the nav doesn't bleed into the article. ### Mobile topbar layout - Hamburger SVG paths changed from `M4 6h16` to `M3 6h18` so the bars span the full 18-unit icon box edge-to-edge (matching the GitHub icon's content distribution). The previous 4..20 span left ~3px of dead space inside the SVG, making the menu glyph visually **inset** versus the right-edge buttons even though the button paddings were symmetric. - On `≤ 880px`, set `.bai-topbar { gap: 6px }` so the search-icon-button → first-action-button distance matches the action-button → action-button distance (which is also 6px). The previous 14px top-level gap clashed with the 6px cluster gap, making the search button look detached from the rest of the icon row. ## Test plan - [ ] `bash scripts/verify.sh` reports `=== ALL PASS ===`. - [ ] `pnpm --filter backend.ai-docs-toolkit test` (162 tests) passes. - [ ] `pnpm --filter backend.ai-webui-docs run build:web:en` regenerates pages without errors. - [ ] On the served site (desktop): the version block sits flush at the top of the sider, label aligns with the nav category labels below, and the bottom border spans the full sider width. Scrolling the nav (wheel) doesn't scroll the article. - [ ] On a real phone (or DevTools mobile emulation ≤ 880px): opening the drawer locks the article behind it; the menu always scrolls regardless of where the touch lands or whether you've already hit the top/bottom of the nav. The hamburger and the rightmost icon button sit at the same visual inset from their respective screen edges; the search icon is grouped with the lang/theme/github cluster at a uniform 6px rhythm. - [ ] Switch language (en/ko/ja/th) — the localized "Version" label still fits horizontally next to the switcher without wrapping. [FR-2770]: https://lablup.atlassian.net/browse/FR-2770?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent b78c555 commit 87e108b

2 files changed

Lines changed: 111 additions & 11 deletions

File tree

packages/backend.ai-docs-toolkit/src/styles-web.ts

Lines changed: 91 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -730,8 +730,38 @@ body.bai-drawer-open .bai-scrim {
730730
border-right: 1px solid var(--bai-border);
731731
box-shadow: var(--bai-shadow-lg);
732732
overflow-y: auto;
733+
/* FR-2768: contain touch scroll inside the drawer so reaching the
734+
top/bottom of the nav doesn't bubble up and scroll the page
735+
behind. Without this, mobile Safari/Chrome route the gesture to
736+
the body whenever the sider hits a boundary — the user sees the
737+
menu freeze and the article scroll instead. Pairs with the
738+
body-lock rule below for defense in depth. */
739+
overscroll-behavior: contain;
740+
-webkit-overflow-scrolling: touch;
741+
touch-action: pan-y;
733742
animation: bai-drawer-in 200ms ease-out;
734743
}
744+
745+
/* In drawer mode, .doc-sidebar is forced back to display:block
746+
above, so the inner .doc-sidebar__scroll wrapper no longer has the
747+
flex-sizing that gives it a constrained height — its content
748+
determines its size, and there is nothing for overflow-y:auto to
749+
scroll. Make that explicit by switching it to overflow:visible so
750+
there is exactly one scrollport (the sider itself) and the
751+
touch-action / overscroll-behavior rules above unambiguously
752+
target the element that actually scrolls. */
753+
body.bai-drawer-open .doc-sidebar__scroll {
754+
overflow: visible;
755+
}
756+
757+
/* Lock the underlying page scroll while the drawer is open. The
758+
scrim already blocks taps, but on touch devices the gesture
759+
hand-off (when the sider has no more room to scroll) still reaches
760+
the body unless its overflow is clipped. */
761+
body.bai-drawer-open {
762+
overflow: hidden;
763+
touch-action: none;
764+
}
735765
}
736766
737767
@keyframes bai-drawer-in {
@@ -782,16 +812,39 @@ body.bai-drawer-open .bai-scrim {
782812
.doc-toc below. */
783813
top: calc(var(--bai-topbar-h) + var(--bai-banner-h, 0px));
784814
height: calc(100vh - var(--bai-topbar-h) - var(--bai-banner-h, 0px));
785-
overflow-y: auto;
815+
/* FR-2768: switch from a single scrollable aside to a flex column
816+
with an internal scrollport. The version block becomes the first
817+
row (no flex), the .doc-sidebar__scroll wrapper takes flex: 1 and
818+
owns the overflow. This pins the version selector to the top of
819+
the sider while the nav list scrolls beneath, matching the design
820+
handoff (and avoiding the position:sticky / z-index dance). */
821+
display: flex;
822+
flex-direction: column;
823+
overflow: hidden;
786824
border-right: 1px solid var(--bai-border);
787825
background: var(--bai-bg-sider);
826+
}
827+
828+
/* The internal scrollport. Holds the synthetic Introduction entry +
829+
the nav groups; everything that should scroll lives inside this
830+
wrapper. Padding moves here from .doc-sidebar so the version block
831+
above can render edge-to-edge with its own padding. */
832+
.doc-sidebar__scroll {
833+
flex: 1 1 auto;
834+
min-height: 0;
835+
overflow-y: auto;
836+
/* Prevent scroll chaining: if the user scrolls the nav past its
837+
top/bottom, the gesture stays inside this container instead of
838+
bubbling out and scrolling the article. Same defense applied to
839+
the mobile drawer override below. */
840+
overscroll-behavior: contain;
788841
padding: 10px 8px 24px;
789842
}
790843
791-
.doc-sidebar::-webkit-scrollbar {
844+
.doc-sidebar__scroll::-webkit-scrollbar {
792845
width: 8px;
793846
}
794-
.doc-sidebar::-webkit-scrollbar-thumb {
847+
.doc-sidebar__scroll::-webkit-scrollbar-thumb {
795848
background: var(--bai-border);
796849
border-radius: 4px;
797850
}
@@ -895,20 +948,43 @@ body.bai-drawer-open .bai-scrim {
895948
image, BAI primary focus outline) so the two site-level controls feel
896949
like a coordinated pair. */
897950
.doc-sidebar-version {
951+
/* FR-2768: horizontal layout matching the design handoff —
952+
uppercase label on the left, switcher pill on the right, soft
953+
bottom rule separating it from the scrolling nav below. The
954+
wrapper is a fixed-height row outside the scrollport so it stays
955+
visible while the nav scrolls.
956+
957+
Horizontal padding is 24px (vs. the design's 18px) to align with
958+
the indentation of nav items below: the .doc-sidebar__scroll
959+
wrapper adds 8px padding and .doc-sidebar-group adds 6px margin
960+
plus the summary's 10px inner padding (FR-2758 introduced this
961+
extra margin). 8 + 6 + 10 = 24px from the sider edge to the
962+
category label — the version label needs to sit at the same
963+
x-coordinate so the grid aligns. */
898964
display: flex;
899-
flex-direction: column;
900-
gap: 6px;
901-
padding: 14px 18px 12px;
902-
border-bottom: 1px solid var(--bai-border);
965+
align-items: center;
966+
justify-content: space-between;
967+
gap: 10px;
968+
flex: 0 0 auto;
969+
padding: 14px 24px 10px;
970+
border-bottom: 1px solid var(--bai-border-soft, var(--bai-border));
903971
}
904972
.doc-sidebar-version__label {
905973
font-size: 10.5px;
906974
font-weight: 500;
907975
letter-spacing: 0.06em;
908976
text-transform: uppercase;
909977
color: var(--bai-text-2);
978+
flex: 0 0 auto;
910979
}
911980
.doc-sidebar-version .version-switcher {
981+
flex: 0 0 auto;
982+
/* FR-2768: native <select> auto-sizes to its longest option, which
983+
for short version labels (e.g., "next") renders much narrower
984+
than the prototype's pill button. Anchor the width so the
985+
control reads as a deliberate UI element rather than a tiny
986+
dropdown. */
987+
min-width: 132px;
912988
appearance: none;
913989
-webkit-appearance: none;
914990
-moz-appearance: none;
@@ -2758,6 +2834,14 @@ details > :last-child {
27582834
.bai-topbar__actions {
27592835
margin-left: 0;
27602836
}
2837+
/* FR-2768: match the topbar's child-to-child gap to .bai-topbar__actions'
2838+
internal gap so the search-icon-button → first-action-button distance
2839+
reads the same as action-button → action-button. Without this the
2840+
14px top-level gap clashed with the 6px cluster gap, making the
2841+
search button look detached from the rest of the icon row on mobile. */
2842+
.bai-topbar {
2843+
gap: 6px;
2844+
}
27612845
}
27622846
27632847
@media (max-width: 880px) {

packages/backend.ai-docs-toolkit/src/website-builder.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -386,11 +386,21 @@ function buildWebsiteSidebar(
386386
</a>
387387
`;
388388

389+
// Wrap intro + nav groups in a scroll container so the version block
390+
// stays pinned at the top while the nav list scrolls beneath it. The
391+
// sider itself is overflow:hidden + flex column (see styles-web.ts);
392+
// .doc-sidebar__scroll is flex: 1 + overflow-y: auto. Mirrors the
393+
// pattern from the FR-2768 design handoff (`.sider__nav` in the
394+
// prototype). Tests still see .doc-sidebar-version inside <aside>
395+
// before .doc-sidebar-groups — the wrapper sits between them but
396+
// doesn't change ordering.
389397
return `
390398
<aside class="doc-sidebar">
391-
${versionBlockHtml}${introHtml} <nav class="doc-sidebar-groups" aria-label="Documentation navigation">
392-
${groupsHtml}
393-
</nav>
399+
${versionBlockHtml} <div class="doc-sidebar__scroll">
400+
${introHtml} <nav class="doc-sidebar-groups" aria-label="Documentation navigation">
401+
${groupsHtml}
402+
</nav>
403+
</div>
394404
</aside>`;
395405
}
396406

@@ -1453,7 +1463,13 @@ function buildBaiTopbar(
14531463

14541464
// Mobile menu icon (Phase 2 emits the surface; Phase 4 wires the
14551465
// drawer toggle).
1456-
const menuIconSvg = `<svg width="18" height="18" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>`;
1466+
// Path data spans x=3..21 (18 units wide) so the bars fill the icon's
1467+
// 18×18 box edge-to-edge, matching the visual weight of the GitHub /
1468+
// theme icons on the right side. The original 4..20 left ~3px of dead
1469+
// space inside the SVG, which made the menu glyph appear inset
1470+
// compared to the right-edge buttons even though the button paddings
1471+
// were symmetric.
1472+
const menuIconSvg = `<svg width="18" height="18" viewBox="0 0 24 24" aria-hidden="true"><path d="M3 6h18M3 12h18M3 18h18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>`;
14571473

14581474
return `<header class="bai-topbar" role="banner">
14591475
<button class="bai-iconbtn bai-topbar__menu" type="button" aria-label="Toggle navigation">${menuIconSvg}</button>

0 commit comments

Comments
 (0)