Add media and AI plugins, refresh editor configs

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Rami Bitar
2026-05-04 22:36:42 -04:00
parent 865e16400c
commit 343a2aa3a7
14 changed files with 197 additions and 2024 deletions

View File

@@ -6,12 +6,15 @@
"headerFont": "Playfair Display", "headerFont": "Playfair Display",
"bodyFont": "Inter", "bodyFont": "Inter",
"primaryColor": "#0a0a0a", "primaryColor": "#0a0a0a",
"secondaryColor": "#64748B",
"accentColor": "#f5f5f5", "accentColor": "#f5f5f5",
"bgColor": "#ffffff", "bgColor": "#ffffff",
"fgColor": "#0a0a0a", "fgColor": "#0a0a0a",
"mutedColor": "#f5f5f5", "mutedColor": "#f5f5f5",
"roundedness": "md", "roundedness": "md",
"radius": "md",
"shadowLevel": "sm", "shadowLevel": "sm",
"shadow": "sm",
"maxWidth": "xl" "maxWidth": "xl"
} }
}, },
@@ -193,12 +196,15 @@
"headerFont": "Playfair Display", "headerFont": "Playfair Display",
"bodyFont": "Inter", "bodyFont": "Inter",
"primaryColor": "#0a0a0a", "primaryColor": "#0a0a0a",
"secondaryColor": "#64748B",
"accentColor": "#f5f5f5", "accentColor": "#f5f5f5",
"bgColor": "#ffffff", "bgColor": "#ffffff",
"fgColor": "#0a0a0a", "fgColor": "#0a0a0a",
"mutedColor": "#f5f5f5", "mutedColor": "#f5f5f5",
"roundedness": "md", "roundedness": "md",
"radius": "md",
"shadowLevel": "sm", "shadowLevel": "sm",
"shadow": "sm",
"maxWidth": "xl" "maxWidth": "xl"
} }
}, },

View File

