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>
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
- lucide imports: the registry ships
import { X } from "lucide-vue-next"but our project uses@lucide/vue. Swap the path before saving the file. - 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 withvar():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.