Réserver une dégustation

Starter T-IN · guide des composants

Contenus

Les composants qui affichent les données du client : grilles de cartes, galeries photo, listes tarifaires, formulaire de contact. Les données viennent des content collections ou de site.ts — jamais du composant.

Card + CardGrid

Cartes cliquables pour actualités, cuvées, chambres, événements. Card : href, image, meta, datetime, title, excerpt, revealDelay. CardGrid : columns (2, 3 ou 4).

Voir le code
<CardGrid columns={3}>
  {news.map((post, i) => (
    <Card
      href={'/actualites/' + post.id + '/'}
      image={{ src: post.data.image, alt: post.data.imageAlt }}
      meta={formatDate(post.data.date)}
      datetime={isoDate(post.data.date)}
      title={post.data.title}
      excerpt={post.data.excerpt}
      revealDelay={i * 0.12}
    />
  ))}
</CardGrid>

Gallery

Grille photos + visionneuse native <dialog> : clic pour agrandir, flèches pour naviguer, Échap pour fermer. Sans JS, chaque vignette reste un lien vers le fichier. Props : photos [{ src, alt, width, height }], columns, caption.

Voir le code
<Gallery
  columns={3}
  caption="Le domaine au fil des saisons"
  photos={[
    { src: '/images/gallery-1.svg', alt: '…' },
    { src: '/images/gallery-2.svg', alt: '…' },
  ]}
/>

Portrait (détourage en premier plan)

Pour présenter les gens de la maison — vigneron, chef, gérants — sans le rectangle de photo d'identité. Le sujet détouré se pose en premier plan sur une photo du lieu assombrie, légèrement de travers, et franchit le bord du cadre : c'est ce franchissement d'une arête droite qui rend lisibles le premier plan et l'inclinaison. L'image détourée doit être recadrée au plus près du sujet — toute marge transparente rouvrirait du vide autour de lui. La coupe basse se fond dans l'ombre du voile : inutile de fournir un sujet en pied. Props : scene, cutout, name, role, ratio, width, tilt, side, overhang, veil, desaturate, revealDelay.

Le vignoble au petit matin
Les vignerons, détourés
Claire & Antoine À la tête du domaine depuis 2019
Le chai et ses barriques
Les vignerons, détourés
Côté gauche, plus incliné side=left · tilt=-7 · width=78 · veil=45
Voir le code
<Portrait
  scene={{ src: salle, alt: 'La salle et ses cerisiers en fleurs', width: 1120, height: 1400 }}
  cutout={{ src: gerants, alt: 'Laure et Pierre, détourés', width: 839, height: 1061 }}
  name="Laure & Pierre"
  role="À la tête de la maison depuis 2024"
/>

// variantes : sujet ancré à gauche, plus incliné, décor moins voilé
<Portrait ... side="left" tilt={-6} width={78} veil={45} />

Journal — la page de presse

Récolté d'antonin_lacour. L'alternative éditoriale à CardGrid : même donnée, mais une page qui se lit au lieu d'une grille qui se parcourt. Manchette + filet épais, la parution la plus récente traitée comme une une (photo large + grand corps), les autres en brèves séparées d'un filet de colonne. La prop event ajoute une bande sombre de clôture — le rendez-vous annuel, qui ne se lit pas comme une actualité de plus. Le titre accepte `*entre astérisques*` → italique léger. À réserver aux clients qui publient vraiment : sous quatre parutions, CardGrid reste plus honnête.

Voir le code
<Journal
  eyebrow="Meursault · quatre parutions"
  title="Le journal *du domaine*"
  issues={news.map((post) => ({
    title: post.data.title,
    href: '/actualites/' + post.id + '/',
    date: post.data.date,
    excerpt: post.data.excerpt,
    image: { src: post.data.image, alt: post.data.imageAlt },
  }))}
  event={{
    image: { src: '/images/fullbleed-1.svg', alt: '…' },
    eyebrow: '1er et 2 août 2026 — l’événement de l’été',
    title: 'La Ronde *des Trois Pierres*',
    text: 'Deux jours de marche gourmande entre les parcelles.',
    link: { href: '/contact', label: 'S’inscrire' },
  }}
/>

GrilleProduits — le catalogue sur fond de nuit

