From 1c034400ca2845b4969e3730fb6cbdb35178efb3 Mon Sep 17 00:00:00 2001 From: Rami Bitar Date: Sun, 10 May 2026 16:47:07 -0400 Subject: [PATCH] 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 --- app.schema.json | 752 +++++++------------ components/Heading.tsx | 117 +++ components/ThemeProvider.tsx | 47 +- components/Typography.tsx | 41 +- components/commerce/collection-card.tsx | 8 +- components/commerce/collection-grid.tsx | 44 +- components/commerce/featured-product.tsx | 4 +- components/commerce/product-card.tsx | 8 +- components/commerce/products-carousel.tsx | 23 +- components/commerce/products-grid.tsx | 23 +- components/commerce/recommended-products.tsx | 18 +- components/commerce/shop-footer.tsx | 24 - components/commerce/shop-header.tsx | 68 -- components/cta/cta.tsx | 22 +- components/faq/faq.tsx | 23 +- components/features/features.tsx | 31 +- components/hero/hero.editor.tsx | 36 +- components/hero/hero.tsx | 91 ++- components/landing/image-gallery.tsx | 26 +- components/landing/newsletter-cta.tsx | 130 ++-- components/logos/logos.tsx | 5 +- components/navigation/navigation.tsx | 11 +- components/testimonials/testimonials.tsx | 14 +- config/root.tsx | 16 + 24 files changed, 747 insertions(+), 835 deletions(-) create mode 100644 components/Heading.tsx delete mode 100644 components/commerce/shop-footer.tsx delete mode 100644 components/commerce/shop-header.tsx diff --git a/app.schema.json b/app.schema.json index becc145..d8108d3 100644 --- a/app.schema.json +++ b/app.schema.json @@ -2,19 +2,20 @@ "/": { "root": { "props": { - "title": "Maison — Considered essentials", - "headerFont": "Playfair Display", + "title": "Pulse — Made to Move", + "headerFont": "Archivo Black", + "headerFontWeight": "400", "bodyFont": "Inter", - "primaryColor": "#0a0a0a", - "secondaryColor": "#64748B", + "primaryColor": "#111111", + "secondaryColor": "#707072", "accentColor": "#f5f5f5", "bgColor": "#ffffff", - "fgColor": "#0a0a0a", + "fgColor": "#111111", "mutedColor": "#f5f5f5", - "radius": "sm", - "buttonRadius": "md", - "shadow": "sm", - "maxWidth": "xl" + "radius": "xl", + "buttonRadius": "full", + "shadow": "none", + "maxWidth": "2xl" } }, "content": [ @@ -22,24 +23,13 @@ "type": "navigation", "props": { "id": "nav-home", - "brand": "Maison", + "brand": "PULSE", "links": [ - { - "label": "Shop", - "href": "/search" - }, - { - "label": "Mens", - "href": "/collections/mens" - }, - { - "label": "Womens", - "href": "/collections/womens" - }, - { - "label": "About", - "href": "/about" - } + { "label": "Run", "href": "/collections/run" }, + { "label": "Train", "href": "/collections/train" }, + { "label": "Recover", "href": "/collections/recover" }, + { "label": "Sale", "href": "/collections/sale" }, + { "label": "About", "href": "/about" } ], "showSearch": "yes", "showAccount": "yes", @@ -53,29 +43,25 @@ "props": { "id": "hero-home", "tagline": "Spring 2026", - "heading": "Made for the way you move", - "subheading": "A considered wardrobe of essentials, cut from natural fibers and designed to last.", - "primaryCta": { - "label": "Shop the collection", - "href": "/collections" - }, - "secondaryCta": { - "label": "Our story", - "href": "/about" - }, - "imageUrl": "https://images.unsplash.com/photo-1490481651871-ab68de25d43d?auto=format&fit=crop&w=2400&q=80", + "heading": "Made to move.", + "subheading": "Performance gear engineered for athletes who train like they mean it. Built light. Built honest. Built for the next mile.", + "buttons": [ + { "label": "Shop the kit", "href": "/search", "variant": "primary" }, + { "label": "Our story", "href": "/about", "variant": "secondary" } + ], + "imageUrl": "https://supabase.frontend-ai.com/storage/v1/object/public/assets/themes/pulse-nike/athletic-assortment.jpeg", "align": "left", "height": "lg", - "tone": "dark" + "tone": "light" } }, { "type": "products-carousel", "props": { "id": "carousel-home", - "tagline": "New", - "heading": "Just dropped", - "subheading": "Fresh additions to the lineup.", + "tagline": "Just dropped", + "heading": "New arrivals", + "subheading": "Fresh kit for the season ahead.", "limit": 12, "slidesPerView": "4", "ctaLabel": "Shop new", @@ -83,69 +69,92 @@ } }, { - "type": "featured-product", + "type": "hero", "props": { - "id": "featured-home", - "product": null, - "tagline": "Featured", - "ctaLabel": "Add to bag", + "id": "cover-home-performance", + "tagline": "Performance Series", + "heading": "Engineered to outlast you.", + "subheading": "Tested on track, trail, and treadmill. Every piece earns its place.", + "buttons": [], + "imageUrl": "https://supabase.frontend-ai.com/storage/v1/object/public/assets/themes/pulse-nike/cover-performance.jpeg", "align": "left", - "tone": "muted" - } - }, - { - "type": "collection-grid", - "props": { - "id": "collections-home", - "tagline": "Shop by collection", - "heading": "Curated edits", - "subheading": "Bundles built around the way you actually live.", - "layout": "tiles", - "limit": 6 + "height": "lg", + "tone": "light" } }, { "type": "features", "props": { "id": "features-home", - "tagline": "Why us", - "heading": "Built with intention", - "subheading": "A small set of values that shape every piece we make.", + "tagline": "Why Pulse", + "heading": "Three rules. No exceptions.", + "subheading": "", "columns": "3", "items": [ { - "title": "Natural fibers", - "body": "Linen, organic cotton, and merino — sourced from mills with traceable supply chains." + "title": "Engineered", + "body": "Every fabric, every seam, every fit decision starts with athletes on a track. Lab work comes after." }, { - "title": "Small batches", - "body": "Made in considered quantities so nothing goes to waste." + "title": "Field tested", + "body": "Twelve weeks. Two race blocks. One full season. Nothing ships without that on the record." }, { - "title": "Built to last", - "body": "Reinforced seams and finishes that age into something better." + "title": "Guaranteed", + "body": "If it fails before its time, we replace it. If it fails on race day, we apologize and replace it." } ] } }, + { + "type": "hero", + "props": { + "id": "cover-home-discipline", + "tagline": "Discipline", + "heading": "The work happens before sunrise.", + "subheading": "We make the kit. You do the work. Five-thirty isn't early — it's the only honest hour.", + "buttons": [], + "imageUrl": "https://supabase.frontend-ai.com/storage/v1/object/public/assets/themes/pulse-nike/cover-discipline.jpeg", + "align": "left", + "height": "lg", + "tone": "light" + } + }, + { + "type": "collection-grid", + "props": { + "id": "collections-home", + "tagline": "Shop by sport", + "heading": "Built for what you actually do.", + "subheading": "", + "layout": "tiles", + "limit": 6 + } + }, { "type": "testimonials", "props": { "id": "testimonials-home", - "tagline": "Reviews", - "heading": "What our customers say", + "tagline": "From the field", + "heading": "Athletes on Pulse", "items": [ { - "quote": "I've been wearing the same linen shirt for two summers now and it's somehow gotten better with every wash.", - "author": "Mara K.", - "role": "Berlin", - "avatar": "https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=200&q=80" + "quote": "Took the shell to Chamonix and back. Still smells, still works, still cheaper than therapy.", + "author": "Mateo S.", + "role": "Sub-3 marathoner / Boulder", + "avatar": "" }, { - "quote": "Considered cuts, neutral palette, real fabric. Exactly what I want when I'm getting dressed in the dark.", - "author": "Theo R.", - "role": "Brooklyn", - "avatar": "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?auto=format&fit=crop&w=200&q=80" + "quote": "I have one drawer of running gear. The Pulse half is the half I actually wear.", + "author": "Priya N.", + "role": "Ultra runner / Portland", + "avatar": "" + }, + { + "quote": "Wore the shorts for an entire 16-week block. They held. That's the whole review.", + "author": "Jonas R.", + "role": "Track coach / Berlin", + "avatar": "" } ] } @@ -154,12 +163,12 @@ "type": "newsletter-cta", "props": { "id": "newsletter-home", - "tagline": "Stay in the loop", - "heading": "Letters from the studio", - "subheading": "New collections, mill stories, and the occasional invitation. Twice a month.", + "tagline": "Field notes", + "heading": "Weekly notes from the lab.", + "subheading": "Test results, training blocks, gear we cut and gear we kept. Every Monday at 5:30am.", "buttonLabel": "Subscribe", "endpoint": "", - "imageUrl": "https://images.unsplash.com/photo-1469334031218-e382a71b716b?auto=format&fit=crop&w=1800&q=80", + "imageUrl": "https://supabase.frontend-ai.com/storage/v1/object/public/assets/themes/pulse-nike/newsletter.png", "layout": "split" } }, @@ -167,92 +176,53 @@ "type": "footer", "props": { "id": "footer-home", - "brand": "Maison", - "tagline": "Considered essentials, made in small batches and built to last beyond the season.", + "brand": "PULSE", + "tagline": "Performance gear for athletes who break their kit before they break themselves.", "columns": [ { "title": "Shop", "links": [ - { - "label": "All", - "href": "/search" - }, - { - "label": "New", - "href": "/collections/new" - }, - { - "label": "Best sellers", - "href": "/collections/best" - } + { "label": "All gear", "href": "/search" }, + { "label": "New", "href": "/collections/new" }, + { "label": "Run", "href": "/collections/run" }, + { "label": "Train", "href": "/collections/train" }, + { "label": "Sale", "href": "/collections/sale" } ] }, { "title": "About", "links": [ - { - "label": "Our story", - "href": "/about" - }, - { - "label": "Materials", - "href": "/materials" - }, - { - "label": "Journal", - "href": "/journal" - } + { "label": "Our story", "href": "/about" }, + { "label": "The lab", "href": "/lab" }, + { "label": "Athletes", "href": "/athletes" } ] }, { "title": "Help", "links": [ - { - "label": "Shipping", - "href": "/help/shipping" - }, - { - "label": "Returns", - "href": "/help/returns" - }, - { - "label": "Contact", - "href": "/contact" - } + { "label": "Shipping", "href": "/help/shipping" }, + { "label": "Returns", "href": "/help/returns" }, + { "label": "Lifetime guarantee", "href": "/help/guarantee" }, + { "label": "Contact", "href": "/contact" } ] }, { "title": "Legal", "links": [ - { - "label": "Terms", - "href": "/terms" - }, - { - "label": "Privacy", - "href": "/privacy" - } + { "label": "Terms", "href": "/terms" }, + { "label": "Privacy", "href": "/privacy" } ] } ], "social": [ - { - "label": "Instagram", - "href": "#" - }, - { - "label": "Pinterest", - "href": "#" - }, - { - "label": "TikTok", - "href": "#" - } + { "label": "Instagram", "href": "#" }, + { "label": "Strava", "href": "#" }, + { "label": "YouTube", "href": "#" } ], "showNewsletter": "no", - "newsletterHeading": "Stay in touch", + "newsletterHeading": "", "newsletterEndpoint": "", - "copyright": "© 2026 Maison. All rights reserved." + "copyright": "© 2026 Pulse. Built to be replaced when used up." } } ] @@ -260,19 +230,20 @@ "/products/:handle": { "root": { "props": { - "title": "Default product", - "headerFont": "Playfair Display", + "title": "Pulse", + "headerFont": "Archivo Black", + "headerFontWeight": "400", "bodyFont": "Inter", - "primaryColor": "#0a0a0a", - "secondaryColor": "#64748B", + "primaryColor": "#111111", + "secondaryColor": "#707072", "accentColor": "#f5f5f5", "bgColor": "#ffffff", - "fgColor": "#0a0a0a", + "fgColor": "#111111", "mutedColor": "#f5f5f5", - "radius": "sm", - "buttonRadius": "md", - "shadow": "sm", - "maxWidth": "xl" + "radius": "xl", + "buttonRadius": "full", + "shadow": "none", + "maxWidth": "2xl" } }, "content": [ @@ -280,24 +251,13 @@ "type": "navigation", "props": { "id": "nav-product", - "brand": "Maison", + "brand": "PULSE", "links": [ - { - "label": "Shop", - "href": "/search" - }, - { - "label": "Mens", - "href": "/collections/mens" - }, - { - "label": "Womens", - "href": "/collections/womens" - }, - { - "label": "About", - "href": "/about" - } + { "label": "Run", "href": "/collections/run" }, + { "label": "Train", "href": "/collections/train" }, + { "label": "Recover", "href": "/collections/recover" }, + { "label": "Sale", "href": "/collections/sale" }, + { "label": "About", "href": "/about" } ], "showSearch": "yes", "showAccount": "yes", @@ -317,8 +277,8 @@ "type": "products-carousel", "props": { "id": "carousel-related", - "tagline": "You might also like", - "heading": "Related pieces", + "tagline": "Pairs well with", + "heading": "Complete the kit", "limit": 8, "slidesPerView": "4", "ctaLabel": "Shop all", @@ -329,50 +289,33 @@ "type": "footer", "props": { "id": "footer-product", - "brand": "Maison", - "tagline": "Considered essentials, made in small batches.", + "brand": "PULSE", + "tagline": "Performance gear for athletes who break their kit before they break themselves.", "columns": [ { "title": "Shop", "links": [ - { - "label": "All", - "href": "/search" - }, - { - "label": "New", - "href": "/collections/new" - } + { "label": "All gear", "href": "/search" }, + { "label": "New", "href": "/collections/new" } ] }, { "title": "Help", "links": [ - { - "label": "Shipping", - "href": "/help/shipping" - }, - { - "label": "Returns", - "href": "/help/returns" - } + { "label": "Shipping", "href": "/help/shipping" }, + { "label": "Returns", "href": "/help/returns" }, + { "label": "Lifetime guarantee", "href": "/help/guarantee" } ] } ], "social": [ - { - "label": "Instagram", - "href": "#" - }, - { - "label": "Pinterest", - "href": "#" - } + { "label": "Instagram", "href": "#" }, + { "label": "Strava", "href": "#" } ], "showNewsletter": "no", - "newsletterHeading": "Stay in touch", + "newsletterHeading": "", "newsletterEndpoint": "", - "copyright": "© 2026 Maison. All rights reserved." + "copyright": "© 2026 Pulse. Built to be replaced when used up." } } ] @@ -380,19 +323,20 @@ "/collections/:handle": { "root": { "props": { - "title": "Collection", - "headerFont": "Playfair Display", + "title": "Collection — Pulse", + "headerFont": "Archivo Black", + "headerFontWeight": "400", "bodyFont": "Inter", - "primaryColor": "#0a0a0a", - "secondaryColor": "#64748B", + "primaryColor": "#111111", + "secondaryColor": "#707072", "accentColor": "#f5f5f5", "bgColor": "#ffffff", - "fgColor": "#0a0a0a", + "fgColor": "#111111", "mutedColor": "#f5f5f5", - "radius": "sm", - "buttonRadius": "md", - "shadow": "sm", - "maxWidth": "xl" + "radius": "xl", + "buttonRadius": "full", + "shadow": "none", + "maxWidth": "2xl" } }, "content": [ @@ -400,24 +344,13 @@ "type": "navigation", "props": { "id": "nav-collection", - "brand": "Maison", + "brand": "PULSE", "links": [ - { - "label": "Shop", - "href": "/search" - }, - { - "label": "Mens", - "href": "/collections/mens" - }, - { - "label": "Womens", - "href": "/collections/womens" - }, - { - "label": "About", - "href": "/about" - } + { "label": "Run", "href": "/collections/run" }, + { "label": "Train", "href": "/collections/train" }, + { "label": "Recover", "href": "/collections/recover" }, + { "label": "Sale", "href": "/collections/sale" }, + { "label": "About", "href": "/about" } ], "showSearch": "yes", "showAccount": "yes", @@ -439,17 +372,26 @@ "defaultSort": "BEST_SELLING", "showAvailability": "yes", "showPriceRange": "yes", - "showProductType": "no", - "productTypeOptions": [], + "showProductType": "yes", + "productTypeOptions": [ + { "label": "Tops" }, + { "label": "Shorts" }, + { "label": "Tights" }, + { "label": "Outerwear" }, + { "label": "Shoes" }, + { "label": "Accessories" } + ], "showVendor": "no", "vendorOptions": [], "showTags": "no", "tagOptions": [], "showColor": "yes", "colorOptions": [ - { "label": "Black", "color": "#000000" }, - { "label": "White", "color": "#FFFFFF" }, - { "label": "Navy", "color": "#1e3a5f" } + { "label": "Black", "color": "#111111" }, + { "label": "White", "color": "#ffffff" }, + { "label": "Grey", "color": "#707072" }, + { "label": "Volt", "color": "#d6ff3f" }, + { "label": "Sodium", "color": "#fa5400" } ], "showStyle": "no", "styleOptions": [], @@ -459,7 +401,8 @@ { "label": "S" }, { "label": "M" }, { "label": "L" }, - { "label": "XL" } + { "label": "XL" }, + { "label": "XXL" } ], "showMaterial": "no", "materialOptions": [], @@ -470,50 +413,32 @@ "type": "footer", "props": { "id": "footer-collection", - "brand": "Maison", - "tagline": "Considered essentials, made in small batches.", + "brand": "PULSE", + "tagline": "Performance gear for athletes who break their kit before they break themselves.", "columns": [ { "title": "Shop", "links": [ - { - "label": "All", - "href": "/search" - }, - { - "label": "New", - "href": "/collections/new" - } + { "label": "All gear", "href": "/search" }, + { "label": "New", "href": "/collections/new" } ] }, { "title": "Help", "links": [ - { - "label": "Shipping", - "href": "/help/shipping" - }, - { - "label": "Returns", - "href": "/help/returns" - } + { "label": "Shipping", "href": "/help/shipping" }, + { "label": "Returns", "href": "/help/returns" } ] } ], "social": [ - { - "label": "Instagram", - "href": "#" - }, - { - "label": "Pinterest", - "href": "#" - } + { "label": "Instagram", "href": "#" }, + { "label": "Strava", "href": "#" } ], "showNewsletter": "no", - "newsletterHeading": "Stay in touch", + "newsletterHeading": "", "newsletterEndpoint": "", - "copyright": "© 2026 Maison. All rights reserved." + "copyright": "© 2026 Pulse. Built to be replaced when used up." } } ] @@ -521,19 +446,20 @@ "/search": { "root": { "props": { - "title": "Shop", - "headerFont": "Playfair Display", + "title": "Shop — Pulse", + "headerFont": "Archivo Black", + "headerFontWeight": "400", "bodyFont": "Inter", - "primaryColor": "#0a0a0a", - "secondaryColor": "#64748B", + "primaryColor": "#111111", + "secondaryColor": "#707072", "accentColor": "#f5f5f5", "bgColor": "#ffffff", - "fgColor": "#0a0a0a", + "fgColor": "#111111", "mutedColor": "#f5f5f5", - "radius": "sm", - "buttonRadius": "md", - "shadow": "sm", - "maxWidth": "xl" + "radius": "xl", + "buttonRadius": "full", + "shadow": "none", + "maxWidth": "2xl" } }, "content": [ @@ -541,24 +467,13 @@ "type": "navigation", "props": { "id": "nav-search", - "brand": "Maison", + "brand": "PULSE", "links": [ - { - "label": "Shop", - "href": "/search" - }, - { - "label": "Mens", - "href": "/collections/mens" - }, - { - "label": "Womens", - "href": "/collections/womens" - }, - { - "label": "About", - "href": "/about" - } + { "label": "Run", "href": "/collections/run" }, + { "label": "Train", "href": "/collections/train" }, + { "label": "Recover", "href": "/collections/recover" }, + { "label": "Sale", "href": "/collections/sale" }, + { "label": "About", "href": "/about" } ], "showSearch": "yes", "showAccount": "yes", @@ -571,58 +486,31 @@ "type": "search-products", "props": { "id": "search-products", - "heading": "Shop", - "subheading": "Browse our full collection.", + "heading": "All gear.", + "subheading": "Filter by sport, fabric, fit, color. Or sort by what's been beaten the hardest.", "columns": "4", "limit": 24, "showAvailability": "yes", "showPriceRange": "yes", "showProductType": "yes", - "showVendor": "yes", + "showVendor": "no", "showTags": "yes", "metafieldFilters": [], "defaultSort": "BEST_SELLING", "productTypeOptions": [ - { - "label": "T-Shirts" - }, - { - "label": "Pants" - }, - { - "label": "Outerwear" - }, - { - "label": "Accessories" - }, - { - "label": "Shoes" - } - ], - "vendorOptions": [ - { - "label": "Maison" - }, - { - "label": "Atelier" - }, - { - "label": "Studio" - } + { "label": "Tops" }, + { "label": "Shorts" }, + { "label": "Tights" }, + { "label": "Outerwear" }, + { "label": "Shoes" }, + { "label": "Accessories" } ], + "vendorOptions": [], "tagOptions": [ - { - "label": "New" - }, - { - "label": "Sale" - }, - { - "label": "Bestseller" - }, - { - "label": "Limited Edition" - } + { "label": "New" }, + { "label": "Race day" }, + { "label": "Field tested" }, + { "label": "Sale" } ] } }, @@ -630,54 +518,33 @@ "type": "footer", "props": { "id": "footer-search", - "brand": "Maison", - "tagline": "Considered essentials, made in small batches.", + "brand": "PULSE", + "tagline": "Performance gear for athletes who break their kit before they break themselves.", "columns": [ { "title": "Shop", "links": [ - { - "label": "All", - "href": "/search" - }, - { - "label": "Mens", - "href": "/collections/mens" - }, - { - "label": "Womens", - "href": "/collections/womens" - } + { "label": "All gear", "href": "/search" }, + { "label": "Run", "href": "/collections/run" }, + { "label": "Train", "href": "/collections/train" } ] }, { "title": "Help", "links": [ - { - "label": "Shipping", - "href": "/help/shipping" - }, - { - "label": "Returns", - "href": "/help/returns" - } + { "label": "Shipping", "href": "/help/shipping" }, + { "label": "Returns", "href": "/help/returns" } ] } ], "social": [ - { - "label": "Instagram", - "href": "#" - }, - { - "label": "Pinterest", - "href": "#" - } + { "label": "Instagram", "href": "#" }, + { "label": "Strava", "href": "#" } ], "showNewsletter": "no", - "newsletterHeading": "Stay in touch", + "newsletterHeading": "", "newsletterEndpoint": "", - "copyright": "© 2026 Maison. All rights reserved." + "copyright": "© 2026 Pulse. Built to be replaced when used up." } } ] @@ -685,19 +552,20 @@ "/about": { "root": { "props": { - "title": "About — Maison", - "headerFont": "Playfair Display", + "title": "About — Pulse", + "headerFont": "Archivo Black", + "headerFontWeight": "400", "bodyFont": "Inter", - "primaryColor": "#0a0a0a", - "secondaryColor": "#64748B", + "primaryColor": "#111111", + "secondaryColor": "#707072", "accentColor": "#f5f5f5", "bgColor": "#ffffff", - "fgColor": "#0a0a0a", + "fgColor": "#111111", "mutedColor": "#f5f5f5", - "radius": "sm", - "buttonRadius": "md", - "shadow": "sm", - "maxWidth": "xl" + "radius": "xl", + "buttonRadius": "full", + "shadow": "none", + "maxWidth": "2xl" } }, "content": [ @@ -705,24 +573,13 @@ "type": "navigation", "props": { "id": "nav-about", - "brand": "Maison", + "brand": "PULSE", "links": [ - { - "label": "Shop", - "href": "/search" - }, - { - "label": "Mens", - "href": "/collections/mens" - }, - { - "label": "Womens", - "href": "/collections/womens" - }, - { - "label": "About", - "href": "/about" - } + { "label": "Run", "href": "/collections/run" }, + { "label": "Train", "href": "/collections/train" }, + { "label": "Recover", "href": "/collections/recover" }, + { "label": "Sale", "href": "/collections/sale" }, + { "label": "About", "href": "/about" } ], "showSearch": "yes", "showAccount": "yes", @@ -735,21 +592,16 @@ "type": "hero", "props": { "id": "hero-about", - "tagline": "Our Story", - "heading": "Built on intention, not trend", - "subheading": "We started Maison with a simple belief: clothing should be made slowly, from honest materials, and designed to outlast the season it was born in.", - "primaryCta": { - "label": "Shop the collection", - "href": "/search" - }, - "secondaryCta": { - "label": "", - "href": "" - }, - "imageUrl": "https://images.unsplash.com/photo-1441986300917-64674bd600d8?auto=format&fit=crop&w=2400&q=80", + "tagline": "The lab", + "heading": "We don't make activewear. We make tools.", + "subheading": "Pulse started in a converted warehouse with three coaches, two athletes, and a whiteboard full of things they wished worked better. Eight years later, the whiteboard is still there. So is the standard.", + "buttons": [ + { "label": "Shop the kit", "href": "/search", "variant": "primary" } + ], + "imageUrl": "https://supabase.frontend-ai.com/storage/v1/object/public/assets/themes/pulse-nike/hero-about.png", "align": "left", - "height": "md", - "tone": "dark" + "height": "lg", + "tone": "light" } }, { @@ -757,35 +609,49 @@ "props": { "id": "features-about", "tagline": "What we believe", - "heading": "The principles behind every piece", + "heading": "Three rules. No exceptions.", "subheading": "", "columns": "3", "items": [ { - "title": "Traceable materials", - "body": "Every fiber we use — linen, organic cotton, merino — comes from mills with transparent, auditable supply chains." + "title": "Engineered", + "body": "Every fabric, every seam, every fit decision starts with athletes on a track. Lab work comes after, if at all." }, { - "title": "Small-batch production", - "body": "We produce in considered quantities. Nothing sits in a warehouse, nothing ends up in landfill." + "title": "Field tested", + "body": "Twelve weeks of training. Two race blocks. One full season. Nothing leaves the lab without that on the record." }, { - "title": "Designed to age well", - "body": "Reinforced construction and natural finishes that soften and improve with every wear and wash." + "title": "Guaranteed", + "body": "If it fails before its time, we replace it. If it fails on race day, we apologize and replace it. That's the deal." } ] } }, + { + "type": "hero", + "props": { + "id": "cover-about-performance", + "tagline": "Used up is the goal", + "heading": "Designed to be worn out, not preserved.", + "subheading": "If you're babying your kit, we did something wrong. Bring it back when it's done — we'll send the next one.", + "buttons": [], + "imageUrl": "https://supabase.frontend-ai.com/storage/v1/object/public/assets/themes/pulse-nike/cover-performance.jpeg", + "align": "left", + "height": "lg", + "tone": "light" + } + }, { "type": "newsletter-cta", "props": { "id": "newsletter-about", - "tagline": "Stay in the loop", - "heading": "Letters from the studio", - "subheading": "New collections, mill stories, and the occasional invitation. Twice a month.", + "tagline": "Field notes", + "heading": "Weekly notes from the lab.", + "subheading": "Test results, training blocks, gear we cut and gear we kept. Every Monday at 5:30am.", "buttonLabel": "Subscribe", "endpoint": "", - "imageUrl": "https://images.unsplash.com/photo-1469334031218-e382a71b716b?auto=format&fit=crop&w=1800&q=80", + "imageUrl": "https://supabase.frontend-ai.com/storage/v1/object/public/assets/themes/pulse-nike/newsletter.png", "layout": "split" } }, @@ -793,81 +659,47 @@ "type": "footer", "props": { "id": "footer-about", - "brand": "Maison", - "tagline": "Considered essentials, made in small batches and built to last beyond the season.", + "brand": "PULSE", + "tagline": "Performance gear for athletes who break their kit before they break themselves.", "columns": [ { "title": "Shop", "links": [ - { - "label": "All", - "href": "/search" - }, - { - "label": "New", - "href": "/collections/new" - }, - { - "label": "Best sellers", - "href": "/collections/best" - } + { "label": "All gear", "href": "/search" }, + { "label": "New", "href": "/collections/new" }, + { "label": "Run", "href": "/collections/run" }, + { "label": "Train", "href": "/collections/train" } ] }, { "title": "About", "links": [ - { - "label": "Our story", - "href": "/about" - }, - { - "label": "Materials", - "href": "/materials" - }, - { - "label": "Journal", - "href": "/journal" - } + { "label": "Our story", "href": "/about" }, + { "label": "The lab", "href": "/lab" }, + { "label": "Athletes", "href": "/athletes" } ] }, { "title": "Help", "links": [ - { - "label": "Shipping", - "href": "/help/shipping" - }, - { - "label": "Returns", - "href": "/help/returns" - }, - { - "label": "Contact", - "href": "/contact" - } + { "label": "Shipping", "href": "/help/shipping" }, + { "label": "Returns", "href": "/help/returns" }, + { "label": "Lifetime guarantee", "href": "/help/guarantee" }, + { "label": "Contact", "href": "/contact" } ] } ], "social": [ - { - "label": "Instagram", - "href": "#" - }, - { - "label": "Pinterest", - "href": "#" - }, - { - "label": "TikTok", - "href": "#" - } + { "label": "Instagram", "href": "#" }, + { "label": "Strava", "href": "#" }, + { "label": "YouTube", "href": "#" } ], "showNewsletter": "no", - "newsletterHeading": "Stay in touch", + "newsletterHeading": "", "newsletterEndpoint": "", - "copyright": "© 2026 Maison. All rights reserved." + "copyright": "© 2026 Pulse. Built to be replaced when used up." } } ] } -} \ No newline at end of file +} diff --git a/components/Heading.tsx b/components/Heading.tsx new file mode 100644 index 0000000..bbcfc7f --- /dev/null +++ b/components/Heading.tsx @@ -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 = { + 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 = { + 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 ( +
+ {tagline ? ( + + {tagline} + + ) : null} + {title ? ( + + {title} + + ) : null} + {subtitle ? ( + + {subtitle} + + ) : null} +
+ ); +} + +export default Heading; diff --git a/components/ThemeProvider.tsx b/components/ThemeProvider.tsx index 6c15686..795a2ad 100644 --- a/components/ThemeProvider.tsx +++ b/components/ThemeProvider.tsx @@ -3,6 +3,7 @@ import { useEffect, useMemo, useRef } from "react"; export type ThemeProps = { headerFont?: string; + headerFontWeight?: string; bodyFont?: string; primaryColor?: string; primaryForegroundColor?: string; @@ -42,19 +43,40 @@ const shadowMap: Record, string> = { 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 { - const fonts = [headerFont, bodyFont].filter( - (f): f is string => !!f && f !== "system-ui" - ); - if (fonts.length === 0) return null; - const families = Array.from(new Set(fonts)) - .map((f) => `family=${encodeURIComponent(f)}:wght@400;500;600;700`) - .join("&"); - return `https://fonts.googleapis.com/css2?${families}&display=swap`; +function googleFontsHref( + headerFont?: string, + bodyFont?: string, + headerFontWeight?: string, +): string | null { + const valid = (f?: string): f is string => !!f && f !== "system-ui"; + const families: string[] = []; + const seen = new Set(); + + 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 (valid(bodyFont) && !seen.has(bodyFont)) { + seen.add(bodyFont); + families.push( + `family=${encodeURIComponent(bodyFont)}:wght@${bodyWeights}`, + ); + } + if (families.length === 0) return null; + return `https://fonts.googleapis.com/css2?${families.join("&")}&display=swap`; } export function ThemeProvider({ headerFont, + headerFontWeight, bodyFont, primaryColor, primaryForegroundColor, @@ -87,9 +109,11 @@ export function ThemeProvider({ if (maxWidth) vars["--container-max-width"] = maxWidthMap[maxWidth]; if (headerFont) vars["--font-header"] = `"${headerFont}", system-ui, sans-serif`; if (bodyFont) vars["--font-body"] = `"${bodyFont}", system-ui, sans-serif`; + if (headerFontWeight) vars["--font-weight-header"] = headerFontWeight; return vars; }, [ headerFont, + headerFontWeight, bodyFont, primaryColor, primaryForegroundColor, @@ -132,8 +156,8 @@ export function ThemeProvider({ }, [cssVars]); const fontsHref = useMemo( - () => googleFontsHref(headerFont, bodyFont), - [headerFont, bodyFont], + () => googleFontsHref(headerFont, bodyFont, headerFontWeight), + [headerFont, bodyFont, headerFontWeight], ); // Plain CSS rules — applied directly, no Tailwind CDN runtime needed. @@ -146,6 +170,7 @@ export function ThemeProvider({ } h1, h2, h3, h4, h5, h6 { font-family: var(--font-header), system-ui, -apple-system, sans-serif; + font-weight: var(--font-weight-header, 600); } `; diff --git a/components/Typography.tsx b/components/Typography.tsx index 5290fc8..6758975 100644 --- a/components/Typography.tsx +++ b/components/Typography.tsx @@ -15,17 +15,17 @@ export type TypographyVariant = | "caption"; const sizeClasses: Record = { - h1: "text-5xl md:text-6xl lg:text-7xl font-semibold tracking-tight leading-[1.05]", - h2: "text-4xl md:text-5xl font-semibold tracking-tight leading-[1.1]", - h3: "text-3xl md:text-4xl font-semibold tracking-tight leading-tight", - h4: "text-2xl md:text-3xl font-semibold tracking-tight leading-snug", - h5: "text-xl md:text-2xl font-semibold leading-snug", - h6: "text-lg md:text-xl font-semibold leading-snug", + h1: "text-5xl md:text-6xl lg:text-7xl tracking-tight leading-[1.05]", + h2: "text-4xl md:text-5xl tracking-tight leading-[1.1]", + h3: "text-3xl md:text-4xl tracking-tight leading-tight", + h4: "text-2xl md:text-3xl tracking-tight leading-snug", + h5: "text-xl md:text-2xl leading-snug", + h6: "text-lg md:text-xl leading-snug", subtitle1: "text-lg md:text-xl leading-relaxed text-muted-foreground", subtitle2: "text-base md:text-lg leading-relaxed text-muted-foreground", body1: "text-lg 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 @@ -33,17 +33,17 @@ const sizeClasses: Record = { // hasn't compiled utility classes yet. The Tailwind classes above still // apply on top once available (responsive breakpoints, leading, etc.). const sizeStyles: Record = { - h1: { fontSize: "clamp(2.5rem, 6vw, 4.5rem)", lineHeight: 1.05, fontWeight: 600, letterSpacing: "-0.02em" }, - h2: { fontSize: "clamp(2rem, 4vw, 3rem)", lineHeight: 1.1, fontWeight: 600, letterSpacing: "-0.02em" }, - h3: { fontSize: "clamp(1.75rem, 3.5vw, 2.25rem)", lineHeight: 1.15, fontWeight: 600, letterSpacing: "-0.015em" }, - h4: { fontSize: "clamp(1.5rem, 3vw, 1.875rem)", lineHeight: 1.2, fontWeight: 600, letterSpacing: "-0.01em" }, - h5: { fontSize: "1.5rem", lineHeight: 1.25, fontWeight: 600 }, - h6: { fontSize: "1.25rem", lineHeight: 1.3, fontWeight: 600 }, + h1: { fontSize: "clamp(2.5rem, 6vw, 4.5rem)", lineHeight: 1.05, 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, letterSpacing: "-0.015em" }, + h4: { fontSize: "clamp(1.5rem, 3vw, 1.875rem)", lineHeight: 1.2, letterSpacing: "-0.01em" }, + h5: { fontSize: "1.5rem", lineHeight: 1.25 }, + h6: { fontSize: "1.25rem", lineHeight: 1.3 }, subtitle1: { fontSize: "1.125rem", lineHeight: 1.6 }, subtitle2: { fontSize: "1rem", lineHeight: 1.6 }, body1: { fontSize: "1.125rem", 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 = { @@ -80,11 +80,22 @@ export function Typography({ const isHeading = variant.startsWith("h"); 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. 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( Tag, { className: cn(fontClass, sizeClasses[variant], className), - style: { ...sizeStyles[variant], ...style }, + style: { ...fontStyles, ...sizeStyles[variant], ...style }, ...rest, }, children, diff --git a/components/commerce/collection-card.tsx b/components/commerce/collection-card.tsx index d2f64ff..f79d138 100644 --- a/components/commerce/collection-card.tsx +++ b/components/commerce/collection-card.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { Link } from 'react-router'; import { Card, CardContent } from '@/components/ui/card'; +import { Typography } from '@/components/Typography'; interface CollectionImage { url: string; @@ -40,9 +41,12 @@ const CollectionCard: React.FC = ({ collection }) => { {/* Collection Info */} -

+ {collection.title} -

+ {collection.description && (

diff --git a/components/commerce/collection-grid.tsx b/components/commerce/collection-grid.tsx index 41545de..52114a0 100644 --- a/components/commerce/collection-grid.tsx +++ b/components/commerce/collection-grid.tsx @@ -2,8 +2,9 @@ import { useEffect, useState } from "react"; import { Link } from "react-router"; import { shopifyFetch } from "@/services/shopify/client"; import { GET_COLLECTIONS_QUERY } from "@/graphql/collections"; -import { Typography } from "@/components/Typography"; import { Container } from "@/components/layout/Container"; +import { Typography } from "@/components/Typography"; +import { Heading } from "@/components/Heading"; export type CollectionGridProps = { tagline: string; @@ -47,19 +48,15 @@ export function CollectionGrid({ return (

-
- {tagline ? ( -

- {tagline} -

- ) : null} - {heading} - {subheading ? ( - - {subheading} - - ) : null} -
+
- + {c.title} - Shop now → + Shop now
) : null} {!isEditorial ? ( -
-

{c.title}

- - → - +
+ + {c.title} +
) : null} diff --git a/components/commerce/featured-product.tsx b/components/commerce/featured-product.tsx index 04e8e3b..80fec83 100644 --- a/components/commerce/featured-product.tsx +++ b/components/commerce/featured-product.tsx @@ -90,9 +90,7 @@ export function FeaturedProductView({
{tagline ? ( -

- {tagline} -

+ {tagline} ) : null} {product.title} {formatted ? ( diff --git a/components/commerce/product-card.tsx b/components/commerce/product-card.tsx index e650265..afb1ec4 100644 --- a/components/commerce/product-card.tsx +++ b/components/commerce/product-card.tsx @@ -1,5 +1,6 @@ import * as React from "react"; import { Link } from "react-router"; +import { Typography } from "@/components/Typography"; type ProductImage = { url: string; altText?: string }; type ProductPrice = { amount: string; currencyCode: string }; @@ -53,7 +54,12 @@ export function ProductCard({ ) : null}
-

{product.title}

+ + {product.title} + {price ? (
{onSale && compare ? ( diff --git a/components/commerce/products-carousel.tsx b/components/commerce/products-carousel.tsx index 70f478a..d669cb2 100644 --- a/components/commerce/products-carousel.tsx +++ b/components/commerce/products-carousel.tsx @@ -4,7 +4,7 @@ import type { ShopifyCollection } from "@reacteditor/field-shopify"; import { getProducts } from "@/hooks/use-shopify-products"; import { getCollectionProducts } from "@/hooks/use-shopify-collections"; import { ProductCard } from "./product-card"; -import { Typography } from "@/components/Typography"; +import { Heading } from "@/components/Heading"; import { Carousel, CarouselContent, @@ -74,19 +74,14 @@ export function ProductsCarousel({
-
- {tagline ? ( -

- {tagline} -

- ) : null} - {heading} - {subheading ? ( - - {subheading} - - ) : null} -
+ {ctaLabel ? (
-
- {tagline ? ( -

- {tagline} -

- ) : null} - {heading} - {subheading ? ( - - {subheading} - - ) : null} -
+ {ctaLabel ? ( -
- {tagline ? ( -

- {tagline} -

- ) : null} - {heading} -
+
{items.length === 0 diff --git a/components/commerce/shop-footer.tsx b/components/commerce/shop-footer.tsx deleted file mode 100644 index 1b3c369..0000000 --- a/components/commerce/shop-footer.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; - -const Footer: React.FC = () => { - return ( -
-
-

- Store -

-

- Your premium shopping destination -

-
-

© 2025 Store. All rights reserved.

-
-
-
- ); -}; - -export default Footer; diff --git a/components/commerce/shop-header.tsx b/components/commerce/shop-header.tsx deleted file mode 100644 index ae446f1..0000000 --- a/components/commerce/shop-header.tsx +++ /dev/null @@ -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 ( - - ); -}; - -const Header: React.FC = () => { - return ( - - ); -}; - -export default Header; diff --git a/components/cta/cta.tsx b/components/cta/cta.tsx index 7f3584b..290e87e 100644 --- a/components/cta/cta.tsx +++ b/components/cta/cta.tsx @@ -1,6 +1,6 @@ import { Link } from "react-router"; import { cn } from "@/lib/utils"; -import { Typography } from "@/components/Typography"; +import { Heading } from "@/components/Heading"; export type CTAProps = { tagline: string; @@ -43,17 +43,15 @@ export function CTA({ align === "center" ? "items-center text-center" : "items-start", )} > - {tagline ? ( -

- {tagline} -

- ) : null} - {heading} - {subheading ? ( - - {subheading} - - ) : null} +
-
- {tagline ? ( -

- {tagline} -

- ) : null} - {heading} - {subheading ? ( - - {subheading} - - ) : null} -
+
{items.map((item, i) => { diff --git a/components/features/features.tsx b/components/features/features.tsx index 1bad0b3..3be3f5d 100644 --- a/components/features/features.tsx +++ b/components/features/features.tsx @@ -1,5 +1,6 @@ import { Typography } from "@/components/Typography"; import { Container } from "@/components/layout/Container"; +import { Heading } from "@/components/Heading"; export type FeaturesProps = { tagline: string; @@ -19,28 +20,24 @@ export function Features({ tagline, heading, subheading, columns, items }: Featu return (
-
- {tagline ? ( -

- {tagline} -

- ) : null} - {heading} - {subheading ? ( - - {subheading} - - ) : null} -
+
{items.map((item, i) => ( -
-

+

+ {String(i + 1).padStart(2, "0")} -

+
{item.title} - + {item.body}
diff --git a/components/hero/hero.editor.tsx b/components/hero/hero.editor.tsx index 79940c3..d00e69c 100644 --- a/components/hero/hero.editor.tsx +++ b/components/hero/hero.editor.tsx @@ -13,8 +13,10 @@ export const heroEditor: ComponentConfig = { heading: "Made for the way you move", subheading: "A considered wardrobe of essentials, cut from natural fibers and designed to last.", - primaryCta: { label: "Shop the collection", href: "/collections" }, - secondaryCta: { label: "Our story", href: "/about" }, + buttons: [ + { label: "Shop the collection", href: "/collections", variant: "primary" }, + { label: "Our story", href: "/about", variant: "secondary" }, + ], imageUrl: "https://images.unsplash.com/photo-1490481651871-ab68de25d43d?auto=format&fit=crop&w=2400&q=80", align: "left", @@ -25,21 +27,29 @@ export const heroEditor: ComponentConfig = { tagline: { label: "Tagline", type: "text", contentEditable: true }, heading: { label: "Heading", type: "textarea", contentEditable: true }, subheading: { label: "Subheading", type: "textarea", contentEditable: true }, - primaryCta: { - label: "Primary CTA", - type: "object", - objectFields: { + buttons: { + label: "Buttons", + type: "array", + arrayFields: { label: { label: "Label", type: "text", contentEditable: true }, 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: { - label: "Secondary CTA", - type: "object", - objectFields: { - label: { label: "Label", type: "text", contentEditable: true }, - href: { label: "Link", type: "text" }, + defaultItemProps: { + label: "Button", + href: "/", + variant: "primary", }, + getItemSummary: (item) => item?.label || "Button", }, imageUrl: { label: "Background image", ...imageField({ adapter: frontendAiMediaAdapter }) }, align: { diff --git a/components/hero/hero.tsx b/components/hero/hero.tsx index 03ff8de..eebf035 100644 --- a/components/hero/hero.tsx +++ b/components/hero/hero.tsx @@ -2,12 +2,19 @@ import { Link } from "react-router"; import { cn } from "@/lib/utils"; import { Typography } from "@/components/Typography"; +export type HeroButtonVariant = "primary" | "secondary" | "outline" | "ghost"; + +export type HeroButton = { + label: string; + href: string; + variant: HeroButtonVariant; +}; + export type HeroProps = { tagline: string; heading: string; subheading: string; - primaryCta: { label: string; href: string }; - secondaryCta: { label: string; href: string }; + buttons: HeroButton[]; imageUrl: string; align: "left" | "center"; height: "md" | "lg" | "full"; @@ -20,18 +27,40 @@ const heightClass: Record = { 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({ tagline, heading, subheading, - primaryCta, - secondaryCta, + buttons, imageUrl, align, height, tone, }: HeroProps) { const isDark = tone === "dark"; + const visibleButtons = (buttons ?? []).filter((b) => b?.label); + return (
{tagline ? ( -

{tagline} -

+ ) : null} {heading} @@ -87,35 +117,24 @@ export function Hero({ ) : null} -
- {primaryCta?.label ? ( - - {primaryCta.label} - - ) : null} - {secondaryCta?.label ? ( - - {secondaryCta.label} - - ) : null} -
+ {visibleButtons.length > 0 ? ( +
+ {visibleButtons.map((b, i) => ( + + {b.label} + + ))} +
+ ) : null}
); diff --git a/components/landing/image-gallery.tsx b/components/landing/image-gallery.tsx index f1db197..6ccc01a 100644 --- a/components/landing/image-gallery.tsx +++ b/components/landing/image-gallery.tsx @@ -1,6 +1,6 @@ import { cn } from "@/lib/utils"; -import { Typography } from "@/components/Typography"; import { Container } from "@/components/layout/Container"; +import { Heading } from "@/components/Heading"; export type ImageGalleryProps = { tagline: string; @@ -14,21 +14,15 @@ export function ImageGallery({ tagline, heading, subheading, layout, items }: Im return (
- {(tagline || heading || subheading) && ( -
- {tagline ? ( -

- {tagline} -

- ) : null} - {heading ? {heading} : null} - {subheading ? ( - - {subheading} - - ) : null} -
- )} + {layout === "masonry" ? (
diff --git a/components/landing/newsletter-cta.tsx b/components/landing/newsletter-cta.tsx index 59b54da..c632b03 100644 --- a/components/landing/newsletter-cta.tsx +++ b/components/landing/newsletter-cta.tsx @@ -1,7 +1,9 @@ import { useState } from "react"; import { cn } from "@/lib/utils"; -import { Typography } from "@/components/Typography"; 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"; @@ -109,92 +111,80 @@ export function NewsletterCta({ const Form = (
- setEmail(e.target.value)} - placeholder="you@example.com" - className="flex-1 bg-transparent py-3 text-sm placeholder:text-muted-foreground focus:outline-none" + placeholder="Enter your email" + className="h-11 flex-1" /> - +
); - if (layout === "split") { - return ( -
- -
-
- {imageUrl ? ( + const isStacked = layout === "stacked"; + + return ( +
+ +
+ {imageUrl ? ( +
+
- ) : null} -
-
- {tagline ? ( -

- {tagline} -

- ) : null} - {heading} - {subheading ? ( - - {subheading} - - ) : null} -
- {submitted ? ( -

- Thanks — we'll be in touch. -

- ) : ( - Form - )}
+ ) : null} +
+ +
+ {submitted ? ( +

+ You're in. See you Monday at 5:30am. +

+ ) : ( + Form + )} +
- -
- ); - } - - return ( -
-
- {tagline ? ( -

- {tagline} -

- ) : null} - {heading} - {subheading ? ( - - {subheading} - - ) : null} -
- {submitted ? ( -

- Thanks — we'll be in touch. -

- ) : ( - Form - )}
-
+
); } diff --git a/components/logos/logos.tsx b/components/logos/logos.tsx index 7f98105..40810b1 100644 --- a/components/logos/logos.tsx +++ b/components/logos/logos.tsx @@ -1,4 +1,5 @@ import { Container } from "@/components/layout/Container"; +import { Typography } from "@/components/Typography"; export type LogosProps = { tagline: string; @@ -11,9 +12,9 @@ export function Logos({ tagline, items, layout }: LogosProps) {
{tagline ? ( -

+ {tagline} -

+ ) : null} {layout === "marquee" ? (
diff --git a/components/navigation/navigation.tsx b/components/navigation/navigation.tsx index 01cf28e..39f59c4 100644 --- a/components/navigation/navigation.tsx +++ b/components/navigation/navigation.tsx @@ -4,6 +4,7 @@ import { Link } from "react-router"; import { useShopifyCart } from "@/hooks/use-shopify-cart"; import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet"; import { Container } from "@/components/layout/Container"; +import { Typography } from "@/components/Typography"; import { cn } from "@/lib/utils"; export type NavigationProps = { @@ -51,11 +52,7 @@ export function Navigation({ )} > - + {logo ? ( ) : ( - brand || "Brand Logo" + + {brand || "Brand Logo"} + )} diff --git a/components/testimonials/testimonials.tsx b/components/testimonials/testimonials.tsx index 77138ee..d766aea 100644 --- a/components/testimonials/testimonials.tsx +++ b/components/testimonials/testimonials.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { ArrowLeft, ArrowRight } from "lucide-react"; -import { Typography } from "@/components/Typography"; +import { Heading } from "@/components/Heading"; export type TestimonialsProps = { tagline: string; @@ -21,12 +21,12 @@ export function Testimonials({ tagline, heading, items }: TestimonialsProps) { return (
- {tagline ? ( -

- {tagline} -

- ) : null} - {heading ? {heading} : null} + {item ? (
diff --git a/config/root.tsx b/config/root.tsx index 2bdcbac..a70a8b5 100644 --- a/config/root.tsx +++ b/config/root.tsx @@ -22,6 +22,7 @@ export const Root: RootConfig<{ defaultProps: { title: "Untitled", headerFont: "Inter", + headerFontWeight: "600", bodyFont: "Inter", // Hex defaults so the color picker reads them and any non-picker // input (typed hex, AI-set value, etc.) is round-trip compatible. @@ -40,6 +41,19 @@ export const Root: RootConfig<{ description: { label: "Description", type: "textarea" }, ogImage: { label: "OG image", ...imageField({ adapter: frontendAiMediaAdapter }) }, 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 }, primaryColor: { label: "Primary color", type: "color", placeholder: "#0a0a0a" }, secondaryColor: { label: "Secondary color", type: "color", placeholder: "#64748B" }, @@ -84,6 +98,7 @@ export const Root: RootConfig<{ render: ({ children, headerFont, + headerFontWeight, bodyFont, primaryColor, secondaryColor, @@ -98,6 +113,7 @@ export const Root: RootConfig<{ return (