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; 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/ { location /vendor/ {
try_files $uri =404; 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 (schéma DB, auth, hébergement, etc.) doit être ajoutée ici après validation
par Kévin. 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) ## 2026-09-16 (récap fin de journée)
Produit passé de "code écrit, rien de déployé" à **réellement en ligne et 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`). 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 Le corps de texte reste sur la pile système (`--font-sans`), aucun poids
supplémentaire pour le texte courant. 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.