Récolté d'antonin_lacour (GammeVins + CarteCuveeSombre). Ouverture photographique sombre, barre de filtres, puis les produits en cartes : image détourée posée au sol d'un cadre 4/5, pastille de catégorie à gauche, statut à droite, nom et prix sur la même ligne de base. Le fond sombre n'est pas décoratif — c'est lui qui fait exister un packshot détouré (fit='contain', défaut ; fit='cover' pour une photo cadrée). Le filtrage est un îlot d'une vingtaine de lignes, sans framework : sans JavaScript toutes les cartes sont dans le HTML, seule la barre ne répond pas. Ne donner qu'une entrée de filtrage : dès qu'il faut en croiser plusieurs, c'est FiltresGrille (fiche suivante) qu'il faut prendre. Cuvées, greens fees, chambres, formules, prestations.

Six cuvées, trois appellations

Nos vins

Six parcellaires vendangés à la main, élevés en fût et en jarre, mis en bouteille sans collage ni filtration.

Voir le code
<GrilleProduits
  image={{ src: '/images/fullbleed-1.svg', alt: '…' }}
  eyebrow="Six cuvées, trois appellations"
  title="Nos *vins*"
  lead="…"
  categories={[{ key: 'blanc', label: 'Blancs' }, { key: 'rouge', label: 'Rouges' }]}
  labels={{ singular: 'cuvée', plural: 'cuvées' }}
  products={[
    {
      href: '/les-vins/les-trois-pierres/',
      name: 'Les Trois Pierres',
      category: 'blanc',
      image: { src: '/images/packshot-1.svg', alt: '…' },
      badge: 'Blanc', badgeColor: 'var(--detail)',
      status: 'Disponible', statusHighlight: true,
      price: '24,00 €',
      description: 'Chardonnay de haut de coteau, dix mois de fût.',
      meta: 'Millésime 2022 · 13 %',
    },
    // …
  ]}
/>

FiltresGrille — le catalogue à plusieurs entrées

Récolté de `recettes` (carnet + explorateur de diététique chinoise). Le grand frère de GrilleProduits : celui-là filtre sur un critère unique et suffit devant dix produits ; celui-ci sert dès qu'il faut croiser plusieurs entrées — couleur × millésime × accord × budget — avec une recherche plein texte. Les groupes sont déclaratifs (cle = paramètre d'URL, champ = attribut data- de la carte, mode = eq | in | lte) : le composant ne connaît aucune taxonomie, seulement ces trois informations. Les cartes sont rendues au build par l'appelant, dans le slot, avec leurs critères posés par attributsCarte() ; l'îlot ne fait que basculer hidden et aria-pressed — sans JavaScript, toutes les cartes sont dans le HTML et la page reste indexable. La recherche s'appuie sur un index JSON normalisé sans accent (taper « perriere » trouve « La Perrière ») plutôt que de recopier le texte dans chaque carte. L'état est reflété dans l'URL et relu au chargement : /les-vins/?couleur=blanc ouvre la page déjà réglée, et une sélection se partage par copier-coller. Critères secondaires rangés dans le tiroir « Affiner », compteur annoncé en role status, bouton « Tout effacer » qui n'apparaît qu'utile, état vide rédigé. Cuvées, chambres, greens fees, prestations, recettes.

Voir le code
<!-- Les groupes sont déclaratifs : le composant ne connaît aucune taxonomie. -->
const groupes: GroupeFiltre[] = [
  { cle: 'couleur', label: 'Couleur', champ: 'couleur', mode: 'eq', tout: 'Toutes',
    options: optionsPresentes(COULEURS, cuvees, (c) => c.couleur) },
  { cle: 'accord', label: 'À table avec', champ: 'accords', mode: 'in', tout: 'Tout',
    repli: true, options: optionsPresentes(ACCORDS, cuvees, (c) => c.accords) },
  { cle: 'prix', label: 'Budget', champ: 'prix', mode: 'lte', tout: 'Sans limite',
    repli: true, options: [{ valeur: '25', label: 'moins de 25 €' }] },
];

