update components

This commit is contained in:
Rami Bitar
2026-06-06 14:21:55 -04:00
parent ed346476e5
commit ef9e10256d
60 changed files with 85 additions and 1035 deletions

View File

@@ -19,7 +19,7 @@
},
"content": [
{
"type": "navigation",
"type": "header",
"props": {
"id": "nav-about",
"brand": "PULSE",
@@ -53,7 +53,7 @@
}
},
{
"type": "hero",
"type": "cover",
"props": {
"id": "hero-about",
"tagline": "The lab",
@@ -73,31 +73,7 @@
}
},
{
"type": "features",
"props": {
"id": "features-about",
"tagline": "What we believe",
"heading": "Three rules. No exceptions.",
"subheading": "",
"columns": "3",
"items": [
{
"title": "Engineered",
"body": "Every fabric, every seam, every fit decision starts with athletes on a track. Lab work comes after, if at all."
},
{
"title": "Field tested",
"body": "Twelve weeks of training. Two race blocks. One full season. Nothing leaves the lab without that on the record."
},
{
"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",
"type": "cover",
"props": {
"id": "cover-about-performance",
"tagline": "Used up is the goal",

View File

@@ -19,7 +19,7 @@
},
"content": [
{
"type": "navigation",
"type": "header",
"props": {
"id": "nav-collection",
"brand": "PULSE",

View File

@@ -19,7 +19,7 @@
},
"content": [
{
"type": "navigation",
"type": "header",
"props": {
"id": "nav-home",
"brand": "PULSE",
@@ -53,7 +53,7 @@
}
},
{
"type": "hero",
"type": "cover",
"props": {
"id": "hero-home",
"tagline": "Spring 2026",
@@ -91,7 +91,7 @@
}
},
{
"type": "hero",
"type": "cover",
"props": {
"id": "cover-home-performance",
"tagline": "Performance Series",
@@ -105,31 +105,7 @@
}
},
{
"type": "features",
"props": {
"id": "features-home",
"tagline": "Why Pulse",
"heading": "Three rules. No exceptions.",
"subheading": "",
"columns": "3",
"items": [
{
"title": "Engineered",
"body": "Every fabric, every seam, every fit decision starts with athletes on a track. Lab work comes after."
},
{
"title": "Field tested",
"body": "Twelve weeks. Two race blocks. One full season. Nothing ships without that on the record."
},
{
"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",
"type": "cover",
"props": {
"id": "cover-home-discipline",
"tagline": "Discipline",
@@ -153,34 +129,6 @@
"limit": 6
}
},
{
"type": "testimonials",
"props": {
"id": "testimonials-home",
"tagline": "From the field",
"heading": "Athletes on Pulse",
"items": [
{
"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": "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": ""
}
]
}
},
{
"type": "newsletter-cta",
"props": {

View File

@@ -19,7 +19,7 @@
},
"content": [
{
"type": "navigation",
"type": "header",
"props": {
"id": "nav-product",
"brand": "PULSE",

View File

@@ -19,7 +19,7 @@
},
"content": [
{
"type": "navigation",
"type": "header",
"props": {
"id": "nav-search",
"brand": "PULSE",

View File

@@ -1,9 +1,9 @@
import { ComponentConfig } from "@reacteditor/core";
import { LayoutTemplate } from "lucide-react";
import { Hero, type HeroProps } from "@/components/hero/hero";
import { Cover, type CoverProps } from "@/components/cover/cover";
const heroEditor: ComponentConfig<HeroProps> = {
label: "Hero",
const coverEditor: ComponentConfig<CoverProps> = {
label: "Cover",
icon: <LayoutTemplate size={16} />,
category: "hero",
defaultProps: {
@@ -76,7 +76,7 @@ const heroEditor: ComponentConfig<HeroProps> = {
],
},
},
render: (props) => <Hero {...props} />,
render: (props) => <Cover {...props} />,
};
export default heroEditor;
export default coverEditor;

View File

@@ -2,32 +2,32 @@ import Link from "next/link";
import { cn } from "@/lib/utils";
import { Typography } from "@/components/Typography";
export type HeroButtonVariant = "primary" | "secondary" | "outline" | "ghost";
export type CoverButtonVariant = "primary" | "secondary" | "outline" | "ghost";
export type HeroButton = {
export type CoverButton = {
label: string;
href: string;
variant: HeroButtonVariant;
variant: CoverButtonVariant;
};
export type HeroProps = {
export type CoverProps = {
tagline: string;
heading: string;
subheading: string;
buttons: HeroButton[];
buttons: CoverButton[];
imageUrl: string;
align: "left" | "center";
height: "md" | "lg" | "full";
tone: "light" | "dark";
};
const heightClass: Record<HeroProps["height"], string> = {
const heightClass: Record<CoverProps["height"], string> = {
md: "min-h-[60vh]",
lg: "min-h-[80vh]",
full: "min-h-screen",
};
function buttonClass(variant: HeroButtonVariant, isDark: boolean): string {
function buttonClass(variant: CoverButtonVariant, isDark: boolean): string {
switch (variant) {
case "primary":
return cn(
@@ -48,7 +48,7 @@ function buttonClass(variant: HeroButtonVariant, isDark: boolean): string {
}
}
export function Hero({
export function Cover({
tagline,
heading,
subheading,
@@ -57,7 +57,7 @@ export function Hero({
align,
height,
tone,
}: HeroProps) {
}: CoverProps) {
const isDark = tone === "dark";
const visibleButtons = (buttons ?? []).filter((b) => b?.label);

View File

@@ -1,53 +0,0 @@
import { ComponentConfig } from "@reacteditor/core";
import { Megaphone } from "lucide-react";
import { CTA, type CTAProps } from "@/components/cta/cta";
const ctaEditor: ComponentConfig<CTAProps> = {
label: "Call to action",
icon: <Megaphone size={16} />,
category: "content",
defaultProps: {
tagline: "",
heading: "Designed once. Worn for years.",
subheading:
"Join 40,000 people building a wardrobe they actually reach for.",
primaryCta: { label: "Shop now", href: "/collections" },
secondaryCta: { label: "Read our story", href: "/about" },
imageUrl:
"https://images.unsplash.com/photo-1483985988355-763728e1935b?auto=format&fit=crop&w=2400&q=80",
align: "center",
},
fields: {
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: {
label: { label: "Label", type: "text", contentEditable: true },
href: { label: "Link", type: "text" },
},
},
secondaryCta: {
label: "Secondary CTA",
type: "object",
objectFields: {
label: { label: "Label", type: "text", contentEditable: true },
href: { label: "Link", type: "text" },
},
},
imageUrl: { label: "Background image", type: "image" },
align: {
label: "Alignment",
type: "radio",
options: [
{ label: "Left", value: "left" },
{ label: "Center", value: "center" },
],
},
},
render: (props) => <CTA {...props} />,
};
export default ctaEditor;

View File

@@ -1,81 +0,0 @@
import Link from "next/link";
import { cn } from "@/lib/utils";
import { Heading } from "@/components/Heading";
export type CTAProps = {
tagline: string;
heading: string;
subheading: string;
primaryCta: { label: string; href: string };
secondaryCta: { label: string; href: string };
imageUrl: string;
align: "left" | "center";
};
export function CTA({
tagline,
heading,
subheading,
primaryCta,
secondaryCta,
imageUrl,
align,
}: CTAProps) {
return (
<section className="relative overflow-hidden py-24 md:py-32">
<div className="absolute inset-0 -z-10">
{imageUrl ? (
<>
<img
src={imageUrl}
alt=""
className="h-full w-full object-cover"
/>
<div className="absolute inset-0 bg-black/45" />
</>
) : (
<div className="h-full w-full bg-foreground" />
)}
</div>
<div
className={cn(
"container mx-auto flex max-w-4xl flex-col px-6 text-white",
align === "center" ? "items-center text-center" : "items-start",
)}
>
<Heading
tagline={tagline}
title={heading}
subtitle={subheading}
align={align === "center" ? "center" : "left"}
size="lg"
tone="light"
subtitleClassName="max-w-xl"
/>
<div
className={cn(
"mt-10 flex flex-wrap gap-3",
align === "center" && "justify-center",
)}
>
{primaryCta?.label ? (
<Link
href={primaryCta.href || "#"}
className="inline-flex items-center justify-center rounded-md bg-white px-6 py-3 text-sm font-medium tracking-wide text-black hover:opacity-90"
>
{primaryCta.label}
</Link>
) : null}
{secondaryCta?.label ? (
<Link
href={secondaryCta.href || "#"}
className="inline-flex items-center justify-center rounded-md border border-white px-6 py-3 text-sm font-medium tracking-wide text-white hover:bg-white/10"
>
{secondaryCta.label}
</Link>
) : null}
</div>
</div>
</section>
);
}

View File

@@ -1,54 +0,0 @@
import { ComponentConfig } from "@reacteditor/core";
import { HelpCircle } from "lucide-react";
import { FAQ, type FAQProps } from "@/components/faq/faq";
const faqEditor: ComponentConfig<FAQProps> = {
label: "FAQ",
icon: <HelpCircle size={16} />,
category: "content",
defaultProps: {
tagline: "Help",
heading: "Common questions",
subheading: "",
items: [
{
question: "What's your return policy?",
answer:
"Free returns within 30 days of delivery. Items should be unworn with original tags attached.",
},
{
question: "Where do you ship?",
answer:
"We ship worldwide. Free standard shipping on orders over $150 in the US, $250 international.",
},
{
question: "How are your products made?",
answer:
"In small batches at family-run mills in Portugal, Italy, and Japan. Every piece is sampled and approved by our team.",
},
{
question: "How do I care for my pieces?",
answer:
"Cold wash, lay flat to dry, iron when damp. Care details are on every product page and on the inner label.",
},
],
},
fields: {
tagline: { label: "Tagline", type: "text", contentEditable: true },
heading: { label: "Heading", type: "text", contentEditable: true },
subheading: { label: "Subheading", type: "textarea", contentEditable: true },
items: {
label: "Items",
type: "array",
defaultItemProps: { question: "", answer: "" },
getItemSummary: (it) => it?.question || "Question",
arrayFields: {
question: { label: "Question", type: "text", contentEditable: true },
answer: { label: "Answer", type: "textarea", contentEditable: true },
},
},
},
render: (props) => <FAQ {...props} />,
};
export default faqEditor;

View File

@@ -1,56 +0,0 @@
import { useState } from "react";
import { Plus, Minus } from "lucide-react";
import { Heading } from "@/components/Heading";
export type FAQProps = {
tagline: string;
heading: string;
subheading: string;
items: Array<{ question: string; answer: string }>;
};
export function FAQ({ tagline, heading, subheading, items }: FAQProps) {
const [open, setOpen] = useState<number | null>(0);
return (
<section className="bg-background py-20 md:py-28">
<div className="container mx-auto max-w-3xl px-6">
<Heading
tagline={tagline}
title={heading}
subtitle={subheading}
align="center"
size="lg"
className="mb-12"
/>
<div className="divide-y divide-border border-y border-border">
{items.map((item, i) => {
const isOpen = open === i;
return (
<div key={i}>
<button
onClick={() => setOpen(isOpen ? null : i)}
className="flex w-full items-center justify-between py-6 text-left"
>
<span className="text-base font-medium tracking-tight md:text-lg">
{item.question}
</span>
{isOpen ? (
<Minus size={18} strokeWidth={1.5} className="flex-shrink-0" />
) : (
<Plus size={18} strokeWidth={1.5} className="flex-shrink-0" />
)}
</button>
{isOpen ? (
<p className="pb-6 pr-8 text-sm leading-relaxed text-muted-foreground md:text-base">
{item.answer}
</p>
) : null}
</div>
);
})}
</div>
</div>
</section>
);
}

View File

@@ -1,56 +0,0 @@
import { ComponentConfig } from "@reacteditor/core";
import { Sparkles } from "lucide-react";
import { Features, type FeaturesProps } from "@/components/features/features";
const featuresEditor: ComponentConfig<FeaturesProps> = {
label: "Features",
icon: <Sparkles size={16} />,
category: "content",
defaultProps: {
tagline: "Why us",
heading: "Built with intention",
subheading: "A small set of values that shape every piece we make.",
columns: "3",
items: [
{
title: "Natural fibers",
body: "Linen, organic cotton, and merino — sourced from mills with traceable supply chains.",
},
{
title: "Small batches",
body: "Made in considered quantities so nothing goes to waste — and nothing gets discounted into the bin.",
},
{
title: "Built to last",
body: "Reinforced seams, double-stitched edges, and finishes that age into something better.",
},
],
},
fields: {
tagline: { label: "Tagline", type: "text", contentEditable: true },
heading: { label: "Heading", type: "text", contentEditable: true },
subheading: { label: "Subheading", type: "textarea", contentEditable: true },
columns: {
label: "Columns",
type: "select",
options: [
{ label: "2 columns", value: "2" },
{ label: "3 columns", value: "3" },
{ label: "4 columns", value: "4" },
],
},
items: {
label: "Items",
type: "array",
defaultItemProps: { title: "Feature", body: "Description." },
getItemSummary: (it) => it?.title || "Feature",
arrayFields: {
title: { label: "Title", type: "text", contentEditable: true },
body: { label: "Body", type: "textarea", contentEditable: true },
},
},
},
render: (props) => <Features {...props} />,
};
export default featuresEditor;

View File

@@ -1,49 +0,0 @@
import { Typography } from "@/components/Typography";
import { Container } from "@/components/layout/Container";
import { Heading } from "@/components/Heading";
export type FeaturesProps = {
tagline: string;
heading: string;
subheading: string;
columns: "2" | "3" | "4";
items: Array<{ title: string; body: string }>;
};
const colClass: Record<FeaturesProps["columns"], string> = {
"2": "md:grid-cols-2",
"3": "md:grid-cols-3",
"4": "md:grid-cols-2 lg:grid-cols-4",
};
export function Features({ tagline, heading, subheading, columns, items }: FeaturesProps) {
return (
<section className="bg-background py-20 md:py-28">
<Container>
<Heading
tagline={tagline}
title={heading}
subtitle={subheading}
align="center"
size="lg"
className="mx-auto mb-16"
maxWidth="max-w-2xl"
/>
<div className={`grid grid-cols-1 gap-x-10 gap-y-12 ${colClass[columns]}`}>
{items.map((item, i) => (
<div key={i} className="flex flex-col gap-3">
<Typography variant="caption">
{String(i + 1).padStart(2, "0")}
</Typography>
<Typography variant="h5">{item.title}</Typography>
<Typography variant="body2" className="text-muted-foreground">
{item.body}
</Typography>
</div>
))}
</div>
</Container>
</section>
);
}

View File

@@ -1,9 +1,9 @@
import { ComponentConfig } from "@reacteditor/core";
import { Menu as MenuIcon } from "lucide-react";
import { Navigation, type NavigationProps } from "@/components/navigation/navigation";
import { Header, type HeaderProps } from "@/components/header/header";
const navigationEditor: ComponentConfig<NavigationProps> = {
label: "Navigation",
const headerEditor: ComponentConfig<HeaderProps> = {
label: "Header",
icon: <MenuIcon size={16} />,
category: "navigation",
global: true,
@@ -68,7 +68,7 @@ const navigationEditor: ComponentConfig<NavigationProps> = {
],
},
},
render: (props) => <Navigation {...props} />,
render: (props) => <Header {...props} />,
};
export default navigationEditor;
export default headerEditor;

View File

@@ -7,7 +7,7 @@ import { Container } from "@/components/layout/Container";
import { Typography } from "@/components/Typography";
import { cn } from "@/lib/utils";
export type NavigationProps = {
export type HeaderProps = {
brand: string;
logo?: string;
links: Array<{ label: string; href: string }>;
@@ -17,7 +17,7 @@ export type NavigationProps = {
tone: "default" | "muted" | "inverse";
};
export function Navigation({
export function Header({
brand,
logo,
links,
@@ -25,12 +25,12 @@ export function Navigation({
showCart,
sticky,
tone,
}: NavigationProps) {
}: HeaderProps) {
const [mobileOpen, setMobileOpen] = useState(false);
const cart = useShopifyCart();
const itemCount = cart?.itemCount ?? 0;
const toneClass: Record<NavigationProps["tone"], string> = {
const toneClass: Record<HeaderProps["tone"], string> = {
default: "bg-background text-foreground border-b border-border",
muted: "bg-muted/40 text-foreground border-b border-border",
inverse: "bg-foreground text-background",

View File

@@ -1,32 +0,0 @@
import { ComponentConfig } from "@reacteditor/core";
import { Megaphone } from "lucide-react";
import { Banner, type BannerProps } from "@/components/landing/banner";
const bannerEditor: ComponentConfig<BannerProps> = {
label: "Announcement bar",
icon: <Megaphone size={16} />,
category: "hero",
defaultProps: {
text: "Free shipping on orders over $150",
ctaLabel: "Shop new",
ctaHref: "/collections/new",
tone: "default",
},
fields: {
text: { label: "Text", type: "text", contentEditable: true },
ctaLabel: { label: "CTA label", type: "text", contentEditable: true },
ctaHref: { label: "CTA link", type: "text" },
tone: {
label: "Tone",
type: "select",
options: [
{ label: "Default (dark)", value: "default" },
{ label: "Inverse (light)", value: "inverse" },
{ label: "Muted", value: "muted" },
],
},
},
render: (props) => <Banner {...props} />,
};
export default bannerEditor;

View File

@@ -1,32 +0,0 @@
import Link from "next/link";
import { cn } from "@/lib/utils";
export type BannerProps = {
text: string;
ctaLabel: string;
ctaHref: string;
tone: "default" | "inverse" | "muted";
};
export function Banner({ text, ctaLabel, ctaHref, tone }: BannerProps) {
const toneClass: Record<BannerProps["tone"], string> = {
default: "bg-foreground text-background",
inverse: "bg-background text-foreground border-y border-border",
muted: "bg-muted text-foreground border-y border-border",
};
return (
<div className={cn("w-full py-2 text-center text-xs tracking-[0.18em] uppercase", toneClass[tone])}>
<div className="container mx-auto flex flex-col items-center justify-center gap-2 px-6 sm:flex-row">
<span>{text}</span>
{ctaLabel ? (
<Link
href={ctaHref || "#"}
className="underline-offset-4 hover:underline"
>
{ctaLabel}
</Link>
) : null}
</div>
</div>
);
}

View File

@@ -1,50 +0,0 @@
import { ComponentConfig } from "@reacteditor/core";
import { Images } from "lucide-react";
import { ImageGallery, type ImageGalleryProps } from "@/components/landing/image-gallery";
const imageGalleryEditor: ComponentConfig<ImageGalleryProps> = {
label: "Image gallery",
icon: <Images size={16} />,
category: "content",
defaultProps: {
tagline: "Lookbook",
heading: "Spring in the studio",
subheading: "",
layout: "editorial",
items: [
{ src: "https://images.unsplash.com/photo-1490481651871-ab68de25d43d?auto=format&fit=crop&w=1600&q=80", alt: "" },
{ src: "https://images.unsplash.com/photo-1483985988355-763728e1935b?auto=format&fit=crop&w=1200&q=80", alt: "" },
{ src: "https://images.unsplash.com/photo-1469334031218-e382a71b716b?auto=format&fit=crop&w=1200&q=80", alt: "" },
{ src: "https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&w=1200&q=80", alt: "" },
{ src: "https://images.unsplash.com/photo-1521572163474-6864f9cf17ab?auto=format&fit=crop&w=1200&q=80", alt: "" },
],
},
fields: {
tagline: { label: "Tagline", type: "text", contentEditable: true },
heading: { label: "Heading", type: "text", contentEditable: true },
subheading: { label: "Subheading", type: "textarea", contentEditable: true },
layout: {
label: "Layout",
type: "select",
options: [
{ label: "Grid", value: "grid" },
{ label: "Masonry", value: "masonry" },
{ label: "Editorial (mosaic)", value: "editorial" },
],
},
items: {
label: "Images",
type: "array",
defaultItemProps: { src: "", alt: "" },
getItemSummary: (it) => it?.caption || "Image",
arrayFields: {
src: { label: "Image", type: "image" },
alt: { label: "Alt text", type: "text", contentEditable: true },
caption: { label: "Caption", type: "text", contentEditable: true },
},
},
},
render: (props) => <ImageGallery {...props} />,
};
export default imageGalleryEditor;

View File

@@ -1,87 +0,0 @@
import { cn } from "@/lib/utils";
import { Container } from "@/components/layout/Container";
import { Heading } from "@/components/Heading";
export type ImageGalleryProps = {
tagline: string;
heading: string;
subheading: string;
layout: "grid" | "masonry" | "editorial";
items: Array<{ src: string; alt: string; caption?: string }>;
};
export function ImageGallery({ tagline, heading, subheading, layout, items }: ImageGalleryProps) {
return (
<section className="bg-background py-20 md:py-28">
<Container>
<Heading
tagline={tagline}
title={heading}
subtitle={subheading}
align="center"
size="lg"
className="mx-auto mb-12"
maxWidth="max-w-2xl"
/>
{layout === "masonry" ? (
<div className="columns-1 gap-4 sm:columns-2 lg:columns-3">
{items.map((it, i) => (
<figure key={i} className="mb-4 break-inside-avoid">
<img
src={it.src}
alt={it.alt}
className="w-full rounded-md object-cover"
/>
{it.caption ? (
<figcaption className="mt-2 text-xs text-muted-foreground">
{it.caption}
</figcaption>
) : null}
</figure>
))}
</div>
) : layout === "editorial" ? (
<div className="grid grid-cols-1 gap-6 md:grid-cols-12">
{items.slice(0, 5).map((it, i) => (
<figure
key={i}
className={cn(
"overflow-hidden rounded-md bg-muted",
i === 0 && "md:col-span-7 md:row-span-2",
i === 1 && "md:col-span-5",
i === 2 && "md:col-span-5",
i === 3 && "md:col-span-6",
i === 4 && "md:col-span-6",
)}
>
<img
src={it.src}
alt={it.alt}
className="h-full w-full object-cover"
/>
</figure>
))}
</div>
) : (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{items.map((it, i) => (
<figure key={i}>
<img
src={it.src}
alt={it.alt}
className="aspect-[4/5] w-full rounded-md object-cover"
/>
{it.caption ? (
<figcaption className="mt-2 text-xs text-muted-foreground">
{it.caption}
</figcaption>
) : null}
</figure>
))}
</div>
)}
</Container>
</section>
);
}

View File

@@ -1,44 +0,0 @@
import { ComponentConfig } from "@reacteditor/core";
import { Award } from "lucide-react";
import { Logos, type LogosProps } from "@/components/logos/logos";
const logosEditor: ComponentConfig<LogosProps> = {
label: "Press / Logos",
icon: <Award size={16} />,
category: "content",
defaultProps: {
tagline: "As seen in",
layout: "row",
items: [
{ src: "https://logo.clearbit.com/vogue.com", alt: "Vogue" },
{ src: "https://logo.clearbit.com/highsnobiety.com", alt: "Highsnobiety" },
{ src: "https://logo.clearbit.com/gq.com", alt: "GQ" },
{ src: "https://logo.clearbit.com/dezeen.com", alt: "Dezeen" },
{ src: "https://logo.clearbit.com/wallpaper.com", alt: "Wallpaper*" },
],
},
fields: {
tagline: { label: "Tagline", type: "text", contentEditable: true },
layout: {
label: "Layout",
type: "radio",
options: [
{ label: "Row", value: "row" },
{ label: "Marquee", value: "marquee" },
],
},
items: {
label: "Logos",
type: "array",
defaultItemProps: { src: "", alt: "" },
getItemSummary: (it) => it?.alt || "Logo",
arrayFields: {
src: { label: "Image", type: "image" },
alt: { label: "Alt text", type: "text", contentEditable: true },
},
},
},
render: (props) => <Logos {...props} />,
};
export default logosEditor;

View File

@@ -1,47 +0,0 @@
import { Container } from "@/components/layout/Container";
import { Typography } from "@/components/Typography";
export type LogosProps = {
tagline: string;
items: Array<{ src: string; alt: string }>;
layout: "row" | "marquee";
};
export function Logos({ tagline, items, layout }: LogosProps) {
return (
<section className="border-y border-border bg-muted/40 py-12">
<Container>
{tagline ? (
<Typography variant="caption" className="mb-8 text-center">
{tagline}
</Typography>
) : null}
{layout === "marquee" ? (
<div className="overflow-hidden">
<div className="flex animate-[marquee_30s_linear_infinite] gap-16 [--gap:4rem]">
{[...items, ...items].map((it, i) => (
<img
key={i}
src={it.src}
alt={it.alt}
className="h-7 w-auto opacity-60 grayscale"
/>
))}
</div>
</div>
) : (
<div className="flex flex-wrap items-center justify-center gap-x-12 gap-y-6">
{items.map((it, i) => (
<img
key={i}
src={it.src}
alt={it.alt}
className="h-7 w-auto opacity-60 grayscale"
/>
))}
</div>
)}
</Container>
</section>
);
}

View File

@@ -1,6 +1,6 @@
import { ComponentConfig, Fields } from "@reacteditor/core";
import { Mail } from "lucide-react";
import { NewsletterCta, type NewsletterCtaProps } from "@/components/landing/newsletter-cta";
import { NewsletterCta, type NewsletterCtaProps } from "@/components/newsletter-cta/newsletter-cta";
const baseFields: Fields<NewsletterCtaProps> = {
tagline: { label: "Tagline", type: "text", contentEditable: true },

View File

@@ -1,6 +1,6 @@
import { ComponentConfig } from "@reacteditor/core";
import { FolderOpen } from "lucide-react";
import { CollectionGrid, type CollectionGridProps } from "@/components/commerce/collection-grid";
import { CollectionGrid, type CollectionGridProps } from "@/components/shopify/collection-grid";
const collectionGridEditor: ComponentConfig<CollectionGridProps> = {
label: "Collections",

View File

@@ -1,6 +1,6 @@
import { ComponentConfig } from "@reacteditor/core";
import { FolderOpen } from "lucide-react";
import { CollectionView, type CollectionProps } from "@/components/commerce/collection";
import { CollectionView, type CollectionProps } from "@/components/shopify/collection";
const collectionEditor: ComponentConfig<CollectionProps> = {
label: "Collection page",

View File

@@ -1,6 +1,6 @@
import { ComponentConfig } from "@reacteditor/core";
import { Star } from "lucide-react";
import { FeaturedProductView, type FeaturedProductProps } from "@/components/commerce/featured-product";
import { FeaturedProductView, type FeaturedProductProps } from "@/components/shopify/featured-product";
const featuredProductEditor: ComponentConfig<FeaturedProductProps> = {
label: "Featured product",

View File

@@ -1,6 +1,6 @@
import { ComponentConfig } from "@reacteditor/core";
import { Package } from "lucide-react";
import { ProductDetailsView, type ProductDetailsProps } from "@/components/commerce/product-details";
import { ProductDetailsView, type ProductDetailsProps } from "@/components/shopify/product-details";
const productDetailsEditor: ComponentConfig<ProductDetailsProps> = {
label: "Product details",

View File

@@ -3,7 +3,7 @@ import { LayoutGrid } from "lucide-react";
import {
ProductRecommendationsView,
type ProductRecommendationsProps,
} from "@/components/commerce/product-recommendations";
} from "@/components/shopify/product-recommendations";
const productRecommendationsEditor: ComponentConfig<ProductRecommendationsProps> = {
label: "Product recommendations",

View File

@@ -1,6 +1,6 @@
import { ComponentConfig } from "@reacteditor/core";
import { GalleryHorizontalEnd } from "lucide-react";
import { ProductsCarousel, type ProductsCarouselProps } from "@/components/commerce/products-carousel";
import { ProductsCarousel, type ProductsCarouselProps } from "@/components/shopify/products-carousel";
const productsCarouselEditor: ComponentConfig<ProductsCarouselProps> = {
label: "Products carousel",

View File

@@ -113,8 +113,8 @@ export function ProductsCarousel({
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious className="left-2 md:-left-12" />
<CarouselNext className="right-2 md:-right-12" />
<CarouselPrevious className="left-2 md:left-4" />
<CarouselNext className="right-2 md:right-4" />
</Carousel>
</Container>
</section>

View File

@@ -1,6 +1,6 @@
import { ComponentConfig } from "@reacteditor/core";
import { LayoutGrid } from "lucide-react";
import { ProductsGrid, type ProductsGridProps } from "@/components/commerce/products-grid";
import { ProductsGrid, type ProductsGridProps } from "@/components/shopify/products-grid";
const productsGridEditor: ComponentConfig<ProductsGridProps> = {
label: "Products grid",

View File

@@ -1,6 +1,6 @@
import { ComponentConfig } from "@reacteditor/core";
import { Sparkles } from "lucide-react";
import { RecommendedProductsView, type RecommendedProductsProps } from "@/components/commerce/recommended-products";
import { RecommendedProductsView, type RecommendedProductsProps } from "@/components/shopify/recommended-products";
const recommendedProductsEditor: ComponentConfig<RecommendedProductsProps> = {
label: "Recommended products",

View File

@@ -1,6 +1,6 @@
import { ComponentConfig } from '@reacteditor/core';
import { Search } from 'lucide-react';
import { SearchProductsView, type SearchProductsProps } from '@/components/commerce/search-products';
import { SearchProductsView, type SearchProductsProps } from '@/components/shopify/search-products';
const searchProductsEditor: ComponentConfig<SearchProductsProps> = {
label: 'Search & filter',

View File

@@ -1,58 +0,0 @@
import { ComponentConfig } from "@reacteditor/core";
import { Quote } from "lucide-react";
import { Testimonials, type TestimonialsProps } from "@/components/testimonials/testimonials";
const testimonialsEditor: ComponentConfig<TestimonialsProps> = {
label: "Testimonials",
icon: <Quote size={16} />,
category: "content",
defaultProps: {
tagline: "Reviews",
heading: "What our customers say",
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:
"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:
"The shipping was thoughtful, the packaging was minimal, and the trousers fit on the first try. Rare combination.",
author: "Ines P.",
role: "Paris",
avatar:
"https://images.unsplash.com/photo-1580489944761-15a19d654956?auto=format&fit=crop&w=200&q=80",
},
],
},
fields: {
tagline: { label: "Tagline", type: "text", contentEditable: true },
heading: { label: "Heading", type: "text", contentEditable: true },
items: {
label: "Items",
type: "array",
defaultItemProps: { quote: "", author: "", role: "" },
getItemSummary: (it) => it?.author || "Testimonial",
arrayFields: {
quote: { label: "Quote", type: "textarea", contentEditable: true },
author: { label: "Author", type: "text", contentEditable: true },
role: { label: "Role", type: "text", contentEditable: true },
avatar: { label: "Avatar", type: "image" },
},
},
},
render: (props) => <Testimonials {...props} />,
};
export default testimonialsEditor;

View File

@@ -1,100 +0,0 @@
import { useState } from "react";
import { ArrowLeft, ArrowRight } from "lucide-react";
import { Heading } from "@/components/Heading";
export type TestimonialsProps = {
tagline: string;
heading: string;
items: Array<{
quote: string;
author: string;
role: string;
avatar?: string;
}>;
};
export function Testimonials({ tagline, heading, items }: TestimonialsProps) {
const [i, setI] = useState(0);
const total = items.length;
const item = items[i];
return (
<section className="bg-muted/40 py-20 md:py-28">
<div className="container mx-auto max-w-4xl px-6 text-center">
<Heading
tagline={tagline}
title={heading}
align="center"
size="lg"
/>
{item ? (
<div className="relative mt-12">
{total > 1 ? (
<button
onClick={() => setI((p) => (p - 1 + total) % total)}
className="absolute left-0 top-1/2 hidden -translate-y-1/2 items-center justify-center rounded-full border h-10 w-10 hover:bg-background md:inline-flex"
aria-label="Previous"
>
<ArrowLeft size={16} />
</button>
) : null}
<figure className="mx-auto flex max-w-2xl flex-col items-center px-12 md:px-16">
<blockquote
className="text-balance text-foreground"
style={{ fontSize: "clamp(1.25rem, 2.4vw, 1.75rem)", lineHeight: 1.4 }}
>
{item.quote}
</blockquote>
<figcaption className="mt-8 flex items-center gap-3">
{item.avatar ? (
<img
src={item.avatar}
alt={item.author}
className="h-10 w-10 rounded-full object-cover"
/>
) : null}
<div className="text-left">
<p className="text-sm font-medium">{item.author}</p>
{item.role ? (
<p className="text-xs text-muted-foreground">{item.role}</p>
) : null}
</div>
</figcaption>
</figure>
{total > 1 ? (
<button
onClick={() => setI((p) => (p + 1) % total)}
className="absolute right-0 top-1/2 hidden -translate-y-1/2 items-center justify-center rounded-full border h-10 w-10 hover:bg-background md:inline-flex"
aria-label="Next"
>
<ArrowRight size={16} />
</button>
) : null}
{total > 1 ? (
<div className="mt-8 flex items-center justify-center gap-3 md:hidden">
<button
onClick={() => setI((p) => (p - 1 + total) % total)}
className="inline-flex h-10 w-10 items-center justify-center rounded-full border hover:bg-background"
aria-label="Previous"
>
<ArrowLeft size={16} />
</button>
<button
onClick={() => setI((p) => (p + 1) % total)}
className="inline-flex h-10 w-10 items-center justify-center rounded-full border hover:bg-background"
aria-label="Next"
>
<ArrowRight size={16} />
</button>
</div>
) : null}
</div>
) : null}
</div>
</section>
);
}

View File

@@ -14,7 +14,7 @@ export const initialData: Record<string, UserData> = {
},
content: [
{
type: "navigation",
type: "header",
props: {
id: "nav-home",
brand: "PULSE",
@@ -34,7 +34,7 @@ export const initialData: Record<string, UserData> = {
},
},
{
type: "hero",
type: "cover",
props: {
id: "hero-home",
tagline: "Spring 2026",
@@ -85,56 +85,6 @@ export const initialData: Record<string, UserData> = {
limit: 6,
},
},
{
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.",
columns: "3",
items: [
{
title: "Natural fibers",
body: "Linen, organic cotton, and merino — sourced from mills with traceable supply chains.",
},
{
title: "Small batches",
body: "Made in considered quantities so nothing goes to waste.",
},
{
title: "Built to last",
body: "Reinforced seams and finishes that age into something better.",
},
],
},
},
{
type: "testimonials",
props: {
id: "testimonials-home",
tagline: "Reviews",
heading: "What our customers say",
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:
"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",
},
],
},
},
{
type: "newsletter-cta",
props: {

View File

@@ -1,6 +1,6 @@
import { DefaultRootProps, RootConfig } from "@reacteditor/core";
import { createFieldGoogleFonts } from "@reacteditor/field-google-fonts";
import { ThemeProvider, ThemeProps } from "@/components/ThemeProvider";
import { ThemeProvider, ThemeProps } from "@/components/theme-provider";
export type RootProps = DefaultRootProps &
ThemeProps & {

View File

@@ -1,34 +1,29 @@
import { Config, Data } from "@reacteditor/core";
import { HeroProps } from "@/components/hero/hero";
import { LogosProps } from "@/components/logos/logos";
import { FeaturesProps } from "@/components/features/features";
import { TestimonialsProps } from "@/components/testimonials/testimonials";
import { CTAProps } from "@/components/cta/cta";
import { FAQProps } from "@/components/faq/faq";
import { NavigationProps } from "@/components/navigation/navigation";
import { CoverProps } from "@/components/cover/cover";
import { HeaderProps } from "@/components/header/header";
import { FooterProps } from "@/components/footer/footer";
import { ProductsGridProps } from "@/components/commerce/products-grid";
import { ProductsCarouselProps } from "@/components/commerce/products-carousel";
import { CollectionGridProps } from "@/components/commerce/collection-grid";
import { CollectionProps } from "@/components/commerce/collection";
import { ProductDetailsProps } from "@/components/commerce/product-details";
import { RecommendedProductsProps } from "@/components/commerce/recommended-products";
import { FeaturedProductProps } from "@/components/commerce/featured-product";
import { ProductsGridProps } from "@/components/shopify/products-grid";
import { ProductsCarouselProps } from "@/components/shopify/products-carousel";
import { CollectionGridProps } from "@/components/shopify/collection-grid";
import { CollectionProps } from "@/components/shopify/collection";
import { ProductDetailsProps } from "@/components/shopify/product-details";
import { RecommendedProductsProps } from "@/components/shopify/recommended-products";
import { ProductRecommendationsProps } from "@/components/shopify/product-recommendations";
import { SearchProductsProps } from "@/components/shopify/search-products";
import { FeaturedProductProps } from "@/components/shopify/featured-product";
import { BannerProps } from "@/components/landing/banner";
import { NewsletterCtaProps } from "@/components/landing/newsletter-cta";
import { ImageGalleryProps } from "@/components/landing/image-gallery";
import { NewsletterCtaProps } from "@/components/newsletter-cta/newsletter-cta";
import { RootProps } from "./root";
export type { RootProps } from "./root";
export type Components = {
navigation: NavigationProps;
hero: HeroProps;
banner: BannerProps;
header: HeaderProps;
cover: CoverProps;
"newsletter-cta": NewsletterCtaProps;
"featured-product": FeaturedProductProps;
"products-grid": ProductsGridProps;
"products-carousel": ProductsCarouselProps;
@@ -36,20 +31,15 @@ export type Components = {
collection: CollectionProps;
"product-details": ProductDetailsProps;
"recommended-products": RecommendedProductsProps;
features: FeaturesProps;
testimonials: TestimonialsProps;
"image-gallery": ImageGalleryProps;
"newsletter-cta": NewsletterCtaProps;
logos: LogosProps;
cta: CTAProps;
faq: FAQProps;
"product-recommendations": ProductRecommendationsProps;
"search-products": SearchProductsProps;
footer: FooterProps;
};
export type UserConfig = Config<{
components: Components;
root: RootProps;
categories: ["navigation", "hero", "commerce", "content", "footer"];
categories: ["header", "cover", "commerce", "content", "footer"];
fields: {
userField: {
type: "userField";

View File

@@ -10,7 +10,7 @@ import {
updateCartLines,
} from '@/hooks/use-shopify-cart';
import { setShopifyCredentials } from '@/services/shopify/client';
import CartDrawer from '@/components/commerce/cart-drawer';
import CartDrawer from '@/components/shopify/cart-drawer';
import { Sheet, SheetContent, SheetHeader, SheetTitle } from '@/components/ui/sheet';
const CART_ID_KEY = 'cartId';

View File

@@ -1,33 +1,25 @@
import navigationEditor from "@/components/navigation/navigation.editor";
import headerEditor from "@/components/header/header.editor";
import footerEditor from "@/components/footer/footer.editor";
import heroEditor from "@/components/hero/hero.editor";
import bannerEditor from "@/components/landing/banner.editor";
import coverEditor from "@/components/cover/cover.editor";
import newsletterCtaEditor from "@/components/newsletter-cta/newsletter-cta.editor";
import featuredProductEditor from "@/components/commerce/featured-product.editor";
import productsGridEditor from "@/components/commerce/products-grid.editor";
import productsCarouselEditor from "@/components/commerce/products-carousel.editor";
import collectionGridEditor from "@/components/commerce/collection-grid.editor";
import collectionEditor from "@/components/commerce/collection.editor";
import productDetailsEditor from "@/components/commerce/product-details.editor";
import recommendedProductsEditor from "@/components/commerce/recommended-products.editor";
import productRecommendationsEditor from "@/components/commerce/product-recommendations.editor";
import searchProductsEditor from "@/components/commerce/search-products.editor";
import featuresEditor from "@/components/features/features.editor";
import testimonialsEditor from "@/components/testimonials/testimonials.editor";
import imageGalleryEditor from "@/components/landing/image-gallery.editor";
import newsletterCtaEditor from "@/components/landing/newsletter-cta.editor";
import logosEditor from "@/components/logos/logos.editor";
import ctaEditor from "@/components/cta/cta.editor";
import faqEditor from "@/components/faq/faq.editor";
import featuredProductEditor from "@/components/shopify/featured-product.editor";
import productsGridEditor from "@/components/shopify/products-grid.editor";
import productsCarouselEditor from "@/components/shopify/products-carousel.editor";
import collectionGridEditor from "@/components/shopify/collection-grid.editor";
import collectionEditor from "@/components/shopify/collection.editor";
import productDetailsEditor from "@/components/shopify/product-details.editor";
import recommendedProductsEditor from "@/components/shopify/recommended-products.editor";
import productRecommendationsEditor from "@/components/shopify/product-recommendations.editor";
import searchProductsEditor from "@/components/shopify/search-products.editor";
import Root from "@/config/root";
import type { UserConfig } from "@/config/types";
const categories = {
navigation: { title: "Navigation" },
hero: { title: "Hero & Banners" },
header: { title: "Header" },
cover: { title: "Cover" },
commerce: { title: "Commerce" },
content: { title: "Content" },
footer: { title: "Footer" },
@@ -37,10 +29,10 @@ export const appConfig: UserConfig = {
root: Root,
categories,
components: {
navigation: navigationEditor,
header: headerEditor,
footer: footerEditor,
hero: heroEditor,
banner: bannerEditor,
cover: coverEditor,
"newsletter-cta": newsletterCtaEditor,
"featured-product": featuredProductEditor,
"products-grid": productsGridEditor,
"products-carousel": productsCarouselEditor,
@@ -50,12 +42,5 @@ export const appConfig: UserConfig = {
"recommended-products": recommendedProductsEditor,
"product-recommendations": productRecommendationsEditor,
"search-products": searchProductsEditor,
features: featuresEditor,
testimonials: testimonialsEditor,
"image-gallery": imageGalleryEditor,
"newsletter-cta": newsletterCtaEditor,
logos: logosEditor,
cta: ctaEditor,
faq: faqEditor,
} as any,
};

File diff suppressed because one or more lines are too long