create-brainerce-store 1.50.5 → 1.51.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/dist/index.js +171 -66
- package/messages/en.json +4 -1
- package/messages/he.json +4 -1
- package/package.json +1 -1
- package/templates/nextjs/base/.claude/commands/design.md +51 -0
- package/templates/nextjs/base/.env.local.ejs +5 -0
- package/templates/nextjs/base/.eslintrc.json +57 -3
- package/templates/nextjs/base/AGENTS.md +41 -0
- package/templates/nextjs/base/AI-GUIDE.md +94 -0
- package/templates/nextjs/base/CLAUDE.md +42 -0
- package/templates/nextjs/base/TRANSLATIONS.md +1 -1
- package/templates/nextjs/base/next.config.ts +2 -2
- package/templates/nextjs/base/src/app/account/page.tsx +5 -5
- package/templates/nextjs/base/src/app/api/auth/logout/route.ts +1 -1
- package/templates/nextjs/base/src/app/api/auth/reset-password/route.ts +2 -2
- package/templates/nextjs/base/src/app/api/store/[...path]/route.ts +1 -1
- package/templates/nextjs/base/src/app/auth/callback/page.tsx +4 -4
- package/templates/nextjs/base/src/app/blog/[slug]/page.tsx.ejs +4 -4
- package/templates/nextjs/base/src/app/blog/page.tsx.ejs +2 -2
- package/templates/nextjs/base/src/app/cart/page.tsx +2 -173
- package/templates/nextjs/base/src/app/checkout/page.tsx +12 -11
- package/templates/nextjs/base/src/app/contact/page.tsx +3 -3
- package/templates/nextjs/base/src/app/faq/page.tsx.ejs +2 -2
- package/templates/nextjs/base/src/app/forgot-password/page.tsx +4 -4
- package/templates/nextjs/base/src/app/layout.tsx.ejs +12 -12
- package/templates/nextjs/base/src/app/login/page.tsx +4 -4
- package/templates/nextjs/base/src/app/order-confirmation/page.tsx +19 -12
- package/templates/nextjs/base/src/app/page.tsx +4 -4
- package/templates/nextjs/base/src/app/pages/[slug]/page.tsx.ejs +3 -3
- package/templates/nextjs/base/src/app/payment-complete/page.tsx +1 -1
- package/templates/nextjs/base/src/app/products/[slug]/page.tsx +6 -6
- package/templates/nextjs/base/src/app/products/page.tsx +4 -623
- package/templates/nextjs/base/src/app/register/page.tsx +4 -4
- package/templates/nextjs/base/src/app/reset-password/page.tsx +5 -5
- package/templates/nextjs/base/src/app/sitemap.ts +1 -1
- package/templates/nextjs/base/src/app/verify-email/page.tsx +5 -5
- package/templates/nextjs/base/src/components/account/address-book.tsx +3 -3
- package/templates/nextjs/base/src/components/account/order-customizations.tsx +3 -3
- package/templates/nextjs/base/src/components/account/order-history.tsx +13 -5
- package/templates/nextjs/base/src/components/account/order-payment-block.tsx +2 -2
- package/templates/nextjs/base/src/components/account/order-shipping-block.tsx +2 -2
- package/templates/nextjs/base/src/components/account/order-status-timeline.tsx +2 -2
- package/templates/nextjs/base/src/components/account/profile-section.tsx +3 -3
- package/templates/nextjs/base/src/components/auth/login-form.tsx +4 -4
- package/templates/nextjs/base/src/components/auth/oauth-buttons.tsx +4 -4
- package/templates/nextjs/base/src/components/auth/register-form.tsx +4 -4
- package/templates/nextjs/base/src/components/brainerce-bot.tsx +2 -2
- package/templates/nextjs/base/src/components/checkout/checkout-form.tsx +24 -3
- package/templates/nextjs/base/src/components/checkout/custom-fields-step.tsx +2 -2
- package/templates/nextjs/base/src/components/checkout/delivery-method-step.tsx +2 -2
- package/templates/nextjs/base/src/components/checkout/order-bump-card.tsx +4 -4
- package/templates/nextjs/base/src/components/checkout/payment-step.tsx +6 -6
- package/templates/nextjs/base/src/components/checkout/pickup-step.tsx +3 -3
- package/templates/nextjs/base/src/components/checkout/shipping-step.tsx +3 -3
- package/templates/nextjs/base/src/components/checkout/tax-display.tsx +3 -3
- package/templates/nextjs/base/src/components/seo/organization-json-ld.tsx +2 -2
- package/templates/nextjs/base/src/components/seo/product-json-ld.tsx +3 -3
- package/templates/nextjs/base/src/core/hooks/use-cart-page.ts +58 -0
- package/templates/nextjs/base/src/core/hooks/use-home-data.ts +51 -0
- package/templates/nextjs/base/src/core/hooks/use-product-listing.ts +295 -0
- package/templates/nextjs/base/src/core/hooks/use-product-page.ts +278 -0
- package/templates/nextjs/base/src/{hooks → core/hooks}/use-search.ts +1 -1
- package/templates/nextjs/base/src/{lib → core/lib}/brainerce.ts.ejs +1 -1
- package/templates/nextjs/base/src/core/lib/product-options.ts +121 -0
- package/templates/nextjs/base/src/{lib → core/lib}/resolve-currency.ts +1 -1
- package/templates/nextjs/base/src/{lib → core/lib}/safe-redirect.ts +1 -1
- package/templates/nextjs/base/src/{lib → core/lib}/sanitize.ts.ejs +1 -1
- package/templates/nextjs/base/src/{lib → core/lib}/use-currency.ts +1 -1
- package/templates/nextjs/base/src/{providers → core/providers}/store-provider.tsx.ejs +4 -4
- package/templates/nextjs/base/src/{components → ui}/cart/cart-bundle-offer.tsx +5 -5
- package/templates/nextjs/base/src/{components → ui}/cart/cart-item.tsx +6 -6
- package/templates/nextjs/base/src/{components → ui}/cart/cart-nudges.tsx +1 -1
- package/templates/nextjs/base/src/{components → ui}/cart/cart-summary.tsx +4 -4
- package/templates/nextjs/base/src/{components → ui}/cart/cart-upgrade-banner.tsx +5 -5
- package/templates/nextjs/base/src/ui/cart/cart-view.tsx +149 -0
- package/templates/nextjs/base/src/{components → ui}/cart/coupon-input.tsx +4 -4
- package/templates/nextjs/base/src/{components → ui}/cart/free-shipping-bar.tsx +4 -4
- package/templates/nextjs/base/src/{components → ui}/cart/reservation-countdown.tsx +2 -2
- package/templates/nextjs/base/src/ui/home/discount-banner-strip.tsx +20 -0
- package/templates/nextjs/base/src/ui/home/featured-products-section.tsx +24 -0
- package/templates/nextjs/base/src/ui/home/hero-section.tsx +29 -0
- package/templates/nextjs/base/src/ui/home/home-client.tsx +29 -0
- package/templates/nextjs/base/src/{components/content → ui/layout}/faq-section.tsx.ejs +2 -2
- package/templates/nextjs/base/src/{components/content → ui/layout}/header-account.tsx +3 -3
- package/templates/nextjs/base/src/{components/content → ui/layout}/rich-text-block.tsx.ejs +1 -1
- package/templates/nextjs/base/src/{components/content → ui/layout}/site-header.tsx.ejs +1 -1
- package/templates/nextjs/base/src/{components/products → ui/product}/customization-fields.tsx +18 -51
- package/templates/nextjs/base/src/{components/products → ui/product}/discount-badge.tsx +1 -1
- package/templates/nextjs/base/src/{components/products → ui/product}/frequently-bought-together.tsx +6 -6
- package/templates/nextjs/base/src/{components/products → ui/product}/modifier-group-selector.tsx +10 -73
- package/templates/nextjs/base/src/{components/shared → ui/product}/price-display.tsx +2 -2
- package/templates/nextjs/base/src/{components/products → ui/product}/product-card.tsx +10 -10
- package/templates/nextjs/base/src/{app/products/[slug] → ui/product}/product-client-section.tsx +45 -206
- package/templates/nextjs/base/src/{components/products → ui/product}/product-grid.tsx +3 -3
- package/templates/nextjs/base/src/ui/product/product-listing.tsx +429 -0
- package/templates/nextjs/base/src/{components/products → ui/product}/recommendation-section.tsx +4 -4
- package/templates/nextjs/base/src/{components/reviews → ui/product}/review-form.tsx +2 -2
- package/templates/nextjs/base/src/{components/reviews → ui/product}/reviews-section.tsx +1 -1
- package/templates/nextjs/base/src/{components/products → ui/product}/stock-badge.tsx +2 -2
- package/templates/nextjs/base/src/{components/products → ui/product}/variant-selector.tsx +3 -3
- package/templates/nextjs/base/src/{components → ui}/shared/cdn-image.tsx +1 -1
- package/templates/nextjs/base/src/{components → ui}/shared/loading-spinner.tsx +2 -2
- package/templates/nextjs/base/tailwind.config.ts +12 -0
- package/templates/nextjs/designs/passepartout/app-overlay/layout.tsx.ejs +253 -0
- package/templates/nextjs/designs/passepartout/globals.css +412 -0
- package/templates/nextjs/designs/passepartout/messages-patch/en.json +87 -0
- package/templates/nextjs/designs/passepartout/messages-patch/he.json +87 -0
- package/templates/nextjs/designs/passepartout/ui/cart/cart-bundle-offer.tsx +111 -0
- package/templates/nextjs/designs/passepartout/ui/cart/cart-nudges.tsx +39 -0
- package/templates/nextjs/designs/passepartout/ui/cart/cart-upgrade-banner.tsx +141 -0
- package/templates/nextjs/designs/passepartout/ui/cart/cart-view.tsx +149 -0
- package/templates/nextjs/designs/passepartout/ui/cart/free-shipping-bar.tsx +62 -0
- package/templates/nextjs/designs/passepartout/ui/cart/reservation-countdown.tsx +103 -0
- package/templates/nextjs/designs/passepartout/ui/home/home-client.tsx +401 -0
- package/templates/nextjs/designs/passepartout/ui/layout/announcement-bar.tsx +139 -0
- package/templates/nextjs/designs/passepartout/ui/layout/cart-link.tsx +45 -0
- package/templates/nextjs/designs/passepartout/ui/layout/faq-section.tsx +103 -0
- package/templates/nextjs/designs/passepartout/ui/layout/site-footer.tsx +159 -0
- package/templates/nextjs/designs/passepartout/ui/layout/site-header.tsx.ejs +155 -0
- package/templates/nextjs/designs/passepartout/ui/layout/trust-icons.tsx +60 -0
- package/templates/nextjs/designs/passepartout/ui/layout/whatsapp-button.tsx +34 -0
- package/templates/nextjs/designs/passepartout/ui/product/discount-badge.tsx +22 -0
- package/templates/nextjs/designs/passepartout/ui/product/frequently-bought-together.tsx +205 -0
- package/templates/nextjs/designs/passepartout/ui/product/price-display.tsx +111 -0
- package/templates/nextjs/designs/passepartout/ui/product/product-card.tsx +265 -0
- package/templates/nextjs/designs/passepartout/ui/product/product-client-section.tsx +667 -0
- package/templates/nextjs/designs/passepartout/ui/product/product-grid.tsx +35 -0
- package/templates/nextjs/designs/passepartout/ui/product/product-listing.tsx +427 -0
- package/templates/nextjs/designs/passepartout/ui/product/recommendation-section.tsx +113 -0
- package/templates/nextjs/designs/passepartout/ui/product/review-form.tsx +294 -0
- package/templates/nextjs/designs/passepartout/ui/product/reviews-section.tsx.ejs +96 -0
- package/templates/nextjs/designs/passepartout/ui/product/stock-badge.tsx +52 -0
- package/templates/nextjs/designs/passepartout/ui/product/variant-selector.tsx +307 -0
- package/templates/nextjs/designs/passepartout/ui/shared/fly-to-cart.ts +85 -0
- package/templates/nextjs/designs/passepartout/ui/shared/loading-spinner.tsx +33 -0
- package/templates/nextjs/ui-canvas/cart/cart-bundle-offer.tsx +87 -0
- package/templates/nextjs/ui-canvas/cart/cart-item.tsx +121 -0
- package/templates/nextjs/ui-canvas/cart/cart-nudges.tsx +25 -0
- package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +97 -0
- package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +103 -0
- package/templates/nextjs/ui-canvas/cart/cart-view.tsx +106 -0
- package/templates/nextjs/ui-canvas/cart/coupon-input.tsx +117 -0
- package/templates/nextjs/ui-canvas/cart/free-shipping-bar.tsx +52 -0
- package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +79 -0
- package/templates/nextjs/ui-canvas/home/discount-banner-strip.tsx +19 -0
- package/templates/nextjs/ui-canvas/home/featured-products-section.tsx +23 -0
- package/templates/nextjs/ui-canvas/home/hero-section.tsx +21 -0
- package/templates/nextjs/ui-canvas/home/home-client.tsx +26 -0
- package/templates/nextjs/ui-canvas/layout/announcement-bar.tsx.ejs +101 -0
- package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +71 -0
- package/templates/nextjs/ui-canvas/layout/header-account.tsx +31 -0
- package/templates/nextjs/ui-canvas/layout/language-switcher.tsx.ejs +74 -0
- package/templates/nextjs/ui-canvas/layout/rich-text-block.tsx.ejs +34 -0
- package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +82 -0
- package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +119 -0
- package/templates/nextjs/ui-canvas/product/customization-fields.tsx +443 -0
- package/templates/nextjs/ui-canvas/product/discount-badge.tsx +17 -0
- package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +166 -0
- package/templates/nextjs/ui-canvas/product/modifier-group-selector.tsx +163 -0
- package/templates/nextjs/ui-canvas/product/price-display.tsx +50 -0
- package/templates/nextjs/ui-canvas/product/product-card.tsx +197 -0
- package/templates/nextjs/ui-canvas/product/product-client-section.tsx +352 -0
- package/templates/nextjs/ui-canvas/product/product-grid.tsx +27 -0
- package/templates/nextjs/ui-canvas/product/product-listing.tsx +224 -0
- package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +95 -0
- package/templates/nextjs/ui-canvas/product/review-form.tsx +269 -0
- package/templates/nextjs/ui-canvas/product/reviews-section.tsx +78 -0
- package/templates/nextjs/ui-canvas/product/stock-badge.tsx +43 -0
- package/templates/nextjs/ui-canvas/product/variant-selector.tsx +210 -0
- package/templates/nextjs/ui-canvas/shared/cdn-image.tsx +29 -0
- package/templates/nextjs/ui-canvas/shared/loading-spinner.tsx +25 -0
- package/templates/nextjs/base/src/app/home-client.tsx +0 -98
- /package/templates/nextjs/base/src/{lib → core/lib}/auth.ts +0 -0
- /package/templates/nextjs/base/src/{lib → core/lib}/csrf.ts +0 -0
- /package/templates/nextjs/base/src/{lib → core/lib}/image-hosts.ts +0 -0
- /package/templates/nextjs/base/src/{lib → core/lib}/navigation.tsx.ejs +0 -0
- /package/templates/nextjs/base/src/{lib → core/lib}/nonce.ts +0 -0
- /package/templates/nextjs/base/src/{lib → core/lib}/sanitize-html.ts +0 -0
- /package/templates/nextjs/base/src/{lib → core/lib}/seo.ts +0 -0
- /package/templates/nextjs/base/src/{lib → core/lib}/store-info.ts +0 -0
- /package/templates/nextjs/base/src/{lib → core/lib}/translations.ts.ejs +0 -0
- /package/templates/nextjs/base/src/{lib → core/lib}/utils.ts +0 -0
- /package/templates/nextjs/base/src/{lib → core/lib}/validation.ts +0 -0
- /package/templates/nextjs/base/src/{components/content → ui/layout}/announcement-bar.tsx.ejs +0 -0
- /package/templates/nextjs/base/src/{components → ui}/layout/language-switcher.tsx.ejs +0 -0
- /package/templates/nextjs/base/src/{components/content → ui/layout}/site-footer.tsx.ejs +0 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
<% if (i18nEnabled) { %>
|
|
2
|
+
import type { Metadata } from 'next';
|
|
3
|
+
import { Assistant, Frank_Ruhl_Libre } from 'next/font/google';
|
|
4
|
+
import { StoreProvider } from '@/core/providers/store-provider';
|
|
5
|
+
import { AnnouncementBar } from '@/ui/layout/announcement-bar';
|
|
6
|
+
import { SiteHeader } from '@/ui/layout/site-header';
|
|
7
|
+
import { SiteFooter } from '@/ui/layout/site-footer';
|
|
8
|
+
import { WhatsAppButton } from '@/ui/layout/whatsapp-button';
|
|
9
|
+
import { BrainerceBotWidget } from '@/components/brainerce-bot';
|
|
10
|
+
import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce';
|
|
11
|
+
import { getDirection, supportedLocales } from '@/i18n';
|
|
12
|
+
import { getNonce } from '@/core/lib/nonce';
|
|
13
|
+
import '../globals.css';
|
|
14
|
+
|
|
15
|
+
// פספרטו · Passe-Partout: print serif for titles, an invisible sans for
|
|
16
|
+
// everything else. Both faces carry Hebrew + Latin subsets.
|
|
17
|
+
const serif = Frank_Ruhl_Libre({
|
|
18
|
+
weight: ['300', '400', '500'],
|
|
19
|
+
subsets: ['hebrew', 'latin'],
|
|
20
|
+
variable: '--font-serif',
|
|
21
|
+
});
|
|
22
|
+
const sans = Assistant({
|
|
23
|
+
weight: ['300', '400', '600'],
|
|
24
|
+
subsets: ['hebrew', 'latin'],
|
|
25
|
+
variable: '--font-sans',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
|
|
29
|
+
|
|
30
|
+
export const metadata: Metadata = {
|
|
31
|
+
metadataBase: new URL(baseUrl),
|
|
32
|
+
title: {
|
|
33
|
+
default: <%- storeNameJs %>,
|
|
34
|
+
template: <%- titleTemplateJs %>,
|
|
35
|
+
},
|
|
36
|
+
description: <%- storeNameJs %>,
|
|
37
|
+
alternates: {
|
|
38
|
+
canonical: '/',
|
|
39
|
+
},
|
|
40
|
+
openGraph: {
|
|
41
|
+
siteName: <%- storeNameJs %>,
|
|
42
|
+
type: 'website',
|
|
43
|
+
},
|
|
44
|
+
robots: {
|
|
45
|
+
index: true,
|
|
46
|
+
follow: true,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const organizationJsonLd = {
|
|
51
|
+
'@context': 'https://schema.org',
|
|
52
|
+
'@type': 'Organization',
|
|
53
|
+
name: <%- storeNameJs %>,
|
|
54
|
+
url: baseUrl,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export function generateStaticParams() {
|
|
58
|
+
return supportedLocales.map((locale) => ({ locale }));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export default async function RootLayout({
|
|
62
|
+
children,
|
|
63
|
+
params,
|
|
64
|
+
}: {
|
|
65
|
+
children: React.ReactNode;
|
|
66
|
+
params: Promise<{ locale: string }>;
|
|
67
|
+
}) {
|
|
68
|
+
const { locale } = await params;
|
|
69
|
+
const dir = getDirection(locale);
|
|
70
|
+
const nonce = await getNonce();
|
|
71
|
+
|
|
72
|
+
// Merchant-driven layout chrome — fetched server-side. Each call falls back
|
|
73
|
+
// to null/[] on 404 so the layout never crashes when the merchant hasn't
|
|
74
|
+
// seeded a particular content type yet. New stores ship with default rows
|
|
75
|
+
// seeded by the backend (StoresService.seedDefaultContent).
|
|
76
|
+
const client = getServerClient(locale);
|
|
77
|
+
const [announcements, siteHeader, siteFooter, storeInfo] = await Promise.all([
|
|
78
|
+
client.content.announcement.list(locale).catch(() => []),
|
|
79
|
+
client.content.header.get('main', locale).catch(() => null),
|
|
80
|
+
client.content.footer.get('main', locale).catch(() => null),
|
|
81
|
+
// SSR-fetch store config so PriceDisplay / FreeShippingBar / upsell UI
|
|
82
|
+
// render with the real currency, feature flags, and i18n config at frame 0
|
|
83
|
+
// — without this, Googlebot sees USD/defaults baked into the HTML.
|
|
84
|
+
fetchStoreInfo(locale),
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<html lang={locale} dir={dir}>
|
|
89
|
+
<head>
|
|
90
|
+
<script
|
|
91
|
+
type="application/ld+json"
|
|
92
|
+
nonce={nonce}
|
|
93
|
+
suppressHydrationWarning
|
|
94
|
+
dangerouslySetInnerHTML={{
|
|
95
|
+
__html: JSON.stringify(organizationJsonLd)
|
|
96
|
+
.replace(/</g, '\\u003c')
|
|
97
|
+
.replace(/>/g, '\\u003e')
|
|
98
|
+
.replace(/&/g, '\\u0026'),
|
|
99
|
+
}}
|
|
100
|
+
/>
|
|
101
|
+
{/* Brainerce cookieless traffic analytics — auto-tracks pageviews +
|
|
102
|
+
SPA route changes. Cookieless (no consent banner needed); the
|
|
103
|
+
merchant can toggle it per sales channel in the dashboard. */}
|
|
104
|
+
<script
|
|
105
|
+
defer
|
|
106
|
+
src={`${process.env.NEXT_PUBLIC_BRAINERCE_PIXEL_URL || 'https://api.brainerce.com'}/t.js`}
|
|
107
|
+
data-channel={
|
|
108
|
+
process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID || '<%= connectionId %>'
|
|
109
|
+
}
|
|
110
|
+
nonce={nonce}
|
|
111
|
+
suppressHydrationWarning
|
|
112
|
+
/>
|
|
113
|
+
</head>
|
|
114
|
+
<body className={`${sans.variable} ${serif.variable} font-sans`}>
|
|
115
|
+
{/* The wall has tooth — a fixed ~2% noise film above everything */}
|
|
116
|
+
<div aria-hidden="true" className="paper-grain" />
|
|
117
|
+
<StoreProvider locale={locale} initialStoreInfo={storeInfo}>
|
|
118
|
+
<div className="min-h-screen flex flex-col">
|
|
119
|
+
<AnnouncementBar announcements={announcements} />
|
|
120
|
+
<SiteHeader header={siteHeader} storeName={<%- storeNameJs %>} />
|
|
121
|
+
<main className="flex-1">{children}</main>
|
|
122
|
+
<SiteFooter footer={siteFooter} storeName={<%- storeNameJs %>} />
|
|
123
|
+
</div>
|
|
124
|
+
<WhatsAppButton />
|
|
125
|
+
<BrainerceBotWidget />
|
|
126
|
+
</StoreProvider>
|
|
127
|
+
</body>
|
|
128
|
+
</html>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
<% } else { %>
|
|
132
|
+
import type { Metadata } from 'next';
|
|
133
|
+
import { Assistant, Frank_Ruhl_Libre } from 'next/font/google';
|
|
134
|
+
import { StoreProvider } from '@/core/providers/store-provider';
|
|
135
|
+
import { AnnouncementBar } from '@/ui/layout/announcement-bar';
|
|
136
|
+
import { SiteHeader } from '@/ui/layout/site-header';
|
|
137
|
+
import { SiteFooter } from '@/ui/layout/site-footer';
|
|
138
|
+
import { WhatsAppButton } from '@/ui/layout/whatsapp-button';
|
|
139
|
+
import { BrainerceBotWidget } from '@/components/brainerce-bot';
|
|
140
|
+
import { getServerClient, fetchStoreInfo } from '@/core/lib/brainerce';
|
|
141
|
+
import { getNonce } from '@/core/lib/nonce';
|
|
142
|
+
import './globals.css';
|
|
143
|
+
|
|
144
|
+
// פספרטו · Passe-Partout: print serif for titles, an invisible sans for
|
|
145
|
+
// everything else. Both faces carry Hebrew + Latin subsets.
|
|
146
|
+
const serif = Frank_Ruhl_Libre({
|
|
147
|
+
weight: ['300', '400', '500'],
|
|
148
|
+
subsets: ['hebrew', 'latin'],
|
|
149
|
+
variable: '--font-serif',
|
|
150
|
+
});
|
|
151
|
+
const sans = Assistant({
|
|
152
|
+
weight: ['300', '400', '600'],
|
|
153
|
+
subsets: ['hebrew', 'latin'],
|
|
154
|
+
variable: '--font-sans',
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://example.com';
|
|
158
|
+
|
|
159
|
+
export const metadata: Metadata = {
|
|
160
|
+
metadataBase: new URL(baseUrl),
|
|
161
|
+
title: {
|
|
162
|
+
default: <%- storeNameJs %>,
|
|
163
|
+
template: <%- titleTemplateJs %>,
|
|
164
|
+
},
|
|
165
|
+
description: <%- storeNameJs %>,
|
|
166
|
+
alternates: {
|
|
167
|
+
canonical: '/',
|
|
168
|
+
},
|
|
169
|
+
openGraph: {
|
|
170
|
+
siteName: <%- storeNameJs %>,
|
|
171
|
+
locale: '<%= ogLocale %>',
|
|
172
|
+
type: 'website',
|
|
173
|
+
},
|
|
174
|
+
robots: {
|
|
175
|
+
index: true,
|
|
176
|
+
follow: true,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const organizationJsonLd = {
|
|
181
|
+
'@context': 'https://schema.org',
|
|
182
|
+
'@type': 'Organization',
|
|
183
|
+
name: <%- storeNameJs %>,
|
|
184
|
+
url: baseUrl,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export default async function RootLayout({
|
|
188
|
+
children,
|
|
189
|
+
}: {
|
|
190
|
+
children: React.ReactNode;
|
|
191
|
+
}) {
|
|
192
|
+
const nonce = await getNonce();
|
|
193
|
+
|
|
194
|
+
// Merchant-driven layout chrome — fetched server-side. Each call falls back
|
|
195
|
+
// to null/[] on 404 so the layout never crashes when the merchant hasn't
|
|
196
|
+
// seeded a particular content type yet. New stores ship with default rows
|
|
197
|
+
// seeded by the backend (StoresService.seedDefaultContent).
|
|
198
|
+
const client = getServerClient();
|
|
199
|
+
const [announcements, siteHeader, siteFooter, storeInfo] = await Promise.all([
|
|
200
|
+
client.content.announcement.list().catch(() => []),
|
|
201
|
+
client.content.header.get('main').catch(() => null),
|
|
202
|
+
client.content.footer.get('main').catch(() => null),
|
|
203
|
+
// SSR-fetch store config so PriceDisplay / FreeShippingBar / upsell UI
|
|
204
|
+
// render with the real currency, feature flags, and i18n config at frame 0
|
|
205
|
+
// — without this, Googlebot sees USD/defaults baked into the HTML.
|
|
206
|
+
fetchStoreInfo(),
|
|
207
|
+
]);
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<html lang="<%= language %>" dir="<%= direction %>">
|
|
211
|
+
<head>
|
|
212
|
+
<script
|
|
213
|
+
type="application/ld+json"
|
|
214
|
+
nonce={nonce}
|
|
215
|
+
suppressHydrationWarning
|
|
216
|
+
dangerouslySetInnerHTML={{
|
|
217
|
+
__html: JSON.stringify(organizationJsonLd)
|
|
218
|
+
.replace(/</g, '\\u003c')
|
|
219
|
+
.replace(/>/g, '\\u003e')
|
|
220
|
+
.replace(/&/g, '\\u0026'),
|
|
221
|
+
}}
|
|
222
|
+
/>
|
|
223
|
+
{/* Brainerce cookieless traffic analytics — auto-tracks pageviews +
|
|
224
|
+
SPA route changes. Cookieless (no consent banner needed); the
|
|
225
|
+
merchant can toggle it per sales channel in the dashboard. */}
|
|
226
|
+
<script
|
|
227
|
+
defer
|
|
228
|
+
src={`${process.env.NEXT_PUBLIC_BRAINERCE_PIXEL_URL || 'https://api.brainerce.com'}/t.js`}
|
|
229
|
+
data-channel={
|
|
230
|
+
process.env.NEXT_PUBLIC_BRAINERCE_SALES_CHANNEL_ID || '<%= connectionId %>'
|
|
231
|
+
}
|
|
232
|
+
nonce={nonce}
|
|
233
|
+
suppressHydrationWarning
|
|
234
|
+
/>
|
|
235
|
+
</head>
|
|
236
|
+
<body className={`${sans.variable} ${serif.variable} font-sans`}>
|
|
237
|
+
{/* The wall has tooth — a fixed ~2% noise film above everything */}
|
|
238
|
+
<div aria-hidden="true" className="paper-grain" />
|
|
239
|
+
<StoreProvider initialStoreInfo={storeInfo}>
|
|
240
|
+
<div className="min-h-screen flex flex-col">
|
|
241
|
+
<AnnouncementBar announcements={announcements} />
|
|
242
|
+
<SiteHeader header={siteHeader} storeName={<%- storeNameJs %>} />
|
|
243
|
+
<main className="flex-1">{children}</main>
|
|
244
|
+
<SiteFooter footer={siteFooter} storeName={<%- storeNameJs %>} />
|
|
245
|
+
</div>
|
|
246
|
+
<WhatsAppButton />
|
|
247
|
+
<BrainerceBotWidget />
|
|
248
|
+
</StoreProvider>
|
|
249
|
+
</body>
|
|
250
|
+
</html>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
<% } %>
|
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
/* ─────────────────────────────────────────────────────────────
|
|
6
|
+
פספרטו · Passe-Partout — a warm gallery hang
|
|
7
|
+
Two whites do all the work: the plaster wall (--background)
|
|
8
|
+
and the paper mat (--surface), separated by a 1px hairline.
|
|
9
|
+
Brass never appears larger than a pin.
|
|
10
|
+
───────────────────────────────────────────────────────────── */
|
|
11
|
+
|
|
12
|
+
@layer base {
|
|
13
|
+
:root {
|
|
14
|
+
--background: 40 33% 97%; /* הקיר — warm plaster wall */
|
|
15
|
+
--foreground: 30 8% 16%; /* דיו — warm ink, never pure black */
|
|
16
|
+
--primary: 30 8% 16%; /* ink-filled buttons — gallery signage */
|
|
17
|
+
--primary-foreground: 40 33% 97%;
|
|
18
|
+
--secondary: 38 25% 94%;
|
|
19
|
+
--secondary-foreground: 30 8% 16%;
|
|
20
|
+
--muted: 38 25% 94%; /* plinth — placeholder fields, steppers */
|
|
21
|
+
--muted-foreground: 30 6% 45%; /* כיתוב — placard gray */
|
|
22
|
+
--accent: 38 32% 52%; /* פליז — brass, pin-scale only */
|
|
23
|
+
--accent-foreground: 40 33% 97%;
|
|
24
|
+
--destructive: 10 42% 44%; /* טרקוטה — sale text, never a red box */
|
|
25
|
+
--destructive-foreground: 0 0% 100%;
|
|
26
|
+
--border: 38 18% 88%; /* the hairline — always 1px */
|
|
27
|
+
--radius: 0rem; /* frames are not rounded */
|
|
28
|
+
|
|
29
|
+
--surface: 0 0% 100%; /* הפספרטו — paper mat behind every photo */
|
|
30
|
+
--plinth: 38 25% 94%;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
* {
|
|
34
|
+
@apply border-border;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
body {
|
|
38
|
+
@apply bg-background text-foreground antialiased;
|
|
39
|
+
font-size: 0.9375rem;
|
|
40
|
+
line-height: 1.7;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Titles — Hebrew print serif. No uppercase, no tracking:
|
|
44
|
+
Hebrew has no case and tracking distorts the serif. */
|
|
45
|
+
h1,
|
|
46
|
+
h2,
|
|
47
|
+
h3,
|
|
48
|
+
h4 {
|
|
49
|
+
font-family: var(--font-serif), 'Frank Ruhl Libre', serif;
|
|
50
|
+
font-weight: 400;
|
|
51
|
+
line-height: 1.25;
|
|
52
|
+
letter-spacing: 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
h1 {
|
|
56
|
+
font-weight: 300;
|
|
57
|
+
line-height: 1.2;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* The right-origin underline — every text link draws its 1px
|
|
61
|
+
ink underline from the right, the RTL reading direction. */
|
|
62
|
+
main a:not([class*='bg-']):not([class*='group']) {
|
|
63
|
+
position: relative;
|
|
64
|
+
transition: color 0.25s cubic-bezier(0.22, 1, 0.36, 1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
main a:not([class*='bg-']):not([class*='group'])::after {
|
|
68
|
+
content: '';
|
|
69
|
+
position: absolute;
|
|
70
|
+
bottom: -1px;
|
|
71
|
+
inset-inline: 0;
|
|
72
|
+
height: 1px;
|
|
73
|
+
background: hsl(var(--foreground));
|
|
74
|
+
transform: scaleX(0);
|
|
75
|
+
transform-origin: right;
|
|
76
|
+
transition: transform 0.25s cubic-bezier(0.22, 1, 0.36, 1);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
main a:not([class*='bg-']):not([class*='group']):hover::after {
|
|
80
|
+
transform: scaleX(1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
::selection {
|
|
84
|
+
background: hsl(var(--accent) / 0.2);
|
|
85
|
+
color: hsl(var(--foreground));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
::-webkit-scrollbar {
|
|
89
|
+
width: 5px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
::-webkit-scrollbar-track {
|
|
93
|
+
background: hsl(var(--background));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
::-webkit-scrollbar-thumb {
|
|
97
|
+
background: hsl(var(--border));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
::-webkit-scrollbar-thumb:hover {
|
|
101
|
+
background: hsl(var(--accent) / 0.6);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
:focus-visible {
|
|
105
|
+
outline: 1px solid hsl(var(--accent));
|
|
106
|
+
outline-offset: 2px;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/* Prices and catalog numbers always read as tabular figures */
|
|
110
|
+
[data-price],
|
|
111
|
+
.tabular {
|
|
112
|
+
font-variant-numeric: tabular-nums;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@layer components {
|
|
117
|
+
/* ─── Buttons — gallery signage ─── */
|
|
118
|
+
|
|
119
|
+
button[class*='bg-primary'],
|
|
120
|
+
a[class*='bg-primary'] {
|
|
121
|
+
font-weight: 600;
|
|
122
|
+
letter-spacing: 0.04em;
|
|
123
|
+
font-size: 0.8125rem;
|
|
124
|
+
border-radius: 0;
|
|
125
|
+
transition: background-color 0.2s cubic-bezier(0.22, 1, 0.36, 1);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
button[class*='bg-primary']:hover,
|
|
129
|
+
a[class*='bg-primary']:hover {
|
|
130
|
+
background: hsl(30 8% 24%) !important;
|
|
131
|
+
opacity: 1 !important;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
[class*='bg-secondary'] {
|
|
135
|
+
border: 1px solid hsl(var(--border));
|
|
136
|
+
background: transparent !important;
|
|
137
|
+
font-size: 0.8125rem;
|
|
138
|
+
letter-spacing: 0.02em;
|
|
139
|
+
border-radius: 0;
|
|
140
|
+
transition: border-color 0.2s ease, background-color 0.2s ease;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
[class*='bg-secondary']:hover {
|
|
144
|
+
border-color: hsl(var(--foreground));
|
|
145
|
+
background: hsl(var(--muted)) !important;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/* ─── Product images — the breath ─── */
|
|
149
|
+
|
|
150
|
+
[class*='group-hover\:scale-105'] {
|
|
151
|
+
transition: transform 0.7s cubic-bezier(0.22, 1, 0.36, 1) !important;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* ─── Forms — hairline boxes on the mat ─── */
|
|
155
|
+
|
|
156
|
+
input[class*='border'],
|
|
157
|
+
select[class*='border'],
|
|
158
|
+
textarea[class*='border'] {
|
|
159
|
+
background: hsl(var(--surface));
|
|
160
|
+
border-radius: 0;
|
|
161
|
+
transition: border-color 0.2s ease;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
input[class*='border']:focus,
|
|
165
|
+
select[class*='border']:focus,
|
|
166
|
+
textarea[class*='border']:focus {
|
|
167
|
+
border-color: hsl(var(--foreground));
|
|
168
|
+
box-shadow: none;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
input::placeholder,
|
|
172
|
+
textarea::placeholder {
|
|
173
|
+
color: hsl(var(--muted-foreground) / 0.7);
|
|
174
|
+
font-size: 0.875rem;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* ─── Header — the gallery entrance ─── */
|
|
178
|
+
|
|
179
|
+
header[class*='sticky'] {
|
|
180
|
+
backdrop-filter: blur(8px);
|
|
181
|
+
background: hsl(var(--background) / 0.92) !important;
|
|
182
|
+
border-bottom: 1px solid hsl(var(--border)) !important;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
nav a {
|
|
186
|
+
font-size: 0.75rem;
|
|
187
|
+
font-weight: 600;
|
|
188
|
+
letter-spacing: 0.02em;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* ─── Placards — quiet caption chips ─── */
|
|
192
|
+
|
|
193
|
+
[class*='bg-muted'][class*='text-xs'] {
|
|
194
|
+
background: transparent !important;
|
|
195
|
+
border: 0;
|
|
196
|
+
color: hsl(var(--muted-foreground));
|
|
197
|
+
font-size: 0.6875rem;
|
|
198
|
+
letter-spacing: 0.08em;
|
|
199
|
+
padding-inline: 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* Sale — terracotta text, never a red box */
|
|
203
|
+
[class*='bg-destructive'] {
|
|
204
|
+
background: transparent !important;
|
|
205
|
+
color: hsl(var(--destructive)) !important;
|
|
206
|
+
font-weight: 600;
|
|
207
|
+
letter-spacing: 0.08em;
|
|
208
|
+
font-size: 0.6875rem;
|
|
209
|
+
padding-inline: 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
[class*='line-through'] {
|
|
213
|
+
opacity: 0.4;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
hr {
|
|
217
|
+
border-color: hsl(var(--border));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/* Sticky summary panels sit on the mat */
|
|
221
|
+
[class*='sticky'][class*='top-'] {
|
|
222
|
+
background: hsl(var(--surface));
|
|
223
|
+
border: 1px solid hsl(var(--border));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/* ─── Gallery furniture ─── */
|
|
227
|
+
|
|
228
|
+
/* Edge-fading hairline — the engraved section divider */
|
|
229
|
+
.hairline-fade {
|
|
230
|
+
height: 1px;
|
|
231
|
+
max-width: 20rem;
|
|
232
|
+
margin-inline: auto;
|
|
233
|
+
background: linear-gradient(to left, transparent, hsl(var(--border)), transparent);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/* The brass tick — a 12px pin centered on a hairline */
|
|
237
|
+
.brass-rule {
|
|
238
|
+
position: relative;
|
|
239
|
+
height: 1px;
|
|
240
|
+
background: hsl(var(--border));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.brass-rule::after {
|
|
244
|
+
content: '';
|
|
245
|
+
position: absolute;
|
|
246
|
+
top: 0;
|
|
247
|
+
right: 50%;
|
|
248
|
+
transform: translateX(50%);
|
|
249
|
+
width: 12px;
|
|
250
|
+
height: 1px;
|
|
251
|
+
background: hsl(var(--accent));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* Overline — the 11px placard caption */
|
|
255
|
+
.placard {
|
|
256
|
+
font-size: 0.6875rem;
|
|
257
|
+
font-weight: 400;
|
|
258
|
+
letter-spacing: 0.08em;
|
|
259
|
+
color: hsl(var(--muted-foreground));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/* Paper grain — the wall has tooth. Sits above everything at ~2%
|
|
263
|
+
opacity so photography and type feel printed, not rendered. */
|
|
264
|
+
.paper-grain {
|
|
265
|
+
position: fixed;
|
|
266
|
+
inset: 0;
|
|
267
|
+
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.021'/%3E%3C/svg%3E");
|
|
268
|
+
pointer-events: none;
|
|
269
|
+
z-index: 9999;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/* A hung work casts a real shadow — soft, warm, falling down the wall */
|
|
273
|
+
.hung {
|
|
274
|
+
box-shadow:
|
|
275
|
+
0 24px 44px -28px hsl(28 30% 22% / 0.3),
|
|
276
|
+
0 6px 14px -10px hsl(28 30% 22% / 0.12);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/* Sunlight on the wall — huge soft washes of warm light */
|
|
280
|
+
.wash {
|
|
281
|
+
pointer-events: none;
|
|
282
|
+
position: absolute;
|
|
283
|
+
border-radius: 9999px;
|
|
284
|
+
filter: blur(48px);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
.wash-sand {
|
|
288
|
+
background: radial-gradient(closest-side, hsl(34 60% 88% / 0.65), transparent);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.wash-blush {
|
|
292
|
+
background: radial-gradient(closest-side, hsl(12 50% 88% / 0.5), transparent);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/* The breathing dot — the only loading state */
|
|
296
|
+
.breathing-dot {
|
|
297
|
+
width: 8px;
|
|
298
|
+
height: 8px;
|
|
299
|
+
border-radius: 9999px;
|
|
300
|
+
background: hsl(var(--accent));
|
|
301
|
+
animation: breathe 1.8s ease-in-out infinite;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/* Scroll reveal — sections rise 16px as they enter the viewport */
|
|
305
|
+
[data-reveal] {
|
|
306
|
+
opacity: 0;
|
|
307
|
+
transform: translateY(16px);
|
|
308
|
+
transition:
|
|
309
|
+
opacity 0.6s cubic-bezier(0.22, 1, 0.36, 1),
|
|
310
|
+
transform 0.6s cubic-bezier(0.22, 1, 0.36, 1);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
[data-reveal].revealed {
|
|
314
|
+
opacity: 1;
|
|
315
|
+
transform: none;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* The marquee — a quiet engraved band that drifts with the reading
|
|
319
|
+
direction; two identical halves make the loop seamless */
|
|
320
|
+
.marquee {
|
|
321
|
+
overflow: hidden;
|
|
322
|
+
white-space: nowrap;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.marquee-track {
|
|
326
|
+
display: inline-flex;
|
|
327
|
+
animation: marquee-drift 36s linear infinite;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.marquee:hover .marquee-track {
|
|
331
|
+
animation-play-state: paused;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/* ─── Motion — nothing moves more than 8px or longer than 700ms ─── */
|
|
336
|
+
|
|
337
|
+
@keyframes gallery-fade {
|
|
338
|
+
from {
|
|
339
|
+
opacity: 0;
|
|
340
|
+
}
|
|
341
|
+
to {
|
|
342
|
+
opacity: 1;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
@keyframes breathe {
|
|
347
|
+
0%,
|
|
348
|
+
100% {
|
|
349
|
+
opacity: 0.3;
|
|
350
|
+
}
|
|
351
|
+
50% {
|
|
352
|
+
opacity: 1;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
@keyframes cart-bump {
|
|
357
|
+
30% {
|
|
358
|
+
transform: scale(1.3);
|
|
359
|
+
}
|
|
360
|
+
60% {
|
|
361
|
+
transform: scale(0.92);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.cart-bump {
|
|
366
|
+
animation: cart-bump 0.45s cubic-bezier(0.22, 1, 0.36, 1);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/* Touch feedback — every filled button settles 1px under the finger */
|
|
370
|
+
button[class*='bg-primary']:active,
|
|
371
|
+
a[class*='bg-primary']:active {
|
|
372
|
+
transform: translateY(1px);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
@keyframes marquee-drift {
|
|
376
|
+
from {
|
|
377
|
+
transform: translateX(0);
|
|
378
|
+
}
|
|
379
|
+
to {
|
|
380
|
+
transform: translateX(50%);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
[dir='ltr'] .marquee-track {
|
|
385
|
+
animation-name: marquee-drift-ltr;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
@keyframes marquee-drift-ltr {
|
|
389
|
+
from {
|
|
390
|
+
transform: translateX(0);
|
|
391
|
+
}
|
|
392
|
+
to {
|
|
393
|
+
transform: translateX(-50%);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
main {
|
|
398
|
+
animation: gallery-fade 0.4s ease both;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
[class*='grid'] > [class*='group'] {
|
|
402
|
+
animation: gallery-fade 0.3s ease both;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
@media (prefers-reduced-motion: reduce) {
|
|
406
|
+
*,
|
|
407
|
+
*::before,
|
|
408
|
+
*::after {
|
|
409
|
+
animation: none !important;
|
|
410
|
+
transition: none !important;
|
|
411
|
+
}
|
|
412
|
+
}
|