Nouvelle DA "Polaroid Néon" : écran galerie invité (maquette visuelle)

Nouvelle direction artistique actée (.claude/skills/photobooth-da/SKILL.md)
qui remplace progressivement la DA corail/prune/doré des 3 passes
précédentes. Décision associée : les invités voient désormais toutes les
photos de l'événement (revirement du "upload only" initial), cf.
docs/decisions.md — implique une mise à jour de la mention RGPD à faire.

Écran 3 du brief (galerie invité) livré en premier, comme demandé,
priorité au rendu visuel : grille de cadres polaroid (rotation aléatoire
-4°/+4°, légende manuscrite Caveat), fond noir chaud #12100E, effet
"chute avec rebond" pour un nouveau polaroid (bouton de démo temporaire
pour l'évaluer sans backend réel).

Contrastes vérifiés par calcul WCAG AA (skill) : texte clair sur fond
sombre largement conforme, mais texte clair sur les 3 couleurs d'accent
échouait (1.3:1 à 2.8:1) — corrigé en texte sombre systématique sur les
surfaces accent (6.3:1 à 13.2:1).

Nouvelles polices auto-hébergées (General Sans 700, Caveat variable,
Inter variable — ~144 Ko cumulés, poids documenté dans src/vendor/NOTICE.md).

⚠️ Maquette avec données factices (src/galerie/mock-photos/) : pas encore
câblée à Supabase, il manque une RPC/Edge Function publique de listing
(aucune fonction actuelle ne convient à un accès invité anonyme sans
admin_token). Route /galerie/<slug> ajoutée à deploy/nginx-photobooth.conf
et déployée sur photobooth.assistantaikev.eu.
This commit is contained in:
2026-09-18 09:04:05 +02:00
parent 2dd0db54c8
commit 107e5c619a
18 changed files with 511 additions and 0 deletions
+33
View File
@@ -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).
+8
View File
@@ -28,6 +28,14 @@ server {
try_files $uri $uri/ /admin/gallery/index.html;
}
# /galerie/<slug> -> 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;
}
+23
View File
@@ -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
+102
View File
@@ -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);
});
+56
View File
@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Galerie de l'événement</title>
<!--
Écran "galerie invité" — nouvelle direction artistique "Polaroid Néon"
(.claude/skills/photobooth-da/SKILL.md). Décision actée le 2026-09-18 :
les invités voient désormais toutes les photos de l'événement (revirement
par rapport au cahier des charges initial "upload only"), cf.
docs/decisions.md.
⚠️ MAQUETTE VISUELLE, PAS ENCORE CÂBLÉE AU BACKEND : les 8 photos
affichées viennent de src/galerie/mock-photos/ et gallery-mock.js, pas de
Supabase. Il manque encore côté backend une RPC/Edge Function publique de
listing (les fonctions actuelles get_event_photos/admin-gallery sont
réservées à service_role + admin_token — aucune ne convient à un accès
invité anonyme). Priorité donnée au rendu visuel par consigne explicite
("le plus abouti visuellement"). Route prévue : /galerie/<slug>, cf.
deploy/nginx-photobooth.conf à mettre à jour lors du câblage réel.
-->
<meta name="robots" content="noindex, nofollow">
<meta name="theme-color" content="#12100E">
<link rel="preload" href="/vendor/fonts/generalsans-700.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/vendor/fonts/caveat-latin-variable.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/galerie/style.css">
</head>
<body>
<main class="page">
<header class="page__header">
<p class="brand" aria-hidden="true">
<svg class="brand__mark" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" focusable="false">
<path d="M4 8h3l1.5-2h7L17 8h3a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1Z"/>
<circle cx="12" cy="13" r="3.5"/>
</svg>
<span class="brand__name">photobooth</span>
</p>
<h1 id="event-name">Galerie de l'événement</h1>
<p class="page__kicker">Tous les souvenirs déposés par les invités, en direct</p>
</header>
<ul id="polaroid-grid" class="polaroid-grid" aria-live="polite"></ul>
<!-- Bouton de démo temporaire : rejoue l'effet "chute" d'un nouveau
polaroid dans la pile, pour évaluer l'animation sans attendre un
vrai envoi. À retirer une fois le câblage Supabase réel en place
(le déclenchement normal sera un nouvel insert détecté en base). -->
<button type="button" id="demo-add-btn" class="demo-add-btn">
+ Simuler un nouvel envoi (démo)
</button>
</main>
<script type="module" src="/galerie/gallery-mock.js"></script>
</body>
</html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

+252
View File
@@ -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;
}
}
+37
View File
@@ -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).
Binary file not shown.
Binary file not shown.
Binary file not shown.