From 03d99555b8e6ae3cd91e37ecd56e4c7187ceee9b Mon Sep 17 00:00:00 2001 From: Taylor Bantle Date: Wed, 16 Sep 2026 13:33:14 -0700 Subject: [PATCH 01/12] Add nav dropdowns and restyle mobile menu Mobile menu becomes a scrolling sheet of full-bleed 60px rows, with a nav item that expands in place and a full-width action slot. Desktop gets a full-bleed dropdown panel with a scrim. Both dropdown triggers use Btn, since this package's preflight leaves button backgrounds opaque on purpose. `.logoLeft` no longer sets `relative` on `.inner`: `.right` is pinned with `ml-auto` instead, so an absolutely positioned panel anchors to the header and can span the full page width. New API: leftLinksMobile, mobileActions, MobileNavDropdown, DesktopNavDropdown. Stories cover each existing consumer's link shape as a regression baseline. Co-Authored-By: Claude Opus 5 (1M context) --- .../Navbar/ForDesktop/NavDropdown.module.css | 35 +++ .../src/Navbar/ForDesktop/NavDropdown.tsx | 80 ++++++ .../src/Navbar/ForDesktop/index.module.css | 21 +- .../Navbar/ForMobile/NavDropdown.module.css | 27 ++ .../src/Navbar/ForMobile/NavDropdown.tsx | 62 +++++ .../src/Navbar/ForMobile/index.module.css | 46 +++- .../components/src/Navbar/ForMobile/index.tsx | 8 + packages/components/src/Navbar/index.tsx | 5 +- .../DesktopNavDropdown.stories.tsx | 173 ++++++++++++ .../src/__stories__/MobileNavbar.stories.tsx | 254 ++++++++++++++++++ packages/components/src/index.ts | 2 + 11 files changed, 701 insertions(+), 12 deletions(-) create mode 100644 packages/components/src/Navbar/ForDesktop/NavDropdown.module.css create mode 100644 packages/components/src/Navbar/ForDesktop/NavDropdown.tsx create mode 100644 packages/components/src/Navbar/ForMobile/NavDropdown.module.css create mode 100644 packages/components/src/Navbar/ForMobile/NavDropdown.tsx create mode 100644 packages/components/src/__stories__/DesktopNavDropdown.stories.tsx create mode 100644 packages/components/src/__stories__/MobileNavbar.stories.tsx diff --git a/packages/components/src/Navbar/ForDesktop/NavDropdown.module.css b/packages/components/src/Navbar/ForDesktop/NavDropdown.module.css new file mode 100644 index 00000000..765a8781 --- /dev/null +++ b/packages/components/src/Navbar/ForDesktop/NavDropdown.module.css @@ -0,0 +1,35 @@ +/* + * Deliberately static: the panel anchors to the navbar header, not to this + * wrapper, which is what lets it span the full page width instead of the + * third-width column the trigger sits in. + */ +.dropdown { + @apply flex items-center; +} + +.trigger { + @apply flex items-center gap-2; +} + +.caret { + @apply shrink-0 text-sm; +} + +/* Starts below the navbar so the bar itself is not dimmed. */ +.scrim { + @apply absolute left-0 right-0 top-full h-screen bg-[rgba(0,0,0,0.56)]; +} + +.panel { + @apply absolute left-0 right-0 top-full text-left; +} + +/* + * The navbar styles every descendant `a`/`button` as a nav link, including the + * `.left a` margins. Panel content is not a nav link, so it opts out here: two + * class selectors outrank those one-class-plus-element rules. + */ +.dropdown .panel a, +.dropdown .panel button { + @apply mx-0 items-start text-base font-normal tracking-normal text-white hover:text-white; +} diff --git a/packages/components/src/Navbar/ForDesktop/NavDropdown.tsx b/packages/components/src/Navbar/ForDesktop/NavDropdown.tsx new file mode 100644 index 00000000..9d594c9a --- /dev/null +++ b/packages/components/src/Navbar/ForDesktop/NavDropdown.tsx @@ -0,0 +1,80 @@ +import { useOnClickOutside } from "@dolthub/react-hooks"; +import { FaCaretDown, FaCaretUp } from "react-icons/fa"; +import cx from "classnames"; +import React, { ReactNode, useRef, useState } from "react"; +import Btn from "../../Btn"; +import css from "./NavDropdown.module.css"; + +type Props = { + label: ReactNode; + children: ReactNode; + defaultOpen?: boolean; + className?: string; + // Applied to the panel, which is where a background colour belongs since the + // panel spans the full page width. + panelClassName?: string; + // Controlled mode. Both must be provided to take effect. + isOpen?: boolean; + setIsOpen?: (o: boolean) => void; + ["data-cy"]?: string; +}; + +// NavDropdown is a nav item whose panel spans the full width of the page, +// dimming the content beneath it. Must be rendered inside DesktopNavbar, whose +// header is the positioned ancestor the panel anchors to. +export default function NavDropdown({ + label, + children, + defaultOpen = false, + isOpen, + setIsOpen, + ...props +}: Props) { + const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen); + const controlled = isOpen !== undefined && setIsOpen !== undefined; + const open = controlled ? isOpen : uncontrolledOpen; + const ref = useRef(null); + + const setOpen = (o: boolean) => { + if (controlled) { + setIsOpen(o); + } else { + setUncontrolledOpen(o); + } + }; + + useOnClickOutside(ref, () => setOpen(false)); + + return ( +
+ setOpen(!open)} + data-cy={props["data-cy"]} + > + {label} + {open ? ( + + ) : ( + + )} + + {open && ( + <> +
setOpen(false)} + /> +
+ {children} +
+ + )} +
+ ); +} diff --git a/packages/components/src/Navbar/ForDesktop/index.module.css b/packages/components/src/Navbar/ForDesktop/index.module.css index 5d8f99a4..e8a8f694 100644 --- a/packages/components/src/Navbar/ForDesktop/index.module.css +++ b/packages/components/src/Navbar/ForDesktop/index.module.css @@ -1,5 +1,12 @@ +/* + * `relative` is what lets a dropdown panel span the full page width: the panel + * anchors here rather than to the third-width `.left` column. It also finally + * activates the `z-50` above, which was inert on a static element. That keeps + * the navbar over page content but still under Modal (1000) and the mobile + * menu (100). + */ .container { - @apply hidden lg:block w-full text-white pt-2 h-12 z-50 mx-auto pb-2; + @apply hidden lg:block relative w-full text-white pt-2 h-12 z-50 mx-auto pb-2; } .inner { @@ -62,8 +69,16 @@ } } +/* + * Previously `relative` here, so that `.right` could be pinned with `absolute + * right-0`. `ml-auto` does the same job in flow, and dropping the positioning + * matters: otherwise `.inner` -- inset by the header's own px-6/px-8 -- became + * the anchor for any absolutely positioned panel, making a full-bleed dropdown + * impossible. Keeping `.right` in flow also means wide left links compress it + * rather than sliding underneath. + */ .logoLeft { - @apply justify-start relative; + @apply justify-start; .left { @apply order-2 w-auto ml-6; @@ -78,6 +93,6 @@ } .right { - @apply order-3 w-auto absolute right-0 mr-8; + @apply order-3 w-auto ml-auto mr-8; } } diff --git a/packages/components/src/Navbar/ForMobile/NavDropdown.module.css b/packages/components/src/Navbar/ForMobile/NavDropdown.module.css new file mode 100644 index 00000000..24f4c304 --- /dev/null +++ b/packages/components/src/Navbar/ForMobile/NavDropdown.module.css @@ -0,0 +1,27 @@ +.dropdown { + @apply flex flex-col items-stretch w-full; +} + +/* The row height, padding and divider come from the menu's `.links button` + * rule; this only has to push the caret out to the trailing edge. */ +.trigger { + @apply w-full justify-between; +} + +.caret { + @apply shrink-0 text-base; +} + +.panel { + @apply flex flex-col items-stretch w-full; +} + +/* + * The menu styles every descendant `a`/`button` as a full-height row. Panel + * content is nested, not a row, so it opts out here: two class selectors + * outrank that one-class-plus-element rule without needing !important. + */ +.dropdown .panel a, +.dropdown .panel button { + @apply h-auto py-2 pl-10 pr-6 text-base font-normal tracking-normal border-b-0 text-white/80; +} diff --git a/packages/components/src/Navbar/ForMobile/NavDropdown.tsx b/packages/components/src/Navbar/ForMobile/NavDropdown.tsx new file mode 100644 index 00000000..207693b7 --- /dev/null +++ b/packages/components/src/Navbar/ForMobile/NavDropdown.tsx @@ -0,0 +1,62 @@ +import { FaCaretDown, FaCaretUp } from "react-icons/fa"; +import cx from "classnames"; +import React, { ReactNode, useState } from "react"; +import Btn from "../../Btn"; +import css from "./NavDropdown.module.css"; + +type Props = { + label: ReactNode; + children: ReactNode; + defaultOpen?: boolean; + className?: string; + // Controlled mode. Both must be provided to take effect. + isOpen?: boolean; + setIsOpen?: (o: boolean) => void; + ["data-cy"]?: string; +}; + +// NavDropdown is a nav item that expands in place, pushing the items below it +// down rather than overlaying them. Intended for the mobile nav menu, where a +// popup would have nowhere to go. +export default function NavDropdown({ + label, + children, + defaultOpen = false, + isOpen, + setIsOpen, + ...props +}: Props) { + const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen); + const controlled = isOpen !== undefined && setIsOpen !== undefined; + const open = controlled ? isOpen : uncontrolledOpen; + const toggle = () => { + if (controlled) { + setIsOpen(!isOpen); + } else { + setUncontrolledOpen(!uncontrolledOpen); + } + }; + + return ( +
+ + {label} + {open ? ( + + ) : ( + + )} + + {open && ( +
+ {children} +
+ )} +
+ ); +} diff --git a/packages/components/src/Navbar/ForMobile/index.module.css b/packages/components/src/Navbar/ForMobile/index.module.css index f3e5a8f5..73e8e151 100644 --- a/packages/components/src/Navbar/ForMobile/index.module.css +++ b/packages/components/src/Navbar/ForMobile/index.module.css @@ -3,20 +3,27 @@ } .topContainer { - @apply w-full text-white px-5 py-2 h-14; + @apply w-full text-white px-5 py-2 h-[62px] shrink-0; +} + +/* Keeps the close button reachable while a long menu scrolls under it. */ +.menuTop { + @apply sticky top-0 z-10; } .top { @apply flex justify-between items-center; button { - @apply text-xl p-2.5 text-white hover:text-white/70; + @apply text-xl p-2 rounded bg-white/10 text-white hover:text-white/70; } } +/* A white chip would wash out against the light/transparent variant, so the + * dark text treatment keeps the plain icon. */ .darkTop { button { - @apply text-background-acc-1 hover:text-primary; + @apply text-background-acc-1 bg-transparent hover:text-primary; } } @@ -31,24 +38,47 @@ } .openMenu { - @apply fixed top-0 bottom-0 left-0 right-0 z-100; + @apply fixed top-0 bottom-0 left-0 right-0 z-100 flex flex-col overflow-y-auto overscroll-contain; } +/* + * Full-bleed 60px rows divided by hairlines. The selector stays a descendant + * one rather than `> a`: both apps' sign-out buttons arrive wrapped in a div, + * and scoping to direct children would leave them unstyled. Nested content + * that should not look like a row overrides this with two classes (see + * NavDropdown.module.css). + */ .links { - @apply flex flex-col items-center mx-0 mt-10; + @apply flex flex-col items-stretch w-full border-t border-white/10; + + a, + button { + @apply flex items-center h-[60px] px-6 text-base font-semibold tracking-[0.16px] text-white/90 border-b border-white/10 hover:text-sky-100; + } +} + +/* + * A full-width call to action sitting between the rows and the socials. It is + * its own region rather than another child of `.links` so the row styling + * never reaches it. Like `.links` and `.bottomLinks`, it styles its children. + */ +.actions { + @apply mt-auto shrink-0 px-6 pt-8; a, button { - @apply text-xl my-4 mx-0 font-semibold tracking-widest text-white/90 hover:text-sky-100; + @apply flex items-center justify-center w-full h-[45px] px-12 rounded border border-white bg-white/10 text-[15px] font-semibold tracking-[0.3px] text-white hover:bg-white/20; } } +/* `mt-auto` pins these to the bottom when the menu is short and lets them fall + * at the end of the content when it is long. */ .bottomLinks { - @apply absolute left-0 right-0 bottom-8 flex justify-between mx-auto px-4 max-w-xs; + @apply mt-auto flex justify-center gap-6 px-6 py-8; img, svg { - @apply text-white h-9 w-9; + @apply text-white h-6 w-6; } } diff --git a/packages/components/src/Navbar/ForMobile/index.tsx b/packages/components/src/Navbar/ForMobile/index.tsx index 2deeda92..29120333 100644 --- a/packages/components/src/Navbar/ForMobile/index.tsx +++ b/packages/components/src/Navbar/ForMobile/index.tsx @@ -12,6 +12,7 @@ type CommonProps = { type Props = CommonProps & { dark?: boolean; children: ReactNode; + mobileActions?: ReactNode; mobileBottomLinks?: ReactNode; }; @@ -47,6 +48,7 @@ export default function ForMobile(props: Props) { type NavProps = CommonProps & { onClose: () => void; children: ReactNode; + mobileActions?: ReactNode; mobileBottomLinks?: ReactNode; }; @@ -58,6 +60,7 @@ function NavMenu(props: NavProps) { > {props.children}
+ {props.mobileActions && ( +
+ {props.mobileActions} +
+ )} {props.mobileBottomLinks && (
{props.mobileBottomLinks} diff --git a/packages/components/src/Navbar/index.tsx b/packages/components/src/Navbar/index.tsx index b060c172..d3cfc0fd 100644 --- a/packages/components/src/Navbar/index.tsx +++ b/packages/components/src/Navbar/index.tsx @@ -14,7 +14,9 @@ type Props = { logoLeft?: boolean; // Mobile-only + mobileActions?: ReactNode; // Full-width call to action below the links mobileBottomLinks?: ReactNode; + leftLinksMobile?: ReactNode; // Overrides `leftLinks` for mobile rightLinksMobile?: ReactNode; // Overrides `rightLinks` for mobile }; @@ -33,10 +35,11 @@ export default function Navbar(props: Props) { - {props.leftLinks} + {props.leftLinksMobile ?? props.leftLinks} {props.rightLinksMobile ?? props.rightLinks} diff --git a/packages/components/src/__stories__/DesktopNavDropdown.stories.tsx b/packages/components/src/__stories__/DesktopNavDropdown.stories.tsx new file mode 100644 index 00000000..92f90e64 --- /dev/null +++ b/packages/components/src/__stories__/DesktopNavDropdown.stories.tsx @@ -0,0 +1,173 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import React from "react"; +import { expect, userEvent, within } from "storybook/test"; +import Navbar from "../Navbar"; +import DesktopNavDropdown from "../Navbar/ForDesktop/NavDropdown"; +import { dolthubLogo } from "./images"; + +/* + * A feasibility check for the Homepage v3 mega menu: a full-bleed panel hung + * off one of the left nav links, dimming the page beneath it. The columns and + * cards below stand in for what the app would pass as children -- they are + * DoltHub marketing content, not library UI. + */ +const meta: Meta = { + title: "DesktopNavDropdown", + component: Navbar, + tags: ["autodocs"], + parameters: { layout: "fullscreen" }, +}; + +export default meta; + +type Story = StoryObj; + +// The nav background is not in the palette yet, so it is an arbitrary value +// here rather than a token. +const navBg = "bg-[#070f25]"; + +type Product = { name: string; description: string }; + +function Card({ name, description }: Product) { + return ( + + + + {name} + {description} + + + ); +} + +function Column({ + heading, + products, +}: { + heading: string; + products: Product[]; +}) { + return ( +
+

{heading}

+ {products.map(p => ( + + ))} +
+ ); +} + +const databases: Product[] = [ + { name: "Dolt", description: "MySQL-compatible" }, + { name: "DoltgreSQL", description: "PostgreSQL-compatible" }, + { name: "DoltLite", description: "SQLite-compatible" }, +]; + +const platforms: Product[] = [ + { name: "DoltHub", description: "Collaborate with forks, clones, and PRs" }, + { name: "DoltLab", description: "DoltHub on your machine" }, + { + name: "Dolt Workbench", + description: "A desktop workbench with agent mode", + }, +]; + +const productsMenu = (defaultOpen: boolean) => ( + + + +); + +function args(defaultOpen: boolean) { + return { + logo: DoltHub, + bgColor: navBg, + logoLeft: true, + large: true, + leftLinks: ( + <> + {productsMenu(defaultOpen)} + Pricing + Docs + Blog + Public Databases + + ), + rightLinks: Sign in, + }; +} + +// A tall hero so the scrim has something to dim. +function withHero(story: React.ComponentType) { + const StoryComponent = story; + return ( +
+ +
+

+ AGENTS NEED BRANCHES +

+

+ Dolt is the Database for Agents +

+
+
+ ); +} + +export const Closed: Story = { + args: args(false), + decorators: [withHero], +}; + +export const Open: Story = { + args: args(true), + decorators: [withHero], +}; + +// Opened the way a user would, rather than via defaultOpen. +export const OpenedByClick: Story = { + args: args(false), + decorators: [withHero], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByRole("button", { name: /products/i })); + await expect( + canvas.getByLabelText("nav dropdown panel"), + ).toBeInTheDocument(); + }, +}; diff --git a/packages/components/src/__stories__/MobileNavbar.stories.tsx b/packages/components/src/__stories__/MobileNavbar.stories.tsx new file mode 100644 index 00000000..c3588169 --- /dev/null +++ b/packages/components/src/__stories__/MobileNavbar.stories.tsx @@ -0,0 +1,254 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { AiFillDiscord } from "react-icons/ai"; +import { FaGithub, FaLinkedinIn, FaYoutube } from "react-icons/fa"; +import React from "react"; +import { expect, userEvent, within } from "storybook/test"; +import Navbar from "../Navbar"; +import MobileNavDropdown from "../Navbar/ForMobile/NavDropdown"; +import { dolthubLogo } from "./images"; + +/* + * These stories exist to pin the mobile navbar's current appearance before the + * Homepage v3 restyle. Each one mirrors the link shape a real consumer passes + * (DoltHub, Hosted, DoltLab, Workbench) so Chromatic catches any regression to + * an app we are not touching yet. The last three cover the new behaviour: a + * menu that scrolls, and a nav item that expands in place. + */ +const meta: Meta = { + title: "MobileNavbar", + component: Navbar, + tags: ["autodocs"], + parameters: { layout: "fullscreen" }, + globals: { viewport: { value: "iphonex" } }, +}; + +export default meta; + +type Story = StoryObj; + +const logo = DoltHub; + +// The menu only mounts once the hamburger is clicked, so every "open" story +// drives it through the same interaction a user would. +async function openMobileMenu(canvasElement: HTMLElement) { + const canvas = within(canvasElement); + await userEvent.click(canvas.getByLabelText("open mobile navbar menu")); + await expect(canvas.getByLabelText("mobile nav menu")).toBeInTheDocument(); +} + +const socialLinks = ( + <> + + + + + + + + + + + + + +); + +// The action slot styles whatever it is given, so a bare button is enough here. +const signInAction = ( + +); + +// EXISTING CONSUMER SHAPES + +const dolthubArgs = { + logo, + leftLinks: ( + <> + Databases + Pricing + Documentation + Blog + + ), + rightLinks: Sign In, + rightLinksMobile: ( + <> + My Organizations + Settings + Sign In + + ), + mobileBottomLinks: socialLinks, + logoLeft: true, +}; + +export const Closed: Story = { args: dolthubArgs }; + +export const OpenDoltHub: Story = { + args: dolthubArgs, + play: async ({ canvasElement }) => openMobileMenu(canvasElement), +}; + +export const OpenDoltLab: Story = { + args: { + logo, + bgColor: "bg-space-700", + logoLeft: true, + leftLinks: ( + <> + Documentation + Blog + Demo + + ), + rightLinks: Discord, + }, + play: async ({ canvasElement }) => openMobileMenu(canvasElement), +}; + +export const OpenHosted: Story = { + args: { + logo, + bgColor: "bg-ocean-400", + large: true, + leftLinks: ( + <> + Deployments + Pricing + Documentation + + ), + rightLinks: Sign In, + rightLinksMobile: ( + <> + Discord + GitHub + Settings + + ), + }, + globals: { theme: "hosted", viewport: { value: "iphonex" } }, + play: async ({ canvasElement }) => openMobileMenu(canvasElement), +}; + +// Workbench passes an empty fragment for `leftLinks`, so the mobile menu is +// made up entirely of right links. +export const OpenWorkbench: Story = { + args: { + logo, + bgColor: "bg-transparent", + logoLeft: true, + leftLinks: <>, + rightLinks: ( + <> + Blog + Discord + GitHub + + ), + }, + globals: { theme: "workbench", viewport: { value: "iphonex" } }, + play: async ({ canvasElement }) => openMobileMenu(canvasElement), +}; + +// NEW BEHAVIOUR + +// More links than fit on a phone. Before the menu became a scrolling flex +// column the overflow was simply unreachable. +export const OpenScrollable: Story = { + args: { + ...dolthubArgs, + leftLinks: ( + <> + {[ + "Databases", + "Pricing", + "Documentation", + "Blog", + "Public Databases", + "Dolt", + "DoltgreSQL", + "DoltLite", + "DoltHub", + "DoltLab", + "Dolt Workbench", + "Hosted Dolt", + ].map(name => ( + + {name} + + ))} + + ), + }, + play: async ({ canvasElement }) => openMobileMenu(canvasElement), +}; + +const products = [ + { name: "Dolt", href: "#dolt" }, + { name: "DoltgreSQL", href: "#doltgresql" }, + { name: "DoltLite", href: "#doltlite" }, + { name: "DoltHub", href: "#dolthub" }, + { name: "DoltLab", href: "#doltlab" }, + { name: "Dolt Workbench", href: "#dolt-workbench" }, +]; + +const withDropdown = (defaultOpen: boolean) => ( + <> + + {products.map(p => ( + + {p.name} + + ))} + + Pricing + Docs + Public Databases + Blog + +); + +export const OpenWithDropdown: Story = { + args: { + ...dolthubArgs, + leftLinksMobile: withDropdown(false), + rightLinksMobile: <>, + mobileActions: signInAction, + }, + play: async ({ canvasElement }) => openMobileMenu(canvasElement), +}; + +// The full-width call to action sits between the rows and the socials rather +// than inside the link list, so the row styling never reaches it. +export const OpenWithSignIn: Story = { + args: { + ...dolthubArgs, + rightLinksMobile: ( + <> + My Organizations + Settings + + ), + mobileActions: signInAction, + }, + play: async ({ canvasElement }) => openMobileMenu(canvasElement), +}; + +// Expanding pushes the items below it down rather than overlaying them, which +// is what makes the menu tall enough to need scrolling. +export const OpenWithDropdownExpanded: Story = { + args: { + ...dolthubArgs, + leftLinksMobile: withDropdown(true), + rightLinksMobile: <>, + mobileActions: signInAction, + }, + play: async ({ canvasElement }) => openMobileMenu(canvasElement), +}; diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 7121e4c4..0a4fcda7 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -35,7 +35,9 @@ export { export { default as FormModal } from "./Modal/ForForm"; export { default as Navbar } from "./Navbar"; export { default as DesktopNavbar } from "./Navbar/ForDesktop"; +export { default as DesktopNavDropdown } from "./Navbar/ForDesktop/NavDropdown"; export { default as MobileNavbar } from "./Navbar/ForMobile"; +export { default as MobileNavDropdown } from "./Navbar/ForMobile/NavDropdown"; export { default as NoneFoundMsg } from "./NoneFoundMsg"; export { default as Popup, PopupProps } from "./Popup"; export { default as QueryHandler } from "./QueryHandler"; From 967355eff7b6213596e68890ad8b780fb689c40b Mon Sep 17 00:00:00 2001 From: Taylor Bantle Date: Wed, 16 Sep 2026 15:03:50 -0700 Subject: [PATCH 02/12] Trim comments Co-Authored-By: Claude Opus 5 (1M context) --- .../Navbar/ForDesktop/NavDropdown.module.css | 12 ++-------- .../src/Navbar/ForDesktop/NavDropdown.tsx | 8 ++----- .../src/Navbar/ForDesktop/index.module.css | 17 ++------------ .../Navbar/ForMobile/NavDropdown.module.css | 8 +------ .../src/Navbar/ForMobile/NavDropdown.tsx | 5 +--- .../src/Navbar/ForMobile/index.module.css | 20 ++++------------ .../DesktopNavDropdown.stories.tsx | 12 +++------- .../src/__stories__/MobileNavbar.stories.tsx | 23 ++++--------------- 8 files changed, 20 insertions(+), 85 deletions(-) diff --git a/packages/components/src/Navbar/ForDesktop/NavDropdown.module.css b/packages/components/src/Navbar/ForDesktop/NavDropdown.module.css index 765a8781..e614d93c 100644 --- a/packages/components/src/Navbar/ForDesktop/NavDropdown.module.css +++ b/packages/components/src/Navbar/ForDesktop/NavDropdown.module.css @@ -1,8 +1,4 @@ -/* - * Deliberately static: the panel anchors to the navbar header, not to this - * wrapper, which is what lets it span the full page width instead of the - * third-width column the trigger sits in. - */ +/* Static, so the panel anchors to the header and spans the full width. */ .dropdown { @apply flex items-center; } @@ -24,11 +20,7 @@ @apply absolute left-0 right-0 top-full text-left; } -/* - * The navbar styles every descendant `a`/`button` as a nav link, including the - * `.left a` margins. Panel content is not a nav link, so it opts out here: two - * class selectors outrank those one-class-plus-element rules. - */ +/* Two classes, to outrank the navbar's `.inner a` / `.left a` link styling. */ .dropdown .panel a, .dropdown .panel button { @apply mx-0 items-start text-base font-normal tracking-normal text-white hover:text-white; diff --git a/packages/components/src/Navbar/ForDesktop/NavDropdown.tsx b/packages/components/src/Navbar/ForDesktop/NavDropdown.tsx index 9d594c9a..76ecf6d1 100644 --- a/packages/components/src/Navbar/ForDesktop/NavDropdown.tsx +++ b/packages/components/src/Navbar/ForDesktop/NavDropdown.tsx @@ -10,18 +10,14 @@ type Props = { children: ReactNode; defaultOpen?: boolean; className?: string; - // Applied to the panel, which is where a background colour belongs since the - // panel spans the full page width. panelClassName?: string; - // Controlled mode. Both must be provided to take effect. isOpen?: boolean; setIsOpen?: (o: boolean) => void; ["data-cy"]?: string; }; -// NavDropdown is a nav item whose panel spans the full width of the page, -// dimming the content beneath it. Must be rendered inside DesktopNavbar, whose -// header is the positioned ancestor the panel anchors to. +// A nav item whose panel spans the page width, dimming the content beneath. +// Must be rendered inside DesktopNavbar, which the panel anchors to. export default function NavDropdown({ label, children, diff --git a/packages/components/src/Navbar/ForDesktop/index.module.css b/packages/components/src/Navbar/ForDesktop/index.module.css index e8a8f694..f2c421f4 100644 --- a/packages/components/src/Navbar/ForDesktop/index.module.css +++ b/packages/components/src/Navbar/ForDesktop/index.module.css @@ -1,10 +1,4 @@ -/* - * `relative` is what lets a dropdown panel span the full page width: the panel - * anchors here rather than to the third-width `.left` column. It also finally - * activates the `z-50` above, which was inert on a static element. That keeps - * the navbar over page content but still under Modal (1000) and the mobile - * menu (100). - */ +/* `relative` so a dropdown panel anchors here and can span the full width. */ .container { @apply hidden lg:block relative w-full text-white pt-2 h-12 z-50 mx-auto pb-2; } @@ -69,14 +63,7 @@ } } -/* - * Previously `relative` here, so that `.right` could be pinned with `absolute - * right-0`. `ml-auto` does the same job in flow, and dropping the positioning - * matters: otherwise `.inner` -- inset by the header's own px-6/px-8 -- became - * the anchor for any absolutely positioned panel, making a full-bleed dropdown - * impossible. Keeping `.right` in flow also means wide left links compress it - * rather than sliding underneath. - */ +/* Not `relative`: that would make this, not the header, the panel's anchor. */ .logoLeft { @apply justify-start; diff --git a/packages/components/src/Navbar/ForMobile/NavDropdown.module.css b/packages/components/src/Navbar/ForMobile/NavDropdown.module.css index 24f4c304..f9bc48a5 100644 --- a/packages/components/src/Navbar/ForMobile/NavDropdown.module.css +++ b/packages/components/src/Navbar/ForMobile/NavDropdown.module.css @@ -2,8 +2,6 @@ @apply flex flex-col items-stretch w-full; } -/* The row height, padding and divider come from the menu's `.links button` - * rule; this only has to push the caret out to the trailing edge. */ .trigger { @apply w-full justify-between; } @@ -16,11 +14,7 @@ @apply flex flex-col items-stretch w-full; } -/* - * The menu styles every descendant `a`/`button` as a full-height row. Panel - * content is nested, not a row, so it opts out here: two class selectors - * outrank that one-class-plus-element rule without needing !important. - */ +/* Two classes, to outrank the menu's `.links a` row styling. */ .dropdown .panel a, .dropdown .panel button { @apply h-auto py-2 pl-10 pr-6 text-base font-normal tracking-normal border-b-0 text-white/80; diff --git a/packages/components/src/Navbar/ForMobile/NavDropdown.tsx b/packages/components/src/Navbar/ForMobile/NavDropdown.tsx index 207693b7..7c42d769 100644 --- a/packages/components/src/Navbar/ForMobile/NavDropdown.tsx +++ b/packages/components/src/Navbar/ForMobile/NavDropdown.tsx @@ -9,15 +9,12 @@ type Props = { children: ReactNode; defaultOpen?: boolean; className?: string; - // Controlled mode. Both must be provided to take effect. isOpen?: boolean; setIsOpen?: (o: boolean) => void; ["data-cy"]?: string; }; -// NavDropdown is a nav item that expands in place, pushing the items below it -// down rather than overlaying them. Intended for the mobile nav menu, where a -// popup would have nowhere to go. +// A nav item that expands in place, pushing the items below it down. export default function NavDropdown({ label, children, diff --git a/packages/components/src/Navbar/ForMobile/index.module.css b/packages/components/src/Navbar/ForMobile/index.module.css index 73e8e151..851a1baf 100644 --- a/packages/components/src/Navbar/ForMobile/index.module.css +++ b/packages/components/src/Navbar/ForMobile/index.module.css @@ -19,8 +19,7 @@ } } -/* A white chip would wash out against the light/transparent variant, so the - * dark text treatment keeps the plain icon. */ +/* A white chip would wash out here, so keep the plain icon. */ .darkTop { button { @apply text-background-acc-1 bg-transparent hover:text-primary; @@ -41,13 +40,7 @@ @apply fixed top-0 bottom-0 left-0 right-0 z-100 flex flex-col overflow-y-auto overscroll-contain; } -/* - * Full-bleed 60px rows divided by hairlines. The selector stays a descendant - * one rather than `> a`: both apps' sign-out buttons arrive wrapped in a div, - * and scoping to direct children would leave them unstyled. Nested content - * that should not look like a row overrides this with two classes (see - * NavDropdown.module.css). - */ +/* Descendant rather than `> a`: sign-out buttons arrive wrapped in a div. */ .links { @apply flex flex-col items-stretch w-full border-t border-white/10; @@ -57,11 +50,7 @@ } } -/* - * A full-width call to action sitting between the rows and the socials. It is - * its own region rather than another child of `.links` so the row styling - * never reaches it. Like `.links` and `.bottomLinks`, it styles its children. - */ +/* Its own region, so the row styling above never reaches it. */ .actions { @apply mt-auto shrink-0 px-6 pt-8; @@ -71,8 +60,7 @@ } } -/* `mt-auto` pins these to the bottom when the menu is short and lets them fall - * at the end of the content when it is long. */ +/* `mt-auto`: bottom of a short menu, end of the content on a long one. */ .bottomLinks { @apply mt-auto flex justify-center gap-6 px-6 py-8; diff --git a/packages/components/src/__stories__/DesktopNavDropdown.stories.tsx b/packages/components/src/__stories__/DesktopNavDropdown.stories.tsx index 92f90e64..7d4af678 100644 --- a/packages/components/src/__stories__/DesktopNavDropdown.stories.tsx +++ b/packages/components/src/__stories__/DesktopNavDropdown.stories.tsx @@ -5,12 +5,8 @@ import Navbar from "../Navbar"; import DesktopNavDropdown from "../Navbar/ForDesktop/NavDropdown"; import { dolthubLogo } from "./images"; -/* - * A feasibility check for the Homepage v3 mega menu: a full-bleed panel hung - * off one of the left nav links, dimming the page beneath it. The columns and - * cards below stand in for what the app would pass as children -- they are - * DoltHub marketing content, not library UI. - */ +// The columns and cards below stand in for what the app would pass as +// children; they are DoltHub marketing content, not library UI. const meta: Meta = { title: "DesktopNavDropdown", component: Navbar, @@ -22,8 +18,7 @@ export default meta; type Story = StoryObj; -// The nav background is not in the palette yet, so it is an arbitrary value -// here rather than a token. +// Not in the palette yet. const navBg = "bg-[#070f25]"; type Product = { name: string; description: string }; @@ -159,7 +154,6 @@ export const Open: Story = { decorators: [withHero], }; -// Opened the way a user would, rather than via defaultOpen. export const OpenedByClick: Story = { args: args(false), decorators: [withHero], diff --git a/packages/components/src/__stories__/MobileNavbar.stories.tsx b/packages/components/src/__stories__/MobileNavbar.stories.tsx index c3588169..ba8be840 100644 --- a/packages/components/src/__stories__/MobileNavbar.stories.tsx +++ b/packages/components/src/__stories__/MobileNavbar.stories.tsx @@ -7,13 +7,8 @@ import Navbar from "../Navbar"; import MobileNavDropdown from "../Navbar/ForMobile/NavDropdown"; import { dolthubLogo } from "./images"; -/* - * These stories exist to pin the mobile navbar's current appearance before the - * Homepage v3 restyle. Each one mirrors the link shape a real consumer passes - * (DoltHub, Hosted, DoltLab, Workbench) so Chromatic catches any regression to - * an app we are not touching yet. The last three cover the new behaviour: a - * menu that scrolls, and a nav item that expands in place. - */ +// Each story mirrors a real consumer's link shape, so Chromatic catches +// regressions to apps this PR does not touch. const meta: Meta = { title: "MobileNavbar", component: Navbar, @@ -28,8 +23,7 @@ type Story = StoryObj; const logo = DoltHub; -// The menu only mounts once the hamburger is clicked, so every "open" story -// drives it through the same interaction a user would. +// The menu only mounts once the hamburger is clicked. async function openMobileMenu(canvasElement: HTMLElement) { const canvas = within(canvasElement); await userEvent.click(canvas.getByLabelText("open mobile navbar menu")); @@ -53,7 +47,6 @@ const socialLinks = ( ); -// The action slot styles whatever it is given, so a bare button is enough here. const signInAction = ( +); + +const account = ( + <> +
+ + Signed in as{" "} + + tbantle + + + +
+
    + {["Settings", "My Organizations", "Contact DoltHub"].map(name => ( +
  • + {name} +
  • + ))} +
+ +); + +// `mobileAccount` sits between the links and the actions, outside the row +// styling. +export const OpenWithAccount: Story = { + args: { + ...dolthubArgs, + leftLinksMobile: withDropdown(false), + rightLinksMobile: <>, + mobileAccount: account, + mobileActions: signOutAction, + }, + play: async ({ canvasElement }) => { + await openMobileMenu(canvasElement); + const canvas = within(canvasElement); + await expect(canvas.getByText(/Signed in as/)).toBeVisible(); + await expect( + canvas.getByRole("button", { name: "Sign out" }), + ).toBeInTheDocument(); + }, +}; + +// A transparent bar over a hero still opens onto a solid menu. +export const OpenWithMenuBgColor: Story = { + args: { + ...dolthubArgs, + bgColor: "bg-transparent", + mobileMenuBgColor: "bg-space-900", + leftLinksMobile: withDropdown(false), + rightLinksMobile: <>, + mobileActions: signInAction, + }, + play: async ({ canvasElement }) => { + await openMobileMenu(canvasElement); + const canvas = within(canvasElement); + await expect(canvas.getByLabelText("mobile nav menu")).toHaveClass( + "bg-space-900", + ); + }, +}; + +function ControlledDropdown() { + const [open, setOpen] = useState(false); + return ( + <> + + {products.map(p => ( + + {p.name} + + ))} + + Pricing + Blog + + ); +} + +// `isOpen`/`setIsOpen` let the parent own the open state; `className` styles +// the dropdown's wrapper. +export const OpenWithControlledDropdown: Story = { + args: { + ...dolthubArgs, + leftLinksMobile: , + rightLinksMobile: <>, + mobileActions: signInAction, + }, + play: async ({ canvasElement }) => { + await openMobileMenu(canvasElement); + const canvas = within(canvasElement); + await userEvent.click( + canvas.getByRole("button", { name: /products \(closed\)/i }), + ); + await expect( + canvas.getByRole("button", { name: /products \(open\)/i }), + ).toBeInTheDocument(); + await expect( + canvas.getByLabelText("nav dropdown panel"), + ).toBeInTheDocument(); + }, +}; From babab3fc1681a6c55a55eb30eb7380bde50a323b Mon Sep 17 00:00:00 2001 From: Taylor Bantle Date: Mon, 5 Oct 2026 15:30:46 -0700 Subject: [PATCH 07/12] Update mobile navbar background tests for the menu's own bar With a transparent navbar, the bar over the page stays transparent while the open menu's sticky bar now takes the menu's color. Adds a test for menuBgColor. Co-Authored-By: Claude Opus 5.5 --- .../components/src/__tests__/Navbar.test.tsx | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/components/src/__tests__/Navbar.test.tsx b/packages/components/src/__tests__/Navbar.test.tsx index 21bdafb6..c749193e 100644 --- a/packages/components/src/__tests__/Navbar.test.tsx +++ b/packages/components/src/__tests__/Navbar.test.tsx @@ -134,12 +134,29 @@ describe("test MobileNavbar", () => { ); await user.click(screen.getByLabelText("open mobile navbar menu")); - const tops = screen.getAllByLabelText("mobile navbar top"); - tops.forEach(top => { - expect(top).toHaveClass(bgColor); - }); + // The bar over the page stays transparent; the open menu's own bar takes + // the menu's solid color so links scrolling under it stay hidden. + const [pageTop, menuTop] = screen.getAllByLabelText("mobile navbar top"); + expect(pageTop).toHaveClass(bgColor); + expect(menuTop).toHaveClass("bg-background-acc-1"); expect(screen.getByLabelText("mobile nav menu")).toHaveClass( "bg-background-acc-1", ); }); + + it("applies menu background color", async () => { + const bgColor = "bg-transparent"; + const menuBgColor = "bg-custom-menu-color"; + const { user } = setup( + + {children} + , + ); + + await user.click(screen.getByLabelText("open mobile navbar menu")); + const [pageTop, menuTop] = screen.getAllByLabelText("mobile navbar top"); + expect(pageTop).toHaveClass(bgColor); + expect(menuTop).toHaveClass(menuBgColor); + expect(screen.getByLabelText("mobile nav menu")).toHaveClass(menuBgColor); + }); }); From 69e21235b7e340d95ac14d2dd35eefac80a1b7c5 Mon Sep 17 00:00:00 2001 From: Taylor Bantle Date: Tue, 6 Oct 2026 10:12:07 -0700 Subject: [PATCH 08/12] Bump contexts --- packages/components/package.json | 2 +- yarn.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package.json b/packages/components/package.json index faaf582a..95032ad8 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -68,7 +68,7 @@ } }, "dependencies": { - "@dolthub/react-contexts": "^0.3.1", + "@dolthub/react-contexts": "^0.3.2", "@dolthub/react-hooks": "^0.3.2", "@dolthub/web-utils": "^0.3.2", "classnames": "^2.5.1", diff --git a/yarn.lock b/yarn.lock index 018b4b38..372c785d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2771,7 +2771,7 @@ __metadata: "@babel/preset-env": "npm:^8.0.2" "@babel/preset-react": "npm:^8.0.1" "@babel/preset-typescript": "npm:^8.0.1" - "@dolthub/react-contexts": "npm:^0.3.1" + "@dolthub/react-contexts": "npm:^0.3.2" "@dolthub/react-hooks": "npm:^0.3.2" "@dolthub/web-utils": "npm:^0.3.2" "@rollup/plugin-commonjs": "npm:^29.0.3" @@ -2852,7 +2852,7 @@ __metadata: languageName: unknown linkType: soft -"@dolthub/react-contexts@npm:^0.3.1, @dolthub/react-contexts@workspace:packages/contexts": +"@dolthub/react-contexts@npm:^0.3.2, @dolthub/react-contexts@workspace:packages/contexts": version: 0.0.0-use.local resolution: "@dolthub/react-contexts@workspace:packages/contexts" dependencies: From cd6a872317bc5682af629a8aa2079816a253f366 Mon Sep 17 00:00:00 2001 From: Taylor Bantle Date: Tue, 6 Oct 2026 10:15:31 -0700 Subject: [PATCH 09/12] Trim comments Also switches the desktop dropdown story to bg-space-900; its comment said the colour was not in the palette, which stopped being true when this branch added the token. Co-Authored-By: Claude Opus 5 (1M context) --- .../Navbar/ForDesktop/NavDropdown.module.css | 25 ++++++------------- .../src/Navbar/ForDesktop/NavDropdown.tsx | 17 +++++-------- .../src/Navbar/ForDesktop/index.module.css | 6 +---- .../src/Navbar/ForMobile/index.module.css | 13 +++------- .../DesktopNavDropdown.stories.tsx | 6 +---- .../src/__stories__/MobileNavbar.stories.tsx | 2 -- 6 files changed, 19 insertions(+), 50 deletions(-) diff --git a/packages/components/src/Navbar/ForDesktop/NavDropdown.module.css b/packages/components/src/Navbar/ForDesktop/NavDropdown.module.css index a5801aef..0bb14859 100644 --- a/packages/components/src/Navbar/ForDesktop/NavDropdown.module.css +++ b/packages/components/src/Navbar/ForDesktop/NavDropdown.module.css @@ -3,16 +3,11 @@ @apply flex self-stretch items-center; } -/* - * Wraps trigger + panel; the scrim sits outside it so it still covers the - * page. Always full height, so the trigger's underline lands on the navbar's - * bottom edge in both alignments. - */ +/* Wraps trigger + panel; the scrim stays outside so it still covers the page. */ .triggerWrap { @apply flex self-stretch items-center; } -/* Only the anchored alignments position the panel against this wrapper. */ .anchor { @apply relative; } @@ -22,11 +17,7 @@ @apply relative flex h-full items-center gap-2; } -/* - * Drawn on a pseudo-element so the trigger's box is identical open or closed. - * Padding or a real border would grow it and, under the row's `items-center`, - * nudge the label up out of line with the sibling links. - */ +/* A pseudo-element, so the box is unchanged and the label does not shift. */ .triggerOpen::after { @apply absolute inset-x-0 bottom-0 h-0.5 bg-sky-400; content: ""; @@ -45,8 +36,7 @@ @apply absolute left-0 right-0 top-full text-left; } -/* Anchored to the trigger rather than the header; order matters, these must - * follow `.panel` to win the left/right tie. */ +/* Must follow `.panel` to win the left/right tie. */ .panelLeft { @apply right-auto; } @@ -55,10 +45,11 @@ @apply left-auto right-0; } -/* Two classes, to outrank the navbar's `.inner a` / `.left a` link styling. */ -/* Deliberately silent on align-items: the navbar's `.inner a` centers content, - * which is what simple rows want. Panels needing top alignment set it on an - * inner element the navbar rules do not reach. */ +/* + * Two classes, to outrank the navbar's `.inner a` / `.left a` styling. Silent + * on align-items: `.inner a` already centers, and cards needing top alignment + * set it on an inner element these rules do not reach. + */ .dropdown .panel a, .dropdown .panel button { @apply mx-0 text-base font-normal tracking-normal text-white hover:text-white; diff --git a/packages/components/src/Navbar/ForDesktop/NavDropdown.tsx b/packages/components/src/Navbar/ForDesktop/NavDropdown.tsx index 8eec179b..3d10c2aa 100644 --- a/packages/components/src/Navbar/ForDesktop/NavDropdown.tsx +++ b/packages/components/src/Navbar/ForDesktop/NavDropdown.tsx @@ -8,14 +8,11 @@ import css from "./NavDropdown.module.css"; type Props = { label: ReactNode; children: ReactNode; - // "full" spans the page width. "left"/"right" anchor the panel to the - // trigger and size it to its content. + // "full" spans the page width; "left"/"right" anchor it to the trigger. align?: "full" | "left" | "right"; hideCaret?: boolean; - // Underlines the trigger while the panel is open. underlineOnOpen?: boolean; - // Opens on hover as well as click. Click still toggles, so the menu stays - // reachable by keyboard and on touch. + // Click still toggles, keeping the menu reachable by keyboard and on touch. openOnHover?: boolean; defaultOpen?: boolean; className?: string; @@ -26,8 +23,8 @@ type Props = { ["data-cy"]?: string; }; -// A nav item with a dropdown panel that dims the page beneath it. Must be -// rendered inside DesktopNavbar, which a full-width panel anchors to. +// A nav item whose panel dims the page beneath it. Must be rendered inside +// DesktopNavbar, which a full-width panel anchors to. export default function NavDropdown({ label, children, @@ -55,8 +52,7 @@ export default function NavDropdown({ useOnClickOutside(ref, () => setOpen(false)); - // Closing is delayed so the pointer can cross the gap between the trigger - // and the panel without the menu snapping shut. + // Delayed, so the pointer can cross the gap to the panel without it closing. const closeTimer = useRef | undefined>( undefined, ); @@ -68,8 +64,7 @@ export default function NavDropdown({ }; useEffect(() => cancelClose, []); - // Bound to the trigger/panel wrapper rather than the root: the scrim is a - // root child covering the page, so hovering it would pin the menu open. + // On the wrapper, not the root: the root's scrim would pin the menu open. const hoverProps = openOnHover ? { onMouseEnter: () => { diff --git a/packages/components/src/Navbar/ForDesktop/index.module.css b/packages/components/src/Navbar/ForDesktop/index.module.css index 97a74c8d..8e9d8178 100644 --- a/packages/components/src/Navbar/ForDesktop/index.module.css +++ b/packages/components/src/Navbar/ForDesktop/index.module.css @@ -28,11 +28,7 @@ } } -/* - * `gap` rather than margins on `a`: a dropdown trigger is a div, so - * margin-based spacing skipped it and left the row unevenly spaced. 48px - * matches the old ml-2 + mr-10 so existing navs keep their rhythm. - */ +/* `gap`, not margins on `a`: those skipped the div-wrapped dropdown triggers. */ .left { @apply flex self-stretch w-1/3 ml-2 order-first items-center gap-12; } diff --git a/packages/components/src/Navbar/ForMobile/index.module.css b/packages/components/src/Navbar/ForMobile/index.module.css index 713cc90a..a2e03f0e 100644 --- a/packages/components/src/Navbar/ForMobile/index.module.css +++ b/packages/components/src/Navbar/ForMobile/index.module.css @@ -40,10 +40,7 @@ @apply fixed top-0 bottom-0 left-0 right-0 z-100 flex flex-col overflow-y-auto overscroll-contain; } -/* - * Descendant rather than `> a`: sign-out buttons arrive wrapped in a div. - * Dropdown panel content is skipped so it keeps its own styling. - */ +/* Descendant, not `> a`: sign-out buttons arrive wrapped in a div. */ .links { @apply flex flex-col items-stretch w-full border-t border-white/10; @@ -53,12 +50,11 @@ } } -/* Like `.actions`, outside `.links` so the row styling never reaches it. */ .account { @apply shrink-0 px-6 pt-5 pb-2; } -/* Its own region, so the row styling above never reaches it. */ +/* Its own region, like `.account`, so the row styling never reaches it. */ .actions { @apply mt-auto shrink-0 px-6 pt-8 md:mx-auto md:w-[375px]; @@ -68,10 +64,7 @@ } } -/* - * `mt-auto`: bottom of a short menu, end of the content on a long one. After - * actions it sits right below them, so the two stay together at the bottom. - */ +/* `mt-auto`: bottom of a short menu, end of the content on a long one. */ .bottomLinks { @apply mt-auto flex justify-center gap-6 px-6 py-8; diff --git a/packages/components/src/__stories__/DesktopNavDropdown.stories.tsx b/packages/components/src/__stories__/DesktopNavDropdown.stories.tsx index 62b1a43e..e2268f68 100644 --- a/packages/components/src/__stories__/DesktopNavDropdown.stories.tsx +++ b/packages/components/src/__stories__/DesktopNavDropdown.stories.tsx @@ -18,8 +18,7 @@ export default meta; type Story = StoryObj; -// Not in the palette yet. -const navBg = "bg-[#070f25]"; +const navBg = "bg-space-900"; type Product = { name: string; description: string }; @@ -195,8 +194,6 @@ export const OpenedByClick: Story = { }, }; -// `align`: "left" and "right" anchor the panel to the trigger instead of -// spanning the page. export const AlignLeft: Story = { args: navArgs( productsMenu(false), @@ -271,7 +268,6 @@ function ControlledDocsMenu() { ); } -// `isOpen`/`setIsOpen` let the parent own the open state. export const Controlled: Story = { args: navArgs(productsMenu(false), ), decorators: [withHero], diff --git a/packages/components/src/__stories__/MobileNavbar.stories.tsx b/packages/components/src/__stories__/MobileNavbar.stories.tsx index c62827bd..56b0cfc0 100644 --- a/packages/components/src/__stories__/MobileNavbar.stories.tsx +++ b/packages/components/src/__stories__/MobileNavbar.stories.tsx @@ -337,8 +337,6 @@ function ControlledDropdown() { ); } -// `isOpen`/`setIsOpen` let the parent own the open state; `className` styles -// the dropdown's wrapper. export const OpenWithControlledDropdown: Story = { args: { ...dolthubArgs, From 649bb7c48ab5506dcf6d399cefc16a701289cd8e Mon Sep 17 00:00:00 2001 From: Taylor Bantle Date: Tue, 6 Oct 2026 13:22:37 -0700 Subject: [PATCH 10/12] Add pill mobile navbar actions --- .../components/src/Navbar/ForMobile/index.module.css | 7 +++++++ packages/components/src/Navbar/ForMobile/index.tsx | 9 ++++++++- packages/components/src/Navbar/index.tsx | 2 ++ .../src/__stories__/MobileNavbar.stories.tsx | 10 ++++++++++ 4 files changed, 27 insertions(+), 1 deletion(-) diff --git a/packages/components/src/Navbar/ForMobile/index.module.css b/packages/components/src/Navbar/ForMobile/index.module.css index a2e03f0e..643a74ce 100644 --- a/packages/components/src/Navbar/ForMobile/index.module.css +++ b/packages/components/src/Navbar/ForMobile/index.module.css @@ -64,6 +64,13 @@ } } +.pillActions { + a, + button { + @apply rounded-full; + } +} + /* `mt-auto`: bottom of a short menu, end of the content on a long one. */ .bottomLinks { @apply mt-auto flex justify-center gap-6 px-6 py-8; diff --git a/packages/components/src/Navbar/ForMobile/index.tsx b/packages/components/src/Navbar/ForMobile/index.tsx index 64c5438e..1318234d 100644 --- a/packages/components/src/Navbar/ForMobile/index.tsx +++ b/packages/components/src/Navbar/ForMobile/index.tsx @@ -14,6 +14,7 @@ type Props = CommonProps & { dark?: boolean; children: ReactNode; mobileActions?: ReactNode; + mobileActionsPill?: boolean; mobileAccount?: ReactNode; mobileBottomLinks?: ReactNode; }; @@ -51,6 +52,7 @@ type NavProps = CommonProps & { onClose: () => void; children: ReactNode; mobileActions?: ReactNode; + mobileActionsPill?: boolean; mobileAccount?: ReactNode; mobileBottomLinks?: ReactNode; }; @@ -83,7 +85,12 @@ function NavMenu(props: NavProps) {
)} {props.mobileActions && ( -
+
{props.mobileActions}
)} diff --git a/packages/components/src/Navbar/index.tsx b/packages/components/src/Navbar/index.tsx index d07af80e..c2425c77 100644 --- a/packages/components/src/Navbar/index.tsx +++ b/packages/components/src/Navbar/index.tsx @@ -15,6 +15,7 @@ type Props = { // Mobile-only mobileActions?: ReactNode; // Full-width call to action below the links + mobileActionsPill?: boolean; // Use pill-shaped corners for mobile actions mobileAccount?: ReactNode; // Signed in user's section between the links and actions mobileMenuBgColor?: string; // Open menu's background, when it should differ from `bgColor` mobileBottomLinks?: ReactNode; @@ -38,6 +39,7 @@ export default function Navbar(props: Props) { bgColor={props.bgColor} logo={props.logo} mobileActions={props.mobileActions} + mobileActionsPill={props.mobileActionsPill} mobileAccount={props.mobileAccount} menuBgColor={props.mobileMenuBgColor} mobileBottomLinks={props.mobileBottomLinks} diff --git a/packages/components/src/__stories__/MobileNavbar.stories.tsx b/packages/components/src/__stories__/MobileNavbar.stories.tsx index 56b0cfc0..372bda62 100644 --- a/packages/components/src/__stories__/MobileNavbar.stories.tsx +++ b/packages/components/src/__stories__/MobileNavbar.stories.tsx @@ -234,6 +234,16 @@ export const OpenWithSignIn: Story = { play: async ({ canvasElement }) => openMobileMenu(canvasElement), }; +export const OpenWithPillActions: Story = { + args: { + ...dolthubArgs, + rightLinksMobile: <>, + mobileActions: signInAction, + mobileActionsPill: true, + }, + play: async ({ canvasElement }) => openMobileMenu(canvasElement), +}; + export const OpenWithDropdownExpanded: Story = { args: { ...dolthubArgs, From df8129384465bf30885e6c15da8dc2937160667f Mon Sep 17 00:00:00 2001 From: Taylor Bantle Date: Tue, 6 Oct 2026 13:34:29 -0700 Subject: [PATCH 11/12] Cut CSS comments down to the load-bearing ones Co-Authored-By: Claude Opus 5 (1M context) --- .../src/Navbar/ForDesktop/NavDropdown.module.css | 9 +-------- .../components/src/Navbar/ForMobile/index.module.css | 4 ---- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/packages/components/src/Navbar/ForDesktop/NavDropdown.module.css b/packages/components/src/Navbar/ForDesktop/NavDropdown.module.css index 0bb14859..c44196b0 100644 --- a/packages/components/src/Navbar/ForDesktop/NavDropdown.module.css +++ b/packages/components/src/Navbar/ForDesktop/NavDropdown.module.css @@ -3,7 +3,6 @@ @apply flex self-stretch items-center; } -/* Wraps trigger + panel; the scrim stays outside so it still covers the page. */ .triggerWrap { @apply flex self-stretch items-center; } @@ -12,7 +11,6 @@ @apply relative; } -/* Full height so the underline below can sit on the navbar's bottom edge. */ .trigger { @apply relative flex h-full items-center gap-2; } @@ -27,7 +25,6 @@ @apply shrink-0 text-sm; } -/* Starts below the navbar so the bar itself is not dimmed. */ .scrim { @apply absolute left-0 right-0 top-full h-screen bg-[rgba(0,0,0,0.56)]; } @@ -45,11 +42,7 @@ @apply left-auto right-0; } -/* - * Two classes, to outrank the navbar's `.inner a` / `.left a` styling. Silent - * on align-items: `.inner a` already centers, and cards needing top alignment - * set it on an inner element these rules do not reach. - */ +/* Two classes, to outrank the navbar's `.inner a` / `.left a` styling. */ .dropdown .panel a, .dropdown .panel button { @apply mx-0 text-base font-normal tracking-normal text-white hover:text-white; diff --git a/packages/components/src/Navbar/ForMobile/index.module.css b/packages/components/src/Navbar/ForMobile/index.module.css index 643a74ce..db6a0a69 100644 --- a/packages/components/src/Navbar/ForMobile/index.module.css +++ b/packages/components/src/Navbar/ForMobile/index.module.css @@ -6,7 +6,6 @@ @apply w-full text-white px-5 py-2 h-[62px] shrink-0; } -/* Keeps the close button reachable while a long menu scrolls under it. */ .menuTop { @apply sticky top-0 z-10; } @@ -19,7 +18,6 @@ } } -/* A white chip would wash out here, so keep the plain icon. */ .darkTop { button { @apply text-background-acc-1 bg-transparent hover:text-primary; @@ -54,7 +52,6 @@ @apply shrink-0 px-6 pt-5 pb-2; } -/* Its own region, like `.account`, so the row styling never reaches it. */ .actions { @apply mt-auto shrink-0 px-6 pt-8 md:mx-auto md:w-[375px]; @@ -71,7 +68,6 @@ } } -/* `mt-auto`: bottom of a short menu, end of the content on a long one. */ .bottomLinks { @apply mt-auto flex justify-center gap-6 px-6 py-8; From 599532d16985527c9710825fb3e9355171c0cc6e Mon Sep 17 00:00:00 2001 From: Taylor Bantle Date: Tue, 6 Oct 2026 14:10:01 -0700 Subject: [PATCH 12/12] Remove mobileMenuBgColor DoltHub was its only consumer and no longer needs it now that its navbar is always opaque. The open menu's sticky bar still takes the menu's color. Co-Authored-By: Claude Opus 5.5 --- .../components/src/Navbar/ForMobile/index.tsx | 3 +-- packages/components/src/Navbar/index.tsx | 2 -- .../src/__stories__/MobileNavbar.stories.tsx | 19 ------------------- .../components/src/__tests__/Navbar.test.tsx | 16 ---------------- 4 files changed, 1 insertion(+), 39 deletions(-) diff --git a/packages/components/src/Navbar/ForMobile/index.tsx b/packages/components/src/Navbar/ForMobile/index.tsx index 1318234d..78c2fa28 100644 --- a/packages/components/src/Navbar/ForMobile/index.tsx +++ b/packages/components/src/Navbar/ForMobile/index.tsx @@ -7,7 +7,6 @@ import css from "./index.module.css"; type CommonProps = { logo: ReactNode; bgColor?: string; - menuBgColor?: string; }; type Props = CommonProps & { @@ -58,7 +57,7 @@ type NavProps = CommonProps & { }; function NavMenu(props: NavProps) { - const menuBgColor = props.menuBgColor ?? getBgColor(props.bgColor, true); + const menuBgColor = getBgColor(props.bgColor, true); return (
{/* The menu's own color, so links scrolling under the sticky bar stay hidden. */} diff --git a/packages/components/src/Navbar/index.tsx b/packages/components/src/Navbar/index.tsx index c2425c77..61b950b5 100644 --- a/packages/components/src/Navbar/index.tsx +++ b/packages/components/src/Navbar/index.tsx @@ -17,7 +17,6 @@ type Props = { mobileActions?: ReactNode; // Full-width call to action below the links mobileActionsPill?: boolean; // Use pill-shaped corners for mobile actions mobileAccount?: ReactNode; // Signed in user's section between the links and actions - mobileMenuBgColor?: string; // Open menu's background, when it should differ from `bgColor` mobileBottomLinks?: ReactNode; leftLinksMobile?: ReactNode; // Overrides `leftLinks` for mobile rightLinksMobile?: ReactNode; // Overrides `rightLinks` for mobile @@ -41,7 +40,6 @@ export default function Navbar(props: Props) { mobileActions={props.mobileActions} mobileActionsPill={props.mobileActionsPill} mobileAccount={props.mobileAccount} - menuBgColor={props.mobileMenuBgColor} mobileBottomLinks={props.mobileBottomLinks} dark={props.dark} > diff --git a/packages/components/src/__stories__/MobileNavbar.stories.tsx b/packages/components/src/__stories__/MobileNavbar.stories.tsx index 372bda62..3b96a37a 100644 --- a/packages/components/src/__stories__/MobileNavbar.stories.tsx +++ b/packages/components/src/__stories__/MobileNavbar.stories.tsx @@ -301,25 +301,6 @@ export const OpenWithAccount: Story = { }, }; -// A transparent bar over a hero still opens onto a solid menu. -export const OpenWithMenuBgColor: Story = { - args: { - ...dolthubArgs, - bgColor: "bg-transparent", - mobileMenuBgColor: "bg-space-900", - leftLinksMobile: withDropdown(false), - rightLinksMobile: <>, - mobileActions: signInAction, - }, - play: async ({ canvasElement }) => { - await openMobileMenu(canvasElement); - const canvas = within(canvasElement); - await expect(canvas.getByLabelText("mobile nav menu")).toHaveClass( - "bg-space-900", - ); - }, -}; - function ControlledDropdown() { const [open, setOpen] = useState(false); return ( diff --git a/packages/components/src/__tests__/Navbar.test.tsx b/packages/components/src/__tests__/Navbar.test.tsx index c749193e..39f1ba18 100644 --- a/packages/components/src/__tests__/Navbar.test.tsx +++ b/packages/components/src/__tests__/Navbar.test.tsx @@ -143,20 +143,4 @@ describe("test MobileNavbar", () => { "bg-background-acc-1", ); }); - - it("applies menu background color", async () => { - const bgColor = "bg-transparent"; - const menuBgColor = "bg-custom-menu-color"; - const { user } = setup( - - {children} - , - ); - - await user.click(screen.getByLabelText("open mobile navbar menu")); - const [pageTop, menuTop] = screen.getAllByLabelText("mobile navbar top"); - expect(pageTop).toHaveClass(bgColor); - expect(menuTop).toHaveClass(menuBgColor); - expect(screen.getByLabelText("mobile nav menu")).toHaveClass(menuBgColor); - }); });