Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit ce8f5a9f3f70bed5c216b3c258942963f72d89f0
parent 281d0173f19cc605a12adfd4e640657e51588dd0
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sat, 29 Aug 2026 21:53:40 -0400

search: only the compact bar sticks, and the phone menu offers Offline

The sticky wrapper enclosed the whole form, so a query tree of a few
leaves — 725px at 390px wide — pinned itself permanently over the
results it produced. Sticky now covers the input row and the chips, and
only in compact mode; the status line sits outside it, and a full tree
scrolls away like anything else. The submit keeps its word when it has
a row to itself instead of being a lone unlabelled magnifier.

The phone menu also gains Offline on a PWA-shipping site, gated the way
the footer gates it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Diffstat:
Mcommon/components/SearchBar.tsx | 174+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mexport/app/components/Header.tsx | 18++++++++++++------
2 files changed, 107 insertions(+), 85 deletions(-)

diff --git a/common/components/SearchBar.tsx b/common/components/SearchBar.tsx @@ -85,8 +85,10 @@ export default function SearchBar({ nav }: { nav?: ReactNode }) { className="shrink-0 h-11 sm:h-9" > <SearchIcon aria-hidden="true" /> - {/* The accessible name stays "Search" at every width; only the ink goes. */} - <span className="sr-only sm:not-sr-only">Search</span> + {/* The accessible name stays "Search" everywhere; only the ink goes, and + only where it has to — beside the input on a narrow compact bar. With + a full tree the button has a row to itself, so it keeps its word. */} + <span className={compact ? "sr-only sm:not-sr-only" : ""}>Search</span> </Button> ); @@ -105,7 +107,7 @@ export default function SearchBar({ nav }: { nav?: ReactNode }) { ); return ( - <div className="flex flex-col gap-2 max-lg:sticky max-lg:top-14 max-lg:z-10 max-lg:bg-background max-lg:py-2"> + <div className="flex flex-col gap-2"> <form onSubmit={(e) => { e.preventDefault(); @@ -114,94 +116,108 @@ export default function SearchBar({ nav }: { nav?: ReactNode }) { className="flex flex-col gap-2" data-testid="search-form" > - {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" /> - )} + {/* 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" /> + )} - {!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> + {/* 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 && ( <Button type="button" variant="outline" size="sm" - onClick={layers.addGroup} + onClick={handleShareCurrentSearch} + title="Copy link encoding the current search + filters" className="shrink-0 text-xs text-muted-foreground" > - + Add group + <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> </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> - </Button> - )} - {resetLayers} + )} + {resetLayers} + </div> </div> {/* One status line under the bar instead of two hints competing for - room inside it. */} + 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) && ( diff --git a/export/app/components/Header.tsx b/export/app/components/Header.tsx @@ -39,11 +39,9 @@ export default function Header() { const showDownloads = hasArchives(); const showDuplicates = hasDuplicates(); - // One list, two renderings: the inline nav from md, and the sheet below it — - // which also takes Changelog (a right-cluster link on a wide header) so the - // phone menu is the complete set. Ask AI is new: the chat was only reachable - // from the workspace control or from Use with AI, which is not where anyone - // looks for it. + // The inline nav from md. Ask AI is new: the chat was only reachable from + // the workspace control or from Use with AI, which is not where anyone looks + // for it. const navLinks = [ { href: "/", label: "Search" }, { href: "/ask/", label: "Ask AI" }, @@ -51,7 +49,15 @@ export default function Header() { ...(showDownloads ? [{ href: "/downloads", label: "Downloads" }] : []), { href: "/use-with-ai", label: "Use with AI" }, ]; - const menuLinks = [...navLinks, { href: "/changelog", label: "Changelog" }]; + // The sheet also takes the two links the wide header keeps in its right + // cluster or its footer: Changelog, and Offline on a PWA-shipping site — + // gated exactly as Footer.tsx gates it, so the two never disagree about + // whether this instance has an offline mode. + const menuLinks = [ + ...navLinks, + ...(site.pwa ? [{ href: "/offline/", label: "Offline" }] : []), + { href: "/changelog", label: "Changelog" }, + ]; return ( <header className="sticky top-0 z-20 border-b border-border bg-background/80 backdrop-blur-md">