Rebrand store as Pulse with athletic theme and shared typography
- Pulse theme tokens in app.schema.json: Archivo Black headings (weight 400) + Inter body, white bg / black pill buttons, xl radius, AI-generated athletic imagery - Add headerFontWeight theme prop so single-weight fonts (Archivo Black) load and render correctly; ThemeProvider applies font-family + weight inline so Typography works regardless of `as` element - New shared Heading component (tagline / title / subtitle with size + align + tone variants) and Typography caption variant for taglines; refactor features, faq, cta, testimonials, products-carousel, products-grid, collection-grid, recommended-products, image-gallery, newsletter-cta to use them - Hero accepts a `buttons` array (label / href / variant) replacing primaryCta/secondaryCta; cover-image component removed and existing cover blocks migrated to Hero blocks with `buttons: []` - Newsletter CTA uses shadcn Button + Input so it inherits theme radius; stacked layout fixed to keep the image - Product/collection card titles use Typography subtitle variants (font-body), heading font weight is theme-controlled - Remove orphan commerce/shop-header.tsx and commerce/shop-footer.tsx; the editor-driven navigation/footer are the live chrome Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
750
app.schema.json
750
app.schema.json
File diff suppressed because it is too large
Load Diff
117
components/Heading.tsx
Normal file
117
components/Heading.tsx
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Typography, type TypographyVariant } from "@/components/Typography";
|
||||||
|
|
||||||
|
export type HeadingSize = "sm" | "md" | "lg" | "xl";
|
||||||
|
export type HeadingAlign = "left" | "center";
|
||||||
|
export type HeadingTone = "default" | "light";
|
||||||
|
|
||||||
|
type SizeMap = {
|
||||||
|
title: TypographyVariant;
|
||||||
|
subtitle: TypographyVariant;
|
||||||
|
taglineGap: string;
|
||||||
|
subtitleGap: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeMap: Record<HeadingSize, SizeMap> = {
|
||||||
|
sm: {
|
||||||
|
title: "h4",
|
||||||
|
subtitle: "subtitle2",
|
||||||
|
taglineGap: "mb-2",
|
||||||
|
subtitleGap: "mt-2",
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
title: "h3",
|
||||||
|
subtitle: "subtitle1",
|
||||||
|
taglineGap: "mb-3",
|
||||||
|
subtitleGap: "mt-3",
|
||||||
|
},
|
||||||
|
lg: {
|
||||||
|
title: "h2",
|
||||||
|
subtitle: "subtitle1",
|
||||||
|
taglineGap: "mb-3",
|
||||||
|
subtitleGap: "mt-3",
|
||||||
|
},
|
||||||
|
xl: {
|
||||||
|
title: "h1",
|
||||||
|
subtitle: "subtitle1",
|
||||||
|
taglineGap: "mb-4",
|
||||||
|
subtitleGap: "mt-4",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const alignClasses: Record<HeadingAlign, string> = {
|
||||||
|
left: "items-start text-left",
|
||||||
|
center: "items-center text-center",
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HeadingProps = {
|
||||||
|
tagline?: React.ReactNode;
|
||||||
|
title?: React.ReactNode;
|
||||||
|
subtitle?: React.ReactNode;
|
||||||
|
size?: HeadingSize;
|
||||||
|
align?: HeadingAlign;
|
||||||
|
tone?: HeadingTone;
|
||||||
|
className?: string;
|
||||||
|
titleClassName?: string;
|
||||||
|
subtitleClassName?: string;
|
||||||
|
taglineClassName?: string;
|
||||||
|
maxWidth?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Heading({
|
||||||
|
tagline,
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
size = "lg",
|
||||||
|
align = "left",
|
||||||
|
tone = "default",
|
||||||
|
className,
|
||||||
|
titleClassName,
|
||||||
|
subtitleClassName,
|
||||||
|
taglineClassName,
|
||||||
|
maxWidth,
|
||||||
|
}: HeadingProps) {
|
||||||
|
if (!tagline && !title && !subtitle) return null;
|
||||||
|
const map = sizeMap[size];
|
||||||
|
const isLight = tone === "light";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("flex flex-col", alignClasses[align], maxWidth, className)}>
|
||||||
|
{tagline ? (
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
className={cn(
|
||||||
|
map.taglineGap,
|
||||||
|
isLight && "text-background/70",
|
||||||
|
taglineClassName,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{tagline}
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
{title ? (
|
||||||
|
<Typography
|
||||||
|
variant={map.title}
|
||||||
|
className={cn(isLight && "text-background", titleClassName)}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
{subtitle ? (
|
||||||
|
<Typography
|
||||||
|
variant={map.subtitle}
|
||||||
|
className={cn(
|
||||||
|
map.subtitleGap,
|
||||||
|
isLight && "text-background/70",
|
||||||
|
subtitleClassName,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{subtitle}
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Heading;
|
||||||
@@ -3,6 +3,7 @@ import { useEffect, useMemo, useRef } from "react";
|
|||||||
|
|
||||||
export type ThemeProps = {
|
export type ThemeProps = {
|
||||||
headerFont?: string;
|
headerFont?: string;
|
||||||
|
headerFontWeight?: string;
|
||||||
bodyFont?: string;
|
bodyFont?: string;
|
||||||
primaryColor?: string;
|
primaryColor?: string;
|
||||||
primaryForegroundColor?: string;
|
primaryForegroundColor?: string;
|
||||||
@@ -42,19 +43,40 @@ const shadowMap: Record<NonNullable<ThemeProps["shadow"]>, string> = {
|
|||||||
xl: "0 20px 25px -5px rgb(0 0 0 / 0.10), 0 8px 10px -6px rgb(0 0 0 / 0.10)",
|
xl: "0 20px 25px -5px rgb(0 0 0 / 0.10), 0 8px 10px -6px rgb(0 0 0 / 0.10)",
|
||||||
};
|
};
|
||||||
|
|
||||||
function googleFontsHref(headerFont?: string, bodyFont?: string): string | null {
|
function googleFontsHref(
|
||||||
const fonts = [headerFont, bodyFont].filter(
|
headerFont?: string,
|
||||||
(f): f is string => !!f && f !== "system-ui"
|
bodyFont?: string,
|
||||||
|
headerFontWeight?: string,
|
||||||
|
): string | null {
|
||||||
|
const valid = (f?: string): f is string => !!f && f !== "system-ui";
|
||||||
|
const families: string[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
|
||||||
|
const headerWeight = headerFontWeight || "400";
|
||||||
|
const bodyWeights = "400;500;600;700";
|
||||||
|
|
||||||
|
if (valid(headerFont)) {
|
||||||
|
seen.add(headerFont);
|
||||||
|
// Header font uses the configured weight only — avoids HTTP 400 from
|
||||||
|
// Google Fonts when a single-weight family (e.g. Archivo Black) is paired
|
||||||
|
// with a multi-weight default request.
|
||||||
|
families.push(
|
||||||
|
`family=${encodeURIComponent(headerFont)}:wght@${headerWeight}`,
|
||||||
);
|
);
|
||||||
if (fonts.length === 0) return null;
|
}
|
||||||
const families = Array.from(new Set(fonts))
|
if (valid(bodyFont) && !seen.has(bodyFont)) {
|
||||||
.map((f) => `family=${encodeURIComponent(f)}:wght@400;500;600;700`)
|
seen.add(bodyFont);
|
||||||
.join("&");
|
families.push(
|
||||||
return `https://fonts.googleapis.com/css2?${families}&display=swap`;
|
`family=${encodeURIComponent(bodyFont)}:wght@${bodyWeights}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (families.length === 0) return null;
|
||||||
|
return `https://fonts.googleapis.com/css2?${families.join("&")}&display=swap`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ThemeProvider({
|
export function ThemeProvider({
|
||||||
headerFont,
|
headerFont,
|
||||||
|
headerFontWeight,
|
||||||
bodyFont,
|
bodyFont,
|
||||||
primaryColor,
|
primaryColor,
|
||||||
primaryForegroundColor,
|
primaryForegroundColor,
|
||||||
@@ -87,9 +109,11 @@ export function ThemeProvider({
|
|||||||
if (maxWidth) vars["--container-max-width"] = maxWidthMap[maxWidth];
|
if (maxWidth) vars["--container-max-width"] = maxWidthMap[maxWidth];
|
||||||
if (headerFont) vars["--font-header"] = `"${headerFont}", system-ui, sans-serif`;
|
if (headerFont) vars["--font-header"] = `"${headerFont}", system-ui, sans-serif`;
|
||||||
if (bodyFont) vars["--font-body"] = `"${bodyFont}", system-ui, sans-serif`;
|
if (bodyFont) vars["--font-body"] = `"${bodyFont}", system-ui, sans-serif`;
|
||||||
|
if (headerFontWeight) vars["--font-weight-header"] = headerFontWeight;
|
||||||
return vars;
|
return vars;
|
||||||
}, [
|
}, [
|
||||||
headerFont,
|
headerFont,
|
||||||
|
headerFontWeight,
|
||||||
bodyFont,
|
bodyFont,
|
||||||
primaryColor,
|
primaryColor,
|
||||||
primaryForegroundColor,
|
primaryForegroundColor,
|
||||||
@@ -132,8 +156,8 @@ export function ThemeProvider({
|
|||||||
}, [cssVars]);
|
}, [cssVars]);
|
||||||
|
|
||||||
const fontsHref = useMemo(
|
const fontsHref = useMemo(
|
||||||
() => googleFontsHref(headerFont, bodyFont),
|
() => googleFontsHref(headerFont, bodyFont, headerFontWeight),
|
||||||
[headerFont, bodyFont],
|
[headerFont, bodyFont, headerFontWeight],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Plain CSS rules — applied directly, no Tailwind CDN runtime needed.
|
// Plain CSS rules — applied directly, no Tailwind CDN runtime needed.
|
||||||
@@ -146,6 +170,7 @@ export function ThemeProvider({
|
|||||||
}
|
}
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1, h2, h3, h4, h5, h6 {
|
||||||
font-family: var(--font-header), system-ui, -apple-system, sans-serif;
|
font-family: var(--font-header), system-ui, -apple-system, sans-serif;
|
||||||
|
font-weight: var(--font-weight-header, 600);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|||||||
@@ -15,17 +15,17 @@ export type TypographyVariant =
|
|||||||
| "caption";
|
| "caption";
|
||||||
|
|
||||||
const sizeClasses: Record<TypographyVariant, string> = {
|
const sizeClasses: Record<TypographyVariant, string> = {
|
||||||
h1: "text-5xl md:text-6xl lg:text-7xl font-semibold tracking-tight leading-[1.05]",
|
h1: "text-5xl md:text-6xl lg:text-7xl tracking-tight leading-[1.05]",
|
||||||
h2: "text-4xl md:text-5xl font-semibold tracking-tight leading-[1.1]",
|
h2: "text-4xl md:text-5xl tracking-tight leading-[1.1]",
|
||||||
h3: "text-3xl md:text-4xl font-semibold tracking-tight leading-tight",
|
h3: "text-3xl md:text-4xl tracking-tight leading-tight",
|
||||||
h4: "text-2xl md:text-3xl font-semibold tracking-tight leading-snug",
|
h4: "text-2xl md:text-3xl tracking-tight leading-snug",
|
||||||
h5: "text-xl md:text-2xl font-semibold leading-snug",
|
h5: "text-xl md:text-2xl leading-snug",
|
||||||
h6: "text-lg md:text-xl font-semibold leading-snug",
|
h6: "text-lg md:text-xl leading-snug",
|
||||||
subtitle1: "text-lg md:text-xl leading-relaxed text-muted-foreground",
|
subtitle1: "text-lg md:text-xl leading-relaxed text-muted-foreground",
|
||||||
subtitle2: "text-base md:text-lg leading-relaxed text-muted-foreground",
|
subtitle2: "text-base md:text-lg leading-relaxed text-muted-foreground",
|
||||||
body1: "text-lg leading-relaxed",
|
body1: "text-lg leading-relaxed",
|
||||||
body2: "text-base leading-relaxed",
|
body2: "text-base leading-relaxed",
|
||||||
caption: "text-sm leading-relaxed text-muted-foreground",
|
caption: "text-xs font-bold uppercase tracking-[0.2em] text-muted-foreground",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Inline-style fallbacks so headings size correctly even when Tailwind
|
// Inline-style fallbacks so headings size correctly even when Tailwind
|
||||||
@@ -33,17 +33,17 @@ const sizeClasses: Record<TypographyVariant, string> = {
|
|||||||
// hasn't compiled utility classes yet. The Tailwind classes above still
|
// hasn't compiled utility classes yet. The Tailwind classes above still
|
||||||
// apply on top once available (responsive breakpoints, leading, etc.).
|
// apply on top once available (responsive breakpoints, leading, etc.).
|
||||||
const sizeStyles: Record<TypographyVariant, React.CSSProperties> = {
|
const sizeStyles: Record<TypographyVariant, React.CSSProperties> = {
|
||||||
h1: { fontSize: "clamp(2.5rem, 6vw, 4.5rem)", lineHeight: 1.05, fontWeight: 600, letterSpacing: "-0.02em" },
|
h1: { fontSize: "clamp(2.5rem, 6vw, 4.5rem)", lineHeight: 1.05, letterSpacing: "-0.02em" },
|
||||||
h2: { fontSize: "clamp(2rem, 4vw, 3rem)", lineHeight: 1.1, fontWeight: 600, letterSpacing: "-0.02em" },
|
h2: { fontSize: "clamp(2rem, 4vw, 3rem)", lineHeight: 1.1, letterSpacing: "-0.02em" },
|
||||||
h3: { fontSize: "clamp(1.75rem, 3.5vw, 2.25rem)", lineHeight: 1.15, fontWeight: 600, letterSpacing: "-0.015em" },
|
h3: { fontSize: "clamp(1.75rem, 3.5vw, 2.25rem)", lineHeight: 1.15, letterSpacing: "-0.015em" },
|
||||||
h4: { fontSize: "clamp(1.5rem, 3vw, 1.875rem)", lineHeight: 1.2, fontWeight: 600, letterSpacing: "-0.01em" },
|
h4: { fontSize: "clamp(1.5rem, 3vw, 1.875rem)", lineHeight: 1.2, letterSpacing: "-0.01em" },
|
||||||
h5: { fontSize: "1.5rem", lineHeight: 1.25, fontWeight: 600 },
|
h5: { fontSize: "1.5rem", lineHeight: 1.25 },
|
||||||
h6: { fontSize: "1.25rem", lineHeight: 1.3, fontWeight: 600 },
|
h6: { fontSize: "1.25rem", lineHeight: 1.3 },
|
||||||
subtitle1: { fontSize: "1.125rem", lineHeight: 1.6 },
|
subtitle1: { fontSize: "1.125rem", lineHeight: 1.6 },
|
||||||
subtitle2: { fontSize: "1rem", lineHeight: 1.6 },
|
subtitle2: { fontSize: "1rem", lineHeight: 1.6 },
|
||||||
body1: { fontSize: "1.125rem", lineHeight: 1.6 },
|
body1: { fontSize: "1.125rem", lineHeight: 1.6 },
|
||||||
body2: { fontSize: "1rem", lineHeight: 1.6 },
|
body2: { fontSize: "1rem", lineHeight: 1.6 },
|
||||||
caption: { fontSize: "0.875rem", lineHeight: 1.5 },
|
caption: { fontSize: "0.75rem", lineHeight: 1.5, fontWeight: 700, letterSpacing: "0.2em", textTransform: "uppercase" },
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultTag: Record<TypographyVariant, keyof JSX.IntrinsicElements> = {
|
const defaultTag: Record<TypographyVariant, keyof JSX.IntrinsicElements> = {
|
||||||
@@ -80,11 +80,22 @@ export function Typography<C extends keyof JSX.IntrinsicElements = "p">({
|
|||||||
const isHeading = variant.startsWith("h");
|
const isHeading = variant.startsWith("h");
|
||||||
const fontClass = isHeading ? "font-heading" : "font-body";
|
const fontClass = isHeading ? "font-heading" : "font-body";
|
||||||
|
|
||||||
|
// Apply the heading font + weight inline so the styling holds even when
|
||||||
|
// the rendered element isn't h1–h6 (e.g. <span> via the `as` prop). The
|
||||||
|
// ThemeProvider's element-scoped CSS rule only matches real h-tags, and
|
||||||
|
// the `font-heading` Tailwind utility can't be relied on in CDN mode.
|
||||||
|
const fontStyles: React.CSSProperties = isHeading
|
||||||
|
? {
|
||||||
|
fontFamily: "var(--font-header), system-ui, sans-serif",
|
||||||
|
fontWeight: "var(--font-weight-header, 600)",
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
return React.createElement(
|
return React.createElement(
|
||||||
Tag,
|
Tag,
|
||||||
{
|
{
|
||||||
className: cn(fontClass, sizeClasses[variant], className),
|
className: cn(fontClass, sizeClasses[variant], className),
|
||||||
style: { ...sizeStyles[variant], ...style },
|
style: { ...fontStyles, ...sizeStyles[variant], ...style },
|
||||||
...rest,
|
...rest,
|
||||||
},
|
},
|
||||||
children,
|
children,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
import { Typography } from '@/components/Typography';
|
||||||
|
|
||||||
interface CollectionImage {
|
interface CollectionImage {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -40,9 +41,12 @@ const CollectionCard: React.FC<CollectionCardProps> = ({ collection }) => {
|
|||||||
|
|
||||||
{/* Collection Info */}
|
{/* Collection Info */}
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<h3 className="text-2xl font-bold text-foreground mb-3 group-hover:text-muted-foreground transition-colors font-heading">
|
<Typography
|
||||||
|
variant="subtitle1"
|
||||||
|
className="mb-3 font-semibold tracking-tight text-foreground transition-colors group-hover:text-muted-foreground"
|
||||||
|
>
|
||||||
{collection.title}
|
{collection.title}
|
||||||
</h3>
|
</Typography>
|
||||||
|
|
||||||
{collection.description && (
|
{collection.description && (
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import { useEffect, useState } from "react";
|
|||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import { shopifyFetch } from "@/services/shopify/client";
|
import { shopifyFetch } from "@/services/shopify/client";
|
||||||
import { GET_COLLECTIONS_QUERY } from "@/graphql/collections";
|
import { GET_COLLECTIONS_QUERY } from "@/graphql/collections";
|
||||||
import { Typography } from "@/components/Typography";
|
|
||||||
import { Container } from "@/components/layout/Container";
|
import { Container } from "@/components/layout/Container";
|
||||||
|
import { Typography } from "@/components/Typography";
|
||||||
|
import { Heading } from "@/components/Heading";
|
||||||
|
|
||||||
export type CollectionGridProps = {
|
export type CollectionGridProps = {
|
||||||
tagline: string;
|
tagline: string;
|
||||||
@@ -47,19 +48,15 @@ export function CollectionGrid({
|
|||||||
return (
|
return (
|
||||||
<section className="bg-background py-20 md:py-28">
|
<section className="bg-background py-20 md:py-28">
|
||||||
<Container>
|
<Container>
|
||||||
<div className="mx-auto mb-12 max-w-2xl text-center">
|
<Heading
|
||||||
{tagline ? (
|
tagline={tagline}
|
||||||
<p className="mb-3 text-xs uppercase tracking-[0.2em] text-muted-foreground">
|
title={heading}
|
||||||
{tagline}
|
subtitle={subheading}
|
||||||
</p>
|
align="center"
|
||||||
) : null}
|
size="lg"
|
||||||
<Typography variant="h2">{heading}</Typography>
|
className="mx-auto mb-12"
|
||||||
{subheading ? (
|
maxWidth="max-w-2xl"
|
||||||
<Typography variant="subtitle1" className="mt-3">
|
/>
|
||||||
{subheading}
|
|
||||||
</Typography>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
@@ -90,22 +87,27 @@ export function CollectionGrid({
|
|||||||
{isEditorial ? (
|
{isEditorial ? (
|
||||||
<div className="absolute inset-0 flex items-end bg-gradient-to-t from-black/60 via-transparent to-transparent p-8">
|
<div className="absolute inset-0 flex items-end bg-gradient-to-t from-black/60 via-transparent to-transparent p-8">
|
||||||
<div>
|
<div>
|
||||||
<Typography variant="h4" className="text-white">
|
<Typography
|
||||||
|
variant="subtitle1"
|
||||||
|
className="font-semibold tracking-tight text-white"
|
||||||
|
>
|
||||||
{c.title}
|
{c.title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<span className="mt-2 inline-flex text-xs uppercase tracking-[0.2em] text-white/80">
|
<span className="mt-2 inline-flex text-xs uppercase tracking-[0.2em] text-white/80">
|
||||||
Shop now →
|
Shop now
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
{!isEditorial ? (
|
{!isEditorial ? (
|
||||||
<div className="mt-4 flex items-center justify-between">
|
<div className="mt-4">
|
||||||
<h3 className="text-sm font-medium tracking-tight">{c.title}</h3>
|
<Typography
|
||||||
<span className="text-xs text-muted-foreground transition-opacity group-hover:opacity-100">
|
variant="subtitle2"
|
||||||
→
|
className="font-medium tracking-tight text-foreground"
|
||||||
</span>
|
>
|
||||||
|
{c.title}
|
||||||
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -90,9 +90,7 @@ export function FeaturedProductView({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-start gap-5">
|
<div className="flex flex-col items-start gap-5">
|
||||||
{tagline ? (
|
{tagline ? (
|
||||||
<p className="text-xs uppercase tracking-[0.2em] text-muted-foreground">
|
<Typography variant="caption">{tagline}</Typography>
|
||||||
{tagline}
|
|
||||||
</p>
|
|
||||||
) : null}
|
) : null}
|
||||||
<Typography variant="h2">{product.title}</Typography>
|
<Typography variant="h2">{product.title}</Typography>
|
||||||
{formatted ? (
|
{formatted ? (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
|
import { Typography } from "@/components/Typography";
|
||||||
|
|
||||||
type ProductImage = { url: string; altText?: string };
|
type ProductImage = { url: string; altText?: string };
|
||||||
type ProductPrice = { amount: string; currencyCode: string };
|
type ProductPrice = { amount: string; currencyCode: string };
|
||||||
@@ -53,7 +54,12 @@ export function ProductCard({
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex items-start justify-between gap-3">
|
<div className="mt-4 flex items-start justify-between gap-3">
|
||||||
<h3 className="text-sm font-medium tracking-tight">{product.title}</h3>
|
<Typography
|
||||||
|
variant="subtitle2"
|
||||||
|
className="font-medium tracking-tight text-foreground"
|
||||||
|
>
|
||||||
|
{product.title}
|
||||||
|
</Typography>
|
||||||
{price ? (
|
{price ? (
|
||||||
<div className="flex flex-col items-end text-sm">
|
<div className="flex flex-col items-end text-sm">
|
||||||
{onSale && compare ? (
|
{onSale && compare ? (
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { ShopifyCollection } from "@reacteditor/field-shopify";
|
|||||||
import { getProducts } from "@/hooks/use-shopify-products";
|
import { getProducts } from "@/hooks/use-shopify-products";
|
||||||
import { getCollectionProducts } from "@/hooks/use-shopify-collections";
|
import { getCollectionProducts } from "@/hooks/use-shopify-collections";
|
||||||
import { ProductCard } from "./product-card";
|
import { ProductCard } from "./product-card";
|
||||||
import { Typography } from "@/components/Typography";
|
import { Heading } from "@/components/Heading";
|
||||||
import {
|
import {
|
||||||
Carousel,
|
Carousel,
|
||||||
CarouselContent,
|
CarouselContent,
|
||||||
@@ -74,19 +74,14 @@ export function ProductsCarousel({
|
|||||||
<section className="bg-background py-20 md:py-28">
|
<section className="bg-background py-20 md:py-28">
|
||||||
<Container>
|
<Container>
|
||||||
<div className="mb-10 flex flex-col gap-6 md:flex-row md:items-end md:justify-between">
|
<div className="mb-10 flex flex-col gap-6 md:flex-row md:items-end md:justify-between">
|
||||||
<div className="max-w-xl">
|
<Heading
|
||||||
{tagline ? (
|
tagline={tagline}
|
||||||
<p className="mb-3 text-xs uppercase tracking-[0.2em] text-muted-foreground">
|
title={heading}
|
||||||
{tagline}
|
subtitle={subheading}
|
||||||
</p>
|
align="left"
|
||||||
) : null}
|
size="lg"
|
||||||
<Typography variant="h2">{heading}</Typography>
|
maxWidth="max-w-xl"
|
||||||
{subheading ? (
|
/>
|
||||||
<Typography variant="subtitle1" className="mt-3">
|
|
||||||
{subheading}
|
|
||||||
</Typography>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
{ctaLabel ? (
|
{ctaLabel ? (
|
||||||
<Link
|
<Link
|
||||||
to={
|
to={
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import type { ShopifyCollection } from "@reacteditor/field-shopify";
|
|||||||
import { getProducts } from "@/hooks/use-shopify-products";
|
import { getProducts } from "@/hooks/use-shopify-products";
|
||||||
import { getCollectionProducts } from "@/hooks/use-shopify-collections";
|
import { getCollectionProducts } from "@/hooks/use-shopify-collections";
|
||||||
import { ProductCard } from "./product-card";
|
import { ProductCard } from "./product-card";
|
||||||
import { Typography } from "@/components/Typography";
|
|
||||||
import { Container } from "@/components/layout/Container";
|
import { Container } from "@/components/layout/Container";
|
||||||
|
import { Heading } from "@/components/Heading";
|
||||||
|
|
||||||
export type ProductsGridProps = {
|
export type ProductsGridProps = {
|
||||||
collection: ShopifyCollection | null;
|
collection: ShopifyCollection | null;
|
||||||
@@ -62,19 +62,14 @@ export function ProductsGrid({
|
|||||||
<section className="bg-background py-20 md:py-28">
|
<section className="bg-background py-20 md:py-28">
|
||||||
<Container>
|
<Container>
|
||||||
<div className="mb-12 flex flex-col items-end justify-between gap-6 md:flex-row md:items-end">
|
<div className="mb-12 flex flex-col items-end justify-between gap-6 md:flex-row md:items-end">
|
||||||
<div className="max-w-xl">
|
<Heading
|
||||||
{tagline ? (
|
tagline={tagline}
|
||||||
<p className="mb-3 text-xs uppercase tracking-[0.2em] text-muted-foreground">
|
title={heading}
|
||||||
{tagline}
|
subtitle={subheading}
|
||||||
</p>
|
align="left"
|
||||||
) : null}
|
size="lg"
|
||||||
<Typography variant="h2">{heading}</Typography>
|
maxWidth="max-w-xl"
|
||||||
{subheading ? (
|
/>
|
||||||
<Typography variant="subtitle1" className="mt-3">
|
|
||||||
{subheading}
|
|
||||||
</Typography>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
{ctaLabel ? (
|
{ctaLabel ? (
|
||||||
<Link
|
<Link
|
||||||
to={ctaHref || (collection?.handle ? `/collections/${collection.handle}` : "/collections")}
|
to={ctaHref || (collection?.handle ? `/collections/${collection.handle}` : "/collections")}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import {
|
|||||||
useProductRecommendations,
|
useProductRecommendations,
|
||||||
} from "@/hooks/use-shopify-products";
|
} from "@/hooks/use-shopify-products";
|
||||||
import { ProductCard } from "./product-card";
|
import { ProductCard } from "./product-card";
|
||||||
import { Typography } from "@/components/Typography";
|
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { Container } from "@/components/layout/Container";
|
import { Container } from "@/components/layout/Container";
|
||||||
|
import { Heading } from "@/components/Heading";
|
||||||
|
|
||||||
export type RecommendedProductsProps = {
|
export type RecommendedProductsProps = {
|
||||||
product: ShopifyProduct | null;
|
product: ShopifyProduct | null;
|
||||||
@@ -46,14 +46,14 @@ export function RecommendedProductsView({
|
|||||||
return (
|
return (
|
||||||
<section className="bg-background py-20 md:py-28">
|
<section className="bg-background py-20 md:py-28">
|
||||||
<Container>
|
<Container>
|
||||||
<div className="mb-12 max-w-xl">
|
<Heading
|
||||||
{tagline ? (
|
tagline={tagline}
|
||||||
<p className="mb-3 text-xs uppercase tracking-[0.2em] text-muted-foreground">
|
title={heading}
|
||||||
{tagline}
|
align="left"
|
||||||
</p>
|
size="md"
|
||||||
) : null}
|
className="mb-12"
|
||||||
<Typography variant="h3">{heading}</Typography>
|
maxWidth="max-w-xl"
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-x-6 gap-y-12 md:grid-cols-4">
|
<div className="grid grid-cols-2 gap-x-6 gap-y-12 md:grid-cols-4">
|
||||||
{items.length === 0
|
{items.length === 0
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
const Footer: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<footer className="bg-foreground text-background py-12">
|
|
||||||
<div className="container mx-auto px-4 text-center">
|
|
||||||
<h3
|
|
||||||
className="text-2xl font-bold mb-4"
|
|
||||||
style={{fontFamily: 'Space Grotesk, sans-serif'}}
|
|
||||||
>
|
|
||||||
Store
|
|
||||||
</h3>
|
|
||||||
<p className="text-background/70 mb-6">
|
|
||||||
Your premium shopping destination
|
|
||||||
</p>
|
|
||||||
<div className="mt-8 pt-8 border-t border-background/20 text-background/70">
|
|
||||||
<p>© 2025 Store. All rights reserved.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Footer;
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { Link } from 'react-router';
|
|
||||||
import { useShopifyCart } from '@/hooks/use-shopify-cart';
|
|
||||||
import config from '@/lib/config.json';
|
|
||||||
|
|
||||||
const CartIcon: React.FC = () => {
|
|
||||||
const { toggleCart, itemCount } = useShopifyCart();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={toggleCart}
|
|
||||||
className="relative p-1 text-foreground hover:text-muted-foreground transition-colors"
|
|
||||||
>
|
|
||||||
<i className="ri-shopping-cart-line text-xl"></i>
|
|
||||||
{itemCount > 0 && (
|
|
||||||
<span className="absolute -top-1 -right-1 bg-primary text-primary-foreground text-[10px] rounded-full w-4 h-4 flex items-center justify-center font-semibold">
|
|
||||||
{itemCount > 99 ? '99+' : itemCount}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Header: React.FC = () => {
|
|
||||||
return (
|
|
||||||
<nav className="bg-background shadow-sm sticky top-0 z-30 h-14">
|
|
||||||
<div className="container mx-auto px-4 h-full">
|
|
||||||
<div className="flex justify-between items-center h-full">
|
|
||||||
{/* Logo */}
|
|
||||||
<Link to="/" className="text-lg font-bold text-foreground font-heading">
|
|
||||||
{config.brand.logo.url ? (
|
|
||||||
<img
|
|
||||||
src={config.brand.logo.url}
|
|
||||||
alt={config.brand.logo.alt || 'Store'}
|
|
||||||
className="h-8"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
'Store'
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{/* Navigation Links */}
|
|
||||||
<div className="flex items-center space-x-6">
|
|
||||||
<Link
|
|
||||||
to="/"
|
|
||||||
className="text-sm text-foreground hover:text-muted-foreground font-medium transition-colors"
|
|
||||||
>
|
|
||||||
Products
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to="/collections"
|
|
||||||
className="text-sm text-foreground hover:text-muted-foreground font-medium transition-colors"
|
|
||||||
>
|
|
||||||
Collections
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{/* Cart Icon */}
|
|
||||||
<CartIcon />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Header;
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Typography } from "@/components/Typography";
|
import { Heading } from "@/components/Heading";
|
||||||
|
|
||||||
export type CTAProps = {
|
export type CTAProps = {
|
||||||
tagline: string;
|
tagline: string;
|
||||||
@@ -43,17 +43,15 @@ export function CTA({
|
|||||||
align === "center" ? "items-center text-center" : "items-start",
|
align === "center" ? "items-center text-center" : "items-start",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{tagline ? (
|
<Heading
|
||||||
<p className="mb-4 text-xs uppercase tracking-[0.2em] text-white/80">
|
tagline={tagline}
|
||||||
{tagline}
|
title={heading}
|
||||||
</p>
|
subtitle={subheading}
|
||||||
) : null}
|
align={align === "center" ? "center" : "left"}
|
||||||
<Typography variant="h2">{heading}</Typography>
|
size="lg"
|
||||||
{subheading ? (
|
tone="light"
|
||||||
<Typography variant="subtitle1" className="mt-5 max-w-xl text-white/80">
|
subtitleClassName="max-w-xl"
|
||||||
{subheading}
|
/>
|
||||||
</Typography>
|
|
||||||
) : null}
|
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"mt-10 flex flex-wrap gap-3",
|
"mt-10 flex flex-wrap gap-3",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Plus, Minus } from "lucide-react";
|
import { Plus, Minus } from "lucide-react";
|
||||||
import { Typography } from "@/components/Typography";
|
import { Heading } from "@/components/Heading";
|
||||||
|
|
||||||
export type FAQProps = {
|
export type FAQProps = {
|
||||||
tagline: string;
|
tagline: string;
|
||||||
@@ -14,19 +14,14 @@ export function FAQ({ tagline, heading, subheading, items }: FAQProps) {
|
|||||||
return (
|
return (
|
||||||
<section className="bg-background py-20 md:py-28">
|
<section className="bg-background py-20 md:py-28">
|
||||||
<div className="container mx-auto max-w-3xl px-6">
|
<div className="container mx-auto max-w-3xl px-6">
|
||||||
<div className="mb-12 text-center">
|
<Heading
|
||||||
{tagline ? (
|
tagline={tagline}
|
||||||
<p className="mb-3 text-xs uppercase tracking-[0.2em] text-muted-foreground">
|
title={heading}
|
||||||
{tagline}
|
subtitle={subheading}
|
||||||
</p>
|
align="center"
|
||||||
) : null}
|
size="lg"
|
||||||
<Typography variant="h2">{heading}</Typography>
|
className="mb-12"
|
||||||
{subheading ? (
|
/>
|
||||||
<Typography variant="subtitle1" className="mt-3">
|
|
||||||
{subheading}
|
|
||||||
</Typography>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="divide-y divide-border border-y border-border">
|
<div className="divide-y divide-border border-y border-border">
|
||||||
{items.map((item, i) => {
|
{items.map((item, i) => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Typography } from "@/components/Typography";
|
import { Typography } from "@/components/Typography";
|
||||||
import { Container } from "@/components/layout/Container";
|
import { Container } from "@/components/layout/Container";
|
||||||
|
import { Heading } from "@/components/Heading";
|
||||||
|
|
||||||
export type FeaturesProps = {
|
export type FeaturesProps = {
|
||||||
tagline: string;
|
tagline: string;
|
||||||
@@ -19,28 +20,24 @@ export function Features({ tagline, heading, subheading, columns, items }: Featu
|
|||||||
return (
|
return (
|
||||||
<section className="bg-background py-20 md:py-28">
|
<section className="bg-background py-20 md:py-28">
|
||||||
<Container>
|
<Container>
|
||||||
<div className="mx-auto mb-16 max-w-2xl text-center">
|
<Heading
|
||||||
{tagline ? (
|
tagline={tagline}
|
||||||
<p className="mb-3 text-xs uppercase tracking-[0.2em] text-muted-foreground">
|
title={heading}
|
||||||
{tagline}
|
subtitle={subheading}
|
||||||
</p>
|
align="center"
|
||||||
) : null}
|
size="lg"
|
||||||
<Typography variant="h2">{heading}</Typography>
|
className="mx-auto mb-16"
|
||||||
{subheading ? (
|
maxWidth="max-w-2xl"
|
||||||
<Typography variant="subtitle1" className="mt-3">
|
/>
|
||||||
{subheading}
|
|
||||||
</Typography>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={`grid grid-cols-1 gap-x-10 gap-y-12 ${colClass[columns]}`}>
|
<div className={`grid grid-cols-1 gap-x-10 gap-y-12 ${colClass[columns]}`}>
|
||||||
{items.map((item, i) => (
|
{items.map((item, i) => (
|
||||||
<div key={i} className="border-t border-border pt-6">
|
<div key={i} className="flex flex-col gap-3">
|
||||||
<p className="mb-3 text-xs tracking-[0.18em] text-muted-foreground">
|
<Typography variant="caption">
|
||||||
{String(i + 1).padStart(2, "0")}
|
{String(i + 1).padStart(2, "0")}
|
||||||
</p>
|
</Typography>
|
||||||
<Typography variant="h5">{item.title}</Typography>
|
<Typography variant="h5">{item.title}</Typography>
|
||||||
<Typography variant="body2" className="mt-3 text-muted-foreground">
|
<Typography variant="body2" className="text-muted-foreground">
|
||||||
{item.body}
|
{item.body}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,8 +13,10 @@ export const heroEditor: ComponentConfig<HeroProps> = {
|
|||||||
heading: "Made for the way you move",
|
heading: "Made for the way you move",
|
||||||
subheading:
|
subheading:
|
||||||
"A considered wardrobe of essentials, cut from natural fibers and designed to last.",
|
"A considered wardrobe of essentials, cut from natural fibers and designed to last.",
|
||||||
primaryCta: { label: "Shop the collection", href: "/collections" },
|
buttons: [
|
||||||
secondaryCta: { label: "Our story", href: "/about" },
|
{ label: "Shop the collection", href: "/collections", variant: "primary" },
|
||||||
|
{ label: "Our story", href: "/about", variant: "secondary" },
|
||||||
|
],
|
||||||
imageUrl:
|
imageUrl:
|
||||||
"https://images.unsplash.com/photo-1490481651871-ab68de25d43d?auto=format&fit=crop&w=2400&q=80",
|
"https://images.unsplash.com/photo-1490481651871-ab68de25d43d?auto=format&fit=crop&w=2400&q=80",
|
||||||
align: "left",
|
align: "left",
|
||||||
@@ -25,21 +27,29 @@ export const heroEditor: ComponentConfig<HeroProps> = {
|
|||||||
tagline: { label: "Tagline", type: "text", contentEditable: true },
|
tagline: { label: "Tagline", type: "text", contentEditable: true },
|
||||||
heading: { label: "Heading", type: "textarea", contentEditable: true },
|
heading: { label: "Heading", type: "textarea", contentEditable: true },
|
||||||
subheading: { label: "Subheading", type: "textarea", contentEditable: true },
|
subheading: { label: "Subheading", type: "textarea", contentEditable: true },
|
||||||
primaryCta: {
|
buttons: {
|
||||||
label: "Primary CTA",
|
label: "Buttons",
|
||||||
type: "object",
|
type: "array",
|
||||||
objectFields: {
|
arrayFields: {
|
||||||
label: { label: "Label", type: "text", contentEditable: true },
|
label: { label: "Label", type: "text", contentEditable: true },
|
||||||
href: { label: "Link", type: "text" },
|
href: { label: "Link", type: "text" },
|
||||||
|
variant: {
|
||||||
|
label: "Variant",
|
||||||
|
type: "select",
|
||||||
|
options: [
|
||||||
|
{ label: "Primary (filled)", value: "primary" },
|
||||||
|
{ label: "Secondary (outline)", value: "secondary" },
|
||||||
|
{ label: "Outline", value: "outline" },
|
||||||
|
{ label: "Ghost", value: "ghost" },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
secondaryCta: {
|
defaultItemProps: {
|
||||||
label: "Secondary CTA",
|
label: "Button",
|
||||||
type: "object",
|
href: "/",
|
||||||
objectFields: {
|
variant: "primary",
|
||||||
label: { label: "Label", type: "text", contentEditable: true },
|
|
||||||
href: { label: "Link", type: "text" },
|
|
||||||
},
|
},
|
||||||
|
getItemSummary: (item) => item?.label || "Button",
|
||||||
},
|
},
|
||||||
imageUrl: { label: "Background image", ...imageField({ adapter: frontendAiMediaAdapter }) },
|
imageUrl: { label: "Background image", ...imageField({ adapter: frontendAiMediaAdapter }) },
|
||||||
align: {
|
align: {
|
||||||
|
|||||||
@@ -2,12 +2,19 @@ import { Link } from "react-router";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Typography } from "@/components/Typography";
|
import { Typography } from "@/components/Typography";
|
||||||
|
|
||||||
|
export type HeroButtonVariant = "primary" | "secondary" | "outline" | "ghost";
|
||||||
|
|
||||||
|
export type HeroButton = {
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
variant: HeroButtonVariant;
|
||||||
|
};
|
||||||
|
|
||||||
export type HeroProps = {
|
export type HeroProps = {
|
||||||
tagline: string;
|
tagline: string;
|
||||||
heading: string;
|
heading: string;
|
||||||
subheading: string;
|
subheading: string;
|
||||||
primaryCta: { label: string; href: string };
|
buttons: HeroButton[];
|
||||||
secondaryCta: { label: string; href: string };
|
|
||||||
imageUrl: string;
|
imageUrl: string;
|
||||||
align: "left" | "center";
|
align: "left" | "center";
|
||||||
height: "md" | "lg" | "full";
|
height: "md" | "lg" | "full";
|
||||||
@@ -20,18 +27,40 @@ const heightClass: Record<HeroProps["height"], string> = {
|
|||||||
full: "min-h-screen",
|
full: "min-h-screen",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function buttonClass(variant: HeroButtonVariant, isDark: boolean): string {
|
||||||
|
switch (variant) {
|
||||||
|
case "primary":
|
||||||
|
return cn(
|
||||||
|
"inline-flex items-center justify-center rounded-md px-6 py-3 text-sm font-medium tracking-wide transition-opacity hover:opacity-90",
|
||||||
|
isDark ? "bg-white text-black" : "bg-foreground text-background",
|
||||||
|
);
|
||||||
|
case "secondary":
|
||||||
|
case "outline":
|
||||||
|
return cn(
|
||||||
|
"inline-flex items-center justify-center rounded-md border px-6 py-3 text-sm font-medium tracking-wide transition-opacity hover:opacity-80",
|
||||||
|
isDark ? "border-white text-white" : "border-foreground text-foreground",
|
||||||
|
);
|
||||||
|
case "ghost":
|
||||||
|
return cn(
|
||||||
|
"inline-flex items-center justify-center rounded-md px-6 py-3 text-sm font-medium tracking-wide transition-opacity hover:opacity-80",
|
||||||
|
isDark ? "text-white" : "text-foreground",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function Hero({
|
export function Hero({
|
||||||
tagline,
|
tagline,
|
||||||
heading,
|
heading,
|
||||||
subheading,
|
subheading,
|
||||||
primaryCta,
|
buttons,
|
||||||
secondaryCta,
|
|
||||||
imageUrl,
|
imageUrl,
|
||||||
align,
|
align,
|
||||||
height,
|
height,
|
||||||
tone,
|
tone,
|
||||||
}: HeroProps) {
|
}: HeroProps) {
|
||||||
const isDark = tone === "dark";
|
const isDark = tone === "dark";
|
||||||
|
const visibleButtons = (buttons ?? []).filter((b) => b?.label);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -63,14 +92,15 @@ export function Hero({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{tagline ? (
|
{tagline ? (
|
||||||
<p
|
<Typography
|
||||||
|
variant="caption"
|
||||||
className={cn(
|
className={cn(
|
||||||
"mb-5 text-xs uppercase tracking-[0.2em]",
|
"mb-5",
|
||||||
isDark ? "text-white/80" : "text-foreground/70",
|
isDark ? "text-white/80" : "text-foreground/70",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{tagline}
|
{tagline}
|
||||||
</p>
|
</Typography>
|
||||||
) : null}
|
) : null}
|
||||||
<Typography variant="h1" className="max-w-3xl">
|
<Typography variant="h1" className="max-w-3xl">
|
||||||
{heading}
|
{heading}
|
||||||
@@ -87,35 +117,24 @@ export function Hero({
|
|||||||
</Typography>
|
</Typography>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{visibleButtons.length > 0 ? (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"mt-10 flex flex-wrap gap-3",
|
"mt-10 flex flex-wrap gap-3",
|
||||||
align === "center" && "justify-center",
|
align === "center" && "justify-center",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{primaryCta?.label ? (
|
{visibleButtons.map((b, i) => (
|
||||||
<Link
|
<Link
|
||||||
to={primaryCta.href || "#"}
|
key={`${b.href}-${b.label}-${i}`}
|
||||||
className={cn(
|
to={b.href || "#"}
|
||||||
"inline-flex items-center justify-center rounded-md px-6 py-3 text-sm font-medium tracking-wide transition-opacity hover:opacity-90",
|
className={buttonClass(b.variant, isDark)}
|
||||||
isDark ? "bg-white text-black" : "bg-foreground text-background",
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{primaryCta.label}
|
{b.label}
|
||||||
</Link>
|
</Link>
|
||||||
) : null}
|
))}
|
||||||
{secondaryCta?.label ? (
|
|
||||||
<Link
|
|
||||||
to={secondaryCta.href || "#"}
|
|
||||||
className={cn(
|
|
||||||
"inline-flex items-center justify-center rounded-md border px-6 py-3 text-sm font-medium tracking-wide transition-opacity hover:opacity-80",
|
|
||||||
isDark ? "border-white text-white" : "border-foreground text-foreground",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{secondaryCta.label}
|
|
||||||
</Link>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Typography } from "@/components/Typography";
|
|
||||||
import { Container } from "@/components/layout/Container";
|
import { Container } from "@/components/layout/Container";
|
||||||
|
import { Heading } from "@/components/Heading";
|
||||||
|
|
||||||
export type ImageGalleryProps = {
|
export type ImageGalleryProps = {
|
||||||
tagline: string;
|
tagline: string;
|
||||||
@@ -14,21 +14,15 @@ export function ImageGallery({ tagline, heading, subheading, layout, items }: Im
|
|||||||
return (
|
return (
|
||||||
<section className="bg-background py-20 md:py-28">
|
<section className="bg-background py-20 md:py-28">
|
||||||
<Container>
|
<Container>
|
||||||
{(tagline || heading || subheading) && (
|
<Heading
|
||||||
<div className="mx-auto mb-12 max-w-2xl text-center">
|
tagline={tagline}
|
||||||
{tagline ? (
|
title={heading}
|
||||||
<p className="mb-3 text-xs uppercase tracking-[0.2em] text-muted-foreground">
|
subtitle={subheading}
|
||||||
{tagline}
|
align="center"
|
||||||
</p>
|
size="lg"
|
||||||
) : null}
|
className="mx-auto mb-12"
|
||||||
{heading ? <Typography variant="h2">{heading}</Typography> : null}
|
maxWidth="max-w-2xl"
|
||||||
{subheading ? (
|
/>
|
||||||
<Typography variant="subtitle1" className="mt-3">
|
|
||||||
{subheading}
|
|
||||||
</Typography>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{layout === "masonry" ? (
|
{layout === "masonry" ? (
|
||||||
<div className="columns-1 gap-4 sm:columns-2 lg:columns-3">
|
<div className="columns-1 gap-4 sm:columns-2 lg:columns-3">
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Typography } from "@/components/Typography";
|
|
||||||
import { Container } from "@/components/layout/Container";
|
import { Container } from "@/components/layout/Container";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Heading } from "@/components/Heading";
|
||||||
|
|
||||||
export type EmailProvider = "none" | "mailchimp" | "klaviyo";
|
export type EmailProvider = "none" | "mailchimp" | "klaviyo";
|
||||||
|
|
||||||
@@ -109,56 +111,72 @@ export function NewsletterCta({
|
|||||||
const Form = (
|
const Form = (
|
||||||
<form
|
<form
|
||||||
onSubmit={submit}
|
onSubmit={submit}
|
||||||
className="flex w-full max-w-md items-center border-b border-foreground/30 focus-within:border-foreground"
|
className="flex w-full max-w-md flex-col gap-3 sm:flex-row sm:items-center"
|
||||||
>
|
>
|
||||||
<input
|
<Input
|
||||||
type="email"
|
type="email"
|
||||||
required
|
required
|
||||||
value={email}
|
value={email}
|
||||||
onChange={(e) => setEmail(e.target.value)}
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
placeholder="you@example.com"
|
placeholder="Enter your email"
|
||||||
className="flex-1 bg-transparent py-3 text-sm placeholder:text-muted-foreground focus:outline-none"
|
className="h-11 flex-1"
|
||||||
/>
|
/>
|
||||||
<button
|
<Button type="submit" size="lg" disabled={submitting} className="h-11">
|
||||||
type="submit"
|
{submitting ? "Joining…" : buttonLabel}
|
||||||
disabled={submitting}
|
</Button>
|
||||||
className="ml-3 text-sm font-medium tracking-wide hover:opacity-70 disabled:opacity-40"
|
|
||||||
>
|
|
||||||
{submitting ? "…" : buttonLabel} →
|
|
||||||
</button>
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (layout === "split") {
|
const isStacked = layout === "stacked";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="bg-background">
|
<section className="bg-background">
|
||||||
<Container className="py-16 md:py-24">
|
<Container className="py-20 md:py-28">
|
||||||
<div className="grid grid-cols-1 items-center gap-12 md:grid-cols-2">
|
<div
|
||||||
<div>
|
className={cn(
|
||||||
|
"grid grid-cols-1 gap-10",
|
||||||
|
isStacked
|
||||||
|
? "mx-auto max-w-3xl items-center text-center"
|
||||||
|
: "items-center md:grid-cols-12 md:gap-16",
|
||||||
|
)}
|
||||||
|
>
|
||||||
{imageUrl ? (
|
{imageUrl ? (
|
||||||
|
<div className={cn(!isStacked && "md:col-span-7")}>
|
||||||
|
<div className="relative overflow-hidden rounded-xl bg-muted">
|
||||||
<img
|
<img
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt=""
|
alt=""
|
||||||
className="aspect-[5/4] w-full rounded-md object-cover"
|
className={cn(
|
||||||
|
"w-full object-cover transition-transform duration-700 hover:scale-105",
|
||||||
|
isStacked ? "aspect-[16/9]" : "aspect-[4/3]",
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-start">
|
</div>
|
||||||
{tagline ? (
|
|
||||||
<p className="mb-3 text-xs uppercase tracking-[0.2em] text-muted-foreground">
|
|
||||||
{tagline}
|
|
||||||
</p>
|
|
||||||
) : null}
|
) : null}
|
||||||
<Typography variant="h2">{heading}</Typography>
|
<div
|
||||||
{subheading ? (
|
className={cn(
|
||||||
<Typography variant="subtitle1" className="mt-3 max-w-md">
|
"flex w-full flex-col",
|
||||||
{subheading}
|
isStacked ? "items-center" : "items-start md:col-span-5",
|
||||||
</Typography>
|
)}
|
||||||
) : null}
|
>
|
||||||
<div className="mt-8 w-full">
|
<Heading
|
||||||
|
tagline={tagline}
|
||||||
|
title={heading}
|
||||||
|
subtitle={subheading}
|
||||||
|
align={isStacked ? "center" : "left"}
|
||||||
|
size="lg"
|
||||||
|
subtitleClassName={isStacked ? "mx-auto max-w-xl" : "max-w-md"}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mt-8 w-full",
|
||||||
|
isStacked && "flex justify-center",
|
||||||
|
)}
|
||||||
|
>
|
||||||
{submitted ? (
|
{submitted ? (
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm font-medium uppercase tracking-wide">
|
||||||
Thanks — we'll be in touch.
|
You're in. See you Monday at 5:30am.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
Form
|
Form
|
||||||
@@ -170,31 +188,3 @@ export function NewsletterCta({
|
|||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="bg-muted/40 py-20 md:py-28">
|
|
||||||
<div className="container mx-auto max-w-2xl px-6 text-center">
|
|
||||||
{tagline ? (
|
|
||||||
<p className="mb-3 text-xs uppercase tracking-[0.2em] text-muted-foreground">
|
|
||||||
{tagline}
|
|
||||||
</p>
|
|
||||||
) : null}
|
|
||||||
<Typography variant="h2">{heading}</Typography>
|
|
||||||
{subheading ? (
|
|
||||||
<Typography variant="subtitle1" className="mt-3">
|
|
||||||
{subheading}
|
|
||||||
</Typography>
|
|
||||||
) : null}
|
|
||||||
<div className={cn("mx-auto mt-10 flex w-full max-w-md justify-center")}>
|
|
||||||
{submitted ? (
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Thanks — we'll be in touch.
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
Form
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Container } from "@/components/layout/Container";
|
import { Container } from "@/components/layout/Container";
|
||||||
|
import { Typography } from "@/components/Typography";
|
||||||
|
|
||||||
export type LogosProps = {
|
export type LogosProps = {
|
||||||
tagline: string;
|
tagline: string;
|
||||||
@@ -11,9 +12,9 @@ export function Logos({ tagline, items, layout }: LogosProps) {
|
|||||||
<section className="border-y border-border bg-muted/40 py-12">
|
<section className="border-y border-border bg-muted/40 py-12">
|
||||||
<Container>
|
<Container>
|
||||||
{tagline ? (
|
{tagline ? (
|
||||||
<p className="mb-8 text-center text-xs uppercase tracking-[0.2em] text-muted-foreground">
|
<Typography variant="caption" className="mb-8 text-center">
|
||||||
{tagline}
|
{tagline}
|
||||||
</p>
|
</Typography>
|
||||||
) : null}
|
) : null}
|
||||||
{layout === "marquee" ? (
|
{layout === "marquee" ? (
|
||||||
<div className="overflow-hidden">
|
<div className="overflow-hidden">
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Link } from "react-router";
|
|||||||
import { useShopifyCart } from "@/hooks/use-shopify-cart";
|
import { useShopifyCart } from "@/hooks/use-shopify-cart";
|
||||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet";
|
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet";
|
||||||
import { Container } from "@/components/layout/Container";
|
import { Container } from "@/components/layout/Container";
|
||||||
|
import { Typography } from "@/components/Typography";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
export type NavigationProps = {
|
export type NavigationProps = {
|
||||||
@@ -51,11 +52,7 @@ export function Navigation({
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Container className="flex h-16 items-center justify-between md:h-20">
|
<Container className="flex h-16 items-center justify-between md:h-20">
|
||||||
<Link
|
<Link to="/" className="inline-flex items-center">
|
||||||
to="/"
|
|
||||||
className="inline-flex items-center font-semibold tracking-tight"
|
|
||||||
style={{ fontSize: "1.125rem", letterSpacing: "0.02em" }}
|
|
||||||
>
|
|
||||||
{logo ? (
|
{logo ? (
|
||||||
<img
|
<img
|
||||||
src={logo}
|
src={logo}
|
||||||
@@ -63,7 +60,9 @@ export function Navigation({
|
|||||||
className="h-8 w-auto object-contain"
|
className="h-8 w-auto object-contain"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
brand || "Brand Logo"
|
<Typography variant="h3" as="span">
|
||||||
|
{brand || "Brand Logo"}
|
||||||
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ArrowLeft, ArrowRight } from "lucide-react";
|
import { ArrowLeft, ArrowRight } from "lucide-react";
|
||||||
import { Typography } from "@/components/Typography";
|
import { Heading } from "@/components/Heading";
|
||||||
|
|
||||||
export type TestimonialsProps = {
|
export type TestimonialsProps = {
|
||||||
tagline: string;
|
tagline: string;
|
||||||
@@ -21,12 +21,12 @@ export function Testimonials({ tagline, heading, items }: TestimonialsProps) {
|
|||||||
return (
|
return (
|
||||||
<section className="bg-muted/40 py-20 md:py-28">
|
<section className="bg-muted/40 py-20 md:py-28">
|
||||||
<div className="container mx-auto max-w-4xl px-6 text-center">
|
<div className="container mx-auto max-w-4xl px-6 text-center">
|
||||||
{tagline ? (
|
<Heading
|
||||||
<p className="mb-3 text-xs uppercase tracking-[0.2em] text-muted-foreground">
|
tagline={tagline}
|
||||||
{tagline}
|
title={heading}
|
||||||
</p>
|
align="center"
|
||||||
) : null}
|
size="lg"
|
||||||
{heading ? <Typography variant="h2">{heading}</Typography> : null}
|
/>
|
||||||
|
|
||||||
{item ? (
|
{item ? (
|
||||||
<div className="relative mt-12">
|
<div className="relative mt-12">
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const Root: RootConfig<{
|
|||||||
defaultProps: {
|
defaultProps: {
|
||||||
title: "Untitled",
|
title: "Untitled",
|
||||||
headerFont: "Inter",
|
headerFont: "Inter",
|
||||||
|
headerFontWeight: "600",
|
||||||
bodyFont: "Inter",
|
bodyFont: "Inter",
|
||||||
// Hex defaults so the color picker reads them and any non-picker
|
// Hex defaults so the color picker reads them and any non-picker
|
||||||
// input (typed hex, AI-set value, etc.) is round-trip compatible.
|
// input (typed hex, AI-set value, etc.) is round-trip compatible.
|
||||||
@@ -40,6 +41,19 @@ export const Root: RootConfig<{
|
|||||||
description: { label: "Description", type: "textarea" },
|
description: { label: "Description", type: "textarea" },
|
||||||
ogImage: { label: "OG image", ...imageField({ adapter: frontendAiMediaAdapter }) },
|
ogImage: { label: "OG image", ...imageField({ adapter: frontendAiMediaAdapter }) },
|
||||||
headerFont: { label: "Header font", ...headerFontField },
|
headerFont: { label: "Header font", ...headerFontField },
|
||||||
|
headerFontWeight: {
|
||||||
|
label: "Header font weight",
|
||||||
|
type: "select",
|
||||||
|
options: [
|
||||||
|
{ label: "300 — Light", value: "300" },
|
||||||
|
{ label: "400 — Regular", value: "400" },
|
||||||
|
{ label: "500 — Medium", value: "500" },
|
||||||
|
{ label: "600 — Semibold", value: "600" },
|
||||||
|
{ label: "700 — Bold", value: "700" },
|
||||||
|
{ label: "800 — Extrabold", value: "800" },
|
||||||
|
{ label: "900 — Black", value: "900" },
|
||||||
|
],
|
||||||
|
},
|
||||||
bodyFont: { label: "Body font", ...bodyFontField },
|
bodyFont: { label: "Body font", ...bodyFontField },
|
||||||
primaryColor: { label: "Primary color", type: "color", placeholder: "#0a0a0a" },
|
primaryColor: { label: "Primary color", type: "color", placeholder: "#0a0a0a" },
|
||||||
secondaryColor: { label: "Secondary color", type: "color", placeholder: "#64748B" },
|
secondaryColor: { label: "Secondary color", type: "color", placeholder: "#64748B" },
|
||||||
@@ -84,6 +98,7 @@ export const Root: RootConfig<{
|
|||||||
render: ({
|
render: ({
|
||||||
children,
|
children,
|
||||||
headerFont,
|
headerFont,
|
||||||
|
headerFontWeight,
|
||||||
bodyFont,
|
bodyFont,
|
||||||
primaryColor,
|
primaryColor,
|
||||||
secondaryColor,
|
secondaryColor,
|
||||||
@@ -98,6 +113,7 @@ export const Root: RootConfig<{
|
|||||||
return (
|
return (
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
headerFont={headerFont}
|
headerFont={headerFont}
|
||||||
|
headerFontWeight={headerFontWeight}
|
||||||
bodyFont={bodyFont}
|
bodyFont={bodyFont}
|
||||||
primaryColor={primaryColor}
|
primaryColor={primaryColor}
|
||||||
secondaryColor={secondaryColor}
|
secondaryColor={secondaryColor}
|
||||||
|
|||||||
Reference in New Issue
Block a user