@@ -1,6 +1,8 @@
import { ComponentConfig } from "@reacteditor/core"; import { ComponentConfig } from "@reacteditor/core";
import { imageField } from "@reacteditor/plugin-media/field";
import { Megaphone } from "lucide-react"; import { Megaphone } from "lucide-react";
import { CTA, type CTAProps } from "@/editor/components/cta/cta"; import { CTA, type CTAProps } from "@/editor/components/cta/cta";
import { frontendAiMediaAdapter } from "@/editor/services/media-adapter";
export const ctaEditor: ComponentConfig<CTAProps> = { export const ctaEditor: ComponentConfig<CTAProps> = {
label: "Call to action", label: "Call to action",
@@ -37,7 +39,7 @@ export const ctaEditor: ComponentConfig<CTAProps> = {
href: { label: "Link", type: "text" }, href: { label: "Link", type: "text" },
}, },
}, },
imageUrl: { label: "Background image URL", type: "text" }, imageUrl: { label: "Background image", ...imageField({ adapter: frontendAiMediaAdapter }) },
align: { align: {
label: "Alignment", label: "Alignment",
type: "radio", type: "radio",

View File

@@ -1,6 +1,8 @@
import { ComponentConfig } from "@reacteditor/core"; import { ComponentConfig } from "@reacteditor/core";
import { imageField } from "@reacteditor/plugin-media/field";
import { LayoutTemplate } from "lucide-react"; import { LayoutTemplate } from "lucide-react";
import { Hero, type HeroProps } from "@/editor/components/hero/hero"; import { Hero, type HeroProps } from "@/editor/components/hero/hero";
import { frontendAiMediaAdapter } from "@/editor/services/media-adapter";
export const heroEditor: ComponentConfig<HeroProps> = { export const heroEditor: ComponentConfig<HeroProps> = {
label: "Hero", label: "Hero",
@@ -39,7 +41,7 @@ export const heroEditor: ComponentConfig<HeroProps> = {
href: { label: "Link", type: "text" }, href: { label: "Link", type: "text" },
}, },
}, },
imageUrl: { label: "Background image URL", type: "text" }, imageUrl: { label: "Background image", ...imageField({ adapter: frontendAiMediaAdapter }) },
align: { align: {
label: "Alignment", label: "Alignment",
type: "radio", type: "radio",

View File

@@ -1,6 +1,8 @@
import { ComponentConfig } from "@reacteditor/core"; import { ComponentConfig } from "@reacteditor/core";
import { imageField } from "@reacteditor/plugin-media/field";
import { Images } from "lucide-react"; import { Images } from "lucide-react";
import { ImageGallery, type ImageGalleryProps } from "@/editor/components/landing/image-gallery"; import { ImageGallery, type ImageGalleryProps } from "@/editor/components/landing/image-gallery";
import { frontendAiMediaAdapter } from "@/editor/services/media-adapter";
export const imageGalleryEditor: ComponentConfig<ImageGalleryProps> = { export const imageGalleryEditor: ComponentConfig<ImageGalleryProps> = {
label: "Image gallery", label: "Image gallery",
@@ -36,9 +38,9 @@ export const imageGalleryEditor: ComponentConfig<ImageGalleryProps> = {
label: "Images", label: "Images",
type: "array", type: "array",
defaultItemProps: { src: "", alt: "" }, defaultItemProps: { src: "", alt: "" },
getItemSummary: (it) => it?.alt || it?.src || "Image", getItemSummary: (it) => it?.caption || "Image",
arrayFields: { arrayFields: {
src: { label: "Image URL", type: "text" }, src: { label: "Image", ...imageField({ adapter: frontendAiMediaAdapter }) },
alt: { label: "Alt text", type: "text", contentEditable: true }, alt: { label: "Alt text", type: "text", contentEditable: true },
caption: { label: "Caption", type: "text", contentEditable: true }, caption: { label: "Caption", type: "text", contentEditable: true },
}, },

View File

@@ -1,6 +1,8 @@
import { ComponentConfig } from "@reacteditor/core"; import { ComponentConfig } from "@reacteditor/core";
import { imageField } from "@reacteditor/plugin-media/field";
import { Mail } from "lucide-react"; import { Mail } from "lucide-react";
import { NewsletterCta, type NewsletterCtaProps } from "@/editor/components/landing/newsletter-cta"; import { NewsletterCta, type NewsletterCtaProps } from "@/editor/components/landing/newsletter-cta";
import { frontendAiMediaAdapter } from "@/editor/services/media-adapter";
export const newsletterCtaEditor: ComponentConfig<NewsletterCtaProps> = { export const newsletterCtaEditor: ComponentConfig<NewsletterCtaProps> = {
label: "Newsletter", label: "Newsletter",
@@ -23,7 +25,7 @@ export const newsletterCtaEditor: ComponentConfig<NewsletterCtaProps> = {
subheading: { label: "Subheading", type: "textarea", contentEditable: true }, subheading: { label: "Subheading", type: "textarea", contentEditable: true },
buttonLabel: { label: "Button label", type: "text", contentEditable: true }, buttonLabel: { label: "Button label", type: "text", contentEditable: true },
endpoint: { label: "Submit endpoint", type: "text" }, endpoint: { label: "Submit endpoint", type: "text" },
imageUrl: { label: "Image URL", type: "text" }, imageUrl: { label: "Image", ...imageField({ adapter: frontendAiMediaAdapter }) },
layout: { layout: {
label: "Layout", label: "Layout",
type: "radio", type: "radio",

View File

@@ -1,6 +1,8 @@
import { ComponentConfig } from "@reacteditor/core"; import { ComponentConfig } from "@reacteditor/core";
import { imageField } from "@reacteditor/plugin-media/field";
import { Award } from "lucide-react"; import { Award } from "lucide-react";
import { Logos, type LogosProps } from "@/editor/components/logos/logos"; import { Logos, type LogosProps } from "@/editor/components/logos/logos";
import { frontendAiMediaAdapter } from "@/editor/services/media-adapter";
export const logosEditor: ComponentConfig<LogosProps> = { export const logosEditor: ComponentConfig<LogosProps> = {
label: "Press / Logos", label: "Press / Logos",
@@ -33,7 +35,7 @@ export const logosEditor: ComponentConfig<LogosProps> = {
defaultItemProps: { src: "", alt: "" }, defaultItemProps: { src: "", alt: "" },
getItemSummary: (it) => it?.alt || "Logo", getItemSummary: (it) => it?.alt || "Logo",
arrayFields: { arrayFields: {
src: { label: "Image URL", type: "text" }, src: { label: "Image", ...imageField({ adapter: frontendAiMediaAdapter }) },
alt: { label: "Alt text", type: "text", contentEditable: true }, alt: { label: "Alt text", type: "text", contentEditable: true },
}, },
}, },

View File

@@ -1,6 +1,8 @@
import { ComponentConfig } from "@reacteditor/core"; import { ComponentConfig } from "@reacteditor/core";
import { imageField } from "@reacteditor/plugin-media/field";
import { Quote } from "lucide-react"; import { Quote } from "lucide-react";
import { Testimonials, type TestimonialsProps } from "@/editor/components/testimonials/testimonials"; import { Testimonials, type TestimonialsProps } from "@/editor/components/testimonials/testimonials";
import { frontendAiMediaAdapter } from "@/editor/services/media-adapter";
export const testimonialsEditor: ComponentConfig<TestimonialsProps> = { export const testimonialsEditor: ComponentConfig<TestimonialsProps> = {
label: "Testimonials", label: "Testimonials",
@@ -48,7 +50,7 @@ export const testimonialsEditor: ComponentConfig<TestimonialsProps> = {
quote: { label: "Quote", type: "textarea", contentEditable: true }, quote: { label: "Quote", type: "textarea", contentEditable: true },
author: { label: "Author", type: "text", contentEditable: true }, author: { label: "Author", type: "text", contentEditable: true },
role: { label: "Role", type: "text", contentEditable: true }, role: { label: "Role", type: "text", contentEditable: true },
avatar: { label: "Avatar URL", type: "text" }, avatar: { label: "Avatar", ...imageField({ adapter: frontendAiMediaAdapter }) },
}, },
}, },
}, },

View File

@@ -1,6 +1,8 @@
import { DefaultRootProps, RootConfig } from "@reacteditor/core"; import { DefaultRootProps, RootConfig } from "@reacteditor/core";
import { createFieldGoogleFonts } from "@reacteditor/field-google-fonts"; import { createFieldGoogleFonts } from "@reacteditor/field-google-fonts";
import { imageField } from "@reacteditor/plugin-media/field";
import { ThemeProvider, ThemeProps } from "@/editor/theme/ThemeProvider"; import { ThemeProvider, ThemeProps } from "@/editor/theme/ThemeProvider";
import { frontendAiMediaAdapter } from "@/editor/services/media-adapter";
export type RootProps = DefaultRootProps & export type RootProps = DefaultRootProps &
ThemeProps & { ThemeProps & {
@@ -24,21 +26,25 @@ export const Root: RootConfig<{
// Hex defaults so the color picker reads them and any non-picker // Hex defaults so the color picker reads them and any non-picker
// input (typed hex, AI-set value, etc.) is round-trip compatible. // input (typed hex, AI-set value, etc.) is round-trip compatible.
primaryColor: "#0a0a0a", primaryColor: "#0a0a0a",
secondaryColor: "#64748B",
accentColor: "#f5f5f5", accentColor: "#f5f5f5",
bgColor: "#ffffff", bgColor: "#ffffff",
fgColor: "#0a0a0a", fgColor: "#0a0a0a",
mutedColor: "#f5f5f5", mutedColor: "#f5f5f5",
roundedness: "md", roundedness: "md",
radius: "md",
shadowLevel: "sm", shadowLevel: "sm",
shadow: "sm",
maxWidth: "xl", maxWidth: "xl",
}, },
fields: { fields: {
title: { label: "Page title", type: "text" }, title: { label: "Page title", type: "text" },
description: { label: "Description", type: "textarea" }, description: { label: "Description", type: "textarea" },
ogImage: { label: "OG image URL", type: "text" }, ogImage: { label: "OG image", ...imageField({ adapter: frontendAiMediaAdapter }) },
headerFont: { label: "Header font", ...headerFontField }, headerFont: { label: "Header font", ...headerFontField },
bodyFont: { label: "Body font", ...bodyFontField }, bodyFont: { label: "Body font", ...bodyFontField },
primaryColor: { label: "Primary color", type: "color", placeholder: "#0a0a0a" }, primaryColor: { label: "Primary color", type: "color", placeholder: "#0a0a0a" },
secondaryColor: { label: "Secondary color", type: "color", placeholder: "#64748B" },
accentColor: { label: "Accent color", type: "color", placeholder: "#f5f5f5" }, accentColor: { label: "Accent color", type: "color", placeholder: "#f5f5f5" },
bgColor: { label: "Background color", type: "color", placeholder: "#ffffff" }, bgColor: { label: "Background color", type: "color", placeholder: "#ffffff" },
fgColor: { label: "Foreground color", type: "color", placeholder: "#0a0a0a" }, fgColor: { label: "Foreground color", type: "color", placeholder: "#0a0a0a" },
@@ -55,6 +61,17 @@ export const Root: RootConfig<{
{ label: "Full (pill)", value: "full" }, { label: "Full (pill)", value: "full" },
], ],
}, },
radius: {
label: "Radius",
type: "select",
options: [
{ label: "None", value: "none" },
{ label: "Extra small", value: "xs" },
{ label: "Small", value: "sm" },
{ label: "Medium", value: "md" },
{ label: "Large", value: "lg" },
],
},
shadowLevel: { shadowLevel: {
label: "Shadow level", label: "Shadow level",
type: "select", type: "select",
@@ -66,6 +83,16 @@ export const Root: RootConfig<{
{ label: "Extra large", value: "xl" }, { label: "Extra large", value: "xl" },
], ],
}, },
shadow: {
label: "Shadow",
type: "select",
options: [
{ label: "None", value: "none" },
{ label: "Small", value: "sm" },
{ label: "Medium", value: "md" },
{ label: "Large", value: "lg" },
],
},
maxWidth: { maxWidth: {
label: "Max width", label: "Max width",
type: "select", type: "select",
@@ -84,24 +111,30 @@ export const Root: RootConfig<{
headerFont, headerFont,
bodyFont, bodyFont,
primaryColor, primaryColor,
secondaryColor,
accentColor, accentColor,
bgColor, bgColor,
fgColor, fgColor,
mutedColor, mutedColor,
roundedness, roundedness,
radius,
shadowLevel, shadowLevel,
shadow,
}) => { }) => {
return ( return (
<ThemeProvider <ThemeProvider
headerFont={headerFont} headerFont={headerFont}
bodyFont={bodyFont} bodyFont={bodyFont}
primaryColor={primaryColor} primaryColor={primaryColor}
secondaryColor={secondaryColor}
accentColor={accentColor} accentColor={accentColor}
bgColor={bgColor} bgColor={bgColor}
fgColor={fgColor} fgColor={fgColor}
mutedColor={mutedColor} mutedColor={mutedColor}
roundedness={roundedness} roundedness={roundedness}
radius={radius}
shadowLevel={shadowLevel} shadowLevel={shadowLevel}
shadow={shadow}
> >
{children} {children}
</ThemeProvider> </ThemeProvider>

View File

@@ -0,0 +1,65 @@
import type {
MediaAdapter,
MediaItem,
MediaPage,
} from "@reacteditor/plugin-media";
const MEDIA_BASE = "https://www.frontend-ai.com";
const MEDIA_API_KEY = (import.meta.env.VITE_API_KEY as string | undefined) ?? "";
export const frontendAiMediaAdapter: MediaAdapter = {
fetchList: async ({ query, cursor, signal }) => {
const url = new URL("/api/media", MEDIA_BASE);
if (query) url.searchParams.set("query", query);
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, {
method: "GET",
headers: { "X-Api-Key": MEDIA_API_KEY },
signal,
});
if (!res.ok) throw new Error(`List failed: ${res.status}`);
return (await res.json()) as MediaPage;
},
upload: (file, opts) =>
new Promise<MediaItem>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("POST", `${MEDIA_BASE}/api/media`);
xhr.setRequestHeader("X-Api-Key", MEDIA_API_KEY);
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) opts?.onProgress?.(e.loaded / e.total);
};
xhr.onload = () => {
if (xhr.status >= 400) {
reject(new Error(xhr.responseText || `Upload failed: ${xhr.status}`));
return;
}
try {
resolve(JSON.parse(xhr.responseText) as MediaItem);
} catch (err) {
reject(err instanceof Error ? err : new Error(String(err)));
}
};
xhr.onerror = () => reject(new Error("Network error"));
xhr.onabort = () => {
const err = new Error("Aborted");
err.name = "AbortError";
reject(err);
};
opts?.signal?.addEventListener("abort", () => xhr.abort());
const fd = new FormData();
fd.append("file", file);
xhr.send(fd);
}),
delete: async (id) => {
const res = await fetch(
`${MEDIA_BASE}/api/media/${encodeURIComponent(id)}`,
{
method: "DELETE",
headers: { "X-Api-Key": MEDIA_API_KEY },
},
);
if (!res.ok) throw new Error(`Delete failed: ${res.status}`);
},
};

View File

@@ -6,6 +6,7 @@ export type ThemeProps = {
bodyFont?: string; bodyFont?: string;
primaryColor?: string; primaryColor?: string;
primaryForegroundColor?: string; primaryForegroundColor?: string;
secondaryColor?: string;
accentColor?: string; accentColor?: string;
bgColor?: string; bgColor?: string;
fgColor?: string; fgColor?: string;
@@ -13,7 +14,9 @@ export type ThemeProps = {
mutedForegroundColor?: string; mutedForegroundColor?: string;
borderColor?: string; borderColor?: string;
roundedness?: "none" | "sm" | "md" | "lg" | "xl" | "full"; roundedness?: "none" | "sm" | "md" | "lg" | "xl" | "full";
radius?: "none" | "xs" | "sm" | "md" | "lg";
shadowLevel?: "none" | "sm" | "md" | "lg" | "xl"; shadowLevel?: "none" | "sm" | "md" | "lg" | "xl";
shadow?: "none" | "sm" | "md" | "lg";
maxWidth?: "sm" | "md" | "lg" | "xl" | "2xl" | "full"; maxWidth?: "sm" | "md" | "lg" | "xl" | "2xl" | "full";
}; };
@@ -26,6 +29,14 @@ const radiusMap: Record<NonNullable<ThemeProps["roundedness"]>, string> = {
full: "9999px", 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> = { const shadowMap: Record<NonNullable<ThemeProps["shadowLevel"]>, string> = {
none: "0 0 #0000", none: "0 0 #0000",
sm: "0 1px 2px 0 rgb(0 0 0 / 0.05)", sm: "0 1px 2px 0 rgb(0 0 0 / 0.05)",
@@ -34,6 +45,13 @@ const shadowMap: Record<NonNullable<ThemeProps["shadowLevel"]>, string> = {
xl: "0 20px 25px -5px rgb(0 0 0 / 0.10), 0 8px 10px -6px rgb(0 0 0 / 0.10)", xl: "0 20px 25px -5px rgb(0 0 0 / 0.10), 0 8px 10px -6px rgb(0 0 0 / 0.10)",
}; };
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 { function googleFontsHref(headerFont?: string, bodyFont?: string): string | null {
const fonts = [headerFont, bodyFont].filter( const fonts = [headerFont, bodyFont].filter(
(f): f is string => !!f && f !== "system-ui" (f): f is string => !!f && f !== "system-ui"
@@ -50,6 +68,7 @@ export function ThemeProvider({
bodyFont, bodyFont,
primaryColor, primaryColor,
primaryForegroundColor, primaryForegroundColor,
secondaryColor,
accentColor, accentColor,
bgColor, bgColor,
fgColor, fgColor,
@@ -57,7 +76,9 @@ export function ThemeProvider({
mutedForegroundColor, mutedForegroundColor,
borderColor, borderColor,
roundedness, roundedness,
radius,
shadowLevel, shadowLevel,
shadow,
children, children,
}: ThemeProps & { children?: React.ReactNode }) { }: ThemeProps & { children?: React.ReactNode }) {
// Recompute CSS-variable map only when a relevant prop changes. // Recompute CSS-variable map only when a relevant prop changes.
@@ -65,14 +86,17 @@ export function ThemeProvider({
const vars: Record<string, string> = {}; const vars: Record<string, string> = {};
if (primaryColor) vars["--primary"] = primaryColor; if (primaryColor) vars["--primary"] = primaryColor;
if (primaryForegroundColor) vars["--primary-foreground"] = primaryForegroundColor; if (primaryForegroundColor) vars["--primary-foreground"] = primaryForegroundColor;
if (secondaryColor) vars["--secondary"] = secondaryColor;
if (accentColor) vars["--accent"] = accentColor; if (accentColor) vars["--accent"] = accentColor;
if (bgColor) vars["--background"] = bgColor; if (bgColor) vars["--background"] = bgColor;
if (fgColor) vars["--foreground"] = fgColor; if (fgColor) vars["--foreground"] = fgColor;
if (mutedColor) vars["--muted"] = mutedColor; if (mutedColor) vars["--muted"] = mutedColor;
if (mutedForegroundColor) vars["--muted-foreground"] = mutedForegroundColor; if (mutedForegroundColor) vars["--muted-foreground"] = mutedForegroundColor;
if (borderColor) vars["--border"] = borderColor; if (borderColor) vars["--border"] = borderColor;
if (roundedness) vars["--radius"] = radiusMap[roundedness]; if (radius) vars["--radius"] = radiusEnumMap[radius];
if (shadowLevel) vars["--shadow"] = shadowMap[shadowLevel]; 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 (headerFont) vars["--font-header"] = `"${headerFont}", system-ui, sans-serif`;
if (bodyFont) vars["--font-body"] = `"${bodyFont}", system-ui, sans-serif`; if (bodyFont) vars["--font-body"] = `"${bodyFont}", system-ui, sans-serif`;
return vars; return vars;
@@ -81,6 +105,7 @@ export function ThemeProvider({
bodyFont, bodyFont,
primaryColor, primaryColor,
primaryForegroundColor, primaryForegroundColor,
secondaryColor,
accentColor, accentColor,
bgColor, bgColor,
fgColor, fgColor,
@@ -88,7 +113,9 @@ export function ThemeProvider({
mutedForegroundColor, mutedForegroundColor,
borderColor, borderColor,
roundedness, roundedness,
radius,
shadowLevel, shadowLevel,
shadow,
]); ]);
// Imperatively push every CSS var onto :root inside the host document // Imperatively push every CSS var onto :root inside the host document

View File

@@ -32,6 +32,7 @@
"@reacteditor/field-google-fonts": "^0.0.1", "@reacteditor/field-google-fonts": "^0.0.1",
"@reacteditor/field-shopify": "^0.0.1", "@reacteditor/field-shopify": "^0.0.1",
"@reacteditor/plugin-ai": "^0.0.3", "@reacteditor/plugin-ai": "^0.0.3",
"@reacteditor/plugin-media": "^0.0.1",
"@reacteditor/plugin-tailwind-cdn": "^0.0.2", "@reacteditor/plugin-tailwind-cdn": "^0.0.2",
"@shopify/storefront-api-client": "^1.0.0", "@shopify/storefront-api-client": "^1.0.0",
"@tailwindcss/postcss": "^4.1.11", "@tailwindcss/postcss": "^4.1.11",

View File

@@ -5,11 +5,18 @@ import {
outlinePlugin, outlinePlugin,
} from "@reacteditor/core"; } from "@reacteditor/core";
import createTailwindCdnPlugin from "@reacteditor/plugin-tailwind-cdn"; import createTailwindCdnPlugin from "@reacteditor/plugin-tailwind-cdn";
import { mediaPlugin } from "@reacteditor/plugin-media";
import "@reacteditor/plugin-media/styles.css";
import { aiPlugin } from "@reacteditor/plugin-ai";
import "@reacteditor/plugin-ai/styles.css";
import { createConfig } from "@/editor/config"; import { createConfig } from "@/editor/config";
import { ShopifyProvider } from "@/editor/contexts/shopify-context"; import { ShopifyProvider } from "@/editor/contexts/shopify-context";
import { frontendAiMediaAdapter } from "@/editor/services/media-adapter";
import { Loader } from "@/components/ui/loader"; import { Loader } from "@/components/ui/loader";
import schemaJson from "../app.schema.json"; import schemaJson from "../app.schema.json";
const AI_API_KEY = (import.meta.env.VITE_API_KEY as string | undefined) ?? "";
type Pages = Record<string, { root: any; content: any[] }>; type Pages = Record<string, { root: any; content: any[] }>;
const SHOPIFY_DOMAIN = const SHOPIFY_DOMAIN =
@@ -46,7 +53,20 @@ export default function App() {
); );
const plugins = useMemo( const plugins = useMemo(
() => [createTailwindCdnPlugin(), blocksPlugin(), outlinePlugin()], () => [
aiPlugin({
api: "https://www.frontend-ai.com/cloud/api/chat",
headers: { "X-Api-Key": AI_API_KEY },
getCurrentRoute: () => ({ path: window.location.pathname }),
}),
createTailwindCdnPlugin(),
blocksPlugin(),
outlinePlugin(),
mediaPlugin({
adapter: frontendAiMediaAdapter,
showSearch: true,
}),
],
[], [],
); );

View File

@@ -1,6 +1,5 @@
@import "tailwindcss"; @import "tailwindcss";
@import "tw-animate-css"; @import "tw-animate-css";
@import "shadcn/tailwind.css";
@import "@fontsource-variable/geist"; @import "@fontsource-variable/geist";
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));

2034
yarn.lock

File diff suppressed because it is too large Load Diff