# Floating nav, blog search bar & crescent toggle — Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Restyle the dark toggle to a crescent moon everywhere, add a visible `⌘K` search pill to the blog hero, and turn the marketing nav into a floating pill on every page except `/blog` and `/docs`.

**Architecture:** Three shared/leaf component edits plus one pathname-aware branch in `Navbar`. The dark toggle is a shared component, so restyling it propagates to nav, footer, docs top bar, and mobile. `SiteSearch` gains a presentational `variant` prop reusing its existing dialog. `Navbar` reads `usePathname()` and renders the current full-width bar on `/blog`, the floating pill elsewhere.

**Tech Stack:** Next.js App Router (client components), Tailwind (token classes: `text-ink-*`, `bg-background`, `bg-surface-2`, `ring-hairline`, `border-hairline`, `bg-foreground/5`), Headless UI, `@tabler/icons-react`, `lucide-react`, `fuse.js`, custom `useTheme` from `providers/theme-provider`.

## Global Constraints

- Branch: `feature/floating-nav-blog-search` (already created off `dev`). Do all work here.
- No new dependencies — everything needed is installed (`motion@^12`, `@tabler/icons-react`, `@headlessui/react`, `lucide-react`, `fuse.js`).
- No unit-test runner exists. Per-task verification = `npx tsc --noEmit` (typecheck) + `npm run format` (Prettier) + a d3k browser visual check. There is no `next lint` script; do not invent tests.
- Conventional Commits. End every commit message with:
  `Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>`
- Stage ONLY the files listed per task. The working tree has pre-existing unrelated changes (deleted `.claude/skills/next-best-practices/*`, modified `docs/assets/navbar.tsx`) — never `git add -A`; never stage those.
- Nav items and destinations do NOT change on any variant. Don't touch the docs layout/nav.

## Dev server for visual checks

