Imported from Dervel7/winery_testing (
AGENTS.md). Install upstream withnpx skills add Dervel7/winery_testing. Copyright stays with the author.
AGENTS.md — Aris Kalamatas and Sons winery site
Single-page, display-only marketing site for a winery in Karditsa, Thessaly, Greece. Cinematic 3D presentation, bilingual Greek (default) / English. Read this file fully before making any change. When this file and your instincts disagree, this file wins.
1. Ground truth
| Fact | Value |
|---|---|
| Business (EN) | Aris Kalamatas and Sons |
| Business (EL) | Άρης Καλαμάτας & Υιοί |
| Location | Karditsa (Καρδίτσα), Thessaly, Greece |
| Locales | el (default), en |
| Site type | Static SPA — display only |
| Auth / DB / API | None. Never add. |
Hard constraints — violating any of these is a failed delivery:
- No login, accounts, database, server code, e-commerce, or cart. Wine pages are showcase only.
- No third-party scripts (analytics, fonts CDN, tag managers) unless the maintainer explicitly asks.
- The build output must remain a purely static
dist/deployable on any static host. - Greek is the default in every context: first load, fallback language, SEO defaults,
<html lang="el">. - Never invent business facts (history, founding year, awards, address, phone, prices). See §9.
2. Tech stack (fixed — do not substitute without maintainer approval)
| Area | Choice |
|---|---|
| Runtime | Node 22 LTS, npm (committed lockfile) |
| Build | Vite, react-ts template |
| UI | React 19 + TypeScript (strict) |
| Styling | Tailwind CSS v4 via @tailwindcss/vite; design tokens in CSS |
| 3D | three + @react-three/fiber + @react-three/drei + @react-three/postprocessing |
| Scroll / motion | gsap + @gsap/react (ScrollTrigger, camera scrub), lenis (smooth scroll), motion (DOM micro-interactions) |
| Shared state | zustand — only for cross-component scene/UI state |
| i18n | i18next + react-i18next, JSON statically imported (no HTTP backend) |
| Fonts | @fontsource-variable/playfair-display (display) + @fontsource-variable/inter (body) — both ship Greek subsets |
| Lint | ESLint flat config + typescript-eslint + react-hooks + react-refresh |
| Routing | None. One page, anchor navigation between sections. |
Why: R3F is the dominant declarative layer over three.js; drei provides the production helpers
(Environment, View, Float, useProgress); GSAP + Lenis is the standard scroll-storytelling
combo; i18next is the i18n standard. React Three Fiber v9 requires React 19 — keep them in sync,
and upgrade three + drei together.
Ask first before adding: Next.js/Remix/Nuxt (no SSR need), any other 3D or i18n framework,
CSS-in-JS, component libraries (MUI etc.), Moment/luxury date libs (use Intl).
3. First-time scaffold (run once, in repo root)
npm create vite@latest . -- --template react-ts
# When warned the directory is not empty (AGENTS.md is here):
# choose "Ignore files and continue".
npm i three @react-three/fiber @react-three/drei @react-three/postprocessing \
gsap @gsap/react lenis motion zustand \
i18next react-i18next \
@fontsource-variable/playfair-display @fontsource-variable/inter
npm i -D @types/three tailwindcss @tailwindcss/vite
Then wire Tailwind v4 (plugin in vite.config.ts + @import "tailwindcss"; in global CSS),
create the structure in §5, and implement scripts/check-i18n.mjs (§7).
4. Commands
| Command | Purpose |
|---|---|
npm run dev |
Dev server with HMR |
npm run build |
Type-check + production build to dist/ |
npm run preview |
Serve the production build locally |
npm run lint |
ESLint |
npm run check:i18n |
EL/EN key-parity check (see §7) — must exit 0 |
Scripts must run on Windows (maintainer machine). No POSIX-only commands in npm scripts —
write small Node scripts under scripts/ instead.
5. Project structure
src/
main.tsx # entry: StrictMode, i18n init
App.tsx # layout composition only
i18n.ts # i18next setup (§7)
styles/global.css # Tailwind import + @theme design tokens (§8)
components/
three/ # R3F scenes, models, camera rigs, particles
ui/ # Navbar, LanguageSwitch, Footer, Preloader, primitives
sections/ # Hero, Story, Wines, Vineyard, Visit, Contact
hooks/ # useReducedMotion, useInView, ...
store/ # zustand stores (scene state, active section)
locales/
el/ # common.json, nav.json, hero.json, story.json,
en/ # wines.json, vineyard.json, visit.json, contact.json, footer.json
public/
models/ # optimized .glb only
textures/ # KTX2 textures, HDRIs (local, not CDN)
images/ # raster fallbacks, OG image, favicon
scripts/
check-i18n.mjs
Default information architecture (nav labels EL/EN): Αρχική/Home · Ιστορία/Story · Οίνοι/Wines · Αμπελώνας/Vineyard · Επίσκεψη/Visit · Επικοινωνία/Contact. Language switch (EL | EN) always visible in the navbar.
6. 3D standards
Scene architecture
- One shared
<Canvas>per page using drei's<View>pattern (canvas tracks DOM sections), or a lazily-mounted Canvas per major scene. Never many always-running canvases. <Canvas dpr={[1, 2]} gl={{ antialias: false, powerPreference: 'high-performance' }}>— antialias via SMAA in the postprocessing pass, not MSAA.frameloop="demand"by default; invalidate on scroll/pointer. Continuous loops only while the owning section is in view.- Wrap every Canvas in an error boundary that falls back to a static image. The site must never render a blank/black area.
- Gate first paint with a Preloader driven by drei
useProgress(percentage + brand mark).
Assets (hard budget)
- Models:
.glbonly, ≤ 100k triangles for the hero object, ≤ 1.5 MB per scene after compression. Compress withnpx @gltf-transform/cli— Meshopt compression, KTX2/Basis textures, texture size ≤ 2048. - HDRIs must be stored locally in
public/textures/(no runtime CDN fetches). - Instanced meshes for particles/repeating elements: ≤ 2000 instances desktop, ≤ 500 mobile.
Look & interaction
- Tone mapping ACESFilmic (R3F default), subtle bloom + vignette via
@react-three/postprocessing. Restraint over sparkle. - Lighting:
Environmentwith Lightformers or a local studio HDRI; one warm key light direction consistent with the design system. - Interactivity = pointer parallax, hover states, scroll-scrubbed camera paths (GSAP ScrollTrigger → camera/store values). No drag-orbit games; this is a brochure.
- Respect
prefers-reduced-motion: static frame, no parallax, no scroll scrub. Detect once, store in zustand, branch inApp(3D → static imagery fallback is acceptable).
Performance budgets (targets; check before declaring a feature done)
- First-load JS ≤ 500 KB gzipped; LCP ≤ 2.5 s on mid-tier mobile.
- 60 fps scroll on desktop, ≥ 30 fps on mobile. Use drei
PerformanceMonitor/AdaptiveDprand degrade (fewer particles, DPR) before dropping frames.
7. i18n rules (Greek default)
supportedLngs: ['el', 'en'],fallbackLng: 'el', initiallng = localStorage['ak-lang'] ?? 'el'. No browser language auto-detection. Persist choice tolocalStorage['ak-lang']on switch.- Locale JSONs are statically imported in
src/i18n.ts(bundled — no fetch failures, no flashed keys).react: { useSuspense: false }so i18n never competes with the 3D preloader. - Keys are English camelCase canonical (
hero.subtitle,wines.items.moschofilero.notes).el/anden/must contain identical key sets —npm run check:i18nrecursively compares the trees, prints every missing/extra key, and exits 1 on any diff. It must pass before every commit. - No user-facing string may be hardcoded in a component — including
alt,aria-label,title,<title>, meta description, and the preloader text. - On
languageChanged: setdocument.documentElement.lang, and swap<title>+ meta description from resources.index.htmlships with Greek defaults (lang="el", Greek title). - Greek typography:
- Fonts must include the Greek subset (Playfair Display and Inter do — verify when changing fonts).
- CSS
text-transform: uppercasekeeps the tonos (Ά) which is wrong for Greek all-caps. Store all-caps Greek labels already uppercased without tonos (ΟΙ ΟΙΝΟΙ ΜΑΣ), or use an i18next post-processor that strips combining tonos forel. - Greek quotation marks «…», el-GR
Intlformats for numbers/currency (EUR)/dates, timezone Europe/Athens.
- Translations must read natural in each language, not literal. English and Greek copy may differ in length — layouts must survive ~30% text swelling without overflow.
8. Design system
Mood: cinematic, dark, premium, lots of negative space. Tailwind v4 tokens live once in
src/styles/global.css:
@import "tailwindcss";
@theme {
--color-noir: #0d0a0b; /* page background */
--color-wine-700: #4a0f1f; /* deep oxblood, primary */
--color-wine-400: #8e2b40; /* hover / secondary */
--color-cream: #f3ead9; /* body text on dark */
--color-gold: #c9a227; /* accents, rules, hover */
--font-display: "Playfair Display Variable", Georgia, serif;
--font-body: "Inter Variable", system-ui, sans-serif;
}
(Maintainer may retune values; the single-source rule is the point.)
- Headings: Playfair Display. Body/UI: Inter. Both variable, Greek subsets loaded.
- Motion language: 200–600 ms, ease-out; fade/slide reveals via
motionwhileInView; camera work via GSAP scrub. Nothing bounces. - Photography/imagery: dark, moody, local files only. No hotlinking. Translated
alttext. - Layout: full-bleed hero, generous section rhythm, container max-width ~1200px for text.
9. Content policy
- Brand strings are single-sourced in
common.json(brand.name= "Aris Kalamatas and Sons" / «Άρης Καλαμάτας & Υιοί») and used everywhere via i18n. Never retype the brand in a component. - Unknown business facts (address, phone, email, hours, socials, wine catalog, history):
- Use obviously-fake placeholders (
+30 24410 00000,info@example.com) inside the proper locale keys so swapping in real data is a JSON-only change. - Mark placeholder batches in the commit/PR description (
content: placeholders). - Never present invented specifics (founding year, awards, exports) as real.
- Use obviously-fake placeholders (
- The wine list may use generic Greek variety names as SAMPLE content (Αγιωργίτικο, Ξινόμαυρο, Μαλαγουζιά, Μοσχοφίλερο, Ροδίτης) with vintage/price fields omitted or clearly placeholder. No per-bottle pricing unless the maintainer supplies it.
10. Definition of Done — every change passes ALL gates
npm run lintandnpm run buildclean (zero TS errors, zero new warnings).npm run check:i18nexits 0.npm run preview+ real-browser verification:- desktop ≥ 1280 px and mobile 390 × 844;
- both languages toggled (Greek renders first on a fresh session);
- 3D loads behind the preloader, no black/blank regions, no layout shift or horizontal overflow;
- OS reduced-motion enabled → site remains fully readable and calm;
- browser console clean.
- Any visual change is proven with a screenshot or live check — never assumed.
- New dependencies require a one-line justification in the PR/commit. Performance budgets (§6) respected; if a change pushes a budget, say so explicitly instead of shipping silently.
11. Do / Don't
Do
- Keep components small and composed: section files assemble
ui/+three/pieces. - One component per file, PascalCase files, named exports;
use-prefixed hooks. useGSAP(not rawuseEffect) for GSAP wiring; clean up timelines.- Destructive or wide-ranging refactors: propose first in the PR description.
Don't
- No hardcoded strings, no inline hex colors when a token exists, no
!important. - No server code, env vars, secrets, or
.envfiles — nothing to put in them. - No new 3D/i18n/styling frameworks (§2), no default-export soup, no committed
dist/. - No committed binary asset > 5 MB (use smaller/compressed variants; flag for git-lfs if truly needed).
12. Git hygiene
- Small, single-concern commits; imperative subject lines (
Add hero bottle scene, notupdates). - Commit
package-lock.json; never commitdist/,node_modules/, or local.env*. - Run the §10 gates before every commit — they are the definition of "works".