single block editor

This commit is contained in:
pqcqaq 2024-07-17 16:16:25 +08:00
parent be59f5832a
commit 91b9c8018b
10 changed files with 409 additions and 313 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -10,6 +10,10 @@
"preview": "vite preview"
},
"dependencies": {
"@uiw/codemirror-extensions-color": "^4.23.0",
"@uiw/codemirror-extensions-langs": "^4.23.0",
"@uiw/react-codemirror": "^4.23.0",
"@uiw/react-json-view": "1.12.0",
"localforage": "^1.10.0",
"match-sorter": "^6.3.4",
"react": "^18.2.0",
@ -18,9 +22,12 @@
"react-transition-group": "^4.4.5",
"sort-by": "^1.2.0",
"sucrase": "^3.31.0",
"terser": "^5.31.3",
"use-editable": "^2.3.3"
},
"devDependencies": {
"@codemirror/autocomplete": "^6.17.0",
"@codemirror/state": "^6.4.1",
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@types/react-transition-group": "^4.4.10",

View File

@ -5,5 +5,5 @@
.preview-box > div *:not(div),
.preview-box > div div *:not(div) {
cursor: default;
/* cursor: initial; */
}

View File

@ -5,8 +5,10 @@ import evalCode from "../../utils/transpile/evalCode";
import transform from "../../utils/transpile/transform";
import { ThemeProvider } from "../../components/Theme/ThemePrivider";
import errorBoundary from "../../utils/transpile/errorBoundary";
import "./style.css"
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import "./style.css";
import { CSSTransition, TransitionGroup } from "react-transition-group";
import minifyCode from "../../utils/minify";
// import ReactDOM from "react-dom/client";
export const App = () => {
// 定义成一个数组便于下面操作,实际上只有一个元素
@ -19,17 +21,25 @@ export const App = () => {
const transforms: Transform[] = ["jsx", "typescript", "imports"];
const render = (element: ComponentType) => {
const Component = errorBoundary(element, errorCallBack);
if (typeof element === "undefined") {
errorCallBack(new SyntaxError("`render` must be called with valid JSX."));
} else {
setResults([errorBoundary(element, errorCallBack)]);
setResults([Component]);
}
// ReactDOM.createRoot(document.querySelector(".test-app") as HTMLElement).render(
// <React.StrictMode>
// < Component />
// </React.StrictMode>
// );
};
// 开发环境下useEffect会执行两次模拟装载和卸载组件生产环境没事。
useEffect(() => {
const allCpns = functionBlockList.map(item => item.code).join("\n");
const allPage = functionBlockList.map((item,index) => `<${item.cpnName} { ...props[${index}] }/>`).join("\n");
const allCpns = functionBlockList.map((item) => item.code).join("\n");
const allPage = functionBlockList
.map((item, index) => `<${item.cpnName} { ...props[${index}] }/>`)
.join("\n");
const allCode = `
${allCpns}
@ -44,13 +54,15 @@ export const App = () => {
render(ApplicationContext)
`;
const propsList = functionBlockList.map(item => item.props);
const propsList = functionBlockList.map((item) => item.props);
const allTransformed = transform({ transforms })(allCode);
setTimeout(() => {
evalCode(allTransformed, { React, render, props: propsList });
},1000)
setTimeout(async () => {
const minifiedCode = await minifyCode(allTransformed);
evalCode(minifiedCode, { React, render, props: propsList });
}, 1000);
}, []);
return (
@ -58,8 +70,7 @@ export const App = () => {
<div className="application">
<TransitionGroup>
{results.map((Cpn, index) => (
<CSSTransition key={index} timeout={500} classNames="fade"
>
<CSSTransition key={index} timeout={500} classNames="fade">
<Cpn />
</CSSTransition>
))}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,98 @@
import { Extension } from '@codemirror/state';
import { CompletionContext, autocompletion, Completion } from '@codemirror/autocomplete';
const colorMap = [
{ label: "--primary-7", type: "variable", info: "(点击click)" },
{ label: "--primary-6", type: "variable", info: "(常规)" },
{ label: "--primary-5", type: "variable", info: "(悬浮hover)" },
{ label: "--primary-4", type: "variable", info: "(特殊场景)" },
{ label: "--primary-3", type: "variable", info: "(一般禁用)" },
{ label: "--primary-2", type: "variable", info: "(文字禁用)" },
{ label: "--primary-1", type: "variable", info: "(浅色/白底悬浮)" },
{ label: "--success-7", type: "variable", info: "(点击click)" },
{ label: "--success-6", type: "variable", info: "(常规)" },
{ label: "--success-5", type: "variable", info: "(悬浮hover)" },
{ label: "--success-4", type: "variable", info: "(特殊场景)" },
{ label: "--success-3", type: "variable", info: "(一般禁用)" },
{ label: "--success-2", type: "variable", info: "(文字禁用)" },
{ label: "--success-1", type: "variable", info: "(浅色/白底悬浮)" },
{ label: "--warning-7", type: "variable", info: "(点击click)" },
{ label: "--warning-6", type: "variable", info: "(常规)" },
{ label: "--warning-5", type: "variable", info: "(悬浮hover)" },
{ label: "--warning-4", type: "variable", info: "(特殊场景)" },
{ label: "--warning-3", type: "variable", info: "(一般禁用)" },
{ label: "--warning-2", type: "variable", info: "(文字禁用)" },
{ label: "--warning-1", type: "variable", info: "(浅色/白底悬浮)" },
{ label: "--danger-7", type: "variable", info: "(点击click)" },
{ label: "--danger-6", type: "variable", info: "(常规)" },
{ label: "--danger-5", type: "variable", info: "(悬浮hover)" },
{ label: "--danger-4", type: "variable", info: "(特殊场景)" },
{ label: "--danger-3", type: "variable", info: "(一般禁用)" },
{ label: "--danger-2", type: "variable", info: "(文字禁用)" },
{ label: "--danger-1", type: "variable", info: "(浅色/白底悬浮)" },
{ label: "--link-7", type: "variable", info: "(点击click)" },
{ label: "--link-6", type: "variable", info: "(常规)" },
{ label: "--link-5", type: "variable", info: "(悬浮hover)" },
{ label: "--link-4", type: "variable", info: "(特殊场景)" },
{ label: "--link-3", type: "variable", info: "(一般禁用)" },
{ label: "--link-2", type: "variable", info: "(文字禁用)" },
{ label: "--link-1", type: "variable", info: "(浅色/白底悬浮)" },
{ label: "--data-1", type: "variable", info: "" },
{ label: "--data-2", type: "variable", info: "" },
{ label: "--data-3", type: "variable", info: "" },
{ label: "--data-4", type: "variable", info: "" },
{ label: "--data-5", type: "variable", info: "" },
{ label: "--data-6", type: "variable", info: "" },
{ label: "--data-7", type: "variable", info: "" },
{ label: "--data-8", type: "variable", info: "" },
{ label: "--data-9", type: "variable", info: "" },
{ label: "--data-10", type: "variable", info: "" },
{ label: "--data-11", type: "variable", info: "" },
{ label: "--data-12", type: "variable", info: "" },
{ label: "--data-13", type: "variable", info: "" },
{ label: "--data-14", type: "variable", info: "" },
{ label: "--data-15", type: "variable", info: "" },
{ label: "--data-16", type: "variable", info: "" },
{ label: "--data-17", type: "variable", info: "" },
{ label: "--data-18", type: "variable", info: "" },
{ label: "--data-19", type: "variable", info: "" },
{ label: "--data-20", type: "variable", info: "" },
{ label: "--color-border-1", type: "variable", info: "(浅色)" },
{ label: "--color-border-2", type: "variable", info: "(一般)" },
{ label: "--color-border-3", type: "variable", info: "(深/悬浮)" },
{ label: "--color-border-4", type: "variable", info: "(重/按钮描边)" },
{ label: "--color-fill-1", type: "variable", info: "(浅/禁用)" },
{ label: "--color-fill-2", type: "variable", info: "(常规/白底悬浮)" },
{ label: "--color-fill-3", type: "variable", info: "(深/灰底悬浮)" },
{ label: "--color-fill-4", type: "variable", info: "(重/特殊场景)" },
{ label: "--color-text-1", type: "variable", info: "(强调/正文标题)" },
{ label: "--color-text-2", type: "variable", info: "(次强调/正文标题)" },
{ label: "--color-text-3", type: "variable", info: "(次要信息)" },
{ label: "--color-text-4", type: "variable", info: "(置灰信息)" },
{ label: "--color-bg-1", type: "variable", info: "(整体背景色)" },
{ label: "--color-bg-2", type: "variable", info: "(一级容器背景)" },
{ label: "--color-bg-3", type: "variable", info: "(二级容器背景)" },
{ label: "--color-bg-4", type: "variable", info: "(三级容器背景)" },
{ label: "--color-bg-5", type: "variable", info: "(下拉弹出框、Tooltip 背景颜色)" },
{ label: "--color-bg-white", type: "variable", info: "(白色背景)" }
];
export function colorCompletions(data: Completion[] = []): Extension {
return autocompletion({
override: [
(context: CompletionContext) => {
let word = context.matchBefore(/--\w+/g);
if (!word) return null;
if (word && word.from == word.to && !context.explicit) {
return null;
}
return {
from: word?.from!,
options: [...data],
};
},
],
});
}
export const plugin = colorCompletions(colorMap);

View File

@ -0,0 +1,191 @@
import React from "react";
import CodeMirror, { basicSetup } from "@uiw/react-codemirror";
import { loadLanguage, langs } from "@uiw/codemirror-extensions-langs";
import LiveProvider from "../../components/Live/LiveProvider";
import LivePreview from "../../components/Live/LivePreview";
import LiveError from "../../components/Live/LiveError";
import { color, colorView, colorTheme } from "@uiw/codemirror-extensions-color";
import JsonViewEditor from "@uiw/react-json-view/editor";
import { ThemeProvider } from "../../components/Theme/ThemePrivider";
import { plugin as colorCom } from "./completions";
loadLanguage("tsx");
const extensions = [color, langs.tsx(), colorView(false), colorTheme, colorCom];
export const Creation = () => {
// useEffect 拦截ctrls等操作避免浏览器默认行为
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.ctrlKey && event.key === "s") {
event.preventDefault();
// 弹出已经保存的提示
}
};
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}, []);
const [componentName, setComponentName] = React.useState("Banner");
const [componentsProps, setComponentsProps] = React.useState({
title: "AI智能建站助手",
subTitle: "让我们帮助您快速构建和优化您的网站",
btnText: "立即开始",
});
const [newPropKey, setNewPropKey] = React.useState("");
const handleAddProp = () => {
if (newPropKey.trim() !== "") {
setComponentsProps((prevProps) => ({
...prevProps,
[newPropKey]: "",
}));
setNewPropKey("");
}
};
const [code, setCode] = React.useState(`
const Banner = (props: {
title: string
subTitle: string
btnText: 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"
>
{props.btnText}
</button>
</div>
</div>
);
};
`);
const renderCode = (code: string) => {
return `
${code}
render(${componentName});
`;
};
const onChange = React.useCallback((value: string) => {
setCode(value);
}, []);
const handleComponentNameChange = (
event: React.ChangeEvent<HTMLInputElement>
) => {
setComponentName(event.target.value);
};
return (
<ThemeProvider>
<div className="p-4">
<div className="mb-4">
<label className="block mb-2 font-bold text-lg border-primary-1">
</label>
<input
type="text"
value={componentName}
onChange={handleComponentNameChange}
className="border border-text-1 rounded p-2 w-full"
/>
</div>
<LiveProvider code={renderCode(code)} noInline>
<div className="h-full grid lg:grid-cols-2 gap-4 border p-4 rounded shadow">
<div className="border rounded p-2">
<CodeMirror
value={code}
height="100%"
extensions={[
...extensions,
basicSetup({
autocompletion: true,
lintKeymap: true,
}),
]}
onChange={onChange}
className="font-mono"
/>
</div>
<div className="border rounded p-2">
<LivePreview {...componentsProps} />
</div>
</div>
<LiveError className="text-danger-6 mt-2 p-2 rounded" />
</LiveProvider>
<div className="mt-4 p-4 border rounded shadow w-2/4">
<h2 className="font-bold text-lg mb-2">Props </h2>
<div className="props-edit">
<div className="mb-4">
<label className="block mb-2 font-bold"></label>
<div className="flex">
<input
type="text"
value={newPropKey}
onChange={(e) => setNewPropKey(e.target.value)}
className="border border-text-1 rounded p-2 flex-grow"
/>
<button onClick={handleAddProp} className="p-2 rounded ml-2">
</button>
</div>
</div>
<JsonViewEditor
value={componentsProps}
keyName="props"
style={
{
"--w-rjv-background-color": "#ffffff",
"--w-rjv-border-left": "1px dashed #ebebeb",
"--w-rjv-update-color": "#ff6ffd",
} as any
}
onEdit={(opts) => {
const updateNestedProp = (
obj: Record<string, any>,
path: string[],
value: unknown
) => {
if (path.length === 1) {
obj[path[0]] = value;
} else {
if (!obj[path[0]]) {
obj[path[0]] = {};
}
updateNestedProp(obj[path[0]], path.slice(1), value);
}
};
const updatedProps = { ...componentsProps };
updateNestedProp(
updatedProps,
(opts as any).namespace,
opts.value
);
setComponentsProps(updatedProps);
return true;
}}
/>
</div>
</div>
</div>
</ThemeProvider>
);
};

View File

@ -112,7 +112,11 @@ export const Editor = () => {
)}
{functionBlockList.map((func, index) => {
return (
<LiveProvider code={code(func.code, func.cpnName)} noInline key={index}>
<LiveProvider
code={code(func.code, func.cpnName)}
noInline
key={index}
>
<ClickContextProvider
meun={[
{

View File

@ -1,8 +1,7 @@
import {
createBrowserRouter,
} from "react-router-dom";
import { createBrowserRouter } from "react-router-dom";
import { Editor } from "../pages/editor";
import { App } from "../pages/app";
import { Creation } from "../pages/creation";
export const router = createBrowserRouter([
{
@ -16,5 +15,9 @@ export const router = createBrowserRouter([
{
path: "/app",
element: <App />,
}
},
{
path: "/create",
element: <Creation />,
},
]);

31
src/utils/minify/index.ts Normal file
View File

@ -0,0 +1,31 @@
import { minify } from "terser";
export default async function minifyCode(code: string) {
const replaced = code
.replace(RegExp(`React.createElement`, "g"), `rc`)
.replace(RegExp(`React.Fragment`, "g"), `rf`)
.replace(RegExp(`React.useState`, "g"), `rs`)
.replace(RegExp(`React.useEffect`, "g"), `re`)
.replace(RegExp(`React.Component`, "g"), `rcp`)
.replace(RegExp(`React.createContext`, "g"), `rcc`)
.replace(RegExp(`React.useContext`, "g"), `rcu`)
.replace(RegExp(`React.useReducer`, "g"), `rd`)
.replace(RegExp(`React.memo`, "g"), `rm`)
.replace(
`"use strict";`,
`"use strict"; const rc=React.createElement,rf=React.Fragment,rs=React.useState,re=React.useEffect,rcp=React.Component,rcc=React.createContext,rcu=React.useContext,rd=React.useReducer,rm=React.memo;`
);
const { code: minifiedCode } = await minify(replaced, {
compress: {
passes: 2,
drop_console: true,
drop_debugger: true,
arguments: true,
},
mangle: {
toplevel: true,
},
});
return minifiedCode || "";
}