feat: Initialize simple-lit-vite project with Lit and Vite setup
- Add README.md for project description and CLI usage - Create main application component with basic structure in app.tsx - Implement CodeMirror editor base functionality in editor.base.ts - Extend CodeMirror editor for JSON support in editor.json.ts - Add support for multiple languages in editor.ts - Create utility functions for editor manipulation in editor.utils.ts - Implement tab key formatting and indentation in tab.ts - Add Tailwind CSS integration in index.css - Develop JSON editor web component in json.ts - Create a template component for rendering in lib.ts - Set up main entry point in main.ts - Configure TypeScript settings in tsconfig.json - Define custom element typings in typings.d.ts - Configure Vite for library and application builds in vite.config.lib.ts and vite.config.ts
This commit is contained in:
56
.cnb.yml
Normal file
56
.cnb.yml
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# .cnb.yml
|
||||||
|
$:
|
||||||
|
vscode:
|
||||||
|
- docker:
|
||||||
|
image: docker.cnb.cool/kevisual/dev-env:latest
|
||||||
|
services:
|
||||||
|
- vscode
|
||||||
|
- docker
|
||||||
|
imports: https://cnb.cool/kevisual/env/-/blob/main/env.yml
|
||||||
|
# 开发环境启动后会执行的任务
|
||||||
|
# stages:
|
||||||
|
# - name: pnpm install
|
||||||
|
# script: pnpm install
|
||||||
|
|
||||||
|
|
||||||
|
main:
|
||||||
|
web_trigger_sync_to_gitea:
|
||||||
|
- services:
|
||||||
|
- docker
|
||||||
|
imports:
|
||||||
|
- https://cnb.cool/kevisual/env/-/blob/main/env.yml
|
||||||
|
stages:
|
||||||
|
- name: 'show username'
|
||||||
|
script: echo "GITEA_USERNAME is ${GITEA_USERNAME} and GITEA_PASSWORD is ${GITEA_PASSWORD}"
|
||||||
|
- name: sync to gitea
|
||||||
|
image: tencentcom/git-sync
|
||||||
|
settings:
|
||||||
|
target_url: https://git.xiongxiao.me/web-components/kv-code.git
|
||||||
|
auth_type: https
|
||||||
|
username: "oauth2"
|
||||||
|
password: ${GITEA_TOKEN}
|
||||||
|
git_user: "abearxiong"
|
||||||
|
git_email: "xiongxiao@xiongxiao.me"
|
||||||
|
sync_mode: rebase
|
||||||
|
branch: main
|
||||||
|
web_trigger_sync_from_gitea:
|
||||||
|
- services:
|
||||||
|
- docker
|
||||||
|
imports:
|
||||||
|
- https://cnb.cool/kevisual/env/-/blob/main/env.yml
|
||||||
|
stages:
|
||||||
|
- name: '添加 gitea的origin'
|
||||||
|
script: |
|
||||||
|
git remote remove gitea 2>/dev/null || true
|
||||||
|
git remote add gitea https://oauth2:${GITEA_TOKEN}@git.xiongxiao.me/web-components/kv-code.git
|
||||||
|
- name: '同步gitea代码到当前仓库'
|
||||||
|
script: git pull gitea main
|
||||||
|
- name: '提交到原本的origin'
|
||||||
|
script: git push origin main
|
||||||
|
|
||||||
|
|
||||||
|
"**":
|
||||||
|
web_trigger_test:
|
||||||
|
- stages:
|
||||||
|
- name: 执行任务
|
||||||
|
script: echo "job"
|
||||||
11
.cnb/web_trigger.yml
Normal file
11
.cnb/web_trigger.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
# .cnb/web_trigger.yml
|
||||||
|
branch:
|
||||||
|
# 如下按钮在分支名以 release 开头的分支详情页面显示
|
||||||
|
- reg: "^main"
|
||||||
|
buttons:
|
||||||
|
- name: 同步代码到gitea
|
||||||
|
desc: 同步代码到gitea
|
||||||
|
event: web_trigger_sync_to_gitea
|
||||||
|
- name: 同步gitea代码到当前仓库
|
||||||
|
desc: 同步gitea代码到当前仓库
|
||||||
|
event: web_trigger_sync_from_gitea
|
||||||
15
.gitignore
vendored
Normal file
15
.gitignore
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
.env
|
||||||
|
!.env*development
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
dist
|
||||||
|
pack-dist
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
.pnpm-store
|
||||||
|
|
||||||
|
.vite
|
||||||
|
|
||||||
|
.astro
|
||||||
15
bun.config.ts
Normal file
15
bun.config.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { build } from 'bun';
|
||||||
|
|
||||||
|
await build({
|
||||||
|
entrypoints: ["./src/lib.ts"],
|
||||||
|
outdir: './dist',
|
||||||
|
target: 'browser',
|
||||||
|
format: 'esm',
|
||||||
|
naming: {
|
||||||
|
entry: 'app.js',
|
||||||
|
},
|
||||||
|
minify: false,
|
||||||
|
sourcemap: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✅ Build complete: dist/app.js');
|
||||||
15
index.html
Normal file
15
index.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Light Code</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
25
kevisual.json
Normal file
25
kevisual.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"name": "kevisual",
|
||||||
|
"share": "public"
|
||||||
|
},
|
||||||
|
"registry": "https://kevisual.cn/root/ai/kevisual/frontend/simple-lit-vite",
|
||||||
|
"clone": {
|
||||||
|
".": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"syncd": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"**/*"
|
||||||
|
],
|
||||||
|
"registry": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sync": {
|
||||||
|
".gitignore": {
|
||||||
|
"url": "/gitignore.txt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
65
package.json
Normal file
65
package.json
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
{
|
||||||
|
"name": "@kevisual/kv-code",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"basename": "/root/kv-code",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"build:lib": "vite build --config vite.config.lib.ts",
|
||||||
|
"prepub": "rm -rf ./dist && rimraf ./pack-dist && pnpm run build:test",
|
||||||
|
"pub": "envision deploy ./dist -k kv-code -v 0.0.1"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "abearxiong <xiongxiao@xiongxiao.me> (https://www.xiongxiao.me)",
|
||||||
|
"license": "MIT",
|
||||||
|
"packageManager": "pnpm@10.26.0",
|
||||||
|
"type": "module",
|
||||||
|
"files": [
|
||||||
|
"src",
|
||||||
|
"mod.ts",
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@kevisual/app": "^0.0.1",
|
||||||
|
"@kevisual/context": "^0.0.4",
|
||||||
|
"@codemirror/autocomplete": "^6.20.0",
|
||||||
|
"@codemirror/commands": "^6.10.1",
|
||||||
|
"@codemirror/lang-css": "^6.3.1",
|
||||||
|
"@codemirror/lang-html": "^6.4.11",
|
||||||
|
"@codemirror/lang-javascript": "^6.2.4",
|
||||||
|
"@codemirror/lang-json": "^6.0.2",
|
||||||
|
"@codemirror/lang-markdown": "^6.5.0",
|
||||||
|
"@codemirror/state": "^6.5.2",
|
||||||
|
"@codemirror/view": "^6.39.4",
|
||||||
|
"codemirror": "^6.0.2",
|
||||||
|
"eventemitter3": "^5.0.1",
|
||||||
|
"prettier": "^3.7.4",
|
||||||
|
"dayjs": "^1.11.19",
|
||||||
|
"lit-html": "^3.3.1",
|
||||||
|
"nanoid": "^5.1.6",
|
||||||
|
"react": "^19.2.3",
|
||||||
|
"react-dom": "^19.2.3",
|
||||||
|
"zustand": "^5.0.9"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@kevisual/query": "0.0.32",
|
||||||
|
"@kevisual/types": "^0.0.10",
|
||||||
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
|
"@types/bun": "^1.3.4",
|
||||||
|
"@types/node": "^25.0.3",
|
||||||
|
"@types/react": "^19.2.7",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react": "^5.1.2",
|
||||||
|
"dotenv": "^17.2.3",
|
||||||
|
"tailwindcss": "^4.1.18",
|
||||||
|
"vite": "^7.3.0"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
"./dist/*": "./dist/*",
|
||||||
|
"./src/*": "./src/*"
|
||||||
|
}
|
||||||
|
}
|
||||||
2207
pnpm-lock.yaml
generated
Normal file
2207
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
15
readme.md
Normal file
15
readme.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# simple-lit-vite
|
||||||
|
|
||||||
|
这是一个使用 Vite 构建的简单 Lit 项目模板。您可以使用此模板快速启动一个新的 Lit 项目,并利用 AI 助手生成代码片段以加速开发过程。少用git。
|
||||||
|
|
||||||
|
## cli
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ev sync clone -i https://kevisual.cn/root/ai/kevisual/frontend/simple-lit-vite/kevisual.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 同步命令
|
||||||
|
```sh
|
||||||
|
ev sync list
|
||||||
|
ev sync upload
|
||||||
|
```
|
||||||
16
src/app.tsx
Normal file
16
src/app.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export const App = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="bg-red-200">Hello Vite + React Simple vite</h1>
|
||||||
|
<div className='card'>
|
||||||
|
<button type='button' onClick={() => alert('Button clicked!')}>
|
||||||
|
Click me
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/* <kv-template></kv-template> */}
|
||||||
|
<kv-code-json value='{"key": "value"}' style={{height:'300px'}}></kv-code-json>
|
||||||
|
</div >
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { };
|
||||||
92
src/codemirror/editor.base.ts
Normal file
92
src/codemirror/editor.base.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { basicSetup } from 'codemirror';
|
||||||
|
import { EditorView } from '@codemirror/view';
|
||||||
|
import { StateEffect } from '@codemirror/state';
|
||||||
|
import { ViewUpdate } from '@codemirror/view';
|
||||||
|
import { formatKeymap } from './extensions/tab.ts';
|
||||||
|
import EventEmitter from 'eventemitter3';
|
||||||
|
export type CodeEditor = EditorView & {
|
||||||
|
emitter?: EventEmitter;
|
||||||
|
};
|
||||||
|
let editor: CodeEditor = null;
|
||||||
|
|
||||||
|
export type EditorOptions = {
|
||||||
|
extensions?: any[];
|
||||||
|
hasBasicSetup?: boolean;
|
||||||
|
onChange?: (content: string) => void;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 创建单例
|
||||||
|
* @param el
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const createEditorInstance = (el?: HTMLDivElement, opts?: EditorOptions) => {
|
||||||
|
if (editor && el) {
|
||||||
|
el.appendChild(editor.dom);
|
||||||
|
return editor;
|
||||||
|
} else if (editor) {
|
||||||
|
return editor;
|
||||||
|
}
|
||||||
|
const extensions = opts?.extensions || [];
|
||||||
|
extensions.push(formatKeymap);
|
||||||
|
const hasBaseicSetup = opts?.hasBasicSetup ?? true;
|
||||||
|
if (hasBaseicSetup) {
|
||||||
|
extensions.unshift(basicSetup);
|
||||||
|
}
|
||||||
|
const emitter = new EventEmitter();
|
||||||
|
createOnChangeListener(emitter, opts.onChange).appendTo(extensions);
|
||||||
|
editor = new EditorView({
|
||||||
|
extensions: extensions,
|
||||||
|
parent: el || document.body,
|
||||||
|
});
|
||||||
|
editor.emitter = emitter;
|
||||||
|
editor.dom.style.height = '100%';
|
||||||
|
return editor as CodeEditor;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每次都创建新的实例
|
||||||
|
* @param el
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const createEditor = (el: HTMLDivElement, opts?: EditorOptions) => {
|
||||||
|
const extensions = opts?.extensions || [];
|
||||||
|
const hasBaseicSetup = opts?.hasBasicSetup ?? true;
|
||||||
|
if (hasBaseicSetup) {
|
||||||
|
extensions.unshift(basicSetup);
|
||||||
|
}
|
||||||
|
extensions.push(formatKeymap);
|
||||||
|
const emitter = new EventEmitter();
|
||||||
|
createOnChangeListener(emitter, opts.onChange).appendTo(extensions);
|
||||||
|
const editor = new EditorView({
|
||||||
|
extensions,
|
||||||
|
parent: el || document.body,
|
||||||
|
}) as CodeEditor;
|
||||||
|
|
||||||
|
editor.emitter = emitter;
|
||||||
|
editor.dom.style.height = '100%';
|
||||||
|
|
||||||
|
return editor as CodeEditor;
|
||||||
|
};
|
||||||
|
export const getEditor = () => editor;
|
||||||
|
|
||||||
|
export { editor, createEditorInstance };
|
||||||
|
|
||||||
|
export const createOnChangeListener = (emitter: EventEmitter, callback?: (content: string) => void) => {
|
||||||
|
const listener = EditorView.updateListener.of((update: ViewUpdate) => {
|
||||||
|
if (update.docChanged) {
|
||||||
|
const editor = update.view;
|
||||||
|
if (callback) {
|
||||||
|
callback(editor.state.doc.toString());
|
||||||
|
}
|
||||||
|
// 触发自定义事件
|
||||||
|
emitter.emit('change', editor.state.doc.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 返回监听器配置,而不是直接应用它
|
||||||
|
return {
|
||||||
|
extension: listener,
|
||||||
|
appendTo: (ext: any[]) => {
|
||||||
|
ext.push(listener);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
33
src/codemirror/editor.json.ts
Normal file
33
src/codemirror/editor.json.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { json } from '@codemirror/lang-json';
|
||||||
|
import * as Base from './editor.base.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建单例
|
||||||
|
* @param el
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const createEditorInstance = (el?: HTMLDivElement, opts?: Base.EditorOptions) => {
|
||||||
|
return Base.createEditorInstance(el, {
|
||||||
|
...opts,
|
||||||
|
extensions: [...(opts?.extensions || []), json()]
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每次都创建新的实例
|
||||||
|
* @param el
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const createEditor = (el: HTMLDivElement, opts?: Base.EditorOptions) => {
|
||||||
|
return Base.createEditor(el, {
|
||||||
|
...opts,
|
||||||
|
extensions: [...(opts?.extensions || []), json()]
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Base };
|
||||||
|
|
||||||
|
export type CodeEditor = Base.CodeEditor;
|
||||||
|
export type EditorOptions = Base.EditorOptions;
|
||||||
|
|
||||||
|
export const editor = Base.editor;
|
||||||
120
src/codemirror/editor.ts
Normal file
120
src/codemirror/editor.ts
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import { basicSetup } from 'codemirror';
|
||||||
|
import { EditorView } from '@codemirror/view';
|
||||||
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
|
import { json } from '@codemirror/lang-json';
|
||||||
|
import { html } from '@codemirror/lang-html';
|
||||||
|
import { markdown } from '@codemirror/lang-markdown';
|
||||||
|
import { css } from '@codemirror/lang-css';
|
||||||
|
import { formatKeymap } from './extensions/tab.ts';
|
||||||
|
import { createOnChangeListener } from './editor.base.ts';
|
||||||
|
import EventEmitter from 'eventemitter3';
|
||||||
|
|
||||||
|
export type CodeEditor = EditorView & {
|
||||||
|
emitter?: EventEmitter;
|
||||||
|
};
|
||||||
|
let editor: CodeEditor = null;
|
||||||
|
|
||||||
|
type CreateOpts = {
|
||||||
|
jsx?: boolean;
|
||||||
|
typescript?: boolean;
|
||||||
|
type?: 'javascript' | 'json' | 'html' | 'markdown' | 'css';
|
||||||
|
hasBasicSetup?: boolean;
|
||||||
|
extensions?: any[];
|
||||||
|
hasKeymap?: boolean;
|
||||||
|
onChange?: (content: string) => void;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 创建单例
|
||||||
|
* @param el
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const createEditorInstance = (el?: HTMLDivElement, opts?: CreateOpts) => {
|
||||||
|
if (editor && el) {
|
||||||
|
el.appendChild(editor.dom);
|
||||||
|
return editor;
|
||||||
|
} else if (editor) {
|
||||||
|
return editor;
|
||||||
|
}
|
||||||
|
const { type = 'javascript' } = opts || {};
|
||||||
|
const extensions = opts?.extensions || [];
|
||||||
|
const hasBaseicSetup = opts?.hasBasicSetup ?? true;
|
||||||
|
const hasKeymap = opts?.hasKeymap ?? true;
|
||||||
|
if (hasBaseicSetup) {
|
||||||
|
extensions.unshift(basicSetup);
|
||||||
|
}
|
||||||
|
if (hasKeymap) {
|
||||||
|
extensions.push(formatKeymap);
|
||||||
|
}
|
||||||
|
switch (type) {
|
||||||
|
case 'json':
|
||||||
|
extensions.push(json());
|
||||||
|
break;
|
||||||
|
case 'javascript':
|
||||||
|
extensions.push(javascript({ jsx: opts?.jsx, typescript: opts?.typescript }));
|
||||||
|
break;
|
||||||
|
case 'css':
|
||||||
|
extensions.push(css());
|
||||||
|
break;
|
||||||
|
case 'html':
|
||||||
|
extensions.push(html());
|
||||||
|
break;
|
||||||
|
case 'markdown':
|
||||||
|
extensions.push(markdown());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const emitter = new EventEmitter();
|
||||||
|
createOnChangeListener(emitter, opts.onChange).appendTo(extensions);
|
||||||
|
editor = new EditorView({
|
||||||
|
extensions: extensions,
|
||||||
|
parent: el || document.body,
|
||||||
|
});
|
||||||
|
editor.dom.style.height = '100%';
|
||||||
|
editor.emitter = emitter;
|
||||||
|
return editor as CodeEditor;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 每次都创建新的实例
|
||||||
|
* @param el
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const createEditor = (el: HTMLDivElement, opts?: CreateOpts) => {
|
||||||
|
const { type = 'javascript' } = opts || {};
|
||||||
|
const extensions = opts?.extensions || [];
|
||||||
|
const hasBaseicSetup = opts?.hasBasicSetup ?? true;
|
||||||
|
const hasKeymap = opts?.hasKeymap ?? true;
|
||||||
|
if (hasBaseicSetup) {
|
||||||
|
extensions.unshift(basicSetup);
|
||||||
|
}
|
||||||
|
if (hasKeymap) {
|
||||||
|
extensions.push(formatKeymap);
|
||||||
|
}
|
||||||
|
switch (type) {
|
||||||
|
case 'json':
|
||||||
|
extensions.push(json());
|
||||||
|
break;
|
||||||
|
case 'javascript':
|
||||||
|
extensions.push(javascript({ jsx: opts?.jsx, typescript: opts?.typescript }));
|
||||||
|
break;
|
||||||
|
case 'css':
|
||||||
|
extensions.push(css());
|
||||||
|
break;
|
||||||
|
case 'html':
|
||||||
|
extensions.push(html());
|
||||||
|
break;
|
||||||
|
case 'markdown':
|
||||||
|
extensions.push(markdown());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const emitter = new EventEmitter();
|
||||||
|
createOnChangeListener(emitter, opts.onChange).appendTo(extensions);
|
||||||
|
const editor = new EditorView({
|
||||||
|
extensions: extensions,
|
||||||
|
parent: el || document.body,
|
||||||
|
}) as CodeEditor;
|
||||||
|
editor.dom.style.height = '100%';
|
||||||
|
editor.emitter = emitter;
|
||||||
|
return editor as CodeEditor;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { editor, createEditorInstance };
|
||||||
59
src/codemirror/editor.utils.ts
Normal file
59
src/codemirror/editor.utils.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { EditorView } from '@codemirror/view';
|
||||||
|
import { CodeEditor } from './editor.base.ts';
|
||||||
|
type ChainOpts = {
|
||||||
|
editor?: CodeEditor;
|
||||||
|
};
|
||||||
|
export class Chain {
|
||||||
|
editor: CodeEditor | EditorView;
|
||||||
|
constructor(opts?: ChainOpts) {
|
||||||
|
this.editor = opts?.editor;
|
||||||
|
}
|
||||||
|
getEditor() {
|
||||||
|
return this.editor;
|
||||||
|
}
|
||||||
|
getContent() {
|
||||||
|
return this.editor?.state.doc.toString() || '';
|
||||||
|
}
|
||||||
|
setContent(content: string) {
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.dispatch({
|
||||||
|
changes: { from: 0, to: this.editor.state.doc.length, insert: content },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
setEditor(editor: EditorView) {
|
||||||
|
this.editor = editor;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
clearEditor() {
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.dispatch({
|
||||||
|
changes: { from: 0, to: this.editor.state.doc.length, insert: '' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
destroy() {
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.destroy();
|
||||||
|
this.editor = null;
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
static create(opts?: ChainOpts) {
|
||||||
|
return new Chain(opts);
|
||||||
|
}
|
||||||
|
setOnChange(callback: (content: string) => void) {
|
||||||
|
if (this.editor) {
|
||||||
|
const editor = this.editor as CodeEditor;
|
||||||
|
if (editor.emitter) {
|
||||||
|
editor.emitter.on('change', callback);
|
||||||
|
return () => {
|
||||||
|
editor.emitter.off('change', callback);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return () => {};
|
||||||
|
}
|
||||||
|
}
|
||||||
56
src/codemirror/extensions/tab.ts
Normal file
56
src/codemirror/extensions/tab.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { EditorView, keymap } from '@codemirror/view';
|
||||||
|
import { EditorState } from '@codemirror/state';
|
||||||
|
import { defaultKeymap, indentWithTab, insertTab } from '@codemirror/commands';
|
||||||
|
import prettier from 'prettier';
|
||||||
|
// import parserBabel from 'prettier/plugins/babel';
|
||||||
|
import parserEstree from 'prettier/plugins/estree';
|
||||||
|
// import parserHtml from 'prettier/plugins/html';
|
||||||
|
import parserTypescript from 'prettier/plugins/typescript';
|
||||||
|
|
||||||
|
// 格式化函数
|
||||||
|
// Function to format the code using Prettier
|
||||||
|
type FormatCodeOptions = {
|
||||||
|
type: 'typescript';
|
||||||
|
plugins?: any[];
|
||||||
|
};
|
||||||
|
async function formatCode(view: EditorView, opts?: FormatCodeOptions) {
|
||||||
|
const editor = view;
|
||||||
|
const code = editor.state.doc.toString();
|
||||||
|
const plugins = opts?.plugins || [];
|
||||||
|
plugins.push(parserEstree);
|
||||||
|
const parser = opts?.type || 'typescript';
|
||||||
|
if (parser === 'typescript') {
|
||||||
|
plugins.push(parserTypescript);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const formattedCode = await prettier.format(code, {
|
||||||
|
parser: parser,
|
||||||
|
plugins: plugins,
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: 0,
|
||||||
|
to: editor.state.doc.length,
|
||||||
|
insert: formattedCode.trim(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error formatting code:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatKeymap = keymap.of([
|
||||||
|
{
|
||||||
|
// bug, 必须小写
|
||||||
|
key: 'alt-shift-f', // 快捷键绑定
|
||||||
|
// mac: 'cmd-shift-f',
|
||||||
|
run: (view) => {
|
||||||
|
formatCode(view);
|
||||||
|
return true; // 表示按键事件被处理
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// indentWithTab, // Tab键自动缩进
|
||||||
|
{ key: 'Tab', run: insertTab }, // 在光标位置插入Tab字符
|
||||||
|
...defaultKeymap, // 默认快捷键
|
||||||
|
]);
|
||||||
1
src/index.css
Normal file
1
src/index.css
Normal file
@@ -0,0 +1 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
229
src/json.ts
Normal file
229
src/json.ts
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
/**
|
||||||
|
* @file json.ts
|
||||||
|
* @description JSON编辑器组件 - 基于CodeMirror的JSON代码编辑器Web组件
|
||||||
|
* @tags json, editor, codemirror, web-component
|
||||||
|
* @createdAt 2025-12-18
|
||||||
|
*/
|
||||||
|
|
||||||
|
// import { render } from 'lit-html';
|
||||||
|
// import { html } from 'lit-html';
|
||||||
|
import { createEditor, editor, CodeEditor } from './codemirror/editor.json.ts';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KvCodeJson - JSON编辑器自定义元素
|
||||||
|
* 支持JSON语法高亮、格式化、验证等功能
|
||||||
|
*/
|
||||||
|
class KvCodeJson extends HTMLElement {
|
||||||
|
private editor: CodeEditor | null = null;
|
||||||
|
private editorContainer: HTMLDivElement | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定义可观察的属性
|
||||||
|
*/
|
||||||
|
static get observedAttributes() {
|
||||||
|
return ['value', 'readonly', 'placeholder'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件挂载时初始化
|
||||||
|
*/
|
||||||
|
connectedCallback() {
|
||||||
|
this.render();
|
||||||
|
this.initializeEditor();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件卸载时清理资源
|
||||||
|
*/
|
||||||
|
disconnectedCallback() {
|
||||||
|
if (this.editor) {
|
||||||
|
this.editor.destroy();
|
||||||
|
this.editor = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 属性变化时的回调
|
||||||
|
*/
|
||||||
|
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
|
||||||
|
if (oldValue === newValue) return;
|
||||||
|
|
||||||
|
switch (name) {
|
||||||
|
case 'value':
|
||||||
|
this.setValue(newValue);
|
||||||
|
break;
|
||||||
|
case 'readonly':
|
||||||
|
this.setReadOnly(newValue !== null);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取或创建内联容器div
|
||||||
|
*/
|
||||||
|
getInlineDiv(opts?: { prefixId?: string }): HTMLDivElement {
|
||||||
|
const prefixId = opts?.prefixId || 'kv-json-editor';
|
||||||
|
let id = this.id;
|
||||||
|
if (!id) {
|
||||||
|
id = `${prefixId}-${Math.random().toString(36).substring(2, 9)}`;
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
const inlineId = `${id}-inline`;
|
||||||
|
let editor: HTMLDivElement = this.shadowRoot!.querySelector(`#${inlineId}`)!;
|
||||||
|
if (!editor) {
|
||||||
|
editor = document.createElement('div') as HTMLDivElement;
|
||||||
|
editor.id = inlineId;
|
||||||
|
editor.style.height = '100%';
|
||||||
|
editor.style.width = '100%';
|
||||||
|
// 直接添加到 shadow root,不使用 slot
|
||||||
|
this.shadowRoot!.appendChild(editor);
|
||||||
|
}
|
||||||
|
return editor as HTMLDivElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化CodeMirror编辑器
|
||||||
|
*/
|
||||||
|
private initializeEditor(): void {
|
||||||
|
const container = this.getInlineDiv();
|
||||||
|
this.editorContainer = container;
|
||||||
|
|
||||||
|
// 创建编辑器实例
|
||||||
|
this.editor = createEditor(container, {
|
||||||
|
onChange: (content: string) => {
|
||||||
|
this.dispatchChangeEvent(content);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置初始值
|
||||||
|
const initialValue = this.getAttribute('value');
|
||||||
|
if (initialValue) {
|
||||||
|
this.setValue(initialValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置样式
|
||||||
|
this.applyStyles();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用编辑器样式
|
||||||
|
*/
|
||||||
|
private applyStyles(): void {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
this.shadowRoot!.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染组件模板
|
||||||
|
*/
|
||||||
|
private render(): void {
|
||||||
|
// 不需要渲染 slot,直接在 shadow root 中创建容器
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置编辑器内容
|
||||||
|
*/
|
||||||
|
setValue(value: string): void {
|
||||||
|
if (!this.editor) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const transaction = this.editor.state.update({
|
||||||
|
changes: {
|
||||||
|
from: 0,
|
||||||
|
to: this.editor.state.doc.length,
|
||||||
|
insert: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.editor.dispatch(transaction);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to set editor value:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取编辑器内容
|
||||||
|
*/
|
||||||
|
getValue(): string {
|
||||||
|
return this.editor?.state.doc.toString() || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置只读模式
|
||||||
|
*/
|
||||||
|
setReadOnly(readonly: boolean): void {
|
||||||
|
if (!this.editor) return;
|
||||||
|
|
||||||
|
// TODO: 实现只读模式
|
||||||
|
console.warn('ReadOnly mode not yet implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化JSON内容
|
||||||
|
*/
|
||||||
|
formatJson(): void {
|
||||||
|
try {
|
||||||
|
const content = this.getValue();
|
||||||
|
const parsed = JSON.parse(content);
|
||||||
|
const formatted = JSON.stringify(parsed, null, 2);
|
||||||
|
this.setValue(formatted);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to format JSON:', error);
|
||||||
|
this.dispatchErrorEvent('Invalid JSON format');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证JSON格式
|
||||||
|
*/
|
||||||
|
validateJson(): boolean {
|
||||||
|
try {
|
||||||
|
JSON.parse(this.getValue());
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 派发内容变化事件
|
||||||
|
*/
|
||||||
|
private dispatchChangeEvent(content: string): void {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('change', {
|
||||||
|
detail: { value: content, valid: this.validateJson() },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 派发错误事件
|
||||||
|
*/
|
||||||
|
private dispatchErrorEvent(message: string): void {
|
||||||
|
this.dispatchEvent(
|
||||||
|
new CustomEvent('error', {
|
||||||
|
detail: { message },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义自定义元素
|
||||||
|
customElements.define('kv-code-json', KvCodeJson);
|
||||||
|
|
||||||
|
export { KvCodeJson, createEditor, editor };
|
||||||
43
src/lib.ts
Normal file
43
src/lib.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { render } from 'lit-html';
|
||||||
|
import { html, TemplateResult } from 'lit-html';
|
||||||
|
|
||||||
|
class KvTemplate extends HTMLElement {
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: 'open' });
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
getInlineDiv(opts?: { prefixId?: string }): HTMLDivElement {
|
||||||
|
const prefixId = opts?.prefixId || 'component';
|
||||||
|
const slotName = 'container';
|
||||||
|
let id = this.id;
|
||||||
|
if (!id) {
|
||||||
|
id = `${prefixId}-${Math.random().toString(36).substring(2, 9)}`;
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
const inlineId = `${id}-inline`;
|
||||||
|
let editor = this.shadowRoot!.querySelector(`#${inlineId}`)!;
|
||||||
|
if (!editor) {
|
||||||
|
editor = document.createElement('div');
|
||||||
|
editor.id = inlineId;
|
||||||
|
editor.slot = slotName;
|
||||||
|
const host = document.querySelector(`#${id}`);
|
||||||
|
host!.appendChild(editor);
|
||||||
|
}
|
||||||
|
return editor as HTMLDivElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
private render(): void {
|
||||||
|
const contentWithHtml = html`
|
||||||
|
<slot name="container"></slot>
|
||||||
|
`;
|
||||||
|
const el = this.getInlineDiv();
|
||||||
|
render(contentWithHtml, el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the custom element globally
|
||||||
|
customElements.define('kv-template', KvTemplate);
|
||||||
7
src/main.ts
Normal file
7
src/main.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import './lib'
|
||||||
|
import { App } from './app.tsx';
|
||||||
|
import './index.css';
|
||||||
|
import './json.ts';
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(App());
|
||||||
20
tsconfig.json
Normal file
20
tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"extends": "@kevisual/types/json/frontend.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"strict": false,
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
],
|
||||||
|
"@/agent": [
|
||||||
|
"./src/agent"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/**/*",
|
||||||
|
"agent/**/*",
|
||||||
|
"typings.d.ts"
|
||||||
|
],
|
||||||
|
}
|
||||||
16
typings.d.ts
vendored
Normal file
16
typings.d.ts
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import type { DetailedHTMLProps, HTMLAttributes } from 'react';
|
||||||
|
|
||||||
|
interface KvCodeJsonAttributes extends HTMLAttributes<HTMLElement> {
|
||||||
|
value?: string;
|
||||||
|
readonly?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module 'react' {
|
||||||
|
namespace JSX {
|
||||||
|
interface IntrinsicElements {
|
||||||
|
'kv-template': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
||||||
|
'kv-code-json': DetailedHTMLProps<KvCodeJsonAttributes, HTMLElement>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
vite.config.lib.ts
Normal file
27
vite.config.lib.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'path';
|
||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
const plugins = [react(), tailwindcss()];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://vitejs.dev/config/
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins,
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
lib: {
|
||||||
|
entry: path.resolve(__dirname, 'mod.ts'),
|
||||||
|
name: 'KvCode',
|
||||||
|
formats: ['es'],
|
||||||
|
fileName: () => `kv-code.js`,
|
||||||
|
},
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
44
vite.config.ts
Normal file
44
vite.config.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'path';
|
||||||
|
import pkgs from './package.json';
|
||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
dotenv.config();
|
||||||
|
const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
const plugins = [react(), tailwindcss()];
|
||||||
|
|
||||||
|
let target = process.env.VITE_API_URL || 'http://localhost:51015';
|
||||||
|
const apiProxy = { target: target, changeOrigin: true, ws: true, rewriteWsOrigin: true, secure: false, cookieDomainRewrite: 'localhost' };
|
||||||
|
let proxy = {
|
||||||
|
'/root/': apiProxy,
|
||||||
|
'/api': apiProxy,
|
||||||
|
'/client': apiProxy,
|
||||||
|
};
|
||||||
|
const ENV_BASE_NAME = process.env.BASE_NAME;
|
||||||
|
|
||||||
|
const _basename = ENV_BASE_NAME || pkgs.basename;
|
||||||
|
const basename = isDev ? undefined : `${_basename}`;
|
||||||
|
/**
|
||||||
|
* @see https://vitejs.dev/config/
|
||||||
|
*/
|
||||||
|
export default defineConfig(() => {
|
||||||
|
return {
|
||||||
|
plugins,
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
base: basename,
|
||||||
|
define: {
|
||||||
|
BASE_NAME: JSON.stringify(basename),
|
||||||
|
BUILD_TIME: JSON.stringify(new Date().toISOString()),
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 7008,
|
||||||
|
host: '0.0.0.0',
|
||||||
|
proxy,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user