refactor shopify storefront

This commit is contained in:
Rami Bitar
2026-05-05 13:42:40 -04:00
parent ba8826030c
commit 62fbdead87
156 changed files with 1688 additions and 8293 deletions

View File

@@ -0,0 +1,190 @@
import * as React from "react";
import { useEffect, useMemo, useRef } from "react";
export type ThemeProps = {
headerFont?: string;
bodyFont?: string;
primaryColor?: string;
primaryForegroundColor?: string;
secondaryColor?: string;
accentColor?: string;
bgColor?: string;
fgColor?: string;
mutedColor?: string;
mutedForegroundColor?: string;
borderColor?: string;
roundedness?: "none" | "sm" | "md" | "lg" | "xl" | "full";
radius?: "none" | "xs" | "sm" | "md" | "lg";
shadowLevel?: "none" | "sm" | "md" | "lg" | "xl";
shadow?: "none" | "sm" | "md" | "lg";
maxWidth?: "sm" | "md" | "lg" | "xl" | "2xl" | "full";
};
const radiusMap: Record<NonNullable<ThemeProps["roundedness"]>, string> = {
none: "0px",
sm: "0.25rem",
md: "0.5rem",
lg: "0.75rem",
xl: "1rem",
full: "9999px",
};
const radiusEnumMap: Record<NonNullable<ThemeProps["radius"]>, string> = {
none: "0px",
xs: "0.125rem",
sm: "0.25rem",
md: "0.5rem",
lg: "0.75rem",
};
const shadowMap: Record<NonNullable<ThemeProps["shadowLevel"]>, string> = {
none: "0 0 #0000",
sm: "0 1px 2px 0 rgb(0 0 0 / 0.05)",
md: "0 4px 6px -1px rgb(0 0 0 / 0.10), 0 2px 4px -2px rgb(0 0 0 / 0.10)",
lg: "0 10px 15px -3px rgb(0 0 0 / 0.10), 0 4px 6px -4px rgb(0 0 0 / 0.10)",
xl: "0 20px 25px -5px rgb(0 0 0 / 0.10), 0 8px 10px -6px rgb(0 0 0 / 0.10)",
};
const shadowEnumMap: Record<NonNullable<ThemeProps["shadow"]>, string> = {
none: "0 0 #0000",
sm: "0 1px 2px 0 rgb(0 0 0 / 0.05)",
md: "0 4px 6px -1px rgb(0 0 0 / 0.10), 0 2px 4px -2px rgb(0 0 0 / 0.10)",
lg: "0 10px 15px -3px rgb(0 0 0 / 0.10), 0 4px 6px -4px 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`;
}
export function ThemeProvider({
headerFont,
bodyFont,
primaryColor,
primaryForegroundColor,
secondaryColor,
accentColor,
bgColor,
fgColor,
mutedColor,
mutedForegroundColor,
borderColor,
roundedness,
radius,
shadowLevel,
shadow,
children,
}: ThemeProps & { children?: React.ReactNode }) {
// Recompute CSS-variable map only when a relevant prop changes.
const cssVars = useMemo<Record<string, string>>(() => {
const vars: Record<string, string> = {};
if (primaryColor) vars["--primary"] = primaryColor;
if (primaryForegroundColor) vars["--primary-foreground"] = primaryForegroundColor;
if (secondaryColor) vars["--secondary"] = secondaryColor;
if (accentColor) vars["--accent"] = accentColor;
if (bgColor) vars["--background"] = bgColor;
if (fgColor) vars["--foreground"] = fgColor;
if (mutedColor) vars["--muted"] = mutedColor;
if (mutedForegroundColor) vars["--muted-foreground"] = mutedForegroundColor;
if (borderColor) vars["--border"] = borderColor;
if (radius) vars["--radius"] = radiusEnumMap[radius];
else if (roundedness) vars["--radius"] = radiusMap[roundedness];
if (shadow) vars["--shadow"] = shadowEnumMap[shadow];
else if (shadowLevel) vars["--shadow"] = shadowMap[shadowLevel];
if (headerFont) vars["--font-header"] = `"${headerFont}", system-ui, sans-serif`;
if (bodyFont) vars["--font-body"] = `"${bodyFont}", system-ui, sans-serif`;
return vars;
}, [
headerFont,
bodyFont,
primaryColor,
primaryForegroundColor,
secondaryColor,
accentColor,
bgColor,
fgColor,
mutedColor,
mutedForegroundColor,
borderColor,
roundedness,
radius,
shadowLevel,
shadow,
]);
// Imperatively push every CSS var onto :root inside the host document
// (which is the iframe's document for the editor preview, and the page
// <html> for the published render). This guarantees descendants pick up
// updates even if React's style-prop diffing missed something or the
// base-layer rules need access via :root inheritance.
const rootRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const doc =
rootRef.current?.ownerDocument ??
(typeof document !== "undefined" ? document : null);
if (!doc) return;
const target = doc.documentElement;
const previous: Record<string, string> = {};
for (const [key, value] of Object.entries(cssVars)) {
previous[key] = target.style.getPropertyValue(key);
target.style.setProperty(key, value);
}
return () => {
// Restore prior values so unmount doesn't leak our overrides.
for (const [key, value] of Object.entries(previous)) {
if (value) target.style.setProperty(key, value);
else target.style.removeProperty(key);
}
};
}, [cssVars]);
const fontsHref = useMemo(
() => googleFontsHref(headerFont, bodyFont),
[headerFont, bodyFont],
);
// Plain CSS rules — applied directly, no Tailwind CDN runtime needed.
// Tailwind preflight resets h1..h6 to font-family: inherit, which would
// make headings pick up the body font. We override that here using the
// `--font-header` CSS var ThemeProvider sets per-page.
const css = `
body, p, span, a, li, button, input, textarea, select {
font-family: var(--font-body), system-ui, -apple-system, sans-serif;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-header), system-ui, -apple-system, sans-serif;
}
`;
// Tailwind theme directives — only useful if/when the CDN compiles
// font-heading / font-body utilities. The plain CSS above handles the
// common case so headers always render with the right font.
const tailwindCss = `
@theme {
--font-family-heading: var(--font-header), system-ui, -apple-system, sans-serif;
--font-family-body: var(--font-body), system-ui, -apple-system, sans-serif;
}
`;
return (
<>
{fontsHref ? <link rel="stylesheet" href={fontsHref} /> : null}
<style dangerouslySetInnerHTML={{ __html: css }} />
<style type="text/tailwindcss" dangerouslySetInnerHTML={{ __html: tailwindCss }} />
<div
ref={rootRef}
data-theme-root
style={cssVars as React.CSSProperties}
className="flex min-h-screen flex-col bg-background text-foreground"
>
{children}
</div>
</>
);
}