<FiltresGrille
  groupes={groupes}
  total={cuvees.length}
  colonnes={3}
  labels={{ singulier: 'cuvée', pluriel: 'cuvées',
            recherche: 'Chercher un vin, un cépage, une appellation',
            placeholder: 'Chardonnay, Meursault, vieilles vignes…',
            affiner: 'Affiner — accord, budget' }}
  index={cuvees.map((c) => ({
    cle: c.slug,
    texte: [c.nom, c.appellation, c.cepage, c.note].join(' '),
  }))}
  vide={{ titre: 'Aucune bouteille avec ces réglages.' }}
>
  {cuvees.map((c, i) => (
    <li {...attributsCarte(
      { couleur: c.couleur, millesime: c.millesime, accords: c.accords, prix: c.prix },
      c.slug,
    )}>
      <Card
        href={'/les-vins/' + c.slug + '/'}
        image={{ src: c.image, alt: 'Cuvée ' + c.nom }}
        meta={c.appellation + ' · ' + c.millesime}
        title={c.nom}
        excerpt={c.note}
        revealDelay={(i % 3) * 0.08}
      />
    </li>
  ))}
</FiltresGrille>

CarteParcelles — la carte dessinée

Récoltée d'antonin_lacour. Une feuille de papier : un losange par parcelle (sa taille dit le poids, sa couleur la catégorie), le nom des lieux, et un panneau qui détaille la parcelle désignée. Les positions ne sont pas dessinées à la main : elles sont projetées au build depuis les coordonnées réelles (lib/carte-parcelles.ts — aplatissement des longitudes, cadrage à échelle unique, écartement déterministe des losanges qui se recouvrent). Une parcelle ajoutée dans le contenu apparaît au bon endroit sans qu'on touche au dessin. Les losanges forment un radiogroup : flèches, Home/Fin, Entrée/Espace. Sans JavaScript, toutes les fiches sont dans le HTML (une visible), la page reste indexable. Parcelles d'un domaine, trous d'un parcours, sites d'un office de tourisme.

carte schématique — positions indicatives, parcelles réelles

Voir le code
<CarteParcelles
  mention="carte schématique — positions indicatives"
  categories={[
    { key: 'chardonnay', label: 'Chardonnay', color: 'var(--detail)' },
    { key: 'pinot', label: 'Pinot noir', color: 'var(--accent)' },
  ]}
  places={[{ name: 'Meursault', lon: 4.7712, lat: 46.9782 }]}
  plots={[
    {
      name: 'Les Trois Pierres',
      place: 'Meursault',
      category: 'chardonnay',
      weight: 1.4,
      lon: 4.7688, lat: 46.9801,
      details: [
        { label: 'Cépage', value: 'Chardonnay' },
        { label: 'Âge des vignes', value: '48 ans (1978)' },
        { label: 'Surface', value: '1,4 ha' },
        { label: 'Exposition', value: 'Sud-est' },
      ],
      note: 'La parcelle historique : les trois bornes de calcaire sont au bout du rang.',
    },
    // …
  ]}
/>

CarteReperes — l'illustration à points

Récoltée d'antonin_lacour (la carte des vins). Le complément de CarteParcelles : ici le fond est une œuvre fournie — une illustration importée, ou n'importe quel SVG passé en slot — et chaque repère est posé dessus en pourcentage du cadre, l'illustration faisant foi. Un repère est un lien ordinaire ; sa fiche minute (surtitre, titre, note, appel) s'ouvre au survol ou au focus en CSS seul — sans JavaScript, tout navigue. Au doigt, premier appui = fiche, second = navigation. Près d'un bord, la fiche s'aligne d'elle-même sur le repère ; sous 40 rem, elle s'ancre au bas de l'écran. Poser data-active-category sur la racine éteint les autres catégories : c'est le contrat qu'utilise un catalogue filtrant posé sur la même page. Cuvées sur leurs villages, trous d'un parcours sur la vue aérienne, chambres sur le plan de la maison.

carte illustrée — positions indicatives, fond de démonstration en slot
Voir le code
<CarteReperes
  image={carte}
  imageAlt="Carte illustrée du domaine"
  linkLabel="Voir la cuvée"
  mention="carte illustrée — positions indicatives"
  categories={[
    { key: 'rouge', label: 'Rouges', color: '#7a4038' },
    { key: 'blanc', label: 'Blancs', color: '#c8b273' },
  ]}
  markers={[
    {
      title: 'Côtes-du-Couchois « Cuvée Amphore »',
      kicker: 'Bourgogne Côtes-du-Couchois',
      note: 'Rouge · Dracy-lès-Couches',
      category: 'rouge',
      href: '/nos-vins/cuvee-amphore/',
      x: 27.2, y: 50.5,
    },
    // …
  ]}
