Add media and AI plugins, refresh editor configs
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 },
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 }) },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
65
editor/services/media-adapter.ts
Normal file
65
editor/services/media-adapter.ts
Normal 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}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
22
src/App.tsx
22
src/App.tsx
@@ -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,
|
||||||
|
}),
|
||||||
|
],
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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 *));
|
||||||
|
|||||||
Reference in New Issue
Block a user