Start once before Task 1 and keep it running (used by every task's visual step):

```bash
d3k agent-browser close --all
d3k --no-agent --no-skills --servers-only --command "npm run dev -- -H 127.0.0.1 -p 3000" --port 3000 --startup-timeout 90 --no-tui
```

Then in a separate step: `curl -I http://127.0.0.1:3000` should return `200`/`307`. Routes used below: `http://127.0.0.1:3000/en` (floating nav), `/en/blog` (normal nav + search pill), `/en/docs/quickstart` (toggle restyle).

---

### Task 1: Crescent-moon dark toggle (shared)

**Files:**
- Modify: `components/shared/mode-toggle.tsx` (full rewrite, 27 lines)

**Interfaces:**
- Consumes: `useTheme()` → `{ resolvedTheme: "light" | "dark", setTheme(t) }` from `@/providers/theme-provider`.
- Produces: `ModeToggle` (named export, no props) — unchanged signature; only visuals change. Used by `navbar.tsx`, `footer.tsx`, `docs-top-bar.tsx`.

- [ ] **Step 1: Rewrite the component**

Replace the entire contents of `components/shared/mode-toggle.tsx` with:

```tsx
"use client";

import React from "react";
import { MoonIcon, SunIcon } from "lucide-react";

import { useTheme } from "@/providers/theme-provider";

export const ModeToggle = () => {
  const { resolvedTheme, setTheme } = useTheme();

  return (
    <button
      onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}
      aria-label="Toggle theme"
      className="text-ink-3 hover:text-ink-1 relative flex size-5 items-center justify-center transition-colors"
    >
      <MoonIcon
        size={18}
        strokeWidth={1.5}
        className="absolute inset-0 m-auto scale-100 rotate-0 transition-all duration-200 dark:scale-0 dark:-rotate-90"
      />
      <SunIcon
        size={18}
        strokeWidth={1.5}
        className="absolute inset-0 m-auto scale-0 rotate-90 transition-all duration-200 dark:scale-100 dark:rotate-0"
      />
    </button>
  );
};
```

Rationale: thinner stroke (`strokeWidth={1.5}`) + larger size gives the slim crescent from the screenshot; the light/dark swap and `aria-label` are preserved for affordance and a11y.

- [ ] **Step 2: Typecheck**

Run: `npx tsc --noEmit`
Expected: exits 0, no errors referencing `mode-toggle.tsx`.

- [ ] **Step 3: Format**

Run: `npm run format`
Expected: Prettier reports the file formatted/unchanged.

- [ ] **Step 4: Visual check (light + dark)**

```bash
d3k agent-browser open http://127.0.0.1:3000/en
d3k agent-browser screenshot /tmp/toggle-light.png
```
Expected: a thin crescent-moon icon in the top bar. Click it and confirm the page switches to dark and the icon swaps to a sun.

- [ ] **Step 5: Commit**

```bash
git add components/shared/mode-toggle.tsx
git commit -m "style(mode-toggle): slim crescent-moon dark toggle

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```

---

### Task 2: `SiteSearch` visible bar variant

**Files:**
- Modify: `components/shared/site-search.tsx` (add `variant` prop + branch the trigger)

**Interfaces:**
- Consumes: `DocsSearchRecord[]` from `@/lib/search/site-index` (unchanged).
- Produces: `SiteSearch` now accepts an optional `variant?: "icon" | "bar"` (default `"icon"`). `"icon"` = today's icon-only button (unchanged). `"bar"` = a full-width rounded pill trigger showing magnifier + placeholder + `⌘K` chip; both open the same dialog. Consumed by `navbar.tsx` (default/icon) and `blog/page.tsx` (`variant="bar"`).

- [ ] **Step 1: Add `variant` to the props type**

In `components/shared/site-search.tsx`, change the props object of `SiteSearch` from:

```tsx
export function SiteSearch({
  records,
  lang,
  label,
  placeholder,
  className,
}: {
  records: DocsSearchRecord[];
  lang: string;
  label: string;
  placeholder: string;
  className?: string;
}) {
```

to:

```tsx
export function SiteSearch({
  records,
  lang,
  label,
  placeholder,
  className,
  variant = "icon",
}: {
  records: DocsSearchRecord[];
  lang: string;
  label: string;
  placeholder: string;
  className?: string;
  variant?: "icon" | "bar";
}) {
```

- [ ] **Step 2: Branch the trigger button**

Replace the existing trigger `<button>` (the one with `aria-label={label}` that wraps the `<IconSearch className="size-5" />`, currently the first child inside the returned `<>`) with this conditional block:

```tsx
      {variant === "bar" ? (
        <button
          type="button"
          onClick={() => setOpen(true)}
          aria-label={label}
          className={`border-hairline hover:bg-foreground/5 group flex w-full items-center gap-3 rounded-full border bg-transparent px-4 py-2.5 text-left transition-colors ${className ?? ""}`}
        >
          <IconSearch className="text-ink-4 size-5 shrink-0" />
          <span className="text-ink-4 flex-1 text-sm">{placeholder}</span>
          <kbd className="text-ink-4 border-hairline hidden rounded border px-1.5 py-0.5 text-[11px] font-medium sm:inline-block">
            ⌘K
          </kbd>
        </button>
      ) : (
        <button
          type="button"
          onClick={() => setOpen(true)}
          aria-label={label}
          className={`text-ink-2 hover:bg-foreground/5 -m-2.5 inline-flex items-center justify-center rounded-lg p-2.5 transition-colors ${className ?? ""}`}
        >
          <IconSearch className="size-5" />
        </button>
      )}
```

Leave the `<Dialog>…</Dialog>` block and all hooks below it unchanged.

- [ ] **Step 3: Typecheck**

Run: `npx tsc --noEmit`
Expected: exits 0. (Existing `<SiteSearch>` usages in `navbar.tsx` omit `variant` and default to `"icon"` — no call-site changes needed.)

- [ ] **Step 4: Format**

Run: `npm run format`
Expected: Prettier formats/leaves the file clean.

- [ ] **Step 5: Visual check (icon variant unchanged)**

```bash
d3k agent-browser open http://127.0.0.1:3000/en
d3k agent-browser snapshot -i
```
Expected: the nav still shows the icon-only search trigger and `⌘K` still opens the dialog (the bar variant is verified in Task 3).

- [ ] **Step 6: Commit**

```bash
git add components/shared/site-search.tsx
git commit -m "feat(site-search): add visible bar variant with cmdk hint

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```

---

### Task 3: Blog hero search bar

**Files:**
- Modify: `app/[lang]/(marketing)/blog/page.tsx` (imports, build search index, render bar in hero)
- Modify: `components/blog/blog.module.css` (add `.heroSearch`)

**Interfaces:**
- Consumes: `SiteSearch` with `variant="bar"` (Task 2); `buildSiteIndex(dict, lang)` from `@/lib/search/site-index`; `buildDocsSearchIndex()` from `@/lib/docs/search-index` (same calls the marketing layout already uses); `dict.nav.searchLabel` / `dict.nav.searchPlaceholder` from the full dictionary.
- Produces: nothing consumed downstream.

- [ ] **Step 1: Add the `.heroSearch` style**

Append to `components/blog/blog.module.css`:

```css
.heroSearch {
  margin-top: 24px;
  max-width: 520px;
}
```

- [ ] **Step 2: Add imports to the blog page**

In `app/[lang]/(marketing)/blog/page.tsx`, add these imports alongside the existing ones:

```tsx
import { SiteSearch } from "@/components/shared/site-search";
import { buildDocsSearchIndex } from "@/lib/docs/search-index";
import { buildSiteIndex } from "@/lib/search/site-index";
```

- [ ] **Step 3: Build the search index in the page body**

In `BlogIndexPage`, the current lines are:

```tsx
  const { lang } = await params;
  if (!hasLocale(lang)) notFound();
  const dict = (await getDictionary(lang)).blog;
```

Replace them with (keep the full dictionary so we can read `nav` labels and build the index):

```tsx
  const { lang } = await params;
  if (!hasLocale(lang)) notFound();
  const fullDict = await getDictionary(lang);
  const dict = fullDict.blog;
  const searchRecords = [
    ...buildSiteIndex(fullDict, lang),
    ...buildDocsSearchIndex().filter((r) => r.lang === lang),
  ];
```

- [ ] **Step 4: Render the bar in the hero**

In the hero `<Reveal className={styles.reveal}>` block, immediately after the lede paragraph:

```tsx
            <p className={styles.lede}>{dict.heroLede}</p>
```

add:

```tsx
            <div className={styles.heroSearch}>
              <SiteSearch
                variant="bar"
                records={searchRecords}
                lang={lang}
                label={fullDict.nav.searchLabel}
                placeholder={fullDict.nav.searchPlaceholder}
              />
            </div>
```

- [ ] **Step 5: Typecheck**

Run: `npx tsc --noEmit`
Expected: exits 0. (Confirms `fullDict.nav.searchLabel`/`searchPlaceholder` exist on the dictionary type — they do, used by the layout/navbar today.)

- [ ] **Step 6: Format**

Run: `npm run format`

- [ ] **Step 7: Visual check**

```bash
d3k agent-browser open http://127.0.0.1:3000/en/blog
d3k agent-browser screenshot /tmp/blog-search.png
d3k errors
```
Expected: a centered rounded search pill (magnifier + "Search a page…" + `⌘K` chip) under the hero lede, max ~520px wide. Clicking it (and pressing `⌘K`) opens the search dialog. No console/server errors.

- [ ] **Step 8: Commit**

```bash
git add "app/[lang]/(marketing)/blog/page.tsx" components/blog/blog.module.css
git commit -m "feat(blog): visible search bar in the hero

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```

---

### Task 4: Floating pill nav (everywhere except `/blog`)

**Files:**
- Modify: `components/layout/navbar.tsx` (pathname branch in `Navbar`; `floating` prop on `DesktopNavbar`)

**Interfaces:**
- Consumes: `usePathname` from `next/navigation`; existing `DesktopNavbar`/`MobileNavbar`, `Container`, `Logo`, `SiteSearch` (icon variant), `LanguageSwitcher`, `ModeToggle`.
- Produces: `Navbar` visual variants only — props unchanged (`{ dict, lang, records }`). Marketing layout needs no change.

- [ ] **Step 1: Import `usePathname`**

At the top of `components/layout/navbar.tsx`, add to the `next/navigation`-adjacent imports:

```tsx
import { usePathname } from "next/navigation";
```

(`Link` from `next/link` is already imported; add the line above near the other `next` imports.)

- [ ] **Step 2: Branch `Navbar` on pathname**

Replace the current `Navbar` export:

```tsx
export const Navbar = ({ dict, lang, records }: NavProps) => {
  return (
    <>
      <div className="bg-background/72 border-hairline fixed inset-x-0 top-0 z-50 border-b backdrop-blur-xl">
        <DesktopNavbar dict={dict} lang={lang} records={records} />
        <MobileNavbar dict={dict} lang={lang} records={records} />
      </div>
      {/* Spacer to offset the fixed bar (mobile py-2 vs desktop py-4 heights). */}
      <div aria-hidden className="h-[57px] lg:h-[73px]" />
    </>
  );
};
```

with:

```tsx
export const Navbar = ({ dict, lang, records }: NavProps) => {
  const pathname = usePathname();
  // Blog keeps the full-width bar; everything else gets the floating pill.
  const isBlog = /^\/[^/]+\/blog(\/|$)/.test(pathname ?? "");

  if (isBlog) {
    return (
      <>
        <div className="bg-background/72 border-hairline fixed inset-x-0 top-0 z-50 border-b backdrop-blur-xl">
          <DesktopNavbar dict={dict} lang={lang} records={records} />
          <MobileNavbar dict={dict} lang={lang} records={records} />
        </div>
        {/* Spacer to offset the fixed bar (mobile py-2 vs desktop py-4 heights). */}
        <div aria-hidden className="h-[57px] lg:h-[73px]" />
      </>
    );
  }

  return (
    <>
      <div className="fixed inset-x-0 top-0 z-50">
        <div className="mx-auto mt-4 max-w-6xl px-4">
          <div className="bg-background/80 ring-hairline rounded-full shadow-lg ring-1 backdrop-blur-xl">
            <DesktopNavbar dict={dict} lang={lang} records={records} floating />
            <MobileNavbar dict={dict} lang={lang} records={records} />
          </div>
        </div>
      </div>
      {/* Spacer offsets the floating pill (mt-4 + bar height). */}
      <div aria-hidden className="h-[73px] lg:h-[89px]" />
    </>
  );
};
```

- [ ] **Step 3: Add `floating` prop to `DesktopNavbar`**

The current `DesktopNavbar` wraps everything in `<Container className="hidden items-center justify-between py-4 lg:flex">`. Refactor so the inner content is shared and the wrapper depends on `floating`. Change the signature and the outer wrapper:

Signature — from:

```tsx
const DesktopNavbar = ({ dict, lang, records }: NavProps) => {
  const navlinks = useNavlinks(dict, lang);
  const services = useServices(dict, lang);
  return (
    <Container className="hidden items-center justify-between py-4 lg:flex">
```

to:

```tsx
const DesktopNavbar = ({
  dict,
  lang,
  records,
  floating,
}: NavProps & { floating?: boolean }) => {
  const navlinks = useNavlinks(dict, lang);
  const services = useServices(dict, lang);
  const Wrapper = floating ? "div" : Container;
  const wrapperClass = floating
    ? "hidden items-center justify-between px-6 py-3 lg:flex"
    : "hidden items-center justify-between py-4 lg:flex";
  return (
    <Wrapper className={wrapperClass}>
```

Then change the matching closing tag at the end of `DesktopNavbar` from `</Container>` to `</Wrapper>`.

(In the floating pill the parent already constrains width to `max-w-6xl`, so the inner uses plain padding instead of `Container`'s `max-w-7xl`.)

- [ ] **Step 4: Typecheck**

Run: `npx tsc --noEmit`
Expected: exits 0. (`Wrapper` is `"div" | typeof Container`; both accept `className` + `children`.)

- [ ] **Step 5: Format**

Run: `npm run format`

- [ ] **Step 6: Visual check — floating + blog + content offset**

```bash
d3k agent-browser open http://127.0.0.1:3000/en
d3k agent-browser screenshot /tmp/nav-floating.png
d3k agent-browser open http://127.0.0.1:3000/en/blog
d3k agent-browser screenshot /tmp/nav-blog.png
d3k errors
```
Expected:
- `/en`: a centered, detached, rounded-full nav pill with a shadow, gap below the viewport top; Services dropdown still opens; page content not hidden behind it.
- `/en/blog`: the original full-width bar (NOT a pill), plus the hero search from Task 3.
- No console/server errors. Resize the browser narrow to confirm the mobile pill + drawer still work.

- [ ] **Step 7: Commit**

```bash
git add components/layout/navbar.tsx
git commit -m "feat(navbar): floating pill nav, full-width bar kept on blog

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>"
```

---

### Final verification (after all tasks)

- [ ] **Production build**

Run: `npm run build`
Expected: build succeeds with no type errors. (Catches anything `tsc --noEmit` plus dev mode missed — e.g. RSC/client boundary issues from importing `SiteSearch` into the server blog page; it's a client component imported into a server component, which is valid.)

- [ ] **Cross-route visual pass**: `/en` (pill), `/en/blog` (bar + hero search), `/en/docs/quickstart` (crescent toggle in docs top bar), each in light and dark. Confirm `⌘K` works from nav icon and blog bar.

---

## Self-Review

**Spec coverage:**
- Floating nav on marketing except blog/docs → Task 4 (pathname branch; docs untouched, separate layout). ✓
- Blog visible search bar → Tasks 2 + 3. ✓
- Crescent toggle everywhere → Task 1 (shared component). ✓
- Keep nav items unchanged → Task 4 reuses existing `DesktopNavbar` content; only the wrapper changes. ✓
- No new deps, no docs-layout edits, blog index only → respected. ✓

**Placeholder scan:** No TBD/TODO; every code step shows full content. ✓

**Type consistency:** `variant?: "icon" | "bar"` defined in Task 2 and consumed in Task 3; `floating?: boolean` defined and used in Task 4; `searchRecords` built with the exact `buildSiteIndex`/`buildDocsSearchIndex` calls the layout already uses. ✓
