boilerplate-website/CLAUDE.md
Padreug fc439eda08 docs(CLAUDE.md): boilerplate-wide guidance for Claude sessions
Captures hard-won lessons from forking the first site (chateau du
faune) off this boilerplate so future sessions don't relitigate them:

- Default to shadcn-vue primitives over hand-rolled UI. Lists the
  installed family (Button, Card, Badge, NavigationMenu, Sheet, Alert)
  and the as-child pattern for wrapping RouterLink / anchor.
- Prefer semantic theme tokens (bg-background, text-foreground,
  border-border, …) over colour literals so site retheming is one
  CSS variable edit instead of a global class sweep.
- Document the shadcn-vue CLI / nix-corepack conflict and the
  registry-JSON workaround.
- Two consistent fixes when mirroring from the registry: lucide
  imports (lucide-vue-next → @lucide/vue) and Tailwind v4 CSS
  variable wrapping (h-[--name] → h-[var(--name)]), the latter
  filed upstream as unovue/shadcn-vue#1853.
- tsconfig + components.json setup for when the CLI does work
  (baseUrl + paths in both tsconfigs, ignoreDeprecations 6.0, no
  framework key, no empty tailwind.config).
- Asset / hero / frosted-glass nav patterns we use and reuse.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 00:44:47 +02:00

6.8 KiB

boilerplate-website / CLAUDE.md

Guidance for Claude sessions working in this repo (and any aiolabs site forked off main). Stack: Vue 3.5 + Vite 8 + TypeScript 6 + Tailwind 4

  • shadcn-vue + reka-ui + vue-router + vue-i18n + Pinia.

Reach for shadcn-vue primitives before hand-rolling

Default: if a UI pattern has a shadcn-vue primitive, use it. Don't roll your own.

Installed (or known to work) primitives in this branch:

Primitive Use for
Button (gold-pill variants tuned for this brand) Every CTA. Use as-child to wrap a <RouterLink> or <a> so the link is the click target.
Card + CardContent + CardHeader + CardTitle Any bordered content block. Pattern: <Card class="p-6">...</Card> for prose cards; <Card class="overflow-hidden">...<img/><CardContent>...</CardContent></Card> for image-bearing cards.
Badge (default/secondary/destructive/outline) Status pills (Open / Coming soon / labels).
NavigationMenu family Desktop top-nav with dropdown groups. Opens on hover/focus, not click — that's by design.
Sheet family Mobile/side panels. Use with <SheetTrigger as-child> wrapping a hamburger button.
Alert + AlertTitle + AlertDescription Callout asides, inline notices.

When you need a new primitive, prefer it over a custom solution unless the trade-off is well-justified in a comment.

The shadcn-vue CLI is broken on nix-managed pnpm

pnpm dlx shadcn-vue@latest add <name> invokes corepack pnpm add ... under the hood, which conflicts with our nix-store pnpm and exits with a vague "Run 'pnpm approve-builds'…" error. The components themselves install fine; only the dep-install step fails.

Workaround: fetch the component JSON from the registry directly and write the files yourself:

# Find the URL
curl -s https://www.shadcn-vue.com/r/styles/default/<name>.json

Each file lives at files[].path / files[].content inside that JSON. Write to src/components/ui/<name>/ matching the path. Then pnpm add <runtime-deps> manually for anything in the registry's dependencies field (typically reka-ui and @vueuse/core, both already installed).

Two fixes to apply when copying from the registry

  1. lucide imports: the registry ships import { X } from "lucide-vue-next" but our project uses @lucide/vue. Swap the path before saving the file.
  2. Tailwind v4 CSS-variable syntax: the registry uses v3 arbitrary values like h-[--reka-navigation-menu-viewport-height]. Under Tailwind v4 the inner string is treated as a literal — the value resolves to nothing and the element collapses. Wrap with var(): h-[var(--reka-navigation-menu-viewport-height)]. Reported upstream as unovue/shadcn-vue#1853.

Semantic theme-aware Tailwind, never colour literals

The shadcn-vue palette is exposed as CSS variables in src/style.css and the @theme inline block maps each variable to a Tailwind utility. Use the semantic tokens so the same component looks right when a site forks off main and swaps the palette to its own theme.

Use these, not the underlying colour:

Use Don't
bg-background bg-zinc-950
bg-card bg-stone-900
bg-secondary bg-emerald-900
bg-muted bg-zinc-800
bg-primary bg-amber-500
bg-accent bg-yellow-600
text-foreground text-stone-50
text-muted-foreground text-zinc-400
text-primary text-amber-500
text-accent-foreground text-zinc-950
border-border border-zinc-800
ring-ring ring-amber-500

A site retheme is then a single edit to src/style.css: change the HSL values behind --background, --primary, --accent etc. and every component re-skins.

Translucent variants (bg-background/65, bg-card/40, etc.) compose the same way — keep using the semantic token, just append the opacity shorthand.

Exception — fully opaque overlays where the colour is the visual intent, not the token (e.g. bg-black/30 for a dialog overlay, plain bg-zinc-950/80 for a near-black callout panel). For everything else, semantic.

Tailwind 4 / vite / tsconfig setup that the shadcn CLI needs

If pnpm dlx shadcn-vue@latest add <name> later starts working, it runs a resolvedPaths validator that fails unless these files match:

tsconfig.json — must declare baseUrl + paths:

{
  "files": [],
  "references": [...],
  "compilerOptions": {
    "baseUrl": ".",
    "paths": { "@/*": ["./src/*"] },
    "ignoreDeprecations": "6.0"
  }
}

tsconfig.app.json — same baseUrl + paths + ignoreDeprecations inside its compilerOptions.

ignoreDeprecations is required because TypeScript 6 emits a deprecation error for baseUrl (slated for removal in TS 7) and vue-tsc treats it as a hard failure.

components.json — no framework key, no empty "tailwind.config": "" field. Both have been removed from current schema versions.

Asset patterns

  • src/assets/* — imported and Vite content-hashes the URL on build. Use this for everything reused or that benefits from cache-busting (logos, hero photography, repeating textures).
  • public/* — served as-is, no hash. Avoid for things that change.
  • Prefer modern formats: AVIF for stills with transparency, WebP for animation, MP4/WebM for longer-duration motion. The browsers we target (Chrome ≥85, Firefox ≥93, Safari ≥16, Edge ≥121) handle all three.
  • <picture> with <source media="(prefers-reduced-motion: reduce)"> is the right hook for animation fallbacks.

Routing and i18n stay vanilla

Vue Router and vue-i18n don't get the shadcn treatment — they stay as the standard plugin wiring. Localised strings live in src/i18n/locales/{en,fr,…}.json; don't inline copy.

Frosted-glass sticky header recipe (used here, reusable)

bg-background/65 backdrop-blur-xl backdrop-saturate-150 border-b border-white/10

bg-background/N keeps it theme-aware. The border-white/10 is intentional — it reads as "glass edge" rather than a hard rule and looks correct across both light and dark themes.

Pinned hero photo pattern

For full-bleed hero photos that stay put as the page scrolls, put a position: fixed <img> at the top of the view's template with a negative z-index, and let the hero section above be transparent:

<img
  :src="heroLandscape"
  alt=""
  aria-hidden="true"
  class="fixed inset-0 -z-50 h-screen w-screen object-cover"
/>
<section class="relative isolate flex min-h-screen flex-col items-center justify-center overflow-hidden">
  <!-- transparent over the pinned image; sections below have their own opaque backgrounds -->
</section>

The opaque sections below (welcome / pricing / footer / etc.) cover the fixed photo as the user scrolls past, so it visually exits when the hero leaves the viewport.