/>

{/* Sans `image`, le slot porte le fond (SVG inline, <picture>…) : */}
<CarteReperes ratio="3 / 2" markers={…}>
  <svg viewBox="0 0 1536 1024">…</svg>
</CarteReperes>

CarteMenu — la carte du restaurant

Récoltée de bellagio (MenuCategorie). PriceList suffit pour deux colonnes label/valeur ; une vraie carte, non : un plat porte un nom, souvent une description, parfois une pastille (« Nouveau », « Végétarien ») et une mention de service (« 15 min de cuisson »). Deux rendus choisis par la donnée seule — sans columns, la liste « nom … prix » reliée par une ligne de conduite pointillée ; avec columns, un tableau de prix (pressions, formats de vin) qui se replie en blocs étiquetés sous 44 rem, un tableau à quatre colonnes étant illisible sur un téléphone. Les prix chiffrés reçoivent la devise, les mentions libres (« offert », « selon arrivage ») sont reprises telles quelles — lib/format.ts. Filets et nuances sont des color-mix de currentColor : fond clair comme sombre. Une catégorie = un fichier de content collection ; la page boucle dessus.

Les

Pâtes

Toutes nos pâtes sont servies avec une salade de saison.

Linguines à la carbonara 14,60 €

Crème, lardons fumés, parmesan affiné 24 mois

Tagliatelles aux cèpes Nouveau 16,80 €

Cèpes de la Côte, persillade, huile de noix

Poulpe en salade tiède 15,60 €

15 min de cuisson

Pâtes du jour selon arrivage

Selon le marché — demandez au service

Bulles

Crémant de Bourgogne Domaine des Trois Pierres, brut nature 6,00 € 30,00 €
Crémant + crème de fruit 7,50 €
Champagne blanc de blancs Récoltant-manipulant, dosage 4 g 12,00 € 68,00 €
Voir le code
{categories.map((cat) => (
  <CarteMenu
    kicker={cat.data.kicker}
    name={cat.data.name}
    note={cat.data.note}
    columns={cat.data.columns}
    items={cat.data.items}
  />
))}

// une catégorie en liste
{ name: 'Pâtes', kicker: 'Les', note: 'Servies avec salade',
  items: [{ name: '…', desc: '…', price: '14,60', badge: 'Nouveau' }] }

// une catégorie à colonnes de prix
{ name: 'Bulles', columns: ['La coupe', 'La bouteille'],
  items: [{ name: '…', prices: ['6,00', '30,00'] }] }

PriceList

Liste tarifaire <dl> : tarifs, cartes, menus, horaires. Fonctionne sur fond clair comme sombre. Props : items [{ label, value, note }]. Slot pour une note de bas de liste.

Dégustation découverte 4 vins · 45 minutes
15 €
Dégustation terroirs 6 vins · 1h15
25 €
Verticale de millésimes Sur rendez-vous · 2h
60 €

Dégustation offerte pour tout achat de six bouteilles.

Voir le code
<PriceList
  items={[
    { label: 'Dégustation découverte', note: '4 vins · 45 minutes', value: '15 €' },
    { label: 'Verticale de millésimes', note: 'Sur rendez-vous', value: '60 €' },
  ]}
>
  Dégustation offerte pour tout achat de six bouteilles.
</PriceList>

ContactForm

Deux modes selon site.formEndpoint : endpoint renseigné → POST natif + fetch avec confirmation inline ; vide (mode actuel de la démo) → repli mailto: composé côté client. Pot-de-miel anti-spam intégré. Prop : subject (pré-remplit l'objet).

Les champs marqués d'un astérisque sont obligatoires. Vos données ne servent qu'à répondre à votre demande et ne sont transmises à personne.

Voir le code
<ContactForm subject="Demande de visite" />

// src/data/site.ts
formEndpoint: 'https://formspree.io/f/xxxxxxx',
Guide