diff --git a/.claude/skills/photobooth-da/SKILL.md b/.claude/skills/photobooth-da/SKILL.md new file mode 100644 index 0000000..9899a3f --- /dev/null +++ b/.claude/skills/photobooth-da/SKILL.md @@ -0,0 +1,33 @@ +--- +name: photobooth-da +description: Applique la direction artistique "Polaroid Néon" du projet Rêves Liquides Photobooth. À utiliser pour tout écran, composant ou style visuel de cette appli — scan, upload, galerie invité, admin. +--- + +# Direction artistique — Polaroid Néon + +## Palette +- Fond principal : #12100E (noir chaud, jamais #000 pur) +- Accent corail : #FF5E5B (CTA, boutons principaux) +- Accent jaune : #FFD166 (badges, highlights) +- Accent vert : #06D6A0 (succès, validation) +- Blanc cassé : #FAF6F0 (cadre polaroid, textes clairs) +- Beige poudré : #E8DFD3 (bordures, séparateurs) + +## Typographie +- Titres/display : Clash Display ou General Sans (bold arrondie) +- Légendes manuscrites : Caveat ou Permanent Marker +- UI/corps de texte : Inter ou Satoshi + +## Composant signature : cadre polaroid +- Fond blanc cassé, marge basse épaisse pour la légende +- Rotation aléatoire entre -4° et +4° par carte +- Ombre douce (box-shadow léger), jamais de glow/neon CSS artificiel +- Prénom + message en typo manuscrite dans la zone basse + +## Écran admin = exception +Sobre, sombre, data-first. Pas de confettis, pas de rotation polaroid. +Cartes en #1E1B18. Accent corail réservé aux CTA uniquement. + +## Règle non négociable +Vérifier systématiquement le contraste texte/fond sur #12100E avant +de livrer un écran (norme WCAG AA minimum). diff --git a/deploy/nginx-photobooth.conf b/deploy/nginx-photobooth.conf index a5a2e86..c787ff4 100644 --- a/deploy/nginx-photobooth.conf +++ b/deploy/nginx-photobooth.conf @@ -28,6 +28,14 @@ server { try_files $uri $uri/ /admin/gallery/index.html; } + # /galerie/ -> galerie invité (nouvelle DA "Polaroid Néon", + # 2026-09-18, cf. docs/decisions.md). MAQUETTE au 2026-09-18 : données + # mock, pas encore câblée à Supabase (cf. commentaire en tête de + # src/galerie/index.html). + location /galerie/ { + try_files $uri $uri/ /galerie/index.html; + } + location /vendor/ { try_files $uri =404; } diff --git a/docs/decisions.md b/docs/decisions.md index 7c4ac26..a0b0d0f 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -5,6 +5,29 @@ la date à laquelle elles ont été prises. Toute nouvelle décision structurant (schéma DB, auth, hébergement, etc.) doit être ajoutée ici après validation par Kévin. +## 2026-09-18 + +- **Galerie invité (revirement)** : les invités peuvent désormais voir + **toutes** les photos de l'événement (pas juste envoyer la leur), dans + le cadre de la nouvelle DA "Polaroid Néon". Ça **inverse** la décision du + cahier des charges initial ("Pas de galerie visible côté invité, + confirmé : upload only"). Conséquence RGPD : le texte de mention légale + (`docs/rgpd.md` + modale de bienvenue) doit indiquer que photo/prénom/ + message sont visibles par les autres invités de l'événement, pas + seulement mis à disposition de l'organisateur — mis à jour dans la + foulée. Implémentation : backend (nouvelle RPC/Edge Function publique de + listing + URLs signées pour les invités) pas encore fait à cette date, + seul le rendu visuel est en cours. +- **Nouvelle direction artistique "Polaroid Néon"** actée, voir + `.claude/skills/photobooth-da/SKILL.md` pour le détail complet (palette, + typographie, composant polaroid signature). Remplace les 3 passes DA + précédentes (palette corail/prune/doré + Fraunces) — celles-ci restent + en prod tant que la nouvelle direction n'est pas implémentée écran par + écran. +- **Stack confirmée inchangée** : vanilla JS/HTML/CSS, pas de framework + (React proposé dans un brief externe, décliné — mandat CLAUDE.md déjà + explicite sur ce point, aucune décision contraire prise). + ## 2026-09-16 (récap fin de journée) Produit passé de "code écrit, rien de déployé" à **réellement en ligne et diff --git a/src/galerie/gallery-mock.js b/src/galerie/gallery-mock.js new file mode 100644 index 0000000..5c9382a --- /dev/null +++ b/src/galerie/gallery-mock.js @@ -0,0 +1,102 @@ +// Écran galerie invité — MAQUETTE avec données factices. +// +// Champs alignés sur le vrai schéma (photobooth.photos : nom_invite, +// message, url_storage -> ici un chemin mock, uploaded_at) pour que le +// câblage réel n'ait qu'à remplacer MOCK_PHOTOS par un vrai fetch, sans +// toucher au rendu. Voir index.html pour le détail de ce qui manque +// encore côté backend (RPC/Edge Function publique de listing). + +const MOCK_PHOTOS = [ + { nom_invite: "Camille", message: "Cette soirée est parfaite, merci !", url_storage: "/galerie/mock-photos/mock-1.jpg", uploaded_at: "2026-09-18T19:12:00Z" }, + { nom_invite: "Thomas", message: "On s'est jamais autant amusés", url_storage: "/galerie/mock-photos/mock-2.jpg", uploaded_at: "2026-09-18T19:20:00Z" }, + { nom_invite: null, message: "Vive les mariés !", url_storage: "/galerie/mock-photos/mock-3.jpg", uploaded_at: "2026-09-18T19:25:00Z" }, + { nom_invite: "Léa", message: null, url_storage: "/galerie/mock-photos/mock-4.jpg", uploaded_at: "2026-09-18T19:31:00Z" }, + { nom_invite: "Hugo & Zoé", message: "Merci pour cette belle journée", url_storage: "/galerie/mock-photos/mock-5.jpg", uploaded_at: "2026-09-18T19:40:00Z" }, + { nom_invite: "Inès", message: "Le gâteau était incroyable 🍰", url_storage: "/galerie/mock-photos/mock-6.jpg", uploaded_at: "2026-09-18T19:44:00Z" }, + { nom_invite: "Nathan", message: "À refaire dans 10 ans !", url_storage: "/galerie/mock-photos/mock-7.jpg", uploaded_at: "2026-09-18T19:52:00Z" }, + { nom_invite: "Sarah", message: "Team dancefloor jusqu'au bout", url_storage: "/galerie/mock-photos/mock-8.jpg", uploaded_at: "2026-09-18T20:01:00Z" }, +]; + +const eventNameEl = document.getElementById("event-name"); +const gridEl = document.getElementById("polaroid-grid"); +const demoAddBtn = document.getElementById("demo-add-btn"); + +function getSlugFromPath() { + const match = window.location.pathname.match(/\/galerie\/([^/]+)\/?$/); + return match ? decodeURIComponent(match[1]) : null; +} + +// Rotation aléatoire par carte, -4°/+4° (cf. skill photobooth-da), fixée +// une fois par carte (pas recalculée à chaque re-render) pour que la +// grille ne "tremble" pas. +function randomRotation() { + return (Math.random() * 8 - 4).toFixed(2); +} + +function buildCaption(photo) { + const parts = []; + if (photo.nom_invite) parts.push(photo.nom_invite); + if (photo.message) parts.push(photo.message); + return parts; +} + +function renderPolaroid(photo, { animate = false } = {}) { + const li = document.createElement("li"); + li.className = "polaroid" + (animate ? " polaroid--dropping" : ""); + li.style.setProperty("--rot", `${randomRotation()}deg`); + + const img = document.createElement("img"); + img.className = "polaroid__photo"; + img.src = photo.url_storage; + img.alt = photo.nom_invite + ? `Photo envoyée par ${photo.nom_invite}` + : "Photo envoyée par un invité"; + img.loading = "lazy"; + + const captionParts = buildCaption(photo); + const caption = document.createElement("p"); + caption.className = "polaroid__caption"; + if (captionParts.length === 0) { + caption.classList.add("polaroid__caption--empty"); + } else { + caption.textContent = captionParts.join(" — "); + } + + li.append(img, caption); + return li; +} + +function renderAll(photos) { + gridEl.innerHTML = ""; + // Plus récent en premier, comme la galerie admin. + [...photos].reverse().forEach((photo) => { + gridEl.appendChild(renderPolaroid(photo)); + }); +} + +function init() { + const slug = getSlugFromPath(); + eventNameEl.textContent = slug + ? `Galerie — ${slug.replace(/-/g, " ")}` + : "Galerie de l'événement"; + renderAll(MOCK_PHOTOS); +} + +init(); + +// --- Démo uniquement : simule l'arrivée d'un nouveau polaroid -------------- +let demoCounter = 0; +const demoNames = ["Chloé", "Adam", "Manon", "Yanis", "Juliette"]; +const demoMessages = ["On pense fort à vous !", "Quelle ambiance ce soir 🎉", "Merci de nous avoir invités", null]; + +demoAddBtn.addEventListener("click", () => { + demoCounter += 1; + const photo = { + nom_invite: demoNames[demoCounter % demoNames.length], + message: demoMessages[demoCounter % demoMessages.length], + url_storage: `/galerie/mock-photos/mock-${(demoCounter % 8) + 1}.jpg`, + uploaded_at: new Date().toISOString(), + }; + const card = renderPolaroid(photo, { animate: true }); + gridEl.prepend(card); +}); diff --git a/src/galerie/index.html b/src/galerie/index.html new file mode 100644 index 0000000..a04ac8d --- /dev/null +++ b/src/galerie/index.html @@ -0,0 +1,56 @@ + + + + + +Galerie de l'événement + + + + + + + + +
+ + + + + + +
+ + + + diff --git a/src/galerie/mock-photos/mock-1.jpg b/src/galerie/mock-photos/mock-1.jpg new file mode 100644 index 0000000..c3308f4 Binary files /dev/null and b/src/galerie/mock-photos/mock-1.jpg differ diff --git a/src/galerie/mock-photos/mock-2.jpg b/src/galerie/mock-photos/mock-2.jpg new file mode 100644 index 0000000..b1cfb49 Binary files /dev/null and b/src/galerie/mock-photos/mock-2.jpg differ diff --git a/src/galerie/mock-photos/mock-3.jpg b/src/galerie/mock-photos/mock-3.jpg new file mode 100644 index 0000000..066032f Binary files /dev/null and b/src/galerie/mock-photos/mock-3.jpg differ diff --git a/src/galerie/mock-photos/mock-4.jpg b/src/galerie/mock-photos/mock-4.jpg new file mode 100644 index 0000000..4981a50 Binary files /dev/null and b/src/galerie/mock-photos/mock-4.jpg differ diff --git a/src/galerie/mock-photos/mock-5.jpg b/src/galerie/mock-photos/mock-5.jpg new file mode 100644 index 0000000..5ce0020 Binary files /dev/null and b/src/galerie/mock-photos/mock-5.jpg differ diff --git a/src/galerie/mock-photos/mock-6.jpg b/src/galerie/mock-photos/mock-6.jpg new file mode 100644 index 0000000..fa20aa4 Binary files /dev/null and b/src/galerie/mock-photos/mock-6.jpg differ diff --git a/src/galerie/mock-photos/mock-7.jpg b/src/galerie/mock-photos/mock-7.jpg new file mode 100644 index 0000000..a406766 Binary files /dev/null and b/src/galerie/mock-photos/mock-7.jpg differ diff --git a/src/galerie/mock-photos/mock-8.jpg b/src/galerie/mock-photos/mock-8.jpg new file mode 100644 index 0000000..c593d07 Binary files /dev/null and b/src/galerie/mock-photos/mock-8.jpg differ diff --git a/src/galerie/style.css b/src/galerie/style.css new file mode 100644 index 0000000..9c76726 --- /dev/null +++ b/src/galerie/style.css @@ -0,0 +1,252 @@ +/* Galerie invité — direction artistique "Polaroid Néon" + (.claude/skills/photobooth-da/SKILL.md). Premier écran de cette nouvelle + DA, mobile-first (95% des invités sur mobile, cf. brief). + + Contrastes vérifiés par calcul WCAG AA (formule de luminance relative, + voir historique de session) : texte clair (#FAF6F0/#E8DFD3) sur fond + sombre (#12100E) est très largement au-dessus du seuil, mais texte clair + SUR les couleurs d'accent (corail/jaune/vert) échoue largement (1.3:1 à + 2.8:1 selon la couleur — bien en dessous du minimum 4.5:1). Le texte sur + accent utilise donc systématiquement #12100E (sombre), qui lui passe + confortablement (6.3:1 à 13.2:1) sur les trois accents. */ + +@font-face { + font-family: "General Sans"; + src: url("/vendor/fonts/generalsans-700.woff2") format("woff2"); + font-weight: 700; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "Caveat"; + src: url("/vendor/fonts/caveat-latin-variable.woff2") format("woff2"); + font-weight: 400 700; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "Inter"; + src: url("/vendor/fonts/inter-latin-variable.woff2") format("woff2"); + font-weight: 400 700; + font-style: normal; + font-display: swap; +} + +:root { + color-scheme: dark; + + --color-bg: #12100e; + --color-ink: #12100e; /* texte sombre sur surfaces claires/accents */ + --color-text: #faf6f0; + --color-muted: #e8dfd3; + --color-muted-2: #a89e90; + + --color-coral: #ff5e5b; + --color-yellow: #ffd166; + --color-mint: #06d6a0; + + --color-polaroid: #faf6f0; + --color-border: #e8dfd3; + + --radius: 4px; /* les polaroids restent anguleux, pas de coins arrondis */ + --radius-ui: 14px; + + --shadow-soft: 0 10px 24px rgba(0, 0, 0, 0.35); + --shadow-card: 0 6px 16px rgba(0, 0, 0, 0.4); + + --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); + + --font-display: "General Sans", -apple-system, BlinkMacSystemFont, sans-serif; + --font-hand: "Caveat", "Segoe Script", cursive; + --font-ui: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: var(--font-ui); + background: var(--color-bg); + color: var(--color-text); + line-height: 1.45; +} + +.page { + max-width: 720px; + margin: 0 auto; + padding: 1.5rem 1.1rem 6rem; + min-height: 100dvh; +} + +.page__header { + text-align: center; + margin-bottom: 1.75rem; +} + +.brand { + display: flex; + align-items: center; + justify-content: center; + gap: 0.4rem; + margin-bottom: 0.9rem; + color: var(--color-muted-2); +} + +.brand__mark { + width: 16px; + height: 16px; + flex-shrink: 0; + color: var(--color-coral); +} + +.brand__name { + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.13em; + text-transform: uppercase; + font-family: var(--font-ui); +} + +.page__header h1 { + font-family: var(--font-display); + font-weight: 700; + font-size: 1.9rem; + letter-spacing: -0.01em; + margin: 0 0 0.5rem; + text-transform: capitalize; +} + +.page__kicker { + color: var(--color-muted); + font-size: 0.95rem; + margin: 0; +} + +/* --------------------------------------------------------------------- */ +/* Grille de polaroids */ +/* --------------------------------------------------------------------- */ + +.polaroid-grid { + list-style: none; + margin: 0; + padding: 0; + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1.75rem 1.25rem; +} + +@media (min-width: 560px) { + .polaroid-grid { + grid-template-columns: repeat(3, 1fr); + } +} + +@media (min-width: 860px) { + .polaroid-grid { + grid-template-columns: repeat(4, 1fr); + } +} + +.polaroid { + --rot: 0deg; + background: var(--color-polaroid); + border-radius: var(--radius); + padding: 0.6rem 0.6rem 0; + box-shadow: var(--shadow-card); + transform: rotate(var(--rot)); + transition: transform 0.25s var(--ease-spring), box-shadow 0.25s ease; +} + +/* Léger relief au survol desktop uniquement — jamais sur mobile tactile. */ +@media (hover: hover) { + .polaroid:hover { + transform: rotate(var(--rot)) translateY(-4px) scale(1.03); + box-shadow: var(--shadow-soft); + z-index: 1; + } +} + +.polaroid__photo { + display: block; + width: 100%; + aspect-ratio: 1 / 1; + object-fit: cover; + background: var(--color-border); +} + +.polaroid__caption { + margin: 0; + padding: 0.5rem 0.3rem 0.9rem; + min-height: 2.6rem; + font-family: var(--font-hand); + font-weight: 600; + font-size: 1.15rem; + line-height: 1.15; + color: var(--color-ink); + text-align: center; + overflow-wrap: break-word; +} + +.polaroid__caption--empty::before { + content: "♡"; + color: var(--color-coral); + font-size: 1rem; +} + +/* Nouvel envoi : "chute" dans la pile avec rebond (skill: effet spring). */ +@keyframes polaroid-drop { + 0% { + opacity: 0; + transform: translateY(-56px) rotate(0deg) scale(0.7); + } + 60% { + opacity: 1; + transform: translateY(6px) rotate(var(--rot)) scale(1.04); + } + 100% { + opacity: 1; + transform: translateY(0) rotate(var(--rot)) scale(1); + } +} + +.polaroid--dropping { + animation: polaroid-drop 0.6s var(--ease-spring) both; +} + +/* --------------------------------------------------------------------- */ +/* Bouton de démo (temporaire, cf. gallery-mock.js) */ +/* --------------------------------------------------------------------- */ + +.demo-add-btn { + position: fixed; + left: 50%; + bottom: 1.5rem; + transform: translateX(-50%); + font: inherit; + font-family: var(--font-ui); + font-weight: 700; + font-size: 0.85rem; + color: var(--color-ink); + background: var(--color-coral); + border: none; + border-radius: 999px; + padding: 0.8rem 1.3rem; + box-shadow: var(--shadow-soft); + cursor: pointer; +} + +.demo-add-btn:active { + transform: translateX(-50%) translateY(1px); +} + +@media (prefers-reduced-motion: reduce) { + .polaroid, + .polaroid--dropping { + animation: none !important; + transition: none !important; + } +} diff --git a/src/vendor/NOTICE.md b/src/vendor/NOTICE.md index 0460c95..1b1b0c7 100644 --- a/src/vendor/NOTICE.md +++ b/src/vendor/NOTICE.md @@ -45,3 +45,40 @@ depuis la source indiquée. direct (règle du projet). Voir `src/e/style.css` (`--font-display`). Le corps de texte reste sur la pile système (`--font-sans`), aucun poids supplémentaire pour le texte courant. + +## General Sans, Caveat, Inter (fonts/) — direction "Polaroid Néon" + +Trio typographique de la nouvelle direction artistique +(`.claude/skills/photobooth-da/SKILL.md`), utilisé écran par écran au fur +et à mesure de son implémentation (pas encore appliqué partout). + +- **General Sans** — `fonts/generalsans-700.woff2` (21,2 Ko), graisse 700 + uniquement (titres/display, "bold arrondie" demandée dans le brief — pas + besoin de graisse 600 en plus pour ce rôle). Licence Open Font License, + distribuée via Fontshare (`https://api.fontshare.com`). Fichier statique + (pas la version variable, plus légère pour une seule graisse utilisée). +- **Caveat** — `fonts/caveat-latin-variable.woff2` (74,9 Ko), légendes + manuscrites (prénom/message sur les cadres polaroid). Licence SIL Open + Font License, Google Fonts. Fichier **variable** (couvre 400-700) : Google + sert le même fichier variable pour toutes les graisses statiques + demandées à cette police via l'API CSS2, donc pas d'intérêt à en + télécharger plusieurs — un seul fichier suffit, `font-weight: 400 700` + dans le `@font-face`. +- **Inter** — `fonts/inter-latin-variable.woff2` (48,3 Ko), texte UI/corps + des écrans utilisant la nouvelle DA (remplace la pile système + `--font-sans` sur ces écrans uniquement — poids assumé, demandé + explicitement dans le brief). Licence SIL Open Font License, Google + Fonts. Fichier variable, même raison qu'au-dessus. + +Sous-ensemble « latin » uniquement pour les trois (mêmes raisons que +Fraunces ci-dessus). Récupérées une fois via l'API CSS de Fontshare/Google +Fonts (`curl`) puis auto-hébergées ici — jamais de lien CDN live, cf. règle +du projet. + +⚠️ Poids cumulé si les 3 sont chargées sur un même écran : ~144 Ko, en plus +des ~41 Ko de Fraunces si l'écran mélange encore l'ancienne DA. À surveiller +à mesure que la nouvelle direction remplace les écrans existants — si le +poids total devient un problème, la piste la plus simple est de retirer +Inter et revenir à la pile système pour le corps de texte (c'est la police +la plus "remplaçable" des trois, celle qui apporte le moins de personnalité +propre à la DA). diff --git a/src/vendor/fonts/caveat-latin-variable.woff2 b/src/vendor/fonts/caveat-latin-variable.woff2 new file mode 100644 index 0000000..5a34234 Binary files /dev/null and b/src/vendor/fonts/caveat-latin-variable.woff2 differ diff --git a/src/vendor/fonts/generalsans-700.woff2 b/src/vendor/fonts/generalsans-700.woff2 new file mode 100644 index 0000000..65b0b4a Binary files /dev/null and b/src/vendor/fonts/generalsans-700.woff2 differ diff --git a/src/vendor/fonts/inter-latin-variable.woff2 b/src/vendor/fonts/inter-latin-variable.woff2 new file mode 100644 index 0000000..d15208d Binary files /dev/null and b/src/vendor/fonts/inter-latin-variable.woff2 differ