From 5a388ee241129976e366f8117b69e304232b907f Mon Sep 17 00:00:00 2001 From: Vineet Sharma Date: Tue, 21 Jul 2026 15:23:34 +0530 Subject: [PATCH] fix(oc-docs): center collapsed search overlay within the docs area MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Below desktop the search collapses to an icon that opens an overlay. Its positioning was gated on viewport media queries, but the responsive mode is derived from the docs-area width — so when the inline playground shrinks the docs column on a wide window, the overlay fell back to the desktop rule and hung off the left edge of the screen. Drive the collapsed treatment off the shell's docs-area mode via a `collapsed` prop / `data-collapsed` attribute instead of viewport media queries. When collapsed, the field wrapper spans the full reveal row so the absolute panel centers within the docs area rather than on a left-aligned field. Includes token/consistency cleanups in the search + topbar styles (z-index token, scroll region derived from the panel cap, --topbar-height). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/components/AppShell/AppShell.tsx | 9 +- .../components/Search/SearchBar/SearchBar.tsx | 10 +- .../Search/SearchBar/StyledWrapper.ts | 137 ++++++++---------- .../src/components/Topbar/StyledWrapper.ts | 26 ++-- .../oc-docs/src/components/Topbar/Topbar.tsx | 26 ++-- 5 files changed, 108 insertions(+), 100 deletions(-) diff --git a/packages/oc-docs/src/components/AppShell/AppShell.tsx b/packages/oc-docs/src/components/AppShell/AppShell.tsx index 2a331325..95afe11e 100644 --- a/packages/oc-docs/src/components/AppShell/AppShell.tsx +++ b/packages/oc-docs/src/components/AppShell/AppShell.tsx @@ -109,7 +109,14 @@ const AppShell: React.FC = ({ logo, testId = 'app-shell' }) => { collectionName={collection?.info?.name || 'API Collection'} version={collection?.info?.version} logo={logo} - searchSlot={} + searchSlot={ + + } searchOpen={searchOpen} onSearchOpenChange={setSearchOpen} onToggleSidebar={toggleSidebar} diff --git a/packages/oc-docs/src/components/Search/SearchBar/SearchBar.tsx b/packages/oc-docs/src/components/Search/SearchBar/SearchBar.tsx index d727a8b7..12d2165c 100644 --- a/packages/oc-docs/src/components/Search/SearchBar/SearchBar.tsx +++ b/packages/oc-docs/src/components/Search/SearchBar/SearchBar.tsx @@ -23,6 +23,12 @@ interface SearchBarProps { onOpenChange: (open: boolean) => void; /** Changes on each hotkey press to refocus the field even when already open. */ focusNonce?: number; + /** + * Below-desktop layout: the field is revealed as a full-width row, so the + * panel widens to center within the docs area. Driven by the shell's + * docs-area-derived mode rather than a viewport media query. + */ + collapsed?: boolean; testId?: string; } @@ -35,7 +41,7 @@ interface SearchBarProps { * rather than opening a centered modal. Open state is controlled so the Topbar * search icon/row and this panel share one state (no redundant affordances). */ -export const SearchBar: React.FC = ({ open, onOpenChange, focusNonce, testId = 'search' }) => { +export const SearchBar: React.FC = ({ open, onOpenChange, focusNonce, collapsed = false, testId = 'search' }) => { const docsNavigate = useDocsNavigate(); const model = useNavModel(); @@ -170,7 +176,7 @@ export const SearchBar: React.FC = ({ open, onOpenChange, focusN const showInitial = !hasQuery && !hasFilter; return ( - +
diff --git a/packages/oc-docs/src/components/Search/SearchBar/StyledWrapper.ts b/packages/oc-docs/src/components/Search/SearchBar/StyledWrapper.ts index 4945e314..0fb32b82 100644 --- a/packages/oc-docs/src/components/Search/SearchBar/StyledWrapper.ts +++ b/packages/oc-docs/src/components/Search/SearchBar/StyledWrapper.ts @@ -3,8 +3,8 @@ import styled from '@emotion/styled'; export const StyledWrapper = styled.div` position: relative; width: 100%; - max-width: 430px; - height: 28px; + max-width: 27.5rem; + height: 1.75rem; font-family: var(--font-sans); .search-panel { @@ -20,39 +20,40 @@ export const StyledWrapper = styled.div` border-radius: var(--oc-radius); overflow: hidden; } + + .search-panel:hover { + border-color: var(--oc-border-border1); + } .search-panel[data-open='true'] { - z-index: 10; - top: -5px; + /* Panel height cap; the scroll region below derives its own max-height from + this so the two never desync. */ + --search-panel-max: min(28.375rem, calc(100vh - 4.5rem)); + z-index: var(--z-popover); + top: -0.3125rem; left: 50%; transform: translateX(-50%); - width: min(660px, 92vw); - max-height: min(454px, calc(100vh - 72px)); + width: min(41.25rem, 92vw); + max-height: var(--search-panel-max); background: var(--oc-background-base); border-color: var(--oc-border-border1); - border-radius: var(--oc-radius); overflow: visible; } .search-inputrow { display: flex; align-items: center; - gap: 8px; - height: 28px; - padding: 7px 8px; + gap: 0.5rem; + height: 1.75rem; + padding: 0.4375rem 0.5rem; box-sizing: border-box; flex-shrink: 0; } - - .search-panel:hover { - border: 1px solid var(--oc-border-border1); - } - .search-panel[data-open='true'] .search-inputrow { - height: 40px; - padding: 0 12px; - gap: 9px; - border-bottom: 1px solid var(--oc-border-border0); + height: 2.5rem; + padding: 0 0.75rem; + gap: 0.5625rem; + border-bottom: 0.0625rem solid var(--oc-border-border0); } .search-field-icon { @@ -61,8 +62,8 @@ export const StyledWrapper = styled.div` color: var(--oc-colors-text-subtext0); } .search-field-icon svg { - width: 12px; - height: 12px; + width: 0.75rem; + height: 0.75rem; } .search-panel[data-open='true'] .search-field-icon { color: var(--oc-colors-text-subtext1); @@ -75,12 +76,12 @@ export const StyledWrapper = styled.div` outline: none; background: transparent; font-family: var(--font-sans); - font-size: 12px; + font-size: 0.75rem; font-weight: 400; color: var(--oc-text); } .search-panel[data-open='true'] .search-input { - font-size: 13px; + font-size: 0.8125rem; } .search-input::placeholder { color: var(--oc-colors-text-subtext1); @@ -90,8 +91,8 @@ export const StyledWrapper = styled.div` display: inline-flex; align-items: center; justify-content: center; - width: 18px; - height: 18px; + width: 1.125rem; + height: 1.125rem; padding: 0; flex-shrink: 0; cursor: pointer; @@ -101,20 +102,20 @@ export const StyledWrapper = styled.div` color: var(--oc-colors-text-subtext1); } .search-close svg { - width: 12px; - height: 12px; + width: 0.75rem; + height: 0.75rem; } .search-close:hover { - background: var(--oc-background-mantle); + background: var(--oc-background-surface0); } .search-filters { display: flex; align-items: center; - gap: 6px; + gap: 0.375rem; flex-wrap: nowrap; - height: 40px; - padding: 0 12px; + height: 2.5rem; + padding: 0 0.75rem; box-sizing: border-box; flex-shrink: 0; border-bottom: 1px solid var(--oc-border-border0); @@ -126,12 +127,12 @@ export const StyledWrapper = styled.div` .search-clear { margin-left: auto; - padding: 2px 4px; + padding: 0.125rem 0.25rem; cursor: pointer; background: transparent; border: 0; font-family: var(--font-sans); - font-size: 12px; + font-size: 0.75rem; font-weight: 400; color: var(--oc-colors-text-subtext1); white-space: nowrap; @@ -140,11 +141,14 @@ export const StyledWrapper = styled.div` color: var(--oc-accents-primary); } + /* Scroll region = panel cap minus the input row (2.5rem) and filter row + (2.5rem), so it tracks whatever --search-panel-max resolves to per + breakpoint. */ .search-results { - max-height: 360px; + max-height: calc(var(--search-panel-max) - 5rem); overflow-y: auto; - padding: 4px; - scroll-padding: 4px; + padding: 0.25rem; + scroll-padding: 0.25rem; } .search-list { @@ -159,18 +163,18 @@ export const StyledWrapper = styled.div` align-items: center; justify-content: center; text-align: center; - height: 180px; - padding: 0 20px; + height: 11.25rem; + padding: 0 1.25rem; box-sizing: border-box; - gap: 4px; + gap: 0.25rem; } .search-empty-icon { display: flex; align-items: center; justify-content: center; - width: 48px; - height: 48px; - margin-bottom: 10px; + width: 3rem; + height: 3rem; + margin-bottom: 0.625rem; border-radius: var(--oc-radius); } .search-empty-icon[data-tone='brand'] { @@ -182,29 +186,29 @@ export const StyledWrapper = styled.div` color: var(--oc-colors-text-subtext1); } .search-empty-icon svg { - width: 22px; - height: 22px; + width: 1.375rem; + height: 1.375rem; } .search-empty-title { - font-size: 14px; + font-size: 0.875rem; font-weight: 600; color: var(--oc-text); } .search-empty-text { - font-size: 12.5px; + font-size: 0.78125rem; color: var(--oc-colors-text-subtext1); line-height: 1.5; - max-width: 320px; + max-width: 20rem; } .search-empty-text b { color: var(--oc-text); font-weight: 500; } .search-empty-clear { - margin-top: 10px; - padding: 6px 14px; + margin-top: 0.625rem; + padding: 0.375rem 0.875rem; cursor: pointer; - font-size: 12px; + font-size: 0.75rem; font-weight: 600; color: var(--oc-accents-primary); background: color-mix(in srgb, var(--oc-accents-primary) 8%, transparent); @@ -212,31 +216,18 @@ export const StyledWrapper = styled.div` border-radius: var(--oc-radius); } - @media (max-width: 1023px) { + /* Collapsed = the below-desktop layout (icon + reveal row), driven by the + shell's docs-area-derived mode — not a viewport media query, because the + inline playground can shrink the docs column while the window stays wide. + The wrapper spans the full row so the absolute panel below centers within + the docs area rather than on a left-aligned field. */ + &[data-collapsed='true'] { max-width: none; height: 0; } - - @media (min-width: 768px) and (max-width: 1023px) { - .search-panel[data-open='true'] { - position: fixed; - top: 56px; - left: 50%; - transform: translateX(-50%); - width: min(660px, calc(100vw - 32px)); - max-height: min(454px, calc(100vh - 96px)); - } - } - - @media (max-width: 767px) { - .search-panel[data-open='true'] { - position: fixed; - top: 60px; - left: 50%; - right: auto; - transform: translateX(-50%); - width: min(660px, calc(100vw - 16px)); - max-height: min(70vh, calc(100vh - 96px)); - } + &[data-collapsed='true'] .search-panel[data-open='true'] { + --search-panel-max: min(28.375rem, calc(100vh - 6rem)); + top: 0.375rem; + width: min(41.25rem, calc(100% - 2rem)); } `; diff --git a/packages/oc-docs/src/components/Topbar/StyledWrapper.ts b/packages/oc-docs/src/components/Topbar/StyledWrapper.ts index 76e9b8d8..3eaa371e 100644 --- a/packages/oc-docs/src/components/Topbar/StyledWrapper.ts +++ b/packages/oc-docs/src/components/Topbar/StyledWrapper.ts @@ -1,6 +1,8 @@ import styled from '@emotion/styled'; export const StyledWrapper = styled.header` + --topbar-height: 3.1875rem; + position: sticky; top: 0; z-index: var(--z-header); @@ -8,28 +10,28 @@ export const StyledWrapper = styled.header` box-sizing: border-box; font-family: var(--font-sans); background: var(--oc-background-base); - border-bottom: 1px solid var(--oc-border-border0); + border-bottom: 0.0625rem solid var(--oc-border-border0); .topbar-bar { display: flex; align-items: center; - gap: 12px; - height: 51px; - padding: 0 20px; + gap: 0.75rem; + height: var(--topbar-height); + padding: 0 1.25rem; box-sizing: border-box; } &[data-mode='mobile'] .topbar-bar { - gap: 8px; - padding: 0 12px; + gap: 0.5rem; + padding: 0 0.75rem; } &[data-mode='mobile'] .topbar-secondary { - gap: 8px; + gap: 0.5rem; } .topbar-menu { - margin-left: -4px; + margin-left: -0.25rem; } .topbar-search { @@ -42,13 +44,13 @@ export const StyledWrapper = styled.header` .topbar-search-inner { width: 100%; - max-width: 440px; + max-width: 27.5rem; } .topbar-secondary { display: flex; align-items: center; - gap: 12px; + gap: 0.75rem; flex-shrink: 0; } @@ -56,6 +58,10 @@ export const StyledWrapper = styled.header` flex: 1 1 auto; } + /* Below desktop the SearchBar renders its own centered overlay when collapsed, + so this row is a zero-height mount point and click-outside boundary rather + than a visible strip; the inner stretches full-width (no cap) so that + overlay centers within the docs area, not on a left-aligned field. */ .topbar-search-row { display: flex; align-items: center; diff --git a/packages/oc-docs/src/components/Topbar/Topbar.tsx b/packages/oc-docs/src/components/Topbar/Topbar.tsx index aee54a83..eab1f59e 100644 --- a/packages/oc-docs/src/components/Topbar/Topbar.tsx +++ b/packages/oc-docs/src/components/Topbar/Topbar.tsx @@ -113,22 +113,20 @@ const Topbar: React.FC = ({ {/* Flex-1 middle: inline search on desktop, else a spacer that keeps the right-hand controls pinned to the right edge (search collapses to an icon below desktop, and may be empty). */} - {hasSearch && isDesktop ? ( + {hasSearch && (isDesktop ? (
{searchInner}
) : ( -
- )} - - {/* Below desktop: search toggle reveals the full-width search row below. */} - {hasSearch && !isDesktop && ( - setSearchOpen(!searchOpen)} - > - - - )} + <> +
+ setSearchOpen(!searchOpen)} + > + + + + ))} {/* Secondary controls (env switcher + show-vars): inline at every breakpoint; the controls condense their own labels when narrow. */}