@salifbiaye/create-boutique 0.2.2 → 0.3.0
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.
- package/package.json +1 -1
- package/templates/storefront/locales/en.json +6 -1
- package/templates/storefront/locales/fr.json +6 -1
- package/templates/storefront/src/app/(main)/page.tsx +30 -0
- package/templates/storefront/src/app/layout.tsx +1 -0
- package/templates/storefront/src/lib/boutique/schema.ts +2 -0
- package/templates/storefront/src/modules/checkout/components/payment/index.tsx +14 -0
- package/templates/storefront/src/modules/checkout/templates/checkout-form/index.tsx +6 -1
- package/templates/storefront/src/modules/ub/harvest.tsx +196 -0
- package/templates/storefront/src/styles/ub/forms.css +26 -0
- package/templates/storefront/src/styles/ub/harvest.css +245 -0
- package/templates/storefront/themes/thanksgiving/theme.json +3 -2
- package/templates/storefront/themes/thanksgiving-2/theme.json +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salifbiaye/create-boutique",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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" },
|
|
@@ -303,5 +303,10 @@
|
|
|
303
303
|
"account.profileDone": "Profile completed",
|
|
304
304
|
"account.completeProfile": "Complete your profile (phone, billing address) to check out faster.",
|
|
305
305
|
"nav.menu": "Menu",
|
|
306
|
-
"cart.variant": "Variant:"
|
|
306
|
+
"cart.variant": "Variant:",
|
|
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).",
|
|
309
|
+
"harvest.gift": "Gift wrapping",
|
|
310
|
+
"harvest.giftSub": "Free on request",
|
|
311
|
+
"harvest.strip": "The season in pictures"
|
|
307
312
|
}
|
|
@@ -303,5 +303,10 @@
|
|
|
303
303
|
"account.profileDone": "Profil complété",
|
|
304
304
|
"account.completeProfile": "Complétez votre profil (téléphone, adresse de facturation) pour commander plus vite.",
|
|
305
305
|
"nav.menu": "Menu",
|
|
306
|
-
"cart.variant": "Variante :"
|
|
306
|
+
"cart.variant": "Variante :",
|
|
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).",
|
|
309
|
+
"harvest.gift": "Emballage cadeau",
|
|
310
|
+
"harvest.giftSub": "Offert sur demande",
|
|
311
|
+
"harvest.strip": "La saison en images"
|
|
307
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,21 @@ 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
|
+
|
|
62
80
|
function GridFallback() {
|
|
63
81
|
return <div className="ub-section" aria-busy="true" style={{ minHeight: 320 }} />
|
|
64
82
|
}
|
|
@@ -71,6 +89,18 @@ export default async function Home() {
|
|
|
71
89
|
return <CampaignEnded nextUrl={campaign.nextCampaignUrl} />
|
|
72
90
|
}
|
|
73
91
|
|
|
92
|
+
if (getBoutique().theme.home === "harvest") {
|
|
93
|
+
return (
|
|
94
|
+
<div className="ub-harvest">
|
|
95
|
+
<HarvestHero campaign={campaign} />
|
|
96
|
+
<HarvestServices freeShipping={campaign.freeShipping} />
|
|
97
|
+
<Suspense fallback={<GridFallback />}>
|
|
98
|
+
<HarvestProducts campaign={campaign} />
|
|
99
|
+
</Suspense>
|
|
100
|
+
</div>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
74
104
|
return (
|
|
75
105
|
<>
|
|
76
106
|
<Hero campaign={campaign} />
|
|
@@ -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"]),
|
|
@@ -19,9 +19,12 @@ import { useCallback, useEffect, useState } from "react"
|
|
|
19
19
|
const Payment = ({
|
|
20
20
|
cart,
|
|
21
21
|
availablePaymentMethods,
|
|
22
|
+
testMode = false,
|
|
22
23
|
}: {
|
|
23
24
|
cart: any
|
|
24
25
|
availablePaymentMethods: any[]
|
|
26
|
+
/** Clé Stripe de test : affiche la carte de test à utiliser (aucun paiement réel) */
|
|
27
|
+
testMode?: boolean
|
|
25
28
|
}) => {
|
|
26
29
|
const activeSession = cart.payment_collection?.payment_sessions?.find(
|
|
27
30
|
(paymentSession: any) => paymentSession.status === "pending"
|
|
@@ -137,6 +140,17 @@ const Payment = ({
|
|
|
137
140
|
</div>
|
|
138
141
|
<div>
|
|
139
142
|
<div className={isOpen ? "block" : "hidden"}>
|
|
143
|
+
{testMode && (
|
|
144
|
+
<div className="ub-testpay" role="note" data-testid="stripe-test-mode">
|
|
145
|
+
<b>
|
|
146
|
+
<T k="checkout.testMode.title" />
|
|
147
|
+
</b>
|
|
148
|
+
<span>
|
|
149
|
+
<T k="checkout.testMode.text" />
|
|
150
|
+
</span>
|
|
151
|
+
<code>4242 4242 4242 4242</code>
|
|
152
|
+
</div>
|
|
153
|
+
)}
|
|
140
154
|
{!paidByGiftcard && availablePaymentMethods?.length && (
|
|
141
155
|
<>
|
|
142
156
|
<RadioGroup
|
|
@@ -30,7 +30,12 @@ export default async function CheckoutForm({
|
|
|
30
30
|
|
|
31
31
|
<Shipping cart={cart} availableShippingMethods={shippingMethods} />
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
{/* Clé Stripe de test : bandeau « aucun paiement réel » + carte de test (disparaît avec une clé pk_live_) */}
|
|
34
|
+
<Payment
|
|
35
|
+
cart={cart}
|
|
36
|
+
availablePaymentMethods={paymentMethods}
|
|
37
|
+
testMode={!!process.env.STRIPE_PUBLISHABLE_KEY?.startsWith("pk_test_")}
|
|
38
|
+
/>
|
|
34
39
|
|
|
35
40
|
<Review cart={cart} />
|
|
36
41
|
</div>
|
|
@@ -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
|
+
}
|
|
@@ -59,3 +59,29 @@
|
|
|
59
59
|
pointer-events: none;
|
|
60
60
|
}
|
|
61
61
|
.ub-phone[data-invalid] .ub-phone__field input { border-color: var(--sale); }
|
|
62
|
+
|
|
63
|
+
/* Paiement en mode test (clé Stripe pk_test_) : carte de test à utiliser, aucun débit réel */
|
|
64
|
+
.ub-testpay {
|
|
65
|
+
display: grid;
|
|
66
|
+
gap: 6px;
|
|
67
|
+
margin-bottom: var(--space-4);
|
|
68
|
+
padding: var(--space-4);
|
|
69
|
+
border: 1px dashed var(--primary);
|
|
70
|
+
border-radius: var(--ub-r-card);
|
|
71
|
+
background: color-mix(in srgb, var(--primary) 10%, transparent);
|
|
72
|
+
font-size: 14px;
|
|
73
|
+
line-height: 21px;
|
|
74
|
+
}
|
|
75
|
+
.ub-testpay b { font-size: 15px; }
|
|
76
|
+
.ub-testpay span { color: var(--ink-muted); }
|
|
77
|
+
.ub-testpay code {
|
|
78
|
+
justify-self: start;
|
|
79
|
+
margin-top: 4px;
|
|
80
|
+
padding: 6px 12px;
|
|
81
|
+
border-radius: var(--radius-sm);
|
|
82
|
+
background: var(--ink);
|
|
83
|
+
color: var(--bg);
|
|
84
|
+
font: 700 15px/20px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
85
|
+
letter-spacing: 0.06em;
|
|
86
|
+
user-select: all;
|
|
87
|
+
}
|
|
@@ -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
|
+
}
|