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:
Rami Bitar
2026-05-10 16:47:07 -04:00
parent 0a1fbd62bb
commit 1c034400ca
24 changed files with 747 additions and 835 deletions

File diff suppressed because it is too large Load Diff

117
components/Heading.tsx Normal file
View 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;

View File

@@ -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);
} }
`; `;

View File

@@ -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 h1h6 (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,

View File

@@ -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">

View File

@@ -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>

View File

@@ -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 ? (

View File

@@ -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 ? (

View File

@@ -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={

View File

@@ -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")}

View File

@@ -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

View File

@@ -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>&copy; 2025 Store. All rights reserved.</p>
</div>
</div>
</footer>
);
};
export default Footer;

View File

@@ -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;

View File

@@ -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",

View File

@@ -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) => {

View File

@@ -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>

View File

@@ -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: {

View File

@@ -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>
); );

View File

@@ -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">

View File

@@ -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>
);
}

View File

@@ -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">

View File

@@ -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>

View File

@@ -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">

View File

@@ -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}