commit 56c1789ba250dc175a56d64603e3a3f3583ffaea
parent ce8f5a9f3f70bed5c216b3c258942963f72d89f0
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Sat, 29 Aug 2026 22:07:40 -0400
search: the compact bar pins under the header, for real this time
position: sticky only travels inside its containing block, so a bar
wrapped in a div of its own stuck for its own height and no further —
the "sticky below the header" the plan asked for was never happening.
The form is now the sticky element and reaches the column that spans
the bar and the results as a direct child, which means SearchBar and
WorkspaceSearchBar return fragments instead of wrappers.
At 390 the pinned block is 172px under a 57px header and stays at
top:56 through the results; a full query tree still scrolls away. The
trim to 172 came out of padding and gaps, not tap targets: the input is
still 44px and every chip 32px.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Diffstat:
4 files changed, 128 insertions(+), 116 deletions(-)
diff --git a/common/components/SearchBar.tsx b/common/components/SearchBar.tsx
@@ -107,136 +107,134 @@ export default function SearchBar({ nav }: { nav?: ReactNode }) {
);
return (
- <div className="flex flex-col gap-2">
+ // A fragment, not a wrapper: `position: sticky` only travels inside its
+ // containing block, so the form has to be a DIRECT child of the tall column
+ // that spans the bar and the results (SiteWorkspace / TranscriptSearch).
+ // Wrapped in a div of its own it stuck for its own height and no further.
+ <>
<form
onSubmit={(e) => {
e.preventDefault();
commitSearch();
}}
- className="flex flex-col gap-2"
+ // Sticky ONLY in compact mode. A full query tree is several leaf cards
+ // and group boxes — 725px at 390px wide — and pinning that would park a
+ // block taller than the viewport over the results it produced.
+ className={`flex flex-col gap-1.5 sm:gap-2 ${
+ compact
+ ? "max-lg:sticky max-lg:top-14 max-lg:z-10 max-lg:bg-background max-lg:py-1.5"
+ : ""
+ }`}
data-testid="search-form"
>
- {/* Sticky ONLY in compact mode, and only over the input row and the
- chip row. A full query tree is several leaf cards and group boxes —
- pinning that to the top of a phone would park a block taller than
- the viewport permanently over the results it produced. Same element
- either way, so flipping compact never remounts the builder. */}
- <div
- className={
- compact
- ? "flex flex-col gap-2 max-lg:sticky max-lg:top-14 max-lg:z-10 max-lg:bg-background max-lg:py-2"
- : "flex flex-col gap-2"
- }
- >
- {mounted ? (
- <QueryBuilder
- root={draftRoot}
- onChange={setDraftRoot}
- leafStates={leafStates}
- groupStates={groupStates}
- // Compact: the submit sits on the input's own line. Full tree: it
- // goes in the row below, exactly as before — either way there is
- // exactly one `search-submit` in the DOM.
- inputTrailing={compact ? submit : undefined}
- showCompactActions={false}
- />
- ) : (
- <div className="h-[5.5rem] rounded-lg border border-border bg-muted" />
- )}
+ {mounted ? (
+ <QueryBuilder
+ root={draftRoot}
+ onChange={setDraftRoot}
+ leafStates={leafStates}
+ groupStates={groupStates}
+ // Compact: the submit sits on the input's own line. Full tree: it
+ // goes in the row below, exactly as before — either way there is
+ // exactly one `search-submit` in the DOM.
+ inputTrailing={compact ? submit : undefined}
+ showCompactActions={false}
+ />
+ ) : (
+ <div className="h-[5.5rem] rounded-lg border border-border bg-muted" />
+ )}
- {!compact && (
- <div className="flex items-center gap-2 flex-wrap">{submit}</div>
- )}
+ {!compact && (
+ <div className="flex items-center gap-2 flex-wrap">{submit}</div>
+ )}
- {/* The chip row. Rendered OUTSIDE the `mounted` gate so the workspace
- nav exists as plain anchors before hydration — a nav click lost to
- the pre-hydration window is a click that goes nowhere.
- It WRAPS rather than scrolling sideways: an `overflow-x-auto` row in
- normal flow still widens `documentElement.scrollWidth` under Chrome's
- mobile emulation (measured: 498px at a 390px viewport, and neither
- `overflow-x: hidden` nor a fixed width fixed it), which is the exact
- horizontal overflow this redesign exists to remove. The modal's
- control strip can scroll because it is `position: fixed` and so
- contributes nothing to the document's scroll width. */}
- <div className="flex flex-wrap items-center gap-2 py-0.5">
- {nav}
- {compact && (
- <>
- <Button
- type="button"
- variant="outline"
- size="sm"
- onClick={layers.addLayer}
- data-testid="compact-add-layer"
- className="shrink-0 text-xs text-muted-foreground"
- >
- <LayersIcon aria-hidden="true" />+ Add layer
- </Button>
- <Button
- type="button"
- variant="outline"
- size="sm"
- onClick={layers.addGroup}
- className="shrink-0 text-xs text-muted-foreground"
- >
- + Add group
- </Button>
- </>
- )}
- {/* Not gated on `channelOptions.length` — the panel also holds the
- profile row and the advanced options, which used to be visible
- unconditionally, and the channel list is empty for the first
- moment of every page load. */}
- {!inlineFilters && (
- <FiltersTrigger
- activeCount={activeFilters}
- onApply={commitSearch}
- />
- )}
- {!inlineFilters && (
+ {/* The chip row. Rendered OUTSIDE the `mounted` gate so the workspace
+ nav exists as plain anchors before hydration — a nav click lost to
+ the pre-hydration window is a click that goes nowhere.
+ It WRAPS rather than scrolling sideways: an `overflow-x-auto` row in
+ normal flow still widens `documentElement.scrollWidth` under Chrome's
+ mobile emulation (measured: 498px at a 390px viewport, and neither
+ `overflow-x: hidden` nor a fixed width fixed it), which is the exact
+ horizontal overflow this redesign exists to remove. The modal's
+ control strip can scroll because it is `position: fixed` and so
+ contributes nothing to the document's scroll width. */}
+ <div className="flex flex-wrap items-center gap-1.5 sm:gap-2">
+ {nav}
+ {compact && (
+ <>
<Button
type="button"
variant="outline"
size="sm"
- onClick={handleShareCurrentSearch}
- title="Copy link encoding the current search + filters"
+ onClick={layers.addLayer}
+ data-testid="compact-add-layer"
className="shrink-0 text-xs text-muted-foreground"
>
- <Share2Icon aria-hidden="true" />
- {/* sr-only still contributes to the accessible name, so the
- button is called "Share current search" — and, once copied,
- "Link copied!" — at every width. Five specs wait on those. */}
- <span className="sr-only sm:not-sr-only">
- {shareCopied ? "Link copied!" : "Share current search"}
- </span>
+ <LayersIcon aria-hidden="true" />+ Add layer
</Button>
- )}
- {resetLayers}
- </div>
- </div>
-
- {/* One status line under the bar instead of two hints competing for
- room inside it — and OUTSIDE the sticky region, so the pinned block
- is never more than the input row plus the chips. */}
- {(queryDirty || filtersDirty || hasSubs) && (
- <p className="flex flex-wrap items-center gap-x-3 gap-y-0.5 text-xs">
- {(queryDirty || filtersDirty) && (
- <span className="text-warning">
- Press Enter or click Search to apply
- </span>
- )}
- {hasSubs && (
- <span
- className="text-muted-foreground"
- title={`${liveChatTotalCount} videos have live chat`}
+ <Button
+ type="button"
+ variant="outline"
+ size="sm"
+ onClick={layers.addGroup}
+ className="shrink-0 text-xs text-muted-foreground"
>
- Live chat available on {liveChatTotalCount} video
- {liveChatTotalCount === 1 ? "" : "s"} — try scope: Live chat.
+ + Add group
+ </Button>
+ </>
+ )}
+ {/* Not gated on `channelOptions.length` — the panel also holds the
+ profile row and the advanced options, which used to be visible
+ unconditionally, and the channel list is empty for the first
+ moment of every page load. */}
+ {!inlineFilters && (
+ <FiltersTrigger
+ activeCount={activeFilters}
+ onApply={commitSearch}
+ />
+ )}
+ {!inlineFilters && (
+ <Button
+ type="button"
+ variant="outline"
+ size="sm"
+ onClick={handleShareCurrentSearch}
+ title="Copy link encoding the current search + filters"
+ className="shrink-0 text-xs text-muted-foreground"
+ >
+ <Share2Icon aria-hidden="true" />
+ {/* sr-only still contributes to the accessible name, so the
+ button is called "Share current search" — and, once copied,
+ "Link copied!" — at every width. Five specs wait on those. */}
+ <span className="sr-only sm:not-sr-only">
+ {shareCopied ? "Link copied!" : "Share current search"}
</span>
- )}
- </p>
- )}
+ </Button>
+ )}
+ {resetLayers}
+ </div>
</form>
- </div>
+
+ {/* One status line under the bar instead of two hints competing for room
+ inside it — and a SIBLING of the form, so the pinned block is never
+ more than the input row plus the chips. */}
+ {(queryDirty || filtersDirty || hasSubs) && (
+ <p className="flex flex-wrap items-center gap-x-3 gap-y-0.5 text-xs">
+ {(queryDirty || filtersDirty) && (
+ <span className="text-warning">
+ Press Enter or click Search to apply
+ </span>
+ )}
+ {hasSubs && (
+ <span
+ className="text-muted-foreground"
+ title={`${liveChatTotalCount} videos have live chat`}
+ >
+ Live chat available on {liveChatTotalCount} video
+ {liveChatTotalCount === 1 ? "" : "s"} — try scope: Live chat.
+ </span>
+ )}
+ </p>
+ )}
+ </>
);
}
diff --git a/common/components/TranscriptSearch.tsx b/common/components/TranscriptSearch.tsx
@@ -19,7 +19,11 @@ import SearchResults from "./SearchResults";
export default function TranscriptSearch() {
return (
<SearchSessionProvider>
- <div className="flex flex-col gap-6">
+ {/* One column, and it is the search form's containing block — the form is
+ `position: sticky` below lg and only stays pinned while its parent is
+ on screen, so it has to sit here beside the results (both
+ WorkspaceSearchBar and SearchBar return fragments to make that so). */}
+ <div className="flex flex-col gap-4">
<WorkspaceSearchBar />
<SearchResults />
</div>
diff --git a/common/components/WorkspaceSearchBar.tsx b/common/components/WorkspaceSearchBar.tsx
@@ -22,10 +22,14 @@ export default function WorkspaceSearchBar({
}: {
nav?: ReactNode;
}) {
+ // A fragment, not a column: the search form is `position: sticky` below lg
+ // and can only travel inside its containing block, so it has to reach its
+ // caller's column — the one that spans the bar AND the results — as a direct
+ // child. A wrapper here would pin it for its own height and no further.
return (
- <div className="flex flex-col gap-4">
+ <>
<SearchBar nav={nav} />
<FiltersSection />
- </div>
+ </>
);
}
diff --git a/export/app/(workspace)/SiteWorkspace.tsx b/export/app/(workspace)/SiteWorkspace.tsx
@@ -33,7 +33,13 @@ export default function SiteWorkspace({
WorkspaceView. The routed `children` is each route's intro
chrome, slotted above its pane. */}
<WorkspaceViewProvider>
- <div className="flex flex-col gap-6">
+ {/* This column is the search form's containing block, and that is
+ load-bearing: the form is `position: sticky` below lg, so it can
+ only stay pinned for as long as its PARENT is on screen. Both
+ WorkspaceSearchBar and SearchBar return fragments so the form
+ lands here directly, beside the results it should stay above.
+ WorkspaceViewProvider renders no DOM of its own. */}
+ <div className="flex flex-col gap-4">
<WorkspaceSearchBar nav={<WorkspaceNav />} />
<WorkspaceView>{children}</WorkspaceView>
</div>