Files
pansy/web/src/components/layout/AppShell.tsx
T
steveandClaude Opus 4.8 0f3dedab73 Address Gadfly review findings on Phase 0
Applied the fixes warranted by the adversarial review of PR #21 (all
findings graded in the gadfly store):

Store (highest impact):
- buildDSN always merges busy_timeout/journal_mode/foreign_keys pragmas
  into the DSN, even for file: URIs or paths with a query string — the
  prior passthrough silently disabled FK enforcement for those PANSY_DB
  values. Also escape '#' in the path and tighten in-memory detection to
  an exact ':memory:' token or mode=memory param (no substring misfire).
- migrate.go: detect duplicate migration versions with a clear error;
  wrap appliedVersions' rows.Err() with the store: prefix.

API:
- SetTrustedProxies failure now falls back to trust-none instead of
  leaving gin's trust-everyone default (X-Forwarded-For spoofing).
- SPA: 404 embedded directories (was a directory listing), 404 bare /api,
  add X-Content-Type-Options: nosniff, cache index.html bytes once at
  startup, single fs.Stat existence check, shared writeAPIError helper.
- Move gin.SetMode out of package init() into New().

Config: validate PANSY_PORT range (fall back to 8080 with a warning).

Web:
- api.ts: serialize the request body before the fetch try so a
  JSON.stringify failure isn't misreported as a network error.
- AppShell: move state-specific/conflicting utilities into
  active/inactiveProps so concatenated Tailwind classes don't collide.

Tests: added store DSN-pragma/memory-detection cases and SPA
directory-404 case. go build/vet/test clean (CGO off); web tsc + build
clean; re-verified in a browser (SPA, deep links, /assets 404, nav).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi
2026-07-18 15:19:30 -04:00

52 lines
1.8 KiB
TypeScript

import { Link, Outlet } from '@tanstack/react-router'
const navLinks = [
{ to: '/gardens', label: 'Gardens' },
{ to: '/plants', label: 'Plants' },
] as const
// TanStack Router concatenates the base className with activeProps/inactiveProps,
// so state-specific and conflicting utilities (text-muted vs text-fg) live in the
// state props — never in the base — to avoid ambiguous overrides.
const navLinkBase = 'rounded-md px-3 py-1.5 text-sm font-medium transition-colors'
const navLinkActive = 'bg-border/60 text-fg'
const navLinkInactive = 'text-muted hover:bg-border/60 hover:text-fg'
/** Top-level chrome: a sticky nav bar plus the routed page in an <Outlet>. */
export function AppShell() {
return (
<div className="flex min-h-full flex-col">
<header className="sticky top-0 z-10 border-b border-border bg-surface/90 backdrop-blur">
<nav className="mx-auto flex max-w-5xl items-center gap-4 px-4 py-3">
<Link to="/gardens" className="text-lg font-semibold text-accent-strong">
🌱 pansy
</Link>
<div className="flex flex-1 items-center gap-1">
{navLinks.map((l) => (
<Link
key={l.to}
to={l.to}
className={navLinkBase}
activeProps={{ className: navLinkActive }}
inactiveProps={{ className: navLinkInactive }}
>
{l.label}
</Link>
))}
</div>
<Link
to="/login"
className="rounded-md px-3 py-1.5 text-sm font-medium text-muted transition-colors hover:text-fg"
>
Sign in
</Link>
</nav>
</header>
<main className="mx-auto w-full max-w-5xl flex-1 px-4 py-6">
<Outlet />
</main>
</div>
)
}