Add Handlebars templates for theming the storefront

Three files gain a .hbr sibling so a storefront can be compiled with a
store's own branding: app/globals.css, app/layout.tsx and
components/logo.tsx. The committed files stay as they are and remain the
hand-maintained default.

Fourteen variables drive them: shopName, logoUrl, primaryColor,
accentColor, bgColor, fontHeader and fontBody are supplied per store,
while fgColor, primaryFgColor, accentFgColor, surfaceColor, mutedColor,
mutedFgColor and borderColor derive from those unless pinned.

Two changes were needed for the fonts to be swappable. The CSS variables
next/font declares are now generic (--font-heading-family,
--font-body-family, --font-mono-family) rather than named after Geist,
and headings set font-family: var(--font-heading) so fontHeader has an
effect when it differs from fontBody.

The compiler lives outside this repo so the template carries no build
script or dependency for it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013amafJZMQmXgeFEotYWfaw
This commit is contained in:
Rami Bitar
2026-08-04 17:36:26 -04:00
co-authored by Claude Opus 5
parent ebdb0bee2f
commit 3442f19816
3 changed files with 343 additions and 0 deletions
+237
View File
@@ -0,0 +1,237 @@
@import 'tailwindcss';
@custom-variant dark (&:is(.dark *));
@theme {
/* Palette generated from the store's brand colours */
--color-background: {{bgColor}};
--color-foreground: {{fgColor}};
/* Card */
--color-card: {{surfaceColor}};
--color-card-foreground: {{fgColor}};
/* Popover */
--color-popover: {{surfaceColor}};
--color-popover-foreground: {{fgColor}};
/* Primary - the store's main brand colour */
--color-primary: {{primaryColor}};
--color-primary-foreground: {{primaryFgColor}};
/* Secondary */
--color-secondary: {{mutedColor}};
--color-secondary-foreground: {{fgColor}};
/* Muted */
--color-muted: {{mutedColor}};
--color-muted-foreground: {{mutedFgColor}};
/* Accent - the store's secondary brand colour */
--color-accent: {{accentColor}};
--color-accent-foreground: {{accentFgColor}};
/* Destructive */
--color-destructive: hsl(0 84.2% 60.2%);
--color-destructive-foreground: hsl(0 0% 98%);
/* Shop (accelerated checkout) purple */
--color-shop: #5a31f4;
/* Border */
--color-border: {{borderColor}};
--color-input: {{borderColor}};
--color-ring: {{primaryColor}};
/* Radius - Modern larger corners */
--radius-sm: 0.5rem;
--radius-md: 0.75rem;
--radius-lg: 1rem;
--radius-xl: 1.25rem;
/* Typography — {{fontHeaderName}} headings / {{fontBodyName}} body.
The family variables are declared by next/font in app/layout.tsx. */
--font-sans: var(--font-body-family), system-ui, sans-serif;
--font-mono: var(--font-mono-family), ui-monospace, SFMono-Regular, monospace;
--font-heading: var(--font-heading-family), system-ui, sans-serif;
--font-body: var(--font-body-family), system-ui, sans-serif;
--font-poppins: var(--font-heading-family), system-ui, sans-serif;
}
/* Base styles for light, modern Shopify storefront */
/* Tailwind v4 defaults an unqualified `border` to currentColor. shadcn
components (e.g. Button's outline variant) rely on this base layer to pick up
the theme's border colour instead of the text colour. */
@layer base {
* {
border-color: var(--color-border);
}
}
body {
font-feature-settings:
'kern' 1,
'tnum' 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* Modern card design for products and collections */
.card-modern {
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
border: 1px solid var(--color-border);
}
.card-modern:hover {
transform: translateY(-8px);
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.1);
border-color: color-mix(in oklab, var(--color-border), {{fgColor}} 4.5%);
}
/* Heading styles — regular weight, slightly tightened like the Geist reference */
h1,
h2,
.font-heading {
font-family: var(--font-heading);
font-weight: 400;
letter-spacing: -0.02em;
}
/* Swipeable rows (mobile galleries) without a visible scrollbar */
.no-scrollbar {
scrollbar-width: none;
-ms-overflow-style: none;
}
.no-scrollbar::-webkit-scrollbar {
display: none;
}
/* Diagonal strike marking an unavailable product option (size pill, swatch) */
.option-unavailable {
background-image: linear-gradient(
to top right,
transparent calc(50% - 0.5px),
currentColor calc(50% - 0.5px),
currentColor calc(50% + 0.5px),
transparent calc(50% + 0.5px)
);
}
/* Product description (HTML returned by the Storefront API) */
.product-description p {
margin-bottom: 1rem;
}
.product-description > :last-child {
margin-bottom: 0;
}
/* Shop policy body (HTML returned by the Storefront API) */
.policy-body p,
.policy-body ul,
.policy-body ol {
margin-bottom: 1.5rem;
}
.policy-body ul,
.policy-body ol {
padding-left: 1.25rem;
list-style: revert;
}
.policy-body li {
margin-bottom: 0.5rem;
}
.policy-body h2,
.policy-body h3,
.policy-body h4 {
margin-top: 2.5rem;
margin-bottom: 1rem;
font-size: 1.25rem;
font-weight: 500;
}
.policy-body a {
text-decoration: underline;
text-underline-offset: 2px;
}
.policy-body strong {
font-weight: 500;
}
.policy-body > :last-child {
margin-bottom: 0;
}
/* Enhanced button styles */
button,
.btn-modern {
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.btn-modern:hover {
transform: translateY(-1px);
box-shadow:
0 10px 15px -3px rgb(0 0 0 / 0.1),
0 4px 6px -4px rgb(0 0 0 / 0.1);
}
/* Hero section styles */
.hero-bg {
background: linear-gradient(
135deg,
color-mix(in oklab, var(--color-muted), {{bgColor}} 50%) 0%,
var(--color-muted) 100%
);
}
/* Product gallery improvements */
.product-gallery-main {
transition: box-shadow 0.3s ease;
}
.product-gallery-main:hover {
box-shadow: 0 0 0 1px
color-mix(in oklab, var(--color-border), {{fgColor}} 10%) inset;
}
/* Clean price display */
.price-display {
font-feature-settings: 'tnum';
}
@theme inline {
--animate-rainbow: rainbow var(--speed, 2s) infinite linear;
--color-color-5: var(--color-5);
--color-color-4: var(--color-4);
--color-color-3: var(--color-3);
--color-color-2: var(--color-2);
--color-color-1: var(--color-1);
@keyframes rainbow {
0% {
background-position: 0%;
}
100% {
background-position: 200%;
}
}
}
:root {
--color-1: oklch(66.2% 0.225 25.9);
--color-2: oklch(60.4% 0.26 302);
--color-3: oklch(69.6% 0.165 251);
--color-4: oklch(80.2% 0.134 225);
--color-5: oklch(90.7% 0.231 133);
}
.dark {
--color-1: oklch(66.2% 0.225 25.9);
--color-2: oklch(60.4% 0.26 302);
--color-3: oklch(69.6% 0.165 251);
--color-4: oklch(80.2% 0.134 225);
--color-5: oklch(90.7% 0.231 133);
}
+48
View File
@@ -0,0 +1,48 @@
import React from 'react';
import './globals.css';
import type { Metadata } from 'next';
import {
{{fontHeader}},
{{#unless sameFont}}
{{fontBody}},
{{/unless}}
Geist_Mono,
} from 'next/font/google';
import StoreAssistant from '@/components/shopify/store-assistant';
const headingFont = {{fontHeader}}({
subsets: ['latin'],
variable: '--font-heading-family',
});
const bodyFont = {{fontBody}}({
subsets: ['latin'],
variable: '--font-body-family',
});
const monoFont = Geist_Mono({
subsets: ['latin'],
variable: '--font-mono-family',
});
export const metadata: Metadata = {
title: {{json shopName}},
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html
lang="en"
className={`${headingFont.variable} ${bodyFont.variable} ${monoFont.variable}`}
>
<body className="font-body antialiased bg-background text-foreground m-0 p-0">
{children}
<StoreAssistant />
</body>
</html>
);
}