@salifbiaye/create-boutique 0.2.3 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -231,9 +231,12 @@ const day = (d, add = 0, end = false) => {
231
231
  };
232
232
  /** Événement d'un thème : « black-friday-2 » → « black-friday ». */
233
233
  const baseTheme = (theme) => theme.replace(/-\d+$/, "");
234
- /** Visuel du hero dans seed/images : hero-<slug>.*, sinon hero-<thème>.*, sinon hero.* (importé par le seed). */
235
- function heroImage(images, slug, theme) {
236
- for (const name of [`hero-${slug}`, `hero-${theme}`, "hero"]) {
234
+ /**
235
+ * Visuel de campagne dans seed/images (importé par le seed) : <prefix>-<slug>.*, sinon <prefix>-<thème>.*, sinon <prefix>.*
236
+ * prefix = "hero" (bannière du haut) ou "promo" (bandeau promo).
237
+ */
238
+ function heroImage(images, slug, theme, prefix = "hero") {
239
+ for (const name of [`${prefix}-${slug}`, `${prefix}-${theme}`, prefix]) {
237
240
  const found = images.find((f) => /\.(png|jpe?g|webp)$/i.test(f) && f.replace(/\.[^.]+$/, "").toLowerCase() === name);
238
241
  if (found)
239
242
  return found;
@@ -295,6 +298,9 @@ export function writeCampaignFiles(dir, p, language) {
295
298
  const hero = heroImage(images, b.slug, b.theme);
296
299
  if (hero)
297
300
  Object.assign(campaign, { hero: { image: hero } });
301
+ const promo = heroImage(images, b.slug, b.theme, "promo");
302
+ if (promo)
303
+ Object.assign(campaign, { promo: { image: promo } });
298
304
  fs.writeFileSync(file, JSON.stringify(campaign, null, 2) + "\n");
299
305
  }
300
306
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salifbiaye/create-boutique",
3
- "version": "0.2.3",
3
+ "version": "0.3.1",
4
4
  "description": "Usine à Boutiques : génère un projet complet (backend Medusa + une boutique Next.js par campagne) prêt pour docker compose",
5
5
  "type": "module",
6
6
  "repository": { "type": "git", "url": "git+https://github.com/salifbiaye/create-boutique.git" },
@@ -305,5 +305,8 @@
305
305
  "nav.menu": "Menu",
306
306
  "cart.variant": "Variant:",
307
307
  "checkout.testMode.title": "Demo store: no real payment",
308
- "checkout.testMode.text": "Card payments are in test mode: you will not be charged. Use the test card below with any future expiry date (e.g. 12/34) and any CVC (e.g. 123)."
308
+ "checkout.testMode.text": "Card payments are in test mode: you will not be charged. Use the test card below with any future expiry date (e.g. 12/34) and any CVC (e.g. 123).",
309
+ "harvest.gift": "Gift wrapping",
310
+ "harvest.giftSub": "Free on request",
311
+ "harvest.strip": "The season in pictures"
309
312
  }
@@ -305,5 +305,8 @@
305
305
  "nav.menu": "Menu",
306
306
  "cart.variant": "Variante :",
307
307
  "checkout.testMode.title": "Boutique de démonstration : aucun paiement réel",
308
- "checkout.testMode.text": "Le paiement par carte est en mode test : aucune somme ne sera débitée. Utilisez la carte de test ci-dessous, avec une date d'expiration future (ex. 12/34) et n'importe quel code (ex. 123)."
308
+ "checkout.testMode.text": "Le paiement par carte est en mode test : aucune somme ne sera débitée. Utilisez la carte de test ci-dessous, avec une date d'expiration future (ex. 12/34) et n'importe quel code (ex. 123).",
309
+ "harvest.gift": "Emballage cadeau",
310
+ "harvest.giftSub": "Offert sur demande",
311
+ "harvest.strip": "La saison en images"
309
312
  }
@@ -10,6 +10,9 @@ import CampaignEnded from "@modules/ub/campaign-ended"
10
10
  import CategoryTiles from "@modules/ub/category-tiles"
11
11
  import { ProductGrid } from "@modules/ub/product-card"
12
12
  import { DealsFold, DealsSpotlight } from "@modules/ub/showcase"
13
+ import { HarvestBanner, HarvestGrid, HarvestHero, HarvestServices, HarvestStrip } from "@modules/ub/harvest"
14
+ import { getBoutique } from "@lib/boutique/config"
15
+ import type { Campaign } from "@lib/boutique/campaign"
13
16
 
14
17
  async function Categories({ title }: { title: string }) {
15
18
  const categories = await listCategories().catch(() => [])
@@ -59,6 +62,35 @@ async function Showcase({ part, eyebrow }: { part: "fold" | "spotlight"; eyebrow
59
62
  )
60
63
  }
61
64
 
65
+ /** Accueil « Harvest » (thèmes Thanksgiving) : autre structure que l'accueil standard. */
66
+ async function HarvestProducts({ campaign }: { campaign: Campaign }) {
67
+ const {
68
+ response: { products },
69
+ } = await listProducts({ queryParams: { limit: 12, fields: "*categories,*collection" } })
70
+ const withImg = products.find((p) => p.thumbnail)
71
+ return (
72
+ <>
73
+ <HarvestGrid products={products} title={campaign.sections?.products ?? t("section.products")} sub={campaign.promo?.text} />
74
+ <HarvestBanner campaign={campaign} image={withImg?.thumbnail ?? undefined} />
75
+ <HarvestStrip products={[...products].reverse()} />
76
+ </>
77
+ )
78
+ }
79
+
80
+ /** Bandeau promo avec, à défaut d'image de campagne, la photo du produit le plus remisé. */
81
+ async function Promo({ campaign }: { campaign: Campaign }) {
82
+ const {
83
+ response: { products },
84
+ } = await listProducts({ queryParams: { limit: 12, fields: "*categories,*collection" } })
85
+ const off = (p: (typeof products)[number]) =>
86
+ Math.max(0, ...(p.variants ?? []).map((v) => {
87
+ const c = v.calculated_price
88
+ return c?.original_amount && c.calculated_amount != null ? 1 - c.calculated_amount / c.original_amount : 0
89
+ }))
90
+ const best = [...products].filter((p) => p.thumbnail).sort((a, b) => off(b) - off(a))[0]
91
+ return <PromoBanner campaign={campaign} fallbackImage={best?.thumbnail ?? undefined} />
92
+ }
93
+
62
94
  function GridFallback() {
63
95
  return <div className="ub-section" aria-busy="true" style={{ minHeight: 320 }} />
64
96
  }
@@ -71,6 +103,18 @@ export default async function Home() {
71
103
  return <CampaignEnded nextUrl={campaign.nextCampaignUrl} />
72
104
  }
73
105
 
106
+ if (getBoutique().theme.home === "harvest") {
107
+ return (
108
+ <div className="ub-harvest">
109
+ <HarvestHero campaign={campaign} />
110
+ <HarvestServices freeShipping={campaign.freeShipping} />
111
+ <Suspense fallback={<GridFallback />}>
112
+ <HarvestProducts campaign={campaign} />
113
+ </Suspense>
114
+ </div>
115
+ )
116
+ }
117
+
74
118
  return (
75
119
  <>
76
120
  <Hero campaign={campaign} />
@@ -84,7 +128,9 @@ export default async function Home() {
84
128
  <Suspense fallback={<GridFallback />}>
85
129
  <Showcase part="spotlight" eyebrow={campaign.hero.eyebrow} />
86
130
  </Suspense>
87
- <PromoBanner campaign={campaign} />
131
+ <Suspense fallback={<GridFallback />}>
132
+ <Promo campaign={campaign} />
133
+ </Suspense>
88
134
  <Suspense fallback={<GridFallback />}>
89
135
  <Products title={campaign.sections?.products ?? t("section.products")} />
90
136
  </Suspense>
@@ -13,6 +13,7 @@ import "styles/ub/showcase.css"
13
13
  import "styles/ub/forms.css"
14
14
  import "styles/ub/account.css"
15
15
  import "styles/ub/black-friday.css"
16
+ import "styles/ub/harvest.css"
16
17
  import "styles/ub/responsive.css"
17
18
  import "styles/ub/fonts.css"
18
19
  import "styles/ub/medusa-ui.css"
@@ -70,6 +70,8 @@ export const themeSchema = z.object({
70
70
  reference: z.string().optional(),
71
71
  /** Variante : reprend la mise en page (règles CSS) de ce thème, avec la palette de styles/ub/variants.css. */
72
72
  layout: z.string().regex(SLUG).optional(),
73
+ /** Page d'accueil : « default » (sections standard) ou « harvest » (accueil éditorial des thèmes Thanksgiving) */
74
+ home: z.enum(["default", "harvest"]).optional(),
73
75
  fonts: z.object({
74
76
  head: z.enum(["inter", "jakarta", "oswald", "outfit", "poppins", "serif", "sora", "space-grotesk"]),
75
77
  body: z.enum(["inter", "jakarta", "oswald", "outfit", "poppins", "serif", "sora", "space-grotesk"]),
@@ -145,9 +145,10 @@ export function TrustBar({ freeShipping }: { freeShipping?: string }) {
145
145
  )
146
146
  }
147
147
 
148
- export function PromoBanner({ campaign }: { campaign: Campaign }) {
148
+ /** Bandeau promo. Image : celle de la campagne (admin → Campagnes), sinon `fallbackImage` (photo d'un produit remisé). */
149
+ export function PromoBanner({ campaign, fallbackImage }: { campaign: Campaign; fallbackImage?: string }) {
149
150
  const { promo } = campaign
150
- const promoImage = promo.image && /^https?:\/\//.test(promo.image) ? promo.image : undefined
151
+ const promoImage = (promo.image && /^https?:\/\//.test(promo.image) ? promo.image : undefined) ?? fallbackImage
151
152
 
152
153
  return (
153
154
  <section className="ub-section" style={{ paddingTop: 0 }}>
@@ -0,0 +1,196 @@
1
+ import Link from "next/link"
2
+ import type { Campaign } from "@lib/boutique/campaign"
3
+ import { getBoutique } from "@lib/boutique/config"
4
+ import { t } from "@lib/boutique/i18n"
5
+ import { getProductPrice } from "@lib/util/get-product-price"
6
+ import { HttpTypes } from "@medusajs/types"
7
+ import { CampaignCountdown } from "./campaign-blocks"
8
+ import { Icon, type IconName } from "./icons"
9
+
10
+ /**
11
+ * Accueil « Harvest » (thèmes Thanksgiving, theme.json → "home": "harvest") : structure différente de l'accueil
12
+ * standard. Hero plein cadre avec titre condensé géant + mot manuscrit et bas en vague (« tache »), services en ligne,
13
+ * grille produits épurée, grande bannière photo, bande photo de fin. Composants serveur, CSS dans styles/ub/harvest.css.
14
+ */
15
+
16
+ type P = HttpTypes.StoreProduct
17
+ const imageOf = (p: P) => p.thumbnail ?? p.images?.[0]?.url ?? undefined
18
+
19
+ /** Bas du hero en forme de coulure / tache : raccord organique avec la section suivante (couleur de la page). */
20
+ function Drip() {
21
+ return (
22
+ <svg className="ub-hv-drip" viewBox="0 0 1440 140" preserveAspectRatio="none" aria-hidden>
23
+ <path d="M0 70 C 90 20 170 110 260 78 C 330 52 350 118 430 104 C 520 88 540 30 640 46 C 720 60 700 128 800 120 C 900 112 920 50 1010 58 C 1090 66 1100 122 1190 110 C 1270 100 1300 40 1370 52 C 1410 60 1430 80 1440 76 L1440 140 L0 140 Z" />
24
+ </svg>
25
+ )
26
+ }
27
+
28
+ export function HarvestHero({ campaign }: { campaign: Campaign }) {
29
+ const { hero } = campaign
30
+ const image = hero.image && /^https?:\/\//.test(hero.image) ? hero.image : undefined
31
+ // Mot manuscrit : en fin de titre, il passe sur sa propre ligne sous le titre condensé ; au milieu de la phrase,
32
+ // il reste à sa place (sinon la phrase devient incompréhensible).
33
+ const hl = hero.highlight && hero.title.includes(hero.highlight) ? hero.highlight : undefined
34
+ const i = hl ? hero.title.indexOf(hl) : -1
35
+ const before = hl ? hero.title.slice(0, i) : hero.title
36
+ const after = hl ? hero.title.slice(i + hl.length) : ""
37
+ const atEnd = !after.replace(/[\s.,;:!?…–-]/g, "")
38
+
39
+ return (
40
+ <section className="ub-hv-hero" style={image ? { ["--hv-img" as string]: `url("${image}")` } : undefined} data-has-image={image ? "" : undefined}>
41
+ {image && <div className="ub-hv-hero__bg" aria-hidden />}
42
+ <div className="ub-wrap ub-hv-hero__in">
43
+ <div className="ub-hv-hero__text">
44
+ {hero.eyebrow && <p className="ub-hv-eyebrow">{hero.eyebrow}</p>}
45
+ <h1 className="ub-hv-title">
46
+ {hl && !atEnd ? (
47
+ <span className="ub-hv-title__main">
48
+ {before}
49
+ <em className="ub-hv-title__inline">{hl}</em>
50
+ {after}
51
+ </span>
52
+ ) : (
53
+ <>
54
+ <span className="ub-hv-title__main">{before.replace(/[\s,;:–-]+$/, "")}</span>
55
+ {hl && <em className="ub-hv-title__script">{hl}</em>}
56
+ </>
57
+ )}
58
+ </h1>
59
+ {hero.sub && <p className="ub-hv-sub">{hero.sub}</p>}
60
+ <div className="ub-hv-actions">
61
+ <Link href="/store" className="ub-hv-btn">
62
+ {hero.cta} <Icon name="arrowRight" />
63
+ </Link>
64
+ {campaign.showCountdown !== false && (
65
+ <div className="ub-hv-cd">
66
+ <CampaignCountdown endsAt={campaign.endsAt} />
67
+ </div>
68
+ )}
69
+ </div>
70
+ </div>
71
+ </div>
72
+ <Drip />
73
+ </section>
74
+ )
75
+ }
76
+
77
+ /** Services sur une ligne, icônes fines centrées (référence : boutique de sacs). */
78
+ export function HarvestServices({ freeShipping }: { freeShipping?: string }) {
79
+ const items: [IconName, string, string][] = [
80
+ ["truck", t("trust.shipping"), freeShipping ? t("trust.shippingSub", { freeShipping }) : t("trust.shippingSubAll")],
81
+ ["returns", t("trust.returns"), t("trust.returnsSub")],
82
+ ["gift", t("harvest.gift"), t("harvest.giftSub")],
83
+ ["shield", t("trust.secure"), t("trust.secureSub")],
84
+ ]
85
+ return (
86
+ <section className="ub-wrap ub-hv-services">
87
+ {items.map(([icon, title, sub]) => (
88
+ <div key={icon}>
89
+ <Icon name={icon} />
90
+ <b>{title}</b>
91
+ <small>{sub}</small>
92
+ </div>
93
+ ))}
94
+ </section>
95
+ )
96
+ }
97
+
98
+ /** Grille épurée : grande image, nom, catégorie, prix barré ; en-tête avec lien « Tout voir ». */
99
+ export function HarvestGrid({ products, title, sub }: { products: P[]; title: string; sub?: string }) {
100
+ if (!products.length) return null
101
+ return (
102
+ <section className="ub-wrap ub-hv-section">
103
+ <div className="ub-hv-head">
104
+ <div>
105
+ <h2 className="ub-hv-h2">{title}</h2>
106
+ {sub && <p>{sub}</p>}
107
+ </div>
108
+ <Link href="/store" className="ub-hv-more">
109
+ {t("section.viewAll")} <Icon name="arrowRight" />
110
+ </Link>
111
+ </div>
112
+ <ul className="ub-hv-grid">
113
+ {products.slice(0, 8).map((p) => {
114
+ const { cheapestPrice: price } = getProductPrice({ product: p })
115
+ const onSale = price?.price_type === "sale"
116
+ const image = imageOf(p)
117
+ return (
118
+ <li key={p.id}>
119
+ <Link href={`/products/${p.handle}`} className="ub-hv-card">
120
+ <span className="ub-hv-card__media">
121
+ {image ? (
122
+ // eslint-disable-next-line @next/next/no-img-element
123
+ <img src={image} alt="" loading="lazy" />
124
+ ) : (
125
+ <span className="ub-ph" />
126
+ )}
127
+ {onSale && price?.percentage_diff && <span className="ub-hv-card__off">-{price.percentage_diff}%</span>}
128
+ <span className="ub-hv-card__add" aria-hidden>
129
+ <Icon name="cart" />
130
+ </span>
131
+ </span>
132
+ <span className="ub-hv-card__name">{p.title}</span>
133
+ {(p.categories?.[0]?.name ?? p.collection?.title) && (
134
+ <span className="ub-hv-card__cat">{p.categories?.[0]?.name ?? p.collection?.title}</span>
135
+ )}
136
+ {price && (
137
+ <span className="ub-hv-card__price">
138
+ {onSale && <s>{price.original_price}</s>}
139
+ <b>{price.calculated_price}</b>
140
+ </span>
141
+ )}
142
+ </Link>
143
+ </li>
144
+ )
145
+ })}
146
+ </ul>
147
+ </section>
148
+ )
149
+ }
150
+
151
+ /** Grande bannière photo arrondie avec texte et bouton (textes du bandeau promo de la campagne). */
152
+ export function HarvestBanner({ campaign, image }: { campaign: Campaign; image?: string }) {
153
+ const promo = campaign.promo
154
+ if (!promo) return null
155
+ const bg = (promo.image && /^https?:\/\//.test(promo.image) ? promo.image : undefined) ?? image
156
+ return (
157
+ <section className="ub-wrap ub-hv-section">
158
+ <div className="ub-hv-banner" style={bg ? { ["--hv-banner" as string]: `url("${bg}")` } : undefined}>
159
+ <div className="ub-hv-banner__text">
160
+ {promo.eyebrow && <p className="ub-hv-eyebrow">{promo.eyebrow}</p>}
161
+ <h2 className="ub-hv-h2">{promo.title}</h2>
162
+ {promo.text && <p>{promo.text}</p>}
163
+ <Link href="/store" className="ub-hv-btn ub-hv-btn--light">
164
+ {promo.cta ?? t("nav.cta")} <Icon name="arrowRight" />
165
+ </Link>
166
+ </div>
167
+ {promo.seal && <span className="ub-hv-banner__seal">{promo.seal}</span>}
168
+ </div>
169
+ </section>
170
+ )
171
+ }
172
+
173
+ /** Bande photo de fin : 5 visuels produits cliquables, titre avec le nom de la boutique. */
174
+ export function HarvestStrip({ products }: { products: P[] }) {
175
+ const withImg = products.filter(imageOf).slice(0, 5)
176
+ if (withImg.length < 3) return null
177
+ const { storeName } = getBoutique()
178
+ return (
179
+ <section className="ub-hv-section ub-hv-strip">
180
+ <div className="ub-wrap ub-hv-strip__head">
181
+ <p className="ub-hv-eyebrow">{t("harvest.strip")}</p>
182
+ <h2 className="ub-hv-h2">@{storeName.toLowerCase().replace(/[^a-z0-9]+/g, "")}</h2>
183
+ </div>
184
+ <ul className="ub-hv-strip__row">
185
+ {withImg.map((p) => (
186
+ <li key={p.id}>
187
+ <Link href={`/products/${p.handle}`} aria-label={p.title ?? ""}>
188
+ {/* eslint-disable-next-line @next/next/no-img-element */}
189
+ <img src={imageOf(p)} alt="" loading="lazy" />
190
+ </Link>
191
+ </li>
192
+ ))}
193
+ </ul>
194
+ </section>
195
+ )
196
+ }
@@ -0,0 +1,245 @@
1
+ /* Accueil « Harvest » (thèmes Thanksgiving) — modules/ub/harvest.tsx.
2
+ Thanksgiving 1 : palette de l'image du hero (crème, citrouille, rouille, chocolat, moutarde), cadre moutarde.
3
+ Thanksgiving 2 : même structure en miroir, vert forêt, titres serif. */
4
+
5
+ /* ---------- Palette Thanksgiving 1 (d'après l'image du hero) ---------- */
6
+ [data-theme="thanksgiving"]:not([data-variant]) {
7
+ --bg: #fbf4e8; --surface: #f6ead7; --tint-1: #f3e2c7; --tint-2: #efd9b8; --tint-3: #f5dccb; --tint-4: #efe3cf;
8
+ --ink: #3b2412; --ink-muted: #7a5a3e; --border: #ead9bf;
9
+ --primary: #d4661b; --on-primary: #ffffff; --accent: #b3401f; --on-accent: #ffffff;
10
+ --sale: #b3401f; --star: #e3a33a; --success: #4d7c2a;
11
+ --hero-bg: #f6ead7; --hero-ink: #3b2412; --hero-muted: #7a5a3e;
12
+ --band-bg: #b3401f; --band-ink: #fff8ec; --bubble-bg: #e3a33a; --bubble-ink: #3b2412;
13
+ --topbar-bg: #4a2a14; --topbar-ink: #f6ead7; --focus: #d4661b;
14
+ --ub-font-head: var(--font-oswald);
15
+ --ub-head-weight: 700; --ub-head-case: uppercase; --ub-head-track: 0;
16
+ --ub-r-card: 18px; --ub-r-btn: var(--radius-pill); --ub-r-tile: 18px; --ub-r-media: 18px;
17
+ }
18
+ /* Cadre moutarde autour de toute la page (référence : emballage « Begg ») */
19
+ [data-theme="thanksgiving"]:not([data-variant]) body::after {
20
+ content: "";
21
+ position: fixed;
22
+ inset: 0;
23
+ z-index: 90;
24
+ border: 10px solid var(--bubble-bg);
25
+ pointer-events: none;
26
+ }
27
+
28
+ .ub-harvest {
29
+ --hv-script: "Segoe Script", "Brush Script MT", "Snell Roundhand", "Apple Chancery", cursive;
30
+ --hv-title: var(--font-oswald);
31
+ }
32
+
33
+ /* ---------- Hero ---------- */
34
+ .ub-hv-hero {
35
+ position: relative;
36
+ display: flex;
37
+ align-items: center;
38
+ min-height: clamp(560px, 52vw, 780px);
39
+ overflow: hidden;
40
+ background: var(--hero-bg) var(--hv-img, none) right center / cover no-repeat;
41
+ color: var(--hero-ink);
42
+ }
43
+ /* Voile pour garder le texte lisible sur l'image, côté texte */
44
+ .ub-hv-hero::before {
45
+ content: "";
46
+ position: absolute;
47
+ inset: 0;
48
+ background: linear-gradient(90deg, var(--hero-bg) 0%, color-mix(in srgb, var(--hero-bg) 70%, transparent) 38%, transparent 62%);
49
+ pointer-events: none;
50
+ }
51
+ .ub-hv-hero:not([data-has-image])::after {
52
+ content: "";
53
+ position: absolute;
54
+ right: -8%;
55
+ top: 10%;
56
+ width: 46%;
57
+ aspect-ratio: 1;
58
+ border-radius: 42% 58% 55% 45% / 48% 42% 58% 52%;
59
+ background: radial-gradient(circle at 35% 35%, var(--bubble-bg), var(--primary) 60%, var(--accent));
60
+ opacity: 0.9;
61
+ }
62
+ .ub-hv-hero__in { position: relative; z-index: 1; width: 100%; padding-top: var(--space-16); padding-bottom: 150px; }
63
+ .ub-hv-hero__text { max-width: 620px; }
64
+ .ub-hv-eyebrow { font-size: 13px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--primary); }
65
+ .ub-hv-title { margin-top: var(--space-3) !important; line-height: 0.9; }
66
+ .ub-hv-title__main {
67
+ display: block;
68
+ font-family: var(--hv-title);
69
+ font-weight: 700;
70
+ font-size: clamp(54px, 7.6vw, 118px);
71
+ text-transform: uppercase;
72
+ letter-spacing: -0.005em;
73
+ color: var(--hero-ink);
74
+ animation: ub-rise 0.7s cubic-bezier(0.2, 0.7, 0.2, 1) both;
75
+ }
76
+ .ub-hv-title__script {
77
+ display: block;
78
+ margin: -0.1em 0 0 0.4em;
79
+ font-family: var(--hv-script);
80
+ font-style: normal;
81
+ font-weight: 400;
82
+ font-size: clamp(42px, 5.6vw, 88px);
83
+ line-height: 1.1;
84
+ color: var(--primary);
85
+ transform: rotate(-5deg);
86
+ transform-origin: left center;
87
+ animation: ub-rise 0.7s 0.12s cubic-bezier(0.2, 0.7, 0.2, 1) both;
88
+ }
89
+ .ub-hv-sub { max-width: 46ch; margin-top: var(--space-6) !important; font-size: 17px; line-height: 27px; color: var(--hero-muted); }
90
+ .ub-hv-actions { display: flex; align-items: center; gap: var(--space-6); flex-wrap: wrap; margin-top: var(--space-8); }
91
+ .ub-hv-btn {
92
+ display: inline-flex;
93
+ align-items: center;
94
+ gap: 10px;
95
+ height: 52px;
96
+ padding: 0 28px;
97
+ border-radius: var(--radius-pill);
98
+ background: var(--primary);
99
+ color: var(--on-primary);
100
+ font-weight: 700;
101
+ font-size: 15px;
102
+ letter-spacing: 0.02em;
103
+ box-shadow: 0 12px 30px -10px color-mix(in srgb, var(--primary) 70%, transparent);
104
+ transition: transform 0.2s, box-shadow 0.2s;
105
+ }
106
+ .ub-hv-btn:hover { transform: translateY(-2px); }
107
+ .ub-hv-btn svg { width: 18px; height: 18px; }
108
+ .ub-hv-btn--light { background: #ffffff; color: var(--ink); box-shadow: none; }
109
+ .ub-hv-cd .ub-countdown__c { background: color-mix(in srgb, var(--bg) 70%, transparent); color: var(--ink); border: 1px solid var(--border); backdrop-filter: blur(6px); }
110
+ .ub-hv-cd .ub-countdown__c small { color: var(--ink-muted); }
111
+ /* Bas en coulure (« tache ») vers la section suivante */
112
+ .ub-hv-drip { position: absolute; left: 0; right: 0; bottom: -1px; width: 100%; height: clamp(60px, 9vw, 130px); fill: var(--bg); z-index: 1; }
113
+
114
+ /* ---------- Services ---------- */
115
+ .ub-hv-services { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-6); padding-top: var(--space-8); padding-bottom: var(--space-8); text-align: center; }
116
+ .ub-hv-services > div { display: grid; justify-items: center; gap: 6px; }
117
+ .ub-hv-services svg { width: 28px; height: 28px; color: var(--primary); margin-bottom: 4px; }
118
+ .ub-hv-services b { font-size: 14px; }
119
+ .ub-hv-services small { font-size: 12px; color: var(--ink-muted); }
120
+
121
+ /* ---------- Sections ---------- */
122
+ .ub-hv-section { padding-top: var(--space-12); padding-bottom: var(--space-12); }
123
+ .ub-hv-head { display: flex; align-items: flex-end; justify-content: space-between; gap: var(--space-4); margin-bottom: var(--space-8); }
124
+ .ub-hv-head p { margin-top: 6px !important; color: var(--ink-muted); max-width: 52ch; }
125
+ .ub-hv-h2 { font-family: var(--ub-font-head); font-weight: 700; text-transform: var(--ub-head-case); font-size: clamp(30px, 3.6vw, 48px); line-height: 1.02; }
126
+ .ub-hv-more { display: inline-flex; align-items: center; gap: 6px; flex: none; font-weight: 700; font-size: 14px; }
127
+ .ub-hv-more svg { width: 16px; height: 16px; transition: transform 0.15s; }
128
+ .ub-hv-more:hover svg { transform: translateX(3px); }
129
+
130
+ /* ---------- Grille épurée ---------- */
131
+ .ub-hv-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--space-8) var(--space-6); margin: 0; padding: 0; list-style: none; }
132
+ .ub-hv-card { display: grid; gap: 4px; }
133
+ .ub-hv-card__media { position: relative; display: block; aspect-ratio: 4 / 5; overflow: hidden; margin-bottom: var(--space-3); border-radius: var(--ub-r-card); background: var(--surface); }
134
+ .ub-hv-card__media img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.6s cubic-bezier(0.2, 0.7, 0.2, 1); }
135
+ .ub-hv-card:hover .ub-hv-card__media img { transform: scale(1.05); }
136
+ .ub-hv-card__media .ub-ph { position: absolute; inset: 0; }
137
+ .ub-hv-card__off { position: absolute; left: 12px; top: 12px; padding: 4px 10px; border-radius: var(--radius-pill); background: var(--accent); color: var(--on-accent); font-size: 12px; font-weight: 700; }
138
+ .ub-hv-card__add { position: absolute; right: 12px; bottom: 12px; display: grid; place-items: center; width: 42px; height: 42px; border-radius: 50%; background: var(--bg); color: var(--ink); box-shadow: var(--shadow-card); opacity: 0; transform: translateY(8px); transition: opacity 0.2s, transform 0.2s; }
139
+ .ub-hv-card__add svg { width: 18px; height: 18px; }
140
+ .ub-hv-card:hover .ub-hv-card__add, .ub-hv-card:focus-visible .ub-hv-card__add { opacity: 1; transform: none; }
141
+ .ub-hv-card__name { font-weight: 600; font-size: 15px; line-height: 21px; }
142
+ .ub-hv-card__cat { font-size: 13px; color: var(--ink-muted); }
143
+ .ub-hv-card__price { display: flex; align-items: baseline; gap: 8px; margin-top: 2px; }
144
+ .ub-hv-card__price s { font-size: 13px; color: var(--ink-muted); }
145
+ .ub-hv-card__price b { color: var(--sale); font-size: 16px; }
146
+
147
+ /* ---------- Grande bannière photo ---------- */
148
+ .ub-hv-banner {
149
+ position: relative;
150
+ display: flex;
151
+ align-items: center;
152
+ min-height: clamp(320px, 34vw, 460px);
153
+ overflow: hidden;
154
+ padding: clamp(28px, 5vw, 64px);
155
+ border-radius: calc(var(--ub-r-card) * 1.4);
156
+ background: var(--band-bg) var(--hv-banner, none) center / cover no-repeat;
157
+ color: #ffffff;
158
+ }
159
+ .ub-hv-banner::before { content: ""; position: absolute; inset: 0; background: linear-gradient(90deg, color-mix(in srgb, var(--band-bg) 92%, #000) 0%, color-mix(in srgb, var(--band-bg) 55%, transparent) 55%, transparent 100%); }
160
+ .ub-hv-banner__text { position: relative; display: grid; gap: var(--space-4); max-width: 520px; justify-items: start; }
161
+ .ub-hv-banner .ub-hv-eyebrow { color: var(--bubble-bg); }
162
+ .ub-hv-banner .ub-hv-h2 { color: #ffffff; }
163
+ .ub-hv-banner p { color: rgb(255 255 255 / 0.85); font-size: 16px; line-height: 25px; }
164
+ .ub-hv-banner__seal { position: absolute; right: clamp(20px, 4vw, 56px); top: 50%; transform: translateY(-50%) rotate(8deg); display: grid; place-items: center; width: 132px; height: 132px; padding: 16px; border-radius: 50%; background: var(--bubble-bg); color: var(--bubble-ink); font-family: var(--hv-script); font-size: 20px; line-height: 1.1; text-align: center; }
165
+
166
+ /* ---------- Bande photo ---------- */
167
+ .ub-hv-strip__head { display: grid; justify-items: center; gap: 6px; margin-bottom: var(--space-8); text-align: center; }
168
+ .ub-hv-strip__row { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: var(--space-3); margin: 0; padding: 0 var(--space-3); list-style: none; }
169
+ .ub-hv-strip__row a { display: block; aspect-ratio: 1; overflow: hidden; border-radius: var(--ub-r-card); }
170
+ .ub-hv-strip__row img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.6s; }
171
+ .ub-hv-strip__row a:hover img { transform: scale(1.06); }
172
+
173
+ /* ---------- Thanksgiving 2 « Harvest » : miroir, vert, serif ---------- */
174
+ [data-variant="thanksgiving-2"] .ub-harvest { --hv-title: "DM Serif Display", Georgia, "Times New Roman", serif; }
175
+ [data-variant="thanksgiving-2"] .ub-hv-hero { background-position: left center; }
176
+ [data-variant="thanksgiving-2"] .ub-hv-hero::before { background: linear-gradient(270deg, var(--hero-bg) 0%, color-mix(in srgb, var(--hero-bg) 72%, transparent) 40%, transparent 64%); }
177
+ [data-variant="thanksgiving-2"] .ub-hv-hero:not([data-has-image])::after { right: auto; left: -8%; }
178
+ [data-variant="thanksgiving-2"] .ub-hv-hero__in { display: flex; justify-content: flex-end; }
179
+ [data-variant="thanksgiving-2"] .ub-hv-title__main { text-transform: none; font-weight: 400; font-style: italic; letter-spacing: -0.01em; font-size: clamp(50px, 6.6vw, 104px); line-height: 0.98; }
180
+ [data-variant="thanksgiving-2"] .ub-hv-title__script { color: var(--accent); }
181
+ [data-variant="thanksgiving-2"] .ub-hv-drip { transform: scaleX(-1); }
182
+ [data-variant="thanksgiving-2"] .ub-hv-h2 { font-family: var(--hv-title); font-weight: 400; font-style: italic; text-transform: none; }
183
+ [data-variant="thanksgiving-2"] .ub-hv-card__media { border-radius: 999px 999px var(--ub-r-card) var(--ub-r-card); }
184
+ [data-variant="thanksgiving-2"] .ub-hv-strip__row a { border-radius: 999px; }
185
+
186
+ /* ---------- Mobile ---------- */
187
+ @media (max-width: 900px) {
188
+ .ub-hv-hero { align-items: flex-start; min-height: 0; background-position: 70% bottom; background-size: 180% auto; }
189
+ .ub-hv-hero::before,
190
+ [data-variant="thanksgiving-2"] .ub-hv-hero::before { background: linear-gradient(180deg, var(--hero-bg) 0%, var(--hero-bg) 48%, color-mix(in srgb, var(--hero-bg) 40%, transparent) 70%, transparent 100%); }
191
+ .ub-hv-hero__in { padding-top: var(--space-12); padding-bottom: 340px; }
192
+ [data-variant="thanksgiving-2"] .ub-hv-hero__in { justify-content: flex-start; }
193
+ .ub-hv-services { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-6) var(--space-4); }
194
+ .ub-hv-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-6) var(--space-3); }
195
+ .ub-hv-banner__seal { display: none; }
196
+ .ub-hv-strip__row { display: flex; overflow-x: auto; scroll-snap-type: x mandatory; scrollbar-width: none; }
197
+ .ub-hv-strip__row li { flex: 0 0 62%; scroll-snap-align: center; }
198
+ .ub-hv-head { flex-direction: column; align-items: flex-start; }
199
+ [data-theme="thanksgiving"]:not([data-variant]) body::after { border-width: 6px; }
200
+ }
201
+ @media (prefers-reduced-motion: reduce) {
202
+ .ub-hv-title__main, .ub-hv-title__script { animation: none; }
203
+ }
204
+
205
+ /* Mot manuscrit au milieu du titre : reste dans la phrase */
206
+ .ub-hv-title__inline {
207
+ display: inline-block;
208
+ padding: 0 0.12em;
209
+ font-family: var(--hv-script);
210
+ font-style: normal;
211
+ font-weight: 400;
212
+ font-size: 0.82em;
213
+ text-transform: none;
214
+ color: var(--primary);
215
+ transform: rotate(-4deg) translateY(0.06em);
216
+ }
217
+ [data-variant="thanksgiving-2"] .ub-hv-title__inline { color: var(--accent); }
218
+
219
+ /* Photos en arche (Thanksgiving 2) : pastille de remise en bas de l'image (sinon coupée par l'arrondi) */
220
+ [data-variant="thanksgiving-2"] .ub-hv-card__off { top: auto; bottom: 12px; }
221
+
222
+ /* Accueil Harvest : pas de 2e bandeau d'appel à l'action juste après la grande bannière */
223
+ body:has(.ub-harvest) .ub-footcta { display: none; }
224
+
225
+ /* ---------- Image du hero sur son propre calque (miroir possible, empilée sur mobile) ---------- */
226
+ .ub-hv-hero { isolation: isolate; background-image: none; }
227
+ .ub-hv-hero__bg { position: absolute; inset: 0; z-index: 0; background: var(--hv-img) right center / cover no-repeat; }
228
+ .ub-hv-hero::before { z-index: 1; }
229
+ .ub-hv-hero__in { z-index: 2; }
230
+ .ub-hv-drip { z-index: 3; }
231
+ /* Thanksgiving 2 (texte à droite) : l'image doit avoir son sujet à gauche et un espace libre à droite (pas de miroir automatique). */
232
+ [data-variant="thanksgiving-2"] .ub-hv-hero__bg { background-position: left center; }
233
+
234
+ @media (max-width: 900px) {
235
+ .ub-hv-hero { flex-direction: column; align-items: stretch; background-size: auto; }
236
+ .ub-hv-hero::before { display: none; }
237
+ .ub-hv-hero__in { order: 1; padding-top: var(--space-8); padding-bottom: var(--space-6); }
238
+ .ub-hv-hero__bg { position: relative; order: 2; inset: auto; height: clamp(260px, 72vw, 380px); background-position: 82% center; }
239
+ [data-variant="thanksgiving-2"] .ub-hv-hero__bg { background-position: 18% center; }
240
+ .ub-hv-hero__bg::before { content: ""; position: absolute; inset: 0 0 auto 0; height: 36%; background: linear-gradient(180deg, var(--hero-bg), transparent); }
241
+ .ub-hv-drip { height: 56px; }
242
+ .ub-hv-title__main { font-size: clamp(44px, 12vw, 64px); }
243
+ .ub-hv-title__script { font-size: clamp(36px, 10vw, 52px); }
244
+ .ub-hv-actions { gap: var(--space-4); }
245
+ }
@@ -3,7 +3,8 @@
3
3
  "name": "Thanksgiving",
4
4
  "reference": "NovaTrend",
5
5
  "fonts": {
6
- "head": "jakarta",
6
+ "head": "oswald",
7
7
  "body": "jakarta"
8
- }
8
+ },
9
+ "home": "harvest"
9
10
  }
@@ -6,5 +6,6 @@
6
6
  "fonts": {
7
7
  "head": "sora",
8
8
  "body": "jakarta"
9
- }
9
+ },
10
+ "home": "harvest"
10
11
  }