Merge pull request 'feat(plugin): theme-aquarelle + hero' (#38) from feat/theme-aquarelle into main
This commit is contained in:
commit
93aebc4e87
6 changed files with 187 additions and 6 deletions
|
|
@ -47,8 +47,31 @@ body[data-theme="guyane"] {
|
|||
radial-gradient(ellipse at bottom, rgba(196, 100, 52, 0.04) 0%, transparent 60%);
|
||||
}
|
||||
|
||||
/* === Theme Aquarelle (plugin theme-aquarelle) === */
|
||||
/* Direction artistique « carnet naturaliste XIXᵉ ». Mutuellement exclusif
|
||||
avec theme-guyane (le hook onEnable du plugin garantit qu'un seul est
|
||||
actif à la fois). */
|
||||
body[data-theme="aquarelle"] {
|
||||
--background: #faf5e9; /* papier crème teinté */
|
||||
--foreground: #2a2418; /* encre sépia foncée */
|
||||
font-family: var(--font-serif), Georgia, serif;
|
||||
/* Texture grain de papier subtile via SVG inline (~1.5 KB, pas de fetch). */
|
||||
background-image:
|
||||
radial-gradient(ellipse at 25% 15%, rgba(196, 100, 52, 0.05) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 75% 85%, rgba(94, 94, 50, 0.05) 0%, transparent 50%),
|
||||
url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' seed='17'/%3E%3CfeColorMatrix values='0 0 0 0 0.65 0 0 0 0 0.55 0 0 0 0 0.40 0 0 0 0.18 0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
||||
/* Surcharges visuelles aquarelle : hairlines sépia partout en remplacement
|
||||
des borders zinc/gray du theme-guyane. */
|
||||
body[data-theme="aquarelle"] [class*="border-zinc-"],
|
||||
body[data-theme="aquarelle"] [class*="border-gray-"] {
|
||||
border-color: rgba(140, 61, 24, 0.25);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="guyane"]) {
|
||||
:root:not([data-theme="guyane"]):not([data-theme="aquarelle"]) {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Metadata } from "next";
|
||||
import { Geist, Geist_Mono, Cormorant_Garamond } from "next/font/google";
|
||||
import { Geist, Geist_Mono, Cormorant_Garamond, PT_Serif } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { PluginProvider } from "@/lib/plugins/client";
|
||||
import { getEnabledPluginKeys, syncPluginsFromRegistry } from "@/lib/plugins/server";
|
||||
|
|
@ -32,6 +32,15 @@ const cormorant = Cormorant_Garamond({
|
|||
display: "swap",
|
||||
});
|
||||
|
||||
// PT Serif : typographie display pour le theme Aquarelle (carnet naturaliste).
|
||||
// Plus dense, plus encrée, parfaite pour les planches d'illustration.
|
||||
const ptSerif = PT_Serif({
|
||||
variable: "--font-serif-aquarelle",
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "700"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || "http://localhost:3000";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
|
|
@ -68,17 +77,26 @@ export default async function RootLayout({
|
|||
enabledKeys = [];
|
||||
}
|
||||
|
||||
const themeGuyane = enabledKeys.includes("theme-guyane");
|
||||
// Aquarelle > Guyane si les deux activés (mutual exclusion garantie par
|
||||
// le hook plugin, mais on est défensif au cas où).
|
||||
const themeAquarelle = enabledKeys.includes("theme-aquarelle");
|
||||
const themeGuyane = !themeAquarelle && enabledKeys.includes("theme-guyane");
|
||||
const dataTheme = themeAquarelle ? "aquarelle" : themeGuyane ? "guyane" : undefined;
|
||||
|
||||
// En thème aquarelle, on substitue la variable --font-serif par PT Serif
|
||||
// (au lieu de Cormorant) pour coller à l'esthétique carnet.
|
||||
const serifVariable = themeAquarelle ? ptSerif.variable : cormorant.variable;
|
||||
|
||||
const locale = await getLocale();
|
||||
const messages = await dict(locale);
|
||||
|
||||
return (
|
||||
<html
|
||||
lang={locale}
|
||||
className={`${geistSans.variable} ${geistMono.variable} ${cormorant.variable} h-full antialiased`}
|
||||
className={`${geistSans.variable} ${geistMono.variable} ${serifVariable} h-full antialiased`}
|
||||
>
|
||||
<body
|
||||
data-theme={themeGuyane ? "guyane" : undefined}
|
||||
data-theme={dataTheme}
|
||||
className="min-h-full flex flex-col font-sans"
|
||||
>
|
||||
<PluginProvider enabledKeys={enabledKeys}>
|
||||
|
|
|
|||
|
|
@ -3,13 +3,20 @@ import { CarbetRiver } from "@/components/illustrations/CarbetRiver";
|
|||
import { LocaleSwitcher } from "@/components/LocaleSwitcher";
|
||||
import { isPluginEnabled } from "@/lib/plugins/server";
|
||||
import { t } from "@/lib/i18n/server";
|
||||
import { aquarelleUrl, getActiveTheme } from "@/lib/theme";
|
||||
|
||||
/**
|
||||
* Hero plein écran. Plugin `landing-hero`. Texte i18n via t() server.
|
||||
* Affiche le LocaleSwitcher en haut à droite si le plugin i18n est activé.
|
||||
* Selon le theme actif :
|
||||
* - aquarelle : illustration MinIO `01-hero-fleuve-maroni` en fond, ambiance
|
||||
* carnet de voyage, texte sépia sur papier teinté, ornement palmier en
|
||||
* coin et bordure hairline sépia
|
||||
* - guyane : SVG vectoriel CarbetRiver, palette tropicale moderne
|
||||
* - none : retombe sur le SVG
|
||||
*/
|
||||
export async function HeroSection() {
|
||||
const i18nOn = await isPluginEnabled("i18n-fr-en");
|
||||
const theme = await getActiveTheme();
|
||||
const eyebrow = await t("hero.eyebrow");
|
||||
const titleLine1 = await t("hero.titleLine1");
|
||||
const titleAccent = await t("hero.titleAccent");
|
||||
|
|
@ -17,6 +24,69 @@ export async function HeroSection() {
|
|||
const ctaDiscover = await t("hero.ctaDiscover");
|
||||
const ctaPropose = await t("hero.ctaPropose");
|
||||
|
||||
if (theme === "aquarelle") {
|
||||
return (
|
||||
<section className="relative isolate overflow-hidden border-b border-[#8c3d18]/25 bg-[#faf5e9]">
|
||||
<div
|
||||
className="absolute inset-0 -z-10 opacity-90"
|
||||
style={{
|
||||
backgroundImage: `url("${aquarelleUrl("01-hero-fleuve-maroni.jpg")}")`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center 60%",
|
||||
}}
|
||||
aria-hidden
|
||||
/>
|
||||
{/* voile crème pour lisibilité texte sépia sur l'aquarelle */}
|
||||
<div className="absolute inset-0 -z-10 bg-gradient-to-t from-[#faf5e9] via-[#faf5e9]/55 to-transparent" aria-hidden />
|
||||
<div className="absolute inset-0 -z-10 bg-gradient-to-r from-[#faf5e9]/85 via-transparent to-transparent" aria-hidden />
|
||||
|
||||
{i18nOn ? (
|
||||
<div className="absolute right-6 top-6 z-10 sm:right-8 lg:right-12">
|
||||
<LocaleSwitcher />
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mx-auto flex min-h-[78vh] max-w-6xl flex-col items-start justify-end gap-6 px-6 pb-16 pt-32 sm:px-8 sm:pb-20 sm:pt-40 lg:px-12">
|
||||
<span className="inline-flex items-center gap-2 rounded-none border-l border-r border-[#8c3d18]/40 bg-transparent px-3 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-[#8c3d18]">
|
||||
<span aria-hidden>~</span>
|
||||
{eyebrow}
|
||||
<span aria-hidden>~</span>
|
||||
</span>
|
||||
|
||||
<h1 className="max-w-3xl font-serif text-5xl font-normal leading-[1.05] tracking-tight text-[#2a2418] sm:text-6xl md:text-7xl lg:text-[5.5rem]">
|
||||
{titleLine1}
|
||||
<br />
|
||||
<span className="italic text-[#8c3d18]">{titleAccent}</span>.
|
||||
</h1>
|
||||
|
||||
<p className="max-w-xl font-serif text-lg leading-relaxed text-[#2a2418]/85">
|
||||
{subtitle}
|
||||
</p>
|
||||
|
||||
<div className="mt-2 flex flex-wrap items-center gap-3">
|
||||
<Link
|
||||
href="/carbets"
|
||||
className="rounded-none border border-[#8c3d18] bg-[#8c3d18] px-6 py-3 text-sm font-semibold uppercase tracking-wider text-[#faf5e9] transition hover:bg-[#5e2a10]"
|
||||
>
|
||||
{ctaDiscover}
|
||||
</Link>
|
||||
<Link
|
||||
href="/espace-hote"
|
||||
className="rounded-none border border-[#8c3d18]/50 bg-[#faf5e9]/70 px-6 py-3 text-sm font-medium uppercase tracking-wider text-[#2a2418] backdrop-blur-sm transition hover:bg-[#faf5e9]"
|
||||
>
|
||||
{ctaPropose}
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<p className="mt-4 font-serif text-xs italic text-[#5e5e32]">
|
||||
— planche I, carnet d'expédition Karbé —
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
// Theme guyane (default actuel) ou pas de theme
|
||||
return (
|
||||
<section className="relative isolate overflow-hidden bg-[var(--color-karbe-canopy-900)] text-[var(--color-karbe-bone)]">
|
||||
<div className="absolute inset-0 -z-10">
|
||||
|
|
|
|||
|
|
@ -30,6 +30,21 @@ import {
|
|||
seedPirogueProviders,
|
||||
} from "./seeds/pirogue-providers-default";
|
||||
import { seedEnglishContentPages } from "./seeds/content-pages-en";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
// Mutuelle exclusion theme-guyane / theme-aquarelle : activer l'un
|
||||
// désactive automatiquement l'autre.
|
||||
async function disableOtherTheme(currentKey: string): Promise<void> {
|
||||
const other = currentKey === "theme-guyane" ? "theme-aquarelle" : "theme-guyane";
|
||||
const row = await prisma.plugin.findUnique({ where: { key: other } });
|
||||
if (row?.enabled) {
|
||||
await prisma.plugin.update({
|
||||
where: { key: other },
|
||||
data: { enabled: false, lastDisabledAt: new Date() },
|
||||
});
|
||||
console.log(`[plugin ${currentKey}] désactive ${other} (mutual exclusion)`);
|
||||
}
|
||||
}
|
||||
|
||||
export const pluginHooks: Record<string, PluginHookSet | undefined> = {
|
||||
"demo-carbets-seed": {
|
||||
|
|
@ -93,4 +108,15 @@ export const pluginHooks: Record<string, PluginHookSet | undefined> = {
|
|||
console.log(`[plugin i18n-fr-en] seed: ${count} pages EN`);
|
||||
},
|
||||
},
|
||||
// Themes : mutuellement exclusifs (un seul actif à la fois).
|
||||
"theme-guyane": {
|
||||
onEnable: async () => {
|
||||
await disableOtherTheme("theme-guyane");
|
||||
},
|
||||
},
|
||||
"theme-aquarelle": {
|
||||
onEnable: async () => {
|
||||
await disableOtherTheme("theme-aquarelle");
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -27,6 +27,14 @@ export const PLUGINS: PluginDescriptor[] = [
|
|||
category: "visual",
|
||||
version: "0.1.0",
|
||||
},
|
||||
{
|
||||
key: "theme-aquarelle",
|
||||
name: "Thème Aquarelle (carnet naturaliste)",
|
||||
description:
|
||||
"Direction artistique « carnet de voyage XIXᵉ » : papier teinté crème, traits sépia fins, aquarelles ocres+verts délavés, typographie display PT Serif. Active automatiquement les illustrations aquarelle si présentes. Mutuellement exclusif avec theme-guyane.",
|
||||
category: "visual",
|
||||
version: "0.1.0",
|
||||
},
|
||||
{
|
||||
key: "landing-hero",
|
||||
name: "Hero d'accueil",
|
||||
|
|
@ -51,6 +59,14 @@ export const PLUGINS: PluginDescriptor[] = [
|
|||
category: "visual",
|
||||
version: "0.1.0",
|
||||
},
|
||||
{
|
||||
key: "image-gallery-aquarelle-seed",
|
||||
name: "Galerie aquarelles seed",
|
||||
description:
|
||||
"14 illustrations aquarelle (6 planches naturalistes carbets, 7 scènes carnet de voyage, 1 ornement palmier) stockées dans MinIO/karbe-medias/seed/aquarelle/. Création des Media liés aux 6 carbets démo. Désactivation : suppression des Media seedés (les fichiers MinIO restent).",
|
||||
category: "visual",
|
||||
version: "0.1.0",
|
||||
},
|
||||
{
|
||||
key: "demo-carbets-seed",
|
||||
name: "Carbets de démo",
|
||||
|
|
|
|||
28
src/lib/theme.ts
Normal file
28
src/lib/theme.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* Helpers theme — server-side.
|
||||
*
|
||||
* Centralise la résolution du theme actif (guyane | aquarelle | none) pour
|
||||
* que chaque composant qui veut un rendu spécifique au theme appelle un seul
|
||||
* helper plutôt que de checker `isPluginEnabled("theme-...")` individuellement.
|
||||
*/
|
||||
|
||||
import "server-only";
|
||||
import { isPluginEnabled } from "@/lib/plugins/server";
|
||||
|
||||
export type ActiveTheme = "guyane" | "aquarelle" | "none";
|
||||
|
||||
export async function getActiveTheme(): Promise<ActiveTheme> {
|
||||
if (await isPluginEnabled("theme-aquarelle")) return "aquarelle";
|
||||
if (await isPluginEnabled("theme-guyane")) return "guyane";
|
||||
return "none";
|
||||
}
|
||||
|
||||
/**
|
||||
* URL publique d'une illustration aquarelle hébergée dans MinIO.
|
||||
* Les fichiers sont uploadés dans karbe-medias/seed/aquarelle/ et servis via
|
||||
* media.karbe.cosmolan.fr (bucket public-download).
|
||||
*/
|
||||
export function aquarelleUrl(filename: string): string {
|
||||
const base = process.env.S3_PUBLIC_URL?.replace(/\/+$/, "") ?? "https://media.karbe.cosmolan.fr/karbe-medias";
|
||||
return `${base}/seed/aquarelle/${filename}`;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue