props and context, fixed css bugs
This commit is contained in:
parent
da63ff64e8
commit
1ccfd44aa0
24
index.html
24
index.html
|
|
@ -9,6 +9,26 @@
|
|||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
colors: {
|
||||
blue: "#1fb6ff",
|
||||
purple: "#7e5bef",
|
||||
pink: "#ff49db",
|
||||
orange: "#ff7849",
|
||||
green: "#13ce66",
|
||||
yellow: "#ffc82c",
|
||||
"gray-dark": "#273444",
|
||||
gray: "#8492a6",
|
||||
"gray-light": "#d3dce6",
|
||||
transparent: "transparent",
|
||||
current: "currentColor",
|
||||
white: "#ffffff",
|
||||
midnight: "#121063",
|
||||
metal: "#565584",
|
||||
tahiti: "#3ab7bf",
|
||||
silver: "#ecebff",
|
||||
"bubble-gum": "#ff77e9",
|
||||
bermuda: "#78dcca",
|
||||
},
|
||||
extend: {
|
||||
spacing: {
|
||||
"8xl": "96rem",
|
||||
|
|
@ -17,9 +37,6 @@
|
|||
borderRadius: {
|
||||
"4xl": "2rem",
|
||||
},
|
||||
fontFamily: {
|
||||
display: "Oswald, ui-serif ", // Adds a new `font-display` class
|
||||
},
|
||||
colors: {
|
||||
primary: {
|
||||
1: "var(--primary-1)",
|
||||
|
|
@ -104,4 +121,5 @@
|
|||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/default.css">
|
||||
</html>
|
||||
|
|
@ -12,7 +12,8 @@
|
|||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-live": "file:../react-live"
|
||||
"sucrase": "^3.31.0",
|
||||
"use-editable": "^2.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.31",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,105 @@
|
|||
/* 下面是统一的颜色定义 */
|
||||
/* 默认的主题样式 */
|
||||
:root {
|
||||
background-color: var(--color-bg-1);
|
||||
color: var(--color-text-1);
|
||||
font: 14px/1.5 "Helvetica Neue",Helvetica,Arial,"Microsoft Yahei","Hiragino Sans GB","Heiti SC","WenQuanYi Micro Hei",sans-serif;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: var(--primary-6);
|
||||
color: var(--color-bg-white);
|
||||
border: var(--border-1) solid var(--color-border-3);
|
||||
border-radius: var(--border-radius-small);
|
||||
padding: var(--size-3) var(--size-4);
|
||||
font-size: var(--font-size-body-3);
|
||||
box-shadow: var(--shadow1-center);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--primary-5);
|
||||
border-color: var(--color-border-4);
|
||||
}
|
||||
|
||||
button:active {
|
||||
background-color: var(--primary-7);
|
||||
border-color: var(--color-border-4);
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
background-color: var(--color-fill-1);
|
||||
color: var(--color-text-1);
|
||||
border: var(--border-1) solid var(--color-border-2);
|
||||
border-radius: var(--border-radius-small);
|
||||
padding: var(--size-3);
|
||||
font-size: var(--font-size-body-3);
|
||||
box-shadow: var(--shadow1-center);
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--color-bg-2);
|
||||
color: var(--color-text-1);
|
||||
border: var(--border-1) solid var(--color-border-1);
|
||||
border-radius: var(--border-radius-large);
|
||||
padding: var(--size-5);
|
||||
box-shadow: var(--shadow2-center);
|
||||
font-size: var(--font-size-body-3);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--link-6);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--link-5);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--color-text-1);
|
||||
font-size: var(--font-size-body-3);
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: var(--border-1) solid var(--color-border-2);
|
||||
padding: var(--size-3);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
thead {
|
||||
background-color: var(--color-bg-2);
|
||||
}
|
||||
|
||||
.alert {
|
||||
background-color: var(--danger-1);
|
||||
color: var(--danger-6);
|
||||
border: var(--border-1) solid var(--danger-4);
|
||||
border-radius: var(--border-radius-small);
|
||||
padding: var(--size-3);
|
||||
}
|
||||
|
||||
.success {
|
||||
background-color: var(--success-1);
|
||||
color: var(--success-6);
|
||||
border: var(--border-1) solid var(--success-4);
|
||||
border-radius: var(--border-radius-small);
|
||||
padding: var(--size-3);
|
||||
}
|
||||
|
||||
.warning {
|
||||
background-color: var(--warning-1);
|
||||
color: var(--warning-6);
|
||||
border: var(--border-1) solid var(--warning-4);
|
||||
border-radius: var(--border-radius-small);
|
||||
padding: var(--size-3);
|
||||
}
|
||||
273
src/app.tsx
273
src/app.tsx
|
|
@ -1,226 +1,11 @@
|
|||
import { LiveProvider, LivePreview, LiveError, LiveEditor } from "react-live";
|
||||
import { ThemeProvider } from "./components/ThemePrivider";
|
||||
import LiveEditor from "./components/Live/LiveEditor";
|
||||
import LiveError from "./components/Live/LiveError";
|
||||
import LivePreview from "./components/Live/LivePreview";
|
||||
import LiveProvider from "./components/Live/LiveProvider";
|
||||
import { ThemeProvider } from "./components/Theme/ThemePrivider";
|
||||
import { useState } from "react";
|
||||
|
||||
const functionBlockList = [
|
||||
`
|
||||
const cpn = () => {
|
||||
function handleClick() {
|
||||
console.log("Hi there!");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-primary-1 py-16 px-8">
|
||||
<div className="max-w-7xl mx-auto text-center">
|
||||
<h1 className="text-primary text-5xl md:text-7xl font-extrabold mb-4">
|
||||
AI智能建站助手
|
||||
</h1>
|
||||
<p className="text-primary text-xl md:text-2xl mb-8">
|
||||
让我们帮助您快速构建和优化您的网站
|
||||
</p>
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className="font-bold py-3 px-6 rounded-full shadow-lg transition duration-300 ease-in-out"
|
||||
>
|
||||
开始使用
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
`,
|
||||
`
|
||||
const cpn = () => {
|
||||
return (
|
||||
<div className="bg-background-2 py-16 px-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h2 className="text-text-1 text-4xl font-bold mb-4 text-center">
|
||||
功能介绍
|
||||
</h2>
|
||||
<p className="text-text-2 text-lg mb-8 text-center">
|
||||
AI智能建站助手提供多种功能,帮助您轻松创建和管理网站
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div className="p-6 rounded-lg shadow-lg text-center">
|
||||
<h3 className="text-primary text-2xl font-semibold mb-4">
|
||||
自动布局
|
||||
</h3>
|
||||
<p className="text-text-3">
|
||||
通过AI技术自动生成最优的网页布局,提高用户体验
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 rounded-lg shadow-lg text-center">
|
||||
<h3 className="text-primary text-2xl font-semibold mb-4">
|
||||
智能优化
|
||||
</h3>
|
||||
<p className="text-text-3">
|
||||
利用大数据分析和优化工具提升网站性能和SEO排名
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 rounded-lg shadow-lg text-center">
|
||||
<h3 className="text-primary text-2xl font-semibold mb-4">
|
||||
个性化定制
|
||||
</h3>
|
||||
<p className="text-text-3">
|
||||
提供多种定制选项,让您的网站独一无二
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
`,
|
||||
`
|
||||
const cpn = () => {
|
||||
return (
|
||||
<div className="bg-background-1 py-16 px-8">
|
||||
<div className="max-w-7xl mx-auto text-center">
|
||||
<h2 className="text-text-1 text-4xl font-bold mb-4">
|
||||
使用案例
|
||||
</h2>
|
||||
<p className="text-text-2 text-lg mb-8">
|
||||
了解其他用户如何利用我们的工具创建令人惊叹的网站
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div className="p-6 rounded-lg shadow-lg">
|
||||
<h3 className="text-primary text-2xl font-semibold mb-4">
|
||||
案例一
|
||||
</h3>
|
||||
<p className="text-text-3">
|
||||
某公司利用我们的AI工具创建了一个高效、美观的企业官网
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 rounded-lg shadow-lg">
|
||||
<h3 className="text-primary text-2xl font-semibold mb-4">
|
||||
案例二
|
||||
</h3>
|
||||
<p className="text-text-3">
|
||||
某设计师通过我们的平台轻松构建了一个个人作品展示网站
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
`,
|
||||
`
|
||||
const cpn = () => {
|
||||
return (
|
||||
<div className="bg-background-2 py-16 px-8">
|
||||
<div className="max-w-7xl mx-auto text-center">
|
||||
<h2 className="text-text-1 text-4xl font-bold mb-4">
|
||||
用户评价
|
||||
</h2>
|
||||
<p className="text-text-2 text-lg mb-8">
|
||||
我们的用户对我们的工具评价很高
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div className="p-6 rounded-lg shadow-lg">
|
||||
<h3 className="text-primary text-2xl font-semibold mb-4">
|
||||
用户A
|
||||
</h3>
|
||||
<p className="text-text-3">
|
||||
“AI智能建站助手帮助我节省了大量时间,让我的网站更专业。”
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 rounded-lg shadow-lg">
|
||||
<h3 className="text-primary text-2xl font-semibold mb-4">
|
||||
用户B
|
||||
</h3>
|
||||
<p className="text-text-3">
|
||||
“非常容易使用,功能强大,效果出色!”
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
`,
|
||||
`
|
||||
const cpn = () => {
|
||||
return (
|
||||
<div className="bg-background-1 py-16 px-8">
|
||||
<div className="max-w-7xl mx-auto text-center">
|
||||
<h2 className="text-text-1 text-4xl font-bold mb-4">
|
||||
常见问题
|
||||
</h2>
|
||||
<p className="text-text-2 text-lg mb-8">
|
||||
解答您在使用过程中可能遇到的问题
|
||||
</p>
|
||||
<div className="text-left max-w-3xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<h3 className="text-primary text-2xl font-semibold mb-2">
|
||||
问题一:如何开始使用AI智能建站助手?
|
||||
</h3>
|
||||
<p className="text-text-3">
|
||||
您只需注册一个账号,选择一个模板即可开始构建您的网站。
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-8">
|
||||
<h3 className="text-primary text-2xl font-semibold mb-2">
|
||||
问题二:是否支持移动端?
|
||||
</h3>
|
||||
<p className="text-text-3">
|
||||
是的,我们的所有模板都是响应式设计,兼容各类设备。
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-8">
|
||||
<h3 className="text-primary text-2xl font-semibold mb-2">
|
||||
问题三:如何获取技术支持?
|
||||
</h3>
|
||||
<p className="text-text-3">
|
||||
您可以通过我们的客服系统或者发送邮件获取技术支持。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
`,
|
||||
`
|
||||
const cpn = () => {
|
||||
return (
|
||||
<div className="bg-primary-1 py-16 px-8">
|
||||
<div className="max-w-7xl mx-auto text-center">
|
||||
<h2 className="text-text-1 text-4xl font-bold mb-4">
|
||||
联系我们
|
||||
</h2>
|
||||
<p className="text-text-2 text-lg mb-8">
|
||||
如果您有任何问题或建议,请随时与我们联系
|
||||
</p>
|
||||
<form className="max-w-3xl mx-auto text-left">
|
||||
<div className="mb-4">
|
||||
<label className="block text-text-1 text-sm font-bold mb-2">
|
||||
姓名
|
||||
</label>
|
||||
<input className="shadow appearance-none border rounded w-full py-2 px-3 text-text-1 leading-tight focus:outline-none focus:shadow-outline" id="name" type="text" placeholder="您的姓名"/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-text-1 text-sm font-bold mb-2">
|
||||
邮箱
|
||||
</label>
|
||||
<input className="shadow appearance-none border rounded w-full py-2 px-3 text-text-1 leading-tight focus:outline-none focus:shadow-outline" id="email" type="email" placeholder="您的邮箱"/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label className="block text-text-1 text-sm font-bold mb-2">
|
||||
留言
|
||||
</label>
|
||||
<textarea className="shadow appearance-none border rounded w-full py-2 px-3 text-text-1 leading-tight focus:outline-none focus:shadow-outline" id="message" placeholder="您的留言"></textarea>
|
||||
</div>
|
||||
<button className="bg-primary-6 font-bold py-2 px-4 transition duration-300 ease-in-out" type="button">
|
||||
提交
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
`,
|
||||
];
|
||||
import { functionBlockList } from "./data/functionBlocks";
|
||||
import ClickContextProvider from "./components/Selected/ClickContext";
|
||||
|
||||
const code = (func: string) => `
|
||||
${func}
|
||||
|
|
@ -229,7 +14,7 @@ render(cpn)
|
|||
|
||||
const editor = (setShowEdit: React.Dispatch<React.SetStateAction<boolean>>) => {
|
||||
return (
|
||||
<div className="fixed inset-0 flex flex-col items-center bg-gray-900 p-4 overflow-y-auto">
|
||||
<div className="bg-primary-1 fixed inset-0 flex flex-col items-center bg-gray-900 p-4 overflow-y-auto">
|
||||
<div className="fixed top-4 right-2">
|
||||
<button
|
||||
className="bg-primary-6 text-white py-2 px-4 rounded-lg mr-2"
|
||||
|
|
@ -246,10 +31,10 @@ const editor = (setShowEdit: React.Dispatch<React.SetStateAction<boolean>>) => {
|
|||
key={index}
|
||||
>
|
||||
<LiveEditor
|
||||
code={code(func)}
|
||||
code={code(func.code)}
|
||||
language={"tsx"}
|
||||
onChange={(code) => {
|
||||
functionBlockList[index] = code
|
||||
functionBlockList[index].code = code
|
||||
.replace("render(cpn)", "")
|
||||
.trim();
|
||||
}}
|
||||
|
|
@ -258,11 +43,10 @@ const editor = (setShowEdit: React.Dispatch<React.SetStateAction<boolean>>) => {
|
|||
<button
|
||||
className="bg-primary-6 text-white py-2 px-4 rounded-lg mt-2"
|
||||
onClick={() => {
|
||||
functionBlockList.splice(
|
||||
index + 1,
|
||||
0,
|
||||
"const cpn = () => {return <>Hello World</>};"
|
||||
);
|
||||
functionBlockList.splice(index + 1, 0, {
|
||||
code: "const cpn = () => {return <>Hello World</>};",
|
||||
props: {},
|
||||
});
|
||||
setShowEdit(false);
|
||||
}}
|
||||
>
|
||||
|
|
@ -327,9 +111,32 @@ export const DemoApp = () => {
|
|||
)}
|
||||
{functionBlockList.map((func, index) => {
|
||||
return (
|
||||
<LiveProvider code={code(func)} noInline key={index}>
|
||||
<LivePreview />
|
||||
<LiveError />
|
||||
<LiveProvider code={code(func.code)} noInline key={index}>
|
||||
<ClickContextProvider
|
||||
meun={[
|
||||
{
|
||||
key: "copy",
|
||||
text: "复制",
|
||||
handler: ({ selected }) => {
|
||||
if (selected) {
|
||||
navigator.clipboard.writeText(selected.outerHTML);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
text: "删除",
|
||||
handler: ({ selected }) => {
|
||||
if (selected) {
|
||||
selected.remove();
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
>
|
||||
<LivePreview {...func.props} />
|
||||
<LiveError />
|
||||
</ClickContextProvider>
|
||||
</LiveProvider>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
import { Highlight, Prism, themes } from "prism-react-renderer";
|
||||
import { CSSProperties, useEffect, useRef, useState } from "react";
|
||||
import { useEditable } from "use-editable";
|
||||
|
||||
export type Props = {
|
||||
className?: string;
|
||||
code: string;
|
||||
disabled?: boolean;
|
||||
language: string;
|
||||
prism?: typeof Prism;
|
||||
style?: CSSProperties;
|
||||
tabMode?: "focus" | "indentation";
|
||||
theme?: typeof themes.nightOwl;
|
||||
onChange?(value: string): void;
|
||||
};
|
||||
|
||||
const CodeEditor = (props: Props) => {
|
||||
const { tabMode = "indentation" } = props;
|
||||
const editorRef = useRef(null);
|
||||
const [code, setCode] = useState(props.code || "");
|
||||
const { theme } = props;
|
||||
|
||||
useEffect(() => {
|
||||
setCode(props.code);
|
||||
}, [props.code]);
|
||||
|
||||
useEditable(editorRef, (text) => setCode(text.slice(0, -1)), {
|
||||
disabled: props.disabled,
|
||||
indentation: tabMode === "indentation" ? 2 : undefined,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (props.onChange) {
|
||||
props.onChange(code);
|
||||
}
|
||||
}, [code]);
|
||||
|
||||
return (
|
||||
<div className={props.className} style={props.style}>
|
||||
<Highlight
|
||||
code={code}
|
||||
theme={props.theme || themes.nightOwl}
|
||||
language={props.language}
|
||||
>
|
||||
{({
|
||||
className: _className,
|
||||
tokens,
|
||||
getLineProps,
|
||||
getTokenProps,
|
||||
style: _style,
|
||||
}) => (
|
||||
<pre
|
||||
className={_className}
|
||||
style={{
|
||||
margin: 0,
|
||||
outline: "none",
|
||||
padding: 10,
|
||||
fontFamily: "inherit",
|
||||
...(theme && typeof theme.plain === "object" ? theme.plain : {}),
|
||||
..._style,
|
||||
}}
|
||||
ref={editorRef}
|
||||
spellCheck="false"
|
||||
>
|
||||
{tokens.map((line, lineIndex) => (
|
||||
<span key={`line-${lineIndex}`} {...getLineProps({ line })}>
|
||||
{line
|
||||
.filter((token) => !token.empty)
|
||||
.map((token, tokenIndex) => (
|
||||
<span
|
||||
key={`token-${tokenIndex}`}
|
||||
{...getTokenProps({ token })}
|
||||
/>
|
||||
))}
|
||||
{"\n"}
|
||||
</span>
|
||||
))}
|
||||
</pre>
|
||||
)}
|
||||
</Highlight>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeEditor;
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { themes } from "prism-react-renderer";
|
||||
import { ComponentType, createContext } from "react";
|
||||
|
||||
type ContextValue = {
|
||||
error?: string;
|
||||
element?: ComponentType | null;
|
||||
code: string;
|
||||
disabled: boolean;
|
||||
language: string;
|
||||
theme?: typeof themes.nightOwl;
|
||||
onError(error: Error): void;
|
||||
onChange(value: string): void;
|
||||
};
|
||||
|
||||
const LiveContext = createContext<ContextValue>({} as ContextValue);
|
||||
|
||||
export default LiveContext;
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { useContext } from "react";
|
||||
import LiveContext from "./LiveContext";
|
||||
import Editor, { Props as EditorProps } from "../Editor";
|
||||
|
||||
export default function LiveEditor(props: Partial<EditorProps>) {
|
||||
const { code, language, theme, disabled, onChange } = useContext(LiveContext);
|
||||
|
||||
return (
|
||||
<Editor
|
||||
theme={theme}
|
||||
code={code}
|
||||
language={language}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { useContext } from "react";
|
||||
import LiveContext from "./LiveContext";
|
||||
|
||||
export default function LiveError<T extends Record<string, unknown>>(props: T) {
|
||||
const { error } = useContext(LiveContext);
|
||||
return error ? <pre {...props}>{error}</pre> : null;
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import React, { useContext } from "react";
|
||||
import LiveContext from "./LiveContext";
|
||||
|
||||
type Props<T extends React.ElementType = React.ElementType> = {
|
||||
Component?: T;
|
||||
} & React.ComponentPropsWithoutRef<T>;
|
||||
|
||||
function LivePreview<T extends keyof JSX.IntrinsicElements>(
|
||||
props: Props<T>
|
||||
): JSX.Element;
|
||||
function LivePreview<T extends React.ElementType>(props: Props<T>): JSX.Element;
|
||||
|
||||
function LivePreview({ Component = "div", ...rest }: Props): JSX.Element {
|
||||
const { element: Element } = useContext(LiveContext);
|
||||
return (
|
||||
<Component {...rest}>
|
||||
{Element ? React.cloneElement(<Element />, rest) : null}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
|
||||
export default LivePreview;
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
import { useEffect, useState, ComponentType, PropsWithChildren } from "react";
|
||||
import LiveContext from "./LiveContext";
|
||||
import { generateElement, renderElementAsync } from "../../utils/transpile";
|
||||
import { themes } from "prism-react-renderer";
|
||||
|
||||
type ProviderState = {
|
||||
element?: ComponentType | null;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
code?: string;
|
||||
disabled?: boolean;
|
||||
enableTypeScript?: boolean;
|
||||
language?: string;
|
||||
noInline?: boolean;
|
||||
scope?: Record<string, unknown>;
|
||||
theme?: typeof themes.nightOwl;
|
||||
transformCode?(code: string): void;
|
||||
};
|
||||
|
||||
function LiveProvider({
|
||||
children,
|
||||
code = "",
|
||||
language = "tsx",
|
||||
theme,
|
||||
enableTypeScript = true,
|
||||
disabled = false,
|
||||
scope,
|
||||
transformCode,
|
||||
noInline = false,
|
||||
}: PropsWithChildren<Props>) {
|
||||
const [state, setState] = useState<ProviderState>({
|
||||
error: undefined,
|
||||
element: undefined,
|
||||
});
|
||||
|
||||
async function transpileAsync(newCode: string) {
|
||||
const errorCallback = (error: Error) => {
|
||||
setState({ error: error.toString(), element: undefined });
|
||||
};
|
||||
|
||||
// - transformCode may be synchronous or asynchronous.
|
||||
// - transformCode may throw an exception or return a rejected promise, e.g.
|
||||
// if newCode is invalid and cannot be transformed.
|
||||
// - Not using async-await to since it requires targeting ES 2017 or
|
||||
// importing regenerator-runtime... in the next major version of
|
||||
// react-live, should target ES 2017+
|
||||
try {
|
||||
const transformResult = transformCode ? transformCode(newCode) : newCode;
|
||||
try {
|
||||
const transformedCode = await Promise.resolve(transformResult);
|
||||
const renderElement = (element: ComponentType) =>
|
||||
setState({ error: undefined, element });
|
||||
|
||||
if (typeof transformedCode !== "string") {
|
||||
throw new Error("Code failed to transform");
|
||||
}
|
||||
|
||||
// Transpilation arguments
|
||||
const input = {
|
||||
code: transformedCode,
|
||||
scope,
|
||||
enableTypeScript,
|
||||
};
|
||||
|
||||
if (noInline) {
|
||||
setState({ error: undefined, element: null }); // Reset output for async (no inline) evaluation
|
||||
renderElementAsync(input, renderElement, errorCallback);
|
||||
} else {
|
||||
renderElement(generateElement(input, errorCallback));
|
||||
}
|
||||
} catch (error) {
|
||||
return errorCallback(error as Error);
|
||||
}
|
||||
} catch (e) {
|
||||
errorCallback(e as Error);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
const onError = (error: Error) => setState({ error: error.toString() });
|
||||
|
||||
useEffect(() => {
|
||||
transpileAsync(code).catch(onError);
|
||||
}, [code, scope, noInline, transformCode]);
|
||||
|
||||
const onChange = (newCode: string) => {
|
||||
transpileAsync(newCode).catch(onError);
|
||||
};
|
||||
|
||||
return (
|
||||
<LiveContext.Provider
|
||||
value={{
|
||||
...state,
|
||||
code,
|
||||
language,
|
||||
theme,
|
||||
disabled,
|
||||
onError,
|
||||
onChange,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</LiveContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default LiveProvider;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
.selected {
|
||||
border: 2px solid black;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
position: absolute;
|
||||
background-color: white;
|
||||
border: 1px solid #ccc;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||
padding: 10px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
ReactNode,
|
||||
CSSProperties,
|
||||
} from "react";
|
||||
import "./ClickContext.css";
|
||||
|
||||
interface ClickContextType {
|
||||
selectedElement: HTMLElement | null;
|
||||
}
|
||||
|
||||
const ClickContext = createContext<ClickContextType | undefined>(undefined);
|
||||
|
||||
type MeunClickHandlerProps = {
|
||||
selected: HTMLElement | null;
|
||||
event: React.MouseEvent<HTMLDivElement, MouseEvent>;
|
||||
};
|
||||
|
||||
type MenuItem = {
|
||||
key: string;
|
||||
text: string;
|
||||
handler: (props: MeunClickHandlerProps) => void;
|
||||
};
|
||||
|
||||
interface ClickContextProviderProps {
|
||||
children: ReactNode;
|
||||
meun: MenuItem[];
|
||||
}
|
||||
|
||||
const ClickContextProvider: React.FC<ClickContextProviderProps> = ({
|
||||
children,
|
||||
meun,
|
||||
}) => {
|
||||
const [selectedElement, setSelectedElement] = useState<HTMLElement | null>(
|
||||
null
|
||||
);
|
||||
const [menuPosition, setMenuPosition] = useState<CSSProperties>({
|
||||
top: 0,
|
||||
left: 0,
|
||||
});
|
||||
const [showMenu, setShowMenu] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
if (target.tagName === "DIV") {
|
||||
if (selectedElement) {
|
||||
selectedElement.classList.remove("selected");
|
||||
}
|
||||
|
||||
setSelectedElement(target);
|
||||
target.classList.add("selected");
|
||||
|
||||
// 设置菜单位置
|
||||
const rect = target.getBoundingClientRect();
|
||||
const top = rect.top + window.scrollY;
|
||||
let left = rect.left + window.scrollX + rect.width + 10;
|
||||
|
||||
// 检查是否超出视口
|
||||
const menuWidth = 60; // 假设菜单宽度为150像素
|
||||
|
||||
if (left + menuWidth > window.innerWidth) {
|
||||
left = rect.right - window.scrollX - menuWidth; // 调整到视口内
|
||||
}
|
||||
|
||||
setMenuPosition({ top, left });
|
||||
setShowMenu(true);
|
||||
} else {
|
||||
if (selectedElement) {
|
||||
selectedElement.classList.remove("selected");
|
||||
setSelectedElement(null);
|
||||
}
|
||||
setShowMenu(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener("click", handleClick);
|
||||
return () => {
|
||||
document.removeEventListener("click", handleClick);
|
||||
};
|
||||
}, [selectedElement]);
|
||||
|
||||
return (
|
||||
<ClickContext.Provider value={{ selectedElement }}>
|
||||
{children}
|
||||
{showMenu && (
|
||||
<div
|
||||
className="context-menu"
|
||||
style={{ top: menuPosition.top, left: menuPosition.left }}
|
||||
>
|
||||
{meun.map((item) => (
|
||||
<div
|
||||
key={item.key}
|
||||
className="context-menu-item"
|
||||
onClick={(event) => {
|
||||
item.handler({ selected: selectedElement, event });
|
||||
setShowMenu(false);
|
||||
}}
|
||||
>
|
||||
{item.text}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</ClickContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useClickContext = () => {
|
||||
const context = useContext(ClickContext);
|
||||
if (context === undefined) {
|
||||
throw new Error(
|
||||
"useClickContext must be used within a ClickContextProvider"
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export default ClickContextProvider;
|
||||
|
|
@ -0,0 +1,293 @@
|
|||
export const functionBlockList: {
|
||||
code: string;
|
||||
props: Record<string, string>;
|
||||
}[] = [
|
||||
{
|
||||
code: `
|
||||
const cpn = (props: {
|
||||
title: string
|
||||
subTitle: string
|
||||
}) => {
|
||||
function handleClick() {
|
||||
console.log("Hi there!");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-primary-1 py-16 px-8">
|
||||
<div className="max-w-7xl mx-auto text-center">
|
||||
<h1 className="text-primary text-5xl md:text-7xl font-extrabold mb-4">
|
||||
{props.title}
|
||||
</h1>
|
||||
<p className="text-primary text-xl md:text-2xl mb-8">
|
||||
{props.subTitle}
|
||||
</p>
|
||||
<button
|
||||
onClick={handleClick}
|
||||
className="font-bold py-3 px-6 rounded-full shadow-lg transition duration-300 ease-in-out"
|
||||
>
|
||||
开始使用
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
`,
|
||||
props: {
|
||||
title: "AI智能建站助手",
|
||||
subTitle: "让我们帮助您快速构建和优化您的网站",
|
||||
},
|
||||
},
|
||||
{
|
||||
code: `
|
||||
const cpn = (props: {
|
||||
title: string
|
||||
subTitle: string
|
||||
featureTitle1: string
|
||||
featureDesc1: string
|
||||
featureTitle2: string
|
||||
featureDesc2: string
|
||||
featureTitle3: string
|
||||
featureDesc3: string
|
||||
}) => {
|
||||
return (
|
||||
<div className="bg-background-2 py-16 px-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<h2 className="text-text-1 text-4xl font-bold mb-4 text-center">
|
||||
{props.title}
|
||||
</h2>
|
||||
<p className="text-text-2 text-lg mb-8 text-center">
|
||||
{props.subTitle}
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
<div className="p-6 rounded-lg shadow-lg text-center">
|
||||
<h3 className="text-primary text-2xl font-semibold mb-4">
|
||||
{props.featureTitle1}
|
||||
</h3>
|
||||
<p className="text-text-3">
|
||||
{props.featureDesc1}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 rounded-lg shadow-lg text-center">
|
||||
<h3 className="text-primary text-2xl font-semibold mb-4">
|
||||
{props.featureTitle2}
|
||||
</h3>
|
||||
<p className="text-text-3">
|
||||
{props.featureDesc2}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 rounded-lg shadow-lg text-center">
|
||||
<h3 className="text-primary text-2xl font-semibold mb-4">
|
||||
{props.featureTitle3}
|
||||
</h3>
|
||||
<p className="text-text-3">
|
||||
{props.featureDesc3}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
`,
|
||||
props: {
|
||||
title: "功能介绍",
|
||||
subTitle: "AI智能建站助手提供多种功能,帮助您轻松创建和管理网站",
|
||||
featureTitle1: "自动布局",
|
||||
featureDesc1: "通过AI技术自动生成最优的网页布局,提高用户体验",
|
||||
featureTitle2: "智能优化",
|
||||
featureDesc2: "利用大数据分析和优化工具提升网站性能和SEO排名",
|
||||
featureTitle3: "个性化定制",
|
||||
featureDesc3: "提供多种定制选项,让您的网站独一无二",
|
||||
},
|
||||
},
|
||||
{
|
||||
code: `
|
||||
const cpn = (props: {
|
||||
title: string
|
||||
subTitle: string
|
||||
caseTitle1: string
|
||||
caseDesc1: string
|
||||
caseTitle2: string
|
||||
caseDesc2: string
|
||||
}) => {
|
||||
return (
|
||||
<div className="bg-background-1 py-16 px-8">
|
||||
<div className="max-w-7xl mx-auto text-center">
|
||||
<h2 className="text-text-1 text-4xl font-bold mb-4">
|
||||
{props.title}
|
||||
</h2>
|
||||
<p className="text-text-2 text-lg mb-8">
|
||||
{props.subTitle}
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div className="p-6 rounded-lg shadow-lg">
|
||||
<h3 className="text-primary text-2xl font-semibold mb-4">
|
||||
{props.caseTitle1}
|
||||
</h3>
|
||||
<p className="text-text-3">
|
||||
{props.caseDesc1}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 rounded-lg shadow-lg">
|
||||
<h3 className="text-primary text-2xl font-semibold mb-4">
|
||||
{props.caseTitle2}
|
||||
</h3>
|
||||
<p className="text-text-3">
|
||||
{props.caseDesc2}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
`,
|
||||
props: {
|
||||
title: "使用案例",
|
||||
subTitle: "了解其他用户如何利用我们的工具创建令人惊叹的网站",
|
||||
caseTitle1: "案例一",
|
||||
caseDesc1: "某公司利用我们的AI工具创建了一个高效、美观的企业官网",
|
||||
caseTitle2: "案例二",
|
||||
caseDesc2: "某设计师通过我们的平台轻松构建了一个个人作品展示网站",
|
||||
},
|
||||
},
|
||||
{
|
||||
code: `
|
||||
const cpn = (props: {
|
||||
title: string
|
||||
subTitle: string
|
||||
reviewUser1: string
|
||||
reviewDesc1: string
|
||||
reviewUser2: string
|
||||
reviewDesc2: string
|
||||
}) => {
|
||||
return (
|
||||
<div className="bg-background-2 py-16 px-8">
|
||||
<div className="max-w-7xl mx-auto text-center">
|
||||
<h2 className="text-text-1 text-4xl font-bold mb-4">
|
||||
{props.title}
|
||||
</h2>
|
||||
<p className="text-text-2 text-lg mb-8">
|
||||
{props.subTitle}
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div className="p-6 rounded-lg shadow-lg">
|
||||
<h3 className="text-primary text-2xl font-semibold mb-4">
|
||||
{props.reviewUser1}
|
||||
</h3>
|
||||
<p className="text-text-3">
|
||||
{props.reviewDesc1}
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-6 rounded-lg shadow-lg">
|
||||
<h3 className="text-primary text-2xl font-semibold mb-4">
|
||||
{props.reviewUser2}
|
||||
</h3>
|
||||
<p className="text-text-3">
|
||||
{props.reviewDesc2}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
`,
|
||||
props: {
|
||||
title: "用户评价",
|
||||
subTitle: "我们的用户对我们的工具评价很高",
|
||||
reviewUser1: "用户A",
|
||||
reviewDesc1: "“AI智能建站助手帮助我节省了大量时间,让我的网站更专业。”",
|
||||
reviewUser2: "用户B",
|
||||
reviewDesc2: "“非常容易使用,功能强大,效果出色!”",
|
||||
},
|
||||
},
|
||||
{
|
||||
code: `
|
||||
const cpn = (props: {
|
||||
title: string
|
||||
subTitle: string
|
||||
faq1: string
|
||||
faqAnswer1: string
|
||||
faq2: string
|
||||
faqAnswer2: string
|
||||
faq3: string
|
||||
faqAnswer3: string
|
||||
}) => {
|
||||
return (
|
||||
<div className="bg-background-1 py-16 px-8">
|
||||
<div className="max-w-7xl mx-auto text-center">
|
||||
<h2 className="text-text-1 text-4xl font-bold mb-4">
|
||||
{props.title}
|
||||
</h2>
|
||||
<p className="text-text-2 text-lg mb-8">
|
||||
{props.subTitle}
|
||||
</p>
|
||||
<div className="text-left max-w-3xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<h3 className="text-primary text-2xl font-semibold mb-2">
|
||||
{props.faq1}
|
||||
</h3>
|
||||
<p className="text-text-3">
|
||||
{props.faqAnswer1}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-8">
|
||||
<h3 className="text-primary text-2xl font-semibold mb-2">
|
||||
{props.faq2}
|
||||
</h3>
|
||||
<p className="text-text-3">
|
||||
{props.faqAnswer2}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-8">
|
||||
<h3 className="text-primary text-2xl font-semibold mb-2">
|
||||
{props.faq3}
|
||||
</h3>
|
||||
<p className="text-text-3">
|
||||
{props.faqAnswer3}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
`,
|
||||
props: {
|
||||
title: "常见问题",
|
||||
subTitle: "解答您在使用过程中可能遇到的问题",
|
||||
faq1: "问题一:如何开始使用AI智能建站助手?",
|
||||
faqAnswer1: "您只需注册一个账号,选择一个模板即可开始构建您的网站。",
|
||||
faq2: "问题二:是否支持移动端?",
|
||||
faqAnswer2: "是的,我们的所有模板都是响应式设计,兼容各类设备。",
|
||||
faq3: "问题三:如何获取技术支持?",
|
||||
faqAnswer3: "您可以通过我们的客服系统或者发送邮件获取技术支持。",
|
||||
},
|
||||
},
|
||||
{
|
||||
code: `
|
||||
const cpn = (props: {
|
||||
title: string
|
||||
subTitle: string
|
||||
}) => {
|
||||
return (
|
||||
<div className="bg-primary-1 py-16 px-8">
|
||||
<div className="max-w-7xl mx-auto text-center">
|
||||
<h2 className="text-text-1 text-4xl font-bold mb-4">
|
||||
{props.title}
|
||||
</h2>
|
||||
<p className="text-text-2 text-lg mb-8">
|
||||
{props.subTitle}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
`,
|
||||
props: {
|
||||
title: "联系我们",
|
||||
subTitle: "如果您有任何疑问或需要帮助,请随时联系我们",
|
||||
},
|
||||
},
|
||||
];
|
||||
107
src/index.scss
107
src/index.scss
|
|
@ -3,112 +3,7 @@
|
|||
@tailwind utilities;
|
||||
|
||||
/* 导入主题 */
|
||||
@import "./theme/index.scss";
|
||||
|
||||
/* 下面是统一的颜色定义 */
|
||||
/* 默认的主题样式 */
|
||||
:root {
|
||||
background-color: var(--color-bg-1);
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: var(--primary-6);
|
||||
color: var(--color-bg-white);
|
||||
border: var(--border-1) solid var(--color-border-3);
|
||||
border-radius: var(--border-radius-small);
|
||||
padding: var(--size-3) var(--size-4);
|
||||
font-size: var(--font-size-body-3);
|
||||
box-shadow: var(--shadow1-center);
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--primary-5);
|
||||
border-color: var(--color-border-4);
|
||||
}
|
||||
|
||||
button:active {
|
||||
background-color: var(--primary-7);
|
||||
border-color: var(--color-border-4);
|
||||
}
|
||||
|
||||
input, textarea {
|
||||
background-color: var(--color-fill-1);
|
||||
color: var(--color-text-1);
|
||||
border: var(--border-1) solid var(--color-border-2);
|
||||
border-radius: var(--border-radius-small);
|
||||
padding: var(--size-3);
|
||||
font-size: var(--font-size-body-3);
|
||||
box-shadow: var(--shadow1-center);
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--color-bg-2);
|
||||
color: var(--color-text-1);
|
||||
border: var(--border-1) solid var(--color-border-1);
|
||||
border-radius: var(--border-radius-large);
|
||||
padding: var(--size-5);
|
||||
box-shadow: var(--shadow2-center);
|
||||
font-size: var(--font-size-body-3);
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--link-6);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--link-5);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--color-text-1);
|
||||
font-size: var(--font-size-body-3);
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: var(--border-1) solid var(--color-border-2);
|
||||
padding: var(--size-3);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
thead {
|
||||
background-color: var(--color-bg-2);
|
||||
}
|
||||
|
||||
.alert {
|
||||
background-color: var(--danger-1);
|
||||
color: var(--danger-6);
|
||||
border: var(--border-1) solid var(--danger-4);
|
||||
border-radius: var(--border-radius-small);
|
||||
padding: var(--size-3);
|
||||
}
|
||||
|
||||
.success {
|
||||
background-color: var(--success-1);
|
||||
color: var(--success-6);
|
||||
border: var(--border-1) solid var(--success-4);
|
||||
border-radius: var(--border-radius-small);
|
||||
padding: var(--size-3);
|
||||
}
|
||||
|
||||
.warning {
|
||||
background-color: var(--warning-1);
|
||||
color: var(--warning-6);
|
||||
border: var(--border-1) solid var(--warning-4);
|
||||
border-radius: var(--border-radius-small);
|
||||
padding: var(--size-3);
|
||||
}
|
||||
@import "./theme/index.css";
|
||||
|
||||
:root {
|
||||
/* .font-size-global { */
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
// 定义主体名称变量
|
||||
$theme-name: dark;
|
||||
|
||||
.theme-color-dark {
|
||||
--primary-7:rgb( 104,159,255);
|
||||
--primary-6:rgb( 60,126,255);
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
@import "./dark/index.css";
|
||||
@import "./light/index.css"
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
@import "./dark/index.scss";
|
||||
@import "./light/index.scss"
|
||||
|
|
@ -1,6 +1,3 @@
|
|||
// 定义主体名称变量
|
||||
$theme-name: light;
|
||||
|
||||
.theme-color-light {
|
||||
--primary-7:rgb( 14, 66,210);
|
||||
--primary-6:rgb( 22, 93,255);
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* Creates a new composite function that invokes the functions from right to left
|
||||
*/
|
||||
|
||||
export default function compose<T>(...functions: ((...args: T[]) => T)[]) {
|
||||
return functions.reduce(
|
||||
(acc, currentFn) =>
|
||||
(...args: T[]) =>
|
||||
acc(currentFn(...args))
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import React, { ComponentType, Component, PropsWithChildren } from "react";
|
||||
|
||||
const errorBoundary = (
|
||||
Element: ComponentType,
|
||||
errorCallback: (error: Error) => void
|
||||
) => {
|
||||
return class ErrorBoundary extends Component<PropsWithChildren> {
|
||||
componentDidCatch(error: Error) {
|
||||
errorCallback(error);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { children, ...props } = this.props;
|
||||
return typeof Element === "function" ? (
|
||||
<Element {...props} />
|
||||
) : React.isValidElement(Element) ? (
|
||||
React.cloneElement(Element, props, children)
|
||||
) : null;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export default errorBoundary;
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import type { ComponentType } from "react";
|
||||
|
||||
const evalCode = (
|
||||
code: string,
|
||||
scope: Record<string, unknown>
|
||||
): ComponentType => {
|
||||
const scopeKeys = Object.keys(scope);
|
||||
const scopeValues = scopeKeys.map((key) => scope[key]);
|
||||
return new Function(...scopeKeys, code)(...scopeValues);
|
||||
};
|
||||
|
||||
export default evalCode;
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import React, { ComponentType } from "react";
|
||||
import transform from "./transform";
|
||||
import errorBoundary from "./errorBoundary";
|
||||
import evalCode from "./evalCode";
|
||||
import compose from "./compose";
|
||||
import { Transform } from "sucrase";
|
||||
|
||||
const jsxConst = 'const _jsxFileName = "";';
|
||||
const trimCode = (code: string) => code.trim().replace(/;$/, "");
|
||||
const spliceJsxConst = (code: string) => code.replace(jsxConst, "").trim();
|
||||
const addJsxConst = (code: string) => jsxConst + code;
|
||||
const wrapReturn = (code: string) => `return (${code})`;
|
||||
|
||||
type GenerateOptions = {
|
||||
code: string;
|
||||
scope?: Record<string, unknown>;
|
||||
enableTypeScript: boolean;
|
||||
};
|
||||
|
||||
export const generateElement = (
|
||||
{ code = "", scope = {}, enableTypeScript = true }: GenerateOptions,
|
||||
errorCallback: (error: Error) => void
|
||||
) => {
|
||||
/**
|
||||
* To enable TypeScript we need to transform the TS to JS code first,
|
||||
* splice off the JSX const, wrap the eval in a return statement, then
|
||||
* transform any imports. The two-phase approach is required to do
|
||||
* the implicit evaluation and not wrap leading Interface or Type
|
||||
* statements in the return.
|
||||
*/
|
||||
|
||||
const firstPassTransforms: Transform[] = ["jsx"];
|
||||
enableTypeScript && firstPassTransforms.push("typescript");
|
||||
|
||||
const transformed = compose<string>(
|
||||
addJsxConst,
|
||||
transform({ transforms: ["imports"] }),
|
||||
spliceJsxConst,
|
||||
trimCode,
|
||||
transform({ transforms: firstPassTransforms }),
|
||||
wrapReturn,
|
||||
trimCode
|
||||
)(code);
|
||||
|
||||
return errorBoundary(
|
||||
evalCode(transformed, { React, ...scope }),
|
||||
errorCallback
|
||||
);
|
||||
};
|
||||
|
||||
export const renderElementAsync = (
|
||||
{ code = "", scope = {}, enableTypeScript = true }: GenerateOptions,
|
||||
resultCallback: (sender: ComponentType) => void,
|
||||
errorCallback: (error: Error) => void
|
||||
// eslint-disable-next-line consistent-return
|
||||
) => {
|
||||
const render = (element: ComponentType) => {
|
||||
if (typeof element === "undefined") {
|
||||
errorCallback(new SyntaxError("`render` must be called with valid JSX."));
|
||||
} else {
|
||||
resultCallback(errorBoundary(element, errorCallback));
|
||||
}
|
||||
};
|
||||
|
||||
if (!/render\s*\(/.test(code)) {
|
||||
return errorCallback(
|
||||
new SyntaxError("No-Inline evaluations must call `render`.")
|
||||
);
|
||||
}
|
||||
|
||||
const transforms: Transform[] = ["jsx", "imports"];
|
||||
enableTypeScript && transforms.splice(1, 0, "typescript");
|
||||
|
||||
evalCode(transform({ transforms })(code), { React, ...scope, render });
|
||||
};
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { transform as _transform, Transform } from "sucrase";
|
||||
|
||||
const defaultTransforms: Transform[] = ["jsx", "imports"];
|
||||
|
||||
type Options = {
|
||||
transforms?: Transform[];
|
||||
};
|
||||
|
||||
export default function transform(opts: Options = {}) {
|
||||
const transforms = Array.isArray(opts.transforms)
|
||||
? opts.transforms.filter(Boolean)
|
||||
: defaultTransforms;
|
||||
|
||||
return (code: string) => _transform(code, { transforms }).code;
|
||||
}
|
||||
|
|
@ -2,41 +2,7 @@
|
|||
module.exports = {
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
colors: {
|
||||
blue: "#1fb6ff",
|
||||
purple: "#7e5bef",
|
||||
pink: "#ff49db",
|
||||
orange: "#ff7849",
|
||||
green: "#13ce66",
|
||||
yellow: "#ffc82c",
|
||||
"gray-dark": "#273444",
|
||||
gray: "#8492a6",
|
||||
"gray-light": "#d3dce6",
|
||||
transparent: "transparent",
|
||||
current: "currentColor",
|
||||
white: "#ffffff",
|
||||
midnight: "#121063",
|
||||
metal: "#565584",
|
||||
tahiti: "#3ab7bf",
|
||||
silver: "#ecebff",
|
||||
"bubble-gum": "#ff77e9",
|
||||
bermuda: "#78dcca",
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ["Graphik", "sans-serif"],
|
||||
serif: ["Merriweather", "serif"],
|
||||
},
|
||||
extend: {
|
||||
spacing: {
|
||||
"8xl": "96rem",
|
||||
"9xl": "128rem",
|
||||
},
|
||||
borderRadius: {
|
||||
"4xl": "2rem",
|
||||
},
|
||||
fontFamily: {
|
||||
display: "Oswald, ui-serif ", // Adds a new `font-display` class
|
||||
},
|
||||
colors: {
|
||||
primary: {
|
||||
1: "var(--primary-1)",
|
||||
|
|
|
|||
Loading…
Reference in New Issue