Initial commit

This commit is contained in:
Rami Bitar
2026-05-03 20:12:12 -04:00
commit 3a3ca1c72a
169 changed files with 22320 additions and 0 deletions

View File

@@ -0,0 +1,163 @@
import * as React from "react";
import { useEffect, useMemo, useRef } from "react";
export type ThemeProps = {
headerFont?: string;
bodyFont?: string;
primaryColor?: string;
primaryForegroundColor?: string;
accentColor?: string;
bgColor?: string;
fgColor?: string;
mutedColor?: string;
mutedForegroundColor?: string;
borderColor?: string;
roundedness?: "none" | "sm" | "md" | "lg" | "xl" | "full";
shadowLevel?: "none" | "sm" | "md" | "lg" | "xl";
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 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)",
};
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,
accentColor,
bgColor,
fgColor,
mutedColor,
mutedForegroundColor,
borderColor,
roundedness,
shadowLevel,
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 (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 (roundedness) vars["--radius"] = radiusMap[roundedness];
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,
accentColor,
bgColor,
fgColor,
mutedColor,
mutedForegroundColor,
borderColor,
roundedness,
shadowLevel,
]);
// 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>
</>
);
}

View File

@@ -0,0 +1,94 @@
import * as React from "react";
import { cn } from "@/editor/lib/utils";
export type TypographyVariant =
| "h1"
| "h2"
| "h3"
| "h4"
| "h5"
| "h6"
| "subtitle1"
| "subtitle2"
| "body1"
| "body2"
| "caption";
const sizeClasses: Record<TypographyVariant, string> = {
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",
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",
};
// Inline-style fallbacks so headings size correctly even when Tailwind
// preflight resets <h1>..<h6> to font-size: inherit and the iframe CDN
// hasn't compiled utility classes yet. The Tailwind classes above still
// apply on top once available (responsive breakpoints, leading, etc.).
const sizeStyles: Record<TypographyVariant, React.CSSProperties> = {
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 },
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 },
};
const defaultTag: Record<TypographyVariant, keyof JSX.IntrinsicElements> = {
h1: "h1",
h2: "h2",
h3: "h3",
h4: "h4",
h5: "h5",
h6: "h6",
subtitle1: "p",
subtitle2: "p",
body1: "p",
body2: "p",
caption: "p",
};
type Props<C extends keyof JSX.IntrinsicElements = "p"> = {
variant: TypographyVariant;
as?: C;
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
} & Omit<React.ComponentPropsWithoutRef<C>, "className" | "children" | "style">;
export function Typography<C extends keyof JSX.IntrinsicElements = "p">({
variant,
as,
className,
style,
children,
...rest
}: Props<C>) {
const Tag = (as ?? defaultTag[variant]) as keyof JSX.IntrinsicElements;
const isHeading = variant.startsWith("h");
const fontClass = isHeading ? "font-heading" : "font-body";
return React.createElement(
Tag,
{
className: cn(fontClass, sizeClasses[variant], className),
style: { ...sizeStyles[variant], ...style },
...rest,
},
children,
);
}
export default Typography;