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.
@@ -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);
|
||||
});
|
||||
@@ -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>
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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).
|
||||